From 834dbcbf4f18184f5de473ca3bd7ff4a86225a15 Mon Sep 17 00:00:00 2001 From: Jasjeet Singh <98077881+07jasjeet@users.noreply.github.com> Date: Sat, 2 Mar 2024 03:37:35 +0530 Subject: [PATCH 01/97] F-droid fixes 1) Removed unused permissions 2) Remove usesClearTextTraffic as it isn't used anywhere. 3) DependencyInfo changes --- app/build.gradle | 8 ++++++++ app/src/main/AndroidManifest.xml | 3 --- .../listenbrainz/android/viewmodel/DashBoardViewModel.kt | 6 +----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8606d928..54dea0b9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -115,6 +115,14 @@ android { excludes += '/META-INF/{AL2.0,LGPL2.1}' } } + dependenciesInfo { + // Disables dependency metadata when building APKs. + // This is for the signed .apk that we post to GitHub, so the dependency metadata isn't relevant. + includeInApk = false + // Disables dependency metadata when building Android App Bundles. + // This is for the Google Play Store, so we'll want the metadata. + includeInBundle = true + } } dependencies { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 71dcc677..a3203f71 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,14 +2,12 @@ - - = Build.VERSION_CODES.TIRAMISU -> { - arrayOf( - Manifest.permission.READ_MEDIA_IMAGES, - Manifest.permission.READ_MEDIA_AUDIO - ) + arrayOf(Manifest.permission.READ_MEDIA_AUDIO) } Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE) @@ -92,7 +89,6 @@ class DashBoardViewModel @Inject constructor( when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> { if ( - checkSelfPermission(application.applicationContext, Manifest.permission.READ_MEDIA_IMAGES) == PermissionChecker.PERMISSION_GRANTED && checkSelfPermission(application.applicationContext, Manifest.permission.READ_MEDIA_AUDIO) == PermissionChecker.PERMISSION_GRANTED ){ setPermissionsPreference(PermissionStatus.GRANTED.name) From d9bebaa00bcda27594a558ef150c5645d7690ff6 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sat, 2 Mar 2024 14:05:49 +0530 Subject: [PATCH 02/97] Initial commit in BP revamp , decoupled search for BP Earlier the search icon in the top bar used to be a global search in the app for users , now in the BP page , the search icon will be used to search local songs --- .../android/model/AppNavigationItem.kt | 2 +- .../android/ui/navigation/TopBar.kt | 2 +- .../brainzplayer/BrainzPlayerScreen.kt | 206 +++++++++--------- .../android/ui/screens/main/MainActivity.kt | 28 ++- .../search/BrainzPlayerSearchScreen.kt | 203 +++++++++++++++++ 5 files changed, 330 insertions(+), 111 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/search/BrainzPlayerSearchScreen.kt diff --git a/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt b/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt index 63947019..0d1fc3ce 100644 --- a/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt +++ b/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt @@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes import org.listenbrainz.android.R sealed class AppNavigationItem(val route: String, @DrawableRes val iconUnselected: Int, @DrawableRes val iconSelected: Int, val title: String) { - object BrainzPlayer : AppNavigationItem("brainzplayer", R.drawable.player_unselected, R.drawable.player_selected, "Player") + object BrainzPlayer : AppNavigationItem("brainzplayer", R.drawable.player_unselected, R.drawable.player_selected, "Local Player") object Explore : AppNavigationItem("explore", R.drawable.explore_unselected, R.drawable.explore_selected, "Explore") object Profile : AppNavigationItem("profile", R.drawable.profile_unselected, R.drawable.profile_selected, "Profile") object Feed : AppNavigationItem("feed", R.drawable.feed_unselected, R.drawable.feed_selected, "Feed") diff --git a/app/src/main/java/org/listenbrainz/android/ui/navigation/TopBar.kt b/app/src/main/java/org/listenbrainz/android/ui/navigation/TopBar.kt index 7d682a5b..5921530c 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/navigation/TopBar.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/navigation/TopBar.kt @@ -29,7 +29,7 @@ import org.listenbrainz.android.ui.theme.ListenBrainzTheme fun TopBar( navController: NavController = rememberNavController(), searchBarState: SearchBarState, - context: Context = LocalContext.current + context: Context = LocalContext.current, ) { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index 6532df39..04303a98 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -99,7 +99,7 @@ fun BrainzPlayerHomeScreen( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { - SearchView(state = searchTextState, brainzPlayerViewModel) +// SearchView(state = searchTextState, brainzPlayerViewModel) } // Recently Played @@ -303,108 +303,108 @@ fun BrainzPlayerHomeScreen( } } -@Composable -fun SearchView(state: MutableState, brainzPlayerViewModel: BrainzPlayerViewModel) { - var searchStarted by remember { - mutableStateOf(false) - } - var searchItems by remember { - mutableStateOf(mutableListOf()) - } - val itemHeights = remember { mutableStateMapOf() } - val baseHeight = 330.dp - val density = LocalDensity.current - val maxHeight = remember(itemHeights.toMap()) { - if (itemHeights.keys.toSet() != searchItems.indices.toSet ()) { - // if we don't have all heights calculated yet, return default value - return@remember baseHeight - } - val baseHeightInt = with(density) { baseHeight.toPx().toInt() } - var sum = with(density) { 8.dp.toPx().toInt() } * 2 - for ((_, itemSize) in itemHeights.toSortedMap()) { - sum += itemSize - if (sum >= baseHeightInt) { - return@remember with(density) { (sum - itemSize / 2).toDp() } - } - } - // all items fit into base height - baseHeight - } - - Box { - TextField( - modifier = Modifier - .fillMaxWidth(0.9f) - .padding(2.dp), - value = state.value, - onValueChange = { value -> - state.value = value - searchItems = brainzPlayerViewModel.searchSongs(value.text)!!.toMutableList() - searchStarted = true - - }, - textStyle = TextStyle(MaterialTheme.colorScheme.onSurface, fontSize = 15.sp), - leadingIcon = { - Icon( - Icons.Default.Search, - contentDescription = "", - modifier = Modifier - .padding(15.dp) - .size(24.dp) - - ) - }, - trailingIcon = { - if (state.value != TextFieldValue("")) { - IconButton(onClick = { - state.value = TextFieldValue("") - } - ) { - Icon( - Icons.Default.Close, - contentDescription = "", - modifier = Modifier - .padding(15.dp) - .size(24.dp) - ) - } - } - }, - singleLine = true, - shape = RoundedCornerShape(25.dp), - colors = TextFieldDefaults.textFieldColors( - - textColor = Color.Black, - disabledTextColor = Color.Transparent, - backgroundColor = Color.Gray, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent - ) - ) - } - DropdownMenu( - modifier = Modifier.requiredSizeIn(maxHeight = maxHeight), - properties = PopupProperties(focusable = false ), - expanded = searchStarted, - onDismissRequest = { searchStarted = false }) { - searchItems.forEachIndexed { index, song -> - - DropdownMenuItem( - modifier = Modifier.onSizeChanged { - itemHeights[index] = it.height - - }, - onClick = { - brainzPlayerViewModel.changePlayable(listOf(song), PlayableType.SONG, song.mediaID,0) - brainzPlayerViewModel.playOrToggleSong(song, true) - searchStarted = false - state.value.text.removeRange(0, state.value.text.length-1.coerceAtLeast(0))}) { - androidx.compose.material3.Text(text = song.title ) - } - } - } -} +//@Composable +//fun SearchView(state: MutableState, brainzPlayerViewModel: BrainzPlayerViewModel) { +// var searchStarted by remember { +// mutableStateOf(false) +// } +// var searchItems by remember { +// mutableStateOf(mutableListOf()) +// } +// val itemHeights = remember { mutableStateMapOf() } +// val baseHeight = 330.dp +// val density = LocalDensity.current +// val maxHeight = remember(itemHeights.toMap()) { +// if (itemHeights.keys.toSet() != searchItems.indices.toSet ()) { +// // if we don't have all heights calculated yet, return default value +// return@remember baseHeight +// } +// val baseHeightInt = with(density) { baseHeight.toPx().toInt() } +// var sum = with(density) { 8.dp.toPx().toInt() } * 2 +// for ((_, itemSize) in itemHeights.toSortedMap()) { +// sum += itemSize +// if (sum >= baseHeightInt) { +// return@remember with(density) { (sum - itemSize / 2).toDp() } +// } +// } +// // all items fit into base height +// baseHeight +// } +// +// Box { +// TextField( +// modifier = Modifier +// .fillMaxWidth(0.9f) +// .padding(2.dp), +// value = state.value, +// onValueChange = { value -> +// state.value = value +// searchItems = brainzPlayerViewModel.searchSongs(value.text)!!.toMutableList() +// searchStarted = true +// +// }, +// textStyle = TextStyle(MaterialTheme.colorScheme.onSurface, fontSize = 15.sp), +// leadingIcon = { +// Icon( +// Icons.Default.Search, +// contentDescription = "", +// modifier = Modifier +// .padding(15.dp) +// .size(24.dp) +// +// ) +// }, +// trailingIcon = { +// if (state.value != TextFieldValue("")) { +// IconButton(onClick = { +// state.value = TextFieldValue("") +// } +// ) { +// Icon( +// Icons.Default.Close, +// contentDescription = "", +// modifier = Modifier +// .padding(15.dp) +// .size(24.dp) +// ) +// } +// } +// }, +// singleLine = true, +// shape = RoundedCornerShape(25.dp), +// colors = TextFieldDefaults.textFieldColors( +// +// textColor = Color.Black, +// disabledTextColor = Color.Transparent, +// backgroundColor = Color.Gray, +// focusedIndicatorColor = Color.Transparent, +// unfocusedIndicatorColor = Color.Transparent, +// disabledIndicatorColor = Color.Transparent +// ) +// ) +// } +// DropdownMenu( +// modifier = Modifier.requiredSizeIn(maxHeight = maxHeight), +// properties = PopupProperties(focusable = false ), +// expanded = searchStarted, +// onDismissRequest = { searchStarted = false }) { +// searchItems.forEachIndexed { index, song -> +// +// DropdownMenuItem( +// modifier = Modifier.onSizeChanged { +// itemHeights[index] = it.height +// +// }, +// onClick = { +// brainzPlayerViewModel.changePlayable(listOf(song), PlayableType.SONG, song.mediaID,0) +// brainzPlayerViewModel.playOrToggleSong(song, true) +// searchStarted = false +// state.value.text.removeRange(0, state.value.text.length-1.coerceAtLeast(0))}) { +// androidx.compose.material3.Text(text = song.title ) +// } +// } +// } +//} @Composable fun BrainzPlayerActivityCards(icon: String, errorIcon : Int, title: String, modifier : Modifier = Modifier) { diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt index 48890922..3b8fba3e 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt @@ -20,11 +20,13 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.lifecycleScope +import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.listenbrainz.android.application.App +import org.listenbrainz.android.model.AppNavigationItem import org.listenbrainz.android.model.PermissionStatus import org.listenbrainz.android.service.ListenSubmissionService import org.listenbrainz.android.ui.components.DialogLB @@ -32,6 +34,7 @@ import org.listenbrainz.android.ui.navigation.AppNavigation import org.listenbrainz.android.ui.navigation.BottomNavigationBar import org.listenbrainz.android.ui.navigation.TopBar import org.listenbrainz.android.ui.screens.brainzplayer.BrainzPlayerBackDropScreen +import org.listenbrainz.android.ui.screens.search.BrainzPlayerSearchScreen import org.listenbrainz.android.ui.screens.search.SearchScreen import org.listenbrainz.android.ui.screens.search.rememberSearchBarState import org.listenbrainz.android.ui.theme.ListenBrainzTheme @@ -140,10 +143,16 @@ class MainActivity : ComponentActivity() { var scrollToTopState by remember { mutableStateOf(false) } val snackbarState = remember { SnackbarHostState() } val searchBarState = rememberSearchBarState() + val brainzplayerSearchBarState = rememberSearchBarState() val scope = rememberCoroutineScope() + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination Scaffold( - topBar = { TopBar(navController = navController, searchBarState = searchBarState) }, + topBar = { TopBar(navController = navController, searchBarState = when (currentDestination?.route) { + AppNavigationItem.BrainzPlayer.route -> brainzplayerSearchBarState + else -> searchBarState + }) }, bottomBar = { BottomNavigationBar( navController = navController, @@ -189,11 +198,18 @@ class MainActivity : ComponentActivity() { } } } - - SearchScreen( - isActive = searchBarState.isActive, - deactivate = {searchBarState.deactivate()} - ) + + val brainzplayerSearchTextState = remember { + mutableStateOf("") + } + when(currentDestination?.route) { + AppNavigationItem.BrainzPlayer.route -> BrainzPlayerSearchScreen(isActive = brainzplayerSearchBarState.isActive , deactivate = {brainzplayerSearchBarState.deactivate()} , brainzplayerQueryState = brainzplayerSearchTextState) + else -> SearchScreen( + isActive = searchBarState.isActive, + deactivate = {searchBarState.deactivate()} + ) + } + } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/search/BrainzPlayerSearchScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/search/BrainzPlayerSearchScreen.kt new file mode 100644 index 00000000..07dc1f84 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/search/BrainzPlayerSearchScreen.kt @@ -0,0 +1,203 @@ +package org.listenbrainz.android.ui.screens.search + + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Cancel +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.platform.SoftwareKeyboardController +import androidx.compose.ui.platform.WindowInfo +import androidx.compose.ui.text.font.FontWeight +import androidx.hilt.navigation.compose.hiltViewModel +import org.listenbrainz.android.model.PlayableType +import org.listenbrainz.android.model.Song +import org.listenbrainz.android.ui.theme.ListenBrainzTheme +import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel + +@Composable +fun BrainzPlayerSearchScreen( + isActive: Boolean, + viewModel: BrainzPlayerViewModel = hiltViewModel(), + deactivate: () -> Unit, + brainzplayerQueryState : MutableState, +) { + var searchItems by remember { + mutableStateOf(mutableListOf()) + } + + AnimatedVisibility( + visible = isActive, + enter = fadeIn(), + exit = fadeOut() + ) { + + SearchScreen( + onDismiss = { + deactivate() + }, + onQueryChange = { + newValue : String -> brainzplayerQueryState.value = newValue + searchItems = viewModel.searchSongs(brainzplayerQueryState.value)!!.toMutableList() + }, + queryState = brainzplayerQueryState, + searchResult = searchItems, + onClick = { + song -> viewModel.changePlayable(listOf(song), PlayableType.SONG, song.mediaID,0) + viewModel.playOrToggleSong(song, true) + deactivate() + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SearchScreen( + onDismiss: () -> Unit, + onQueryChange: (String) -> Unit, + keyboardController: SoftwareKeyboardController? = LocalSoftwareKeyboardController.current, + onSearch: (String) -> Unit = { + keyboardController?.hide() + }, + focusRequester: FocusRequester = remember { FocusRequester() }, + window: WindowInfo = LocalWindowInfo.current, + queryState : MutableState, + searchResult : MutableList, + onClick: (song : Song) -> Unit +) { + // Used for initial window focus. + LaunchedEffect(window) { + snapshotFlow { window.isWindowFocused }.collect { isWindowFocused -> + if (isWindowFocused) { + focusRequester.requestFocus() + keyboardController?.show() + } + } + } + + SearchBar( + modifier = Modifier.focusRequester(focusRequester), + query = queryState.value, + onQueryChange = onQueryChange, + onSearch = onSearch, + active = true, + onActiveChange = { isActive -> + if (!isActive) + onDismiss() + }, + leadingIcon = { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + modifier = Modifier + .clip(CircleShape) + .clickable { + keyboardController?.hide() + onDismiss() + }, + contentDescription = "Search", + tint = ListenBrainzTheme.colorScheme.hint + ) + }, + trailingIcon = { + Icon(imageVector = Icons.Rounded.Cancel, + modifier = Modifier + .clip(CircleShape) + .clickable { + queryState.value = "" + keyboardController?.show() + }, + contentDescription = "Close Search", + tint = ListenBrainzTheme.colorScheme.hint + ) + }, + placeholder = { + Text(text = "Search local songs", color = MaterialTheme.colorScheme.onSurface.copy(0.5f)) + }, + colors = SearchBarDefaults.colors( + containerColor = ListenBrainzTheme.colorScheme.background, + dividerColor = ListenBrainzTheme.colorScheme.text, + inputFieldColors = SearchBarDefaults.inputFieldColors( + focusedPlaceholderColor = Color.Unspecified, + focusedTextColor = ListenBrainzTheme.colorScheme.text, + cursorColor = ListenBrainzTheme.colorScheme.lbSignatureInverse, + ) + ), + ) { + + Column( + modifier = Modifier + .pointerInput(key1 = "Keyboard"){ + // Tap to hide keyboard. + detectTapGestures { + keyboardController?.hide() + } + } + ) { + SongList(searchResult = searchResult , onClick = { + song -> onClick(song) + }) + } + } +} + + +@Composable +private fun SongList (searchResult: MutableList , onClick: (song : Song) -> Unit) { + LazyColumn(contentPadding = PaddingValues(ListenBrainzTheme.paddings.lazyListAdjacent)) { + itemsIndexed(searchResult) {_, song -> + Column { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(ListenBrainzTheme.paddings.lazyListAdjacent) + ) { + + TextButton( + { onClick(song) } + ){ + Text( + text = song.title, + color = ListenBrainzTheme.colorScheme.text, + fontWeight = FontWeight.Normal + ) + } + } + } + } + } +} \ No newline at end of file From 82ddd9c061df6a664d494a13ab705467c4e50249 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sat, 2 Mar 2024 14:11:36 +0530 Subject: [PATCH 03/97] Removed unnecessary comments --- .../brainzplayer/BrainzPlayerScreen.kt | 114 ------------------ 1 file changed, 114 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index 04303a98..e922d4e0 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -91,17 +91,6 @@ fun BrainzPlayerHomeScreen( .padding(horizontal = 8.dp) .verticalScroll(rememberScrollState()) ) { - // Search Bar - Row( - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { -// SearchView(state = searchTextState, brainzPlayerViewModel) - } - // Recently Played Text( text = "Recently Played", @@ -303,109 +292,6 @@ fun BrainzPlayerHomeScreen( } } -//@Composable -//fun SearchView(state: MutableState, brainzPlayerViewModel: BrainzPlayerViewModel) { -// var searchStarted by remember { -// mutableStateOf(false) -// } -// var searchItems by remember { -// mutableStateOf(mutableListOf()) -// } -// val itemHeights = remember { mutableStateMapOf() } -// val baseHeight = 330.dp -// val density = LocalDensity.current -// val maxHeight = remember(itemHeights.toMap()) { -// if (itemHeights.keys.toSet() != searchItems.indices.toSet ()) { -// // if we don't have all heights calculated yet, return default value -// return@remember baseHeight -// } -// val baseHeightInt = with(density) { baseHeight.toPx().toInt() } -// var sum = with(density) { 8.dp.toPx().toInt() } * 2 -// for ((_, itemSize) in itemHeights.toSortedMap()) { -// sum += itemSize -// if (sum >= baseHeightInt) { -// return@remember with(density) { (sum - itemSize / 2).toDp() } -// } -// } -// // all items fit into base height -// baseHeight -// } -// -// Box { -// TextField( -// modifier = Modifier -// .fillMaxWidth(0.9f) -// .padding(2.dp), -// value = state.value, -// onValueChange = { value -> -// state.value = value -// searchItems = brainzPlayerViewModel.searchSongs(value.text)!!.toMutableList() -// searchStarted = true -// -// }, -// textStyle = TextStyle(MaterialTheme.colorScheme.onSurface, fontSize = 15.sp), -// leadingIcon = { -// Icon( -// Icons.Default.Search, -// contentDescription = "", -// modifier = Modifier -// .padding(15.dp) -// .size(24.dp) -// -// ) -// }, -// trailingIcon = { -// if (state.value != TextFieldValue("")) { -// IconButton(onClick = { -// state.value = TextFieldValue("") -// } -// ) { -// Icon( -// Icons.Default.Close, -// contentDescription = "", -// modifier = Modifier -// .padding(15.dp) -// .size(24.dp) -// ) -// } -// } -// }, -// singleLine = true, -// shape = RoundedCornerShape(25.dp), -// colors = TextFieldDefaults.textFieldColors( -// -// textColor = Color.Black, -// disabledTextColor = Color.Transparent, -// backgroundColor = Color.Gray, -// focusedIndicatorColor = Color.Transparent, -// unfocusedIndicatorColor = Color.Transparent, -// disabledIndicatorColor = Color.Transparent -// ) -// ) -// } -// DropdownMenu( -// modifier = Modifier.requiredSizeIn(maxHeight = maxHeight), -// properties = PopupProperties(focusable = false ), -// expanded = searchStarted, -// onDismissRequest = { searchStarted = false }) { -// searchItems.forEachIndexed { index, song -> -// -// DropdownMenuItem( -// modifier = Modifier.onSizeChanged { -// itemHeights[index] = it.height -// -// }, -// onClick = { -// brainzPlayerViewModel.changePlayable(listOf(song), PlayableType.SONG, song.mediaID,0) -// brainzPlayerViewModel.playOrToggleSong(song, true) -// searchStarted = false -// state.value.text.removeRange(0, state.value.text.length-1.coerceAtLeast(0))}) { -// androidx.compose.material3.Text(text = song.title ) -// } -// } -// } -//} - @Composable fun BrainzPlayerActivityCards(icon: String, errorIcon : Int, title: String, modifier : Modifier = Modifier) { Box( From 05f87e49aec3dbd2088dab285d8e2883575ca527 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sun, 3 Mar 2024 11:40:23 +0530 Subject: [PATCH 04/97] Added overview screen --- .../ui/screens/brainzplayer/OverviewScreen.kt | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt new file mode 100644 index 00000000..fa6a8b1f --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt @@ -0,0 +1,134 @@ +package org.listenbrainz.android.ui.screens.brainzplayer + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.listenbrainz.android.R +import org.listenbrainz.android.model.Album +import org.listenbrainz.android.model.Artist +import org.listenbrainz.android.model.PlayableType +import org.listenbrainz.android.model.Playlist +import org.listenbrainz.android.ui.theme.ListenBrainzTheme +import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel + +@Composable +fun OverviewScreen ( + recentlyPlayedSongs: Playlist, + brainzPlayerViewModel : BrainzPlayerViewModel, + artists : List, + albums: List +) { + Column (modifier = Modifier.verticalScroll(rememberScrollState())) { + RecentlyPlayedOverview(recentlyPlayedSongs = recentlyPlayedSongs, brainzPlayerViewModel = brainzPlayerViewModel) + ArtistsOverview(artists = artists) + AlbumsOverview(albums = albums) + } + + +} + +@Composable +private fun RecentlyPlayedOverview( + recentlyPlayedSongs: Playlist, + brainzPlayerViewModel : BrainzPlayerViewModel +) { + Text("Recently Played" , style = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = ListenBrainzTheme.colorScheme.lbSignature + ) , modifier = Modifier.padding(start = 17.dp)) + LazyRow( + modifier = Modifier + .background( + brush = Brush.verticalGradient( + colors = listOf(Color(1010), Color(1010)) + ) + ) + .height(250.dp)){ + items(items = recentlyPlayedSongs.items) { + song -> + BrainzPlayerActivityCards(icon = song.albumArt, + errorIcon = R.drawable.ic_artist, + title = song.title, + artist = song.artist, + modifier = Modifier + .clickable { + brainzPlayerViewModel.changePlayable(recentlyPlayedSongs.items, PlayableType.ALL_SONGS, song.mediaID,recentlyPlayedSongs.items.sortedBy { it.discNumber }.indexOf(song),0L) + brainzPlayerViewModel.playOrToggleSong(song, true) + } + ) + } + } +} + +@Composable +private fun ArtistsOverview( + artists : List +) { + Text("Artists" , style = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = ListenBrainzTheme.colorScheme.lbSignature + ) , modifier = Modifier.padding(start = 17.dp)) + LazyRow( + modifier = Modifier + .background( + brush = Brush.verticalGradient( + colors = listOf(Color(1010), Color(1010)) + ) + ) + .height(250.dp)){ + items(items = artists) { + artist -> + BrainzPlayerActivityCards(icon = "", + errorIcon = R.drawable.ic_artist, + title = "", + artist = artist.name, + ) + } + } +} + +@Composable +private fun AlbumsOverview( + albums: List, +){ + Text("Albums" , style = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = ListenBrainzTheme.colorScheme.lbSignature + ) , modifier = Modifier.padding(start = 17.dp)) + LazyRow( + modifier = Modifier + .background( + brush = Brush.verticalGradient( + colors = listOf(Color(1010), Color(1010)) + ) + ) + .height(250.dp)){ + items(items = albums) { + album -> + BrainzPlayerActivityCards(icon = album.albumArt, + errorIcon = R.drawable.ic_artist, + title = album.title, + artist = album.artist, + ) + } + } +} \ No newline at end of file From d9142f822ab4a17662a1ef57d73cf0143e0364e1 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Wed, 6 Mar 2024 09:21:20 +0530 Subject: [PATCH 05/97] Added lastListenedTo field in SongDAO, recently played working as expected --- .../org/listenbrainz/android/model/Song.kt | 9 +- .../listenbrainz/android/model/SongEntity.kt | 3 +- .../listenbrainz/android/model/dao/SongDao.kt | 6 + .../repository/brainzplayer/SongRepository.kt | 3 + .../brainzplayer/SongRepositoryImpl.kt | 14 + .../brainzplayer/BrainzPlayerScreen.kt | 519 ++++++++++-------- .../ui/screens/brainzplayer/OverviewScreen.kt | 14 +- .../screens/brainzplayer/RecentPlaysScreen.kt | 9 + .../ui/screens/brainzplayer/SongScreen.kt | 8 +- .../navigation/BrainzPlayerNavigation.kt | 3 +- .../listenbrainz/android/util/Transformer.kt | 6 +- .../viewmodel/BrainzPlayerViewModel.kt | 18 +- 12 files changed, 381 insertions(+), 231 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysScreen.kt diff --git a/app/src/main/java/org/listenbrainz/android/model/Song.kt b/app/src/main/java/org/listenbrainz/android/model/Song.kt index 18fd431f..fba860d8 100644 --- a/app/src/main/java/org/listenbrainz/android/model/Song.kt +++ b/app/src/main/java/org/listenbrainz/android/model/Song.kt @@ -13,7 +13,8 @@ data class Song ( val albumID: Long=0L, val album: String="", val albumArt: String="", - val discNumber : Long = 0L + val discNumber : Long = 0L, + var lastListenedTo : Long = 0L ) { companion object { val emptySong = Song( @@ -29,7 +30,8 @@ data class Song ( albumID = 0L, album = "", albumArt = "", - discNumber = 0L + discNumber = 0L, + lastListenedTo = 0L ) fun preview(): Song = @@ -46,7 +48,8 @@ data class Song ( albumID = 0L, album = "Album", albumArt = "", - discNumber = 0L + discNumber = 0L, + lastListenedTo = 0L ) } } diff --git a/app/src/main/java/org/listenbrainz/android/model/SongEntity.kt b/app/src/main/java/org/listenbrainz/android/model/SongEntity.kt index 1508ea18..013ca89d 100644 --- a/app/src/main/java/org/listenbrainz/android/model/SongEntity.kt +++ b/app/src/main/java/org/listenbrainz/android/model/SongEntity.kt @@ -18,5 +18,6 @@ data class SongEntity( val duration : Long, val dateModified : Long, val artistId : Long, - val discNumber : Long + val discNumber : Long, + val lastListenedTo : Long ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/dao/SongDao.kt b/app/src/main/java/org/listenbrainz/android/model/dao/SongDao.kt index 0eb010c2..183e546e 100644 --- a/app/src/main/java/org/listenbrainz/android/model/dao/SongDao.kt +++ b/app/src/main/java/org/listenbrainz/android/model/dao/SongDao.kt @@ -5,6 +5,7 @@ import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Update import kotlinx.coroutines.flow.Flow import org.listenbrainz.android.model.SongEntity @@ -20,8 +21,13 @@ interface SongDao { @Query(value = "SELECT * FROM SONGS ORDER BY `title`") fun getSongEntitiesAsList() : List + @Query(value = "SELECT * FROM SONGS ORDER BY `lastListenedTo` DESC") + fun getRecentlyPlayedSongs() : Flow> + @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun addSongs(songEntities: List) + @Update(onConflict = OnConflictStrategy.REPLACE) + suspend fun updateSong(songEntity: SongEntity) @Delete suspend fun deleteSong(songEntity: SongEntity) diff --git a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepository.kt b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepository.kt index 6f5ecb08..edc94557 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepository.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepository.kt @@ -2,9 +2,12 @@ package org.listenbrainz.android.repository.brainzplayer import kotlinx.coroutines.flow.Flow import org.listenbrainz.android.model.Song +import org.listenbrainz.android.model.SongEntity interface SongRepository { fun getSongsStream() : Flow> + fun getRecentlyPlayedSongs() : Flow> suspend fun addSongs(userRequestedRefresh: Boolean = false): Boolean + suspend fun updateSong(song : Song) } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepositoryImpl.kt index b65813b0..2d0739aa 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepositoryImpl.kt @@ -3,6 +3,7 @@ package org.listenbrainz.android.repository.brainzplayer import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import org.listenbrainz.android.model.Song +import org.listenbrainz.android.model.SongEntity import org.listenbrainz.android.model.dao.SongDao import org.listenbrainz.android.util.SongsData import org.listenbrainz.android.util.Transformer.toSong @@ -38,4 +39,17 @@ class SongRepositoryImpl @Inject constructor( return songs.isNotEmpty() } + + override suspend fun updateSong(song : Song) { + songDao.updateSong(song.toSongEntity()) + } + + override fun getRecentlyPlayedSongs(): Flow> = + songDao.getRecentlyPlayedSongs() + .map { it -> + it.map{ + it.toSong() + } + } + } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index e922d4e0..61ae325c 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -13,12 +13,15 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.rounded.ArrowForwardIos import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.ElevatedSuggestionChip import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SuggestionChipDefaults import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.inset import androidx.compose.ui.layout.ContentScale @@ -27,6 +30,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign @@ -36,10 +40,12 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.PopupProperties import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.AsyncImage +import kotlinx.coroutines.launch import org.listenbrainz.android.R import org.listenbrainz.android.model.* import org.listenbrainz.android.ui.components.forwardingPainter import org.listenbrainz.android.ui.screens.brainzplayer.navigation.Navigation +import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.viewmodel.* @OptIn(ExperimentalMaterial3Api::class) @@ -50,13 +56,14 @@ fun BrainzPlayerScreen() { val songsViewModel = hiltViewModel() val artistViewModel = hiltViewModel() val playlistViewModel = hiltViewModel() + val brainzPlayerViewModel = hiltViewModel() // Data streams val albums = albumViewModel.albums.collectAsState(initial = listOf()).value // TODO: Introduce initial values to avoid flicker. val songs = songsViewModel.songs.collectAsState(initial = listOf()).value val artists = artistViewModel.artists.collectAsState(initial = listOf()).value val playlists by playlistViewModel.playlists.collectAsState(initial = listOf()) - val recentlyPlayed = Playlist.recentlyPlayed + val recentlyPlayed = brainzPlayerViewModel.recentlyPlayed.collectAsState(initial = listOf()).value Column( modifier = Modifier @@ -73,7 +80,7 @@ fun BrainzPlayerHomeScreen( albums: List, artists: List, playlists: List, - recentlyPlayedSongs: Playlist, + recentlyPlayedSongs: List, brainzPlayerViewModel: BrainzPlayerViewModel = hiltViewModel(), navigateToSongsScreen: () -> Unit, navigateToArtistsScreen: () -> Unit, @@ -83,221 +90,283 @@ fun BrainzPlayerHomeScreen( navigateToAlbum: (id: Long) -> Unit, navigateToPlaylist: (id: Long) -> Unit ) { - val searchTextState = remember { - mutableStateOf(TextFieldValue("")) - } - Column(modifier = Modifier - .padding(horizontal = 8.dp) - .verticalScroll(rememberScrollState()) - ) { - // Recently Played - Text( - text = "Recently Played", - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - fontWeight = FontWeight.Bold, - fontSize = 24.sp, - textAlign = TextAlign.Start, - color = MaterialTheme.colorScheme.onSurface - ) - LazyRow(modifier = Modifier.height(200.dp)) { - items(items = recentlyPlayedSongs.items) { - BrainzPlayerActivityCards(icon = it.albumArt, - errorIcon = R.drawable.ic_artist, - title = it.title, - modifier = Modifier - .clickable { - brainzPlayerViewModel.changePlayable(recentlyPlayedSongs.items, PlayableType.ALL_SONGS, it.mediaID,recentlyPlayedSongs.items.sortedBy { it.discNumber }.indexOf(it),0L) - brainzPlayerViewModel.playOrToggleSong(it, true) - } - ) - } - } - - // Songs button - Card( - modifier = Modifier.padding(16.dp).clickable { - navigateToSongsScreen() - }, - shape = RoundedCornerShape(16.dp), - backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, - elevation = 5.dp - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = "Songs", - modifier = Modifier - .padding(top = 16.dp, bottom = 16.dp, start = 16.dp), - fontWeight = FontWeight.Bold, - fontSize = 24.sp, - textAlign = TextAlign.Start, - color = MaterialTheme.colorScheme.onSurface - ) - Icon( - imageVector = Icons.Rounded.ArrowForwardIos, - modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp, start = 8.dp), - contentDescription = "Navigate to songs screen", - tint = MaterialTheme.colorScheme.onSurface - ) - } - } - LazyRow(modifier = Modifier.height(200.dp)) { - items(items = songs) { song -> - BrainzPlayerActivityCards(icon = song.albumArt, - errorIcon = R.drawable.ic_artist, - title = song.title, - modifier = Modifier - .clickable { - brainzPlayerViewModel.changePlayable( - songs.sortedBy { it.discNumber }, - PlayableType.ALL_SONGS, - song.mediaID, - songs - .sortedBy { it.discNumber } - .indexOf(song) - ) - brainzPlayerViewModel.playOrToggleSong(song, true) - } - ) - } - } - - // Artists - Card( - modifier = Modifier.padding(16.dp).clickable { - navigateToArtistsScreen() - }, - shape = RoundedCornerShape(16.dp), - backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, - elevation = 5.dp - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = "Artists", - modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, start = 16.dp), - fontWeight = FontWeight.Bold, - fontSize = 24.sp, - textAlign = TextAlign.Start, - color = MaterialTheme.colorScheme.onSurface - ) - Icon( - imageVector = Icons.Rounded.ArrowForwardIos, - modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp, start = 8.dp), - contentDescription = "Navigate to artists screen", - tint = MaterialTheme.colorScheme.onSurface + val currentTab : MutableState = remember {mutableStateOf(0)} + Column { + Row(modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .background( + Brush.verticalGradient( + listOf( + ListenBrainzTheme.colorScheme.background, + Color.Transparent + ) ) - } - - } - LazyRow(modifier = Modifier.height(200.dp)) { - items(items = artists) { - BrainzPlayerActivityCards(icon = "", - errorIcon = R.drawable.ic_artist, - title = it.name, - modifier = Modifier - .clickable { - navigateToArtist(it.id) + )) { + Spacer(modifier = Modifier.width(ListenBrainzTheme.paddings.chipsHorizontal / 2)) + repeat(5) { position -> + ElevatedSuggestionChip( + modifier = Modifier.padding(ListenBrainzTheme.paddings.chipsHorizontal), + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + if (position == currentTab.value) { + ListenBrainzTheme.colorScheme.chipSelected + } else { + ListenBrainzTheme.colorScheme.chipUnselected } + ), + shape = ListenBrainzTheme.shapes.chips, + elevation = SuggestionChipDefaults.elevatedSuggestionChipElevation(elevation = 4.dp), + label = { + androidx.compose.material3.Text( + text = when (position) { + 0 -> "Overview" + 1 -> "Recent" + 2 -> "Artists" + 3 -> "Albums" + 4 -> "Songs" + else -> "Overview" + }, + style = ListenBrainzTheme.textStyles.chips, + color = ListenBrainzTheme.colorScheme.text, + ) + }, + onClick = {currentTab.value = position} ) } } - - - // Albums - Card( - modifier = Modifier.padding(16.dp).clickable { - navigateToAlbumsScreen() - }, - shape = RoundedCornerShape(16.dp), - backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, - elevation = 5.dp - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = "Albums", - modifier = Modifier - .padding(top = 16.dp, bottom = 16.dp, start = 16.dp), - fontWeight = FontWeight.Bold, - fontSize = 24.sp, - textAlign = TextAlign.Start, - color = MaterialTheme.colorScheme.onSurface - ) - Icon( - imageVector = Icons.Rounded.ArrowForwardIos, - modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp, start = 8.dp), - contentDescription = "Navigate to albums screen", - tint = MaterialTheme.colorScheme.onSurface - ) - } - - } - LazyRow(modifier = Modifier.height(200.dp)) { - items(albums) { - BrainzPlayerActivityCards(it.albumArt, - R.drawable.ic_album, - title = it.title, - modifier = Modifier - .clickable { - navigateToAlbum(it.albumId) - } - ) - } - } - - - // Playlists - Card( - modifier = Modifier.padding(16.dp).clickable { - navigateToPlaylistsScreen() - }, - shape = RoundedCornerShape(16.dp), - backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, - elevation = 5.dp - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = "Playlists", - modifier = Modifier - .padding(top = 16.dp, bottom = 16.dp, start = 16.dp), - fontWeight = FontWeight.Bold, - fontSize = 24.sp, - textAlign = TextAlign.Start, - color = MaterialTheme.colorScheme.onSurface - ) - Icon( - imageVector = Icons.Rounded.ArrowForwardIos, - modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp, start = 8.dp), - contentDescription = "Navigate to playlists screen", - tint = MaterialTheme.colorScheme.onSurface - ) - } - - } - LazyRow(modifier = Modifier.height(200.dp)) { - items(playlists.filter { - it.id != (-1).toLong() - }) { - BrainzPlayerActivityCards( - icon = "", - errorIcon = it.art, - title = it.title, - modifier = Modifier.clickable { navigateToPlaylist(it.id) } - ) - } + when (currentTab.value) { + 0 -> OverviewScreen( + recentlyPlayedSongs = recentlyPlayedSongs, + brainzPlayerViewModel = brainzPlayerViewModel, + artists = artists, + albums = albums + ) + 1 -> RecentPlaysScreen() + 2 -> ArtistScreen(navigateToArtistScreen = {id -> navigateToArtistsScreen()}) } - - } + + +// Column(modifier = Modifier +// .padding(horizontal = 8.dp) +// .verticalScroll(rememberScrollState()) +// ) { +// // Recently Played +//// Text( +//// text = "Recently Played", +//// modifier = Modifier +//// .fillMaxWidth() +//// .padding(16.dp), +//// fontWeight = FontWeight.Bold, +//// fontSize = 24.sp, +//// textAlign = TextAlign.Start, +//// color = MaterialTheme.colorScheme.onSurface +//// ) +// LazyRow(modifier = Modifier.height(200.dp)) { +// items(items = recentlyPlayedSongs.items) { +// BrainzPlayerActivityCards(icon = it.albumArt, +// errorIcon = R.drawable.ic_artist, +// title = it.title, +// modifier = Modifier +// .clickable { +// brainzPlayerViewModel.changePlayable(recentlyPlayedSongs.items, PlayableType.ALL_SONGS, it.mediaID,recentlyPlayedSongs.items.sortedBy { it.discNumber }.indexOf(it),0L) +// brainzPlayerViewModel.playOrToggleSong(it, true) +// } +// ) +// } +// } +// +// // Songs button +// Card( +// modifier = Modifier +// .padding(16.dp) +// .clickable { +// navigateToSongsScreen() +// }, +// shape = RoundedCornerShape(16.dp), +// backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, +// elevation = 5.dp +// ) { +// Row(verticalAlignment = Alignment.CenterVertically) { +// Text( +// text = "Songs", +// modifier = Modifier +// .padding(top = 16.dp, bottom = 16.dp, start = 16.dp), +// fontWeight = FontWeight.Bold, +// fontSize = 24.sp, +// textAlign = TextAlign.Start, +// color = MaterialTheme.colorScheme.onSurface +// ) +// Icon( +// imageVector = Icons.Rounded.ArrowForwardIos, +// modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp, start = 8.dp), +// contentDescription = "Navigate to songs screen", +// tint = MaterialTheme.colorScheme.onSurface +// ) +// } +// } +// LazyRow(modifier = Modifier.height(200.dp)) { +// items(items = songs) { song -> +// BrainzPlayerActivityCards(icon = song.albumArt, +// errorIcon = R.drawable.ic_artist, +// title = song.title, +// modifier = Modifier +// .clickable { +// brainzPlayerViewModel.changePlayable( +// songs.sortedBy { it.discNumber }, +// PlayableType.ALL_SONGS, +// song.mediaID, +// songs +// .sortedBy { it.discNumber } +// .indexOf(song) +// ) +// brainzPlayerViewModel.playOrToggleSong(song, true) +// } +// ) +// } +// } +// +// // Artists +// Card( +// modifier = Modifier +// .padding(16.dp) +// .clickable { +// navigateToArtistsScreen() +// }, +// shape = RoundedCornerShape(16.dp), +// backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, +// elevation = 5.dp +// ) { +// Row(verticalAlignment = Alignment.CenterVertically) { +// Text( +// text = "Artists", +// modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, start = 16.dp), +// fontWeight = FontWeight.Bold, +// fontSize = 24.sp, +// textAlign = TextAlign.Start, +// color = MaterialTheme.colorScheme.onSurface +// ) +// Icon( +// imageVector = Icons.Rounded.ArrowForwardIos, +// modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp, start = 8.dp), +// contentDescription = "Navigate to artists screen", +// tint = MaterialTheme.colorScheme.onSurface +// ) +// } +// +// } +// LazyRow(modifier = Modifier.height(200.dp)) { +// items(items = artists) { +// BrainzPlayerActivityCards(icon = "", +// errorIcon = R.drawable.ic_artist, +// title = it.name, +// modifier = Modifier +// .clickable { +// navigateToArtist(it.id) +// } +// ) +// } +// } +// +// +// // Albums +// Card( +// modifier = Modifier +// .padding(16.dp) +// .clickable { +// navigateToAlbumsScreen() +// }, +// shape = RoundedCornerShape(16.dp), +// backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, +// elevation = 5.dp +// ) { +// Row(verticalAlignment = Alignment.CenterVertically) { +// Text( +// text = "Albums", +// modifier = Modifier +// .padding(top = 16.dp, bottom = 16.dp, start = 16.dp), +// fontWeight = FontWeight.Bold, +// fontSize = 24.sp, +// textAlign = TextAlign.Start, +// color = MaterialTheme.colorScheme.onSurface +// ) +// Icon( +// imageVector = Icons.Rounded.ArrowForwardIos, +// modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp, start = 8.dp), +// contentDescription = "Navigate to albums screen", +// tint = MaterialTheme.colorScheme.onSurface +// ) +// } +// +// } +// LazyRow(modifier = Modifier.height(200.dp)) { +// items(albums) { +// BrainzPlayerActivityCards(it.albumArt, +// R.drawable.ic_album, +// title = it.title, +// modifier = Modifier +// .clickable { +// navigateToAlbum(it.albumId) +// } +// ) +// } +// } +// +// +// // Playlists +// Card( +// modifier = Modifier +// .padding(16.dp) +// .clickable { +// navigateToPlaylistsScreen() +// }, +// shape = RoundedCornerShape(16.dp), +// backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, +// elevation = 5.dp +// ) { +// Row(verticalAlignment = Alignment.CenterVertically) { +// Text( +// text = "Playlists", +// modifier = Modifier +// .padding(top = 16.dp, bottom = 16.dp, start = 16.dp), +// fontWeight = FontWeight.Bold, +// fontSize = 24.sp, +// textAlign = TextAlign.Start, +// color = MaterialTheme.colorScheme.onSurface +// ) +// Icon( +// imageVector = Icons.Rounded.ArrowForwardIos, +// modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp, start = 8.dp), +// contentDescription = "Navigate to playlists screen", +// tint = MaterialTheme.colorScheme.onSurface +// ) +// } +// +// } +// LazyRow(modifier = Modifier.height(200.dp)) { +// items(playlists.filter { +// it.id != (-1).toLong() +// }) { +// BrainzPlayerActivityCards( +// icon = "", +// errorIcon = it.art, +// title = it.title, +// modifier = Modifier.clickable { navigateToPlaylist(it.id) } +// ) +// } +// } +// +// +// } } @Composable -fun BrainzPlayerActivityCards(icon: String, errorIcon : Int, title: String, modifier : Modifier = Modifier) { +fun BrainzPlayerActivityCards(icon: String, errorIcon : Int, title: String, artist : String,modifier : Modifier = Modifier) { Box( modifier = modifier .padding(4.dp) - .height(200.dp) + .height(250.dp) .width(180.dp) .clip(RoundedCornerShape(10.dp)) .clickable { }, @@ -307,15 +376,14 @@ fun BrainzPlayerActivityCards(icon: String, errorIcon : Int, title: String, modi Box( modifier = modifier .padding(10.dp) - .clip(CircleShape) + .clip(RoundedCornerShape(15.dp)) .background(color = colorResource(id = R.color.bp_bottom_song_viewpager)) .size(150.dp) ) { AsyncImage( modifier = modifier .fillMaxSize() - .align(Alignment.TopCenter) - .clip(CircleShape), + .align(Alignment.TopCenter), model = icon, contentDescription = "", error = forwardingPainter( @@ -330,15 +398,36 @@ fun BrainzPlayerActivityCards(icon: String, errorIcon : Int, title: String, modi contentScale = ContentScale.Crop ) } - Text( - text = title, - fontSize = 17.sp, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurface - ) + Row(modifier = Modifier + .fillMaxWidth() + .padding(start = 15.dp)){ + Text( + text = artist, + fontSize = 17.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Left, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.onSurface + ) + } + + Spacer(modifier = Modifier.height(6.dp)) + Row(modifier = Modifier + .fillMaxWidth() + .padding(start = 15.dp)){ + Text( + text = title, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + textAlign = TextAlign.Left, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.onSurface + ) + } + + } } } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt index fa6a8b1f..120617c3 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt @@ -10,7 +10,7 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -19,18 +19,20 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel import org.listenbrainz.android.R import org.listenbrainz.android.model.Album import org.listenbrainz.android.model.Artist import org.listenbrainz.android.model.PlayableType import org.listenbrainz.android.model.Playlist +import org.listenbrainz.android.model.Song import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel @Composable fun OverviewScreen ( - recentlyPlayedSongs: Playlist, - brainzPlayerViewModel : BrainzPlayerViewModel, + recentlyPlayedSongs: List, + brainzPlayerViewModel : BrainzPlayerViewModel = hiltViewModel(), artists : List, albums: List ) { @@ -45,7 +47,7 @@ fun OverviewScreen ( @Composable private fun RecentlyPlayedOverview( - recentlyPlayedSongs: Playlist, + recentlyPlayedSongs: List, brainzPlayerViewModel : BrainzPlayerViewModel ) { Text("Recently Played" , style = TextStyle( @@ -61,7 +63,7 @@ private fun RecentlyPlayedOverview( ) ) .height(250.dp)){ - items(items = recentlyPlayedSongs.items) { + items(items = recentlyPlayedSongs) { song -> BrainzPlayerActivityCards(icon = song.albumArt, errorIcon = R.drawable.ic_artist, @@ -69,7 +71,7 @@ private fun RecentlyPlayedOverview( artist = song.artist, modifier = Modifier .clickable { - brainzPlayerViewModel.changePlayable(recentlyPlayedSongs.items, PlayableType.ALL_SONGS, song.mediaID,recentlyPlayedSongs.items.sortedBy { it.discNumber }.indexOf(song),0L) + brainzPlayerViewModel.changePlayable(recentlyPlayedSongs, PlayableType.ALL_SONGS, song.mediaID,recentlyPlayedSongs.sortedBy { it.discNumber }.indexOf(song),0L) brainzPlayerViewModel.playOrToggleSong(song, true) } ) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysScreen.kt new file mode 100644 index 00000000..4fb9cfa5 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysScreen.kt @@ -0,0 +1,9 @@ +package org.listenbrainz.android.ui.screens.brainzplayer + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable + +@Composable +fun RecentPlaysScreen() { + Text(text = "Recent players screen!") +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongScreen.kt index 1251750e..5fd8896a 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongScreen.kt @@ -218,7 +218,10 @@ fun SongScreen() { .indexOf(it), 0L ) - brainzPlayerViewModel.playOrToggleSong(it, true) + brainzPlayerViewModel.playOrToggleSong( + it, + true, + ) } ) { DropdownMenu( @@ -308,7 +311,8 @@ fun SongScreen() { .clip(CircleShape) .background(Color.LightGray) .clickable { - songCardMoreOptionsDropMenuExpanded = songs.value.indexOf(it) + songCardMoreOptionsDropMenuExpanded = + songs.value.indexOf(it) } .align(Alignment.BottomEnd), contentAlignment = Alignment.Center diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt index c7abbee6..50a0e6c2 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt @@ -18,6 +18,7 @@ import org.listenbrainz.android.ui.screens.brainzplayer.BrainzPlayerHomeScreen import org.listenbrainz.android.ui.screens.brainzplayer.OnAlbumClickScreen import org.listenbrainz.android.ui.screens.brainzplayer.OnArtistClickScreen import org.listenbrainz.android.ui.screens.brainzplayer.OnPlaylistClickScreen +import org.listenbrainz.android.ui.screens.brainzplayer.OverviewScreen import org.listenbrainz.android.ui.screens.brainzplayer.PlaylistScreen import org.listenbrainz.android.ui.screens.brainzplayer.SongScreen @@ -28,7 +29,7 @@ fun Navigation( albums: List, artists: List, playlists: List, - recentlyPlayedSongs: Playlist, + recentlyPlayedSongs: List, songs: List, navHostController: NavHostController = rememberNavController() ) { diff --git a/app/src/main/java/org/listenbrainz/android/util/Transformer.kt b/app/src/main/java/org/listenbrainz/android/util/Transformer.kt index 9b1da402..f6803b5c 100644 --- a/app/src/main/java/org/listenbrainz/android/util/Transformer.kt +++ b/app/src/main/java/org/listenbrainz/android/util/Transformer.kt @@ -23,7 +23,8 @@ object Transformer { trackNumber = trackNumber, duration = duration, discNumber = discNumber, - year = year + year = year, + lastListenedTo = lastListenedTo ) fun Song.toSongEntity() = SongEntity( @@ -39,7 +40,8 @@ object Transformer { trackNumber = trackNumber, duration = duration, discNumber = discNumber, - year = year + year = year, + lastListenedTo = lastListenedTo ) fun AlbumEntity.toAlbum() = Album( diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt index 5f6208e3..b9bcf828 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt @@ -1,17 +1,22 @@ package org.listenbrainz.android.viewmodel +import android.os.Build import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.session.PlaybackStateCompat.REPEAT_MODE_ALL import android.support.v4.media.session.PlaybackStateCompat.REPEAT_MODE_NONE import android.support.v4.media.session.PlaybackStateCompat.REPEAT_MODE_ONE import android.support.v4.media.session.PlaybackStateCompat.SHUFFLE_MODE_ALL import android.support.v4.media.session.PlaybackStateCompat.SHUFFLE_MODE_NONE +import android.util.Log +import androidx.annotation.RequiresApi import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -34,6 +39,8 @@ import org.listenbrainz.android.util.BrainzPlayerExtensions.isPrepared import org.listenbrainz.android.util.BrainzPlayerExtensions.toSong import org.listenbrainz.android.util.BrainzPlayerUtils.MEDIA_ROOT_ID import org.listenbrainz.android.util.Resource +import org.listenbrainz.android.util.Transformer.toSongEntity +import java.time.Instant import javax.inject.Inject @HiltViewModel @@ -50,6 +57,7 @@ class BrainzPlayerViewModel @Inject constructor( val progress = _progress.asStateFlow() val songCurrentPosition = _songCurrentPosition.asStateFlow() val songs = songRepository.getSongsStream() + val recentlyPlayed = songRepository.getRecentlyPlayedSongs() private val playbackState = brainzPlayerServiceConnection.playbackState val isShuffled = brainzPlayerServiceConnection.shuffleState val currentlyPlayingSong = brainzPlayerServiceConnection.currentPlayingSong @@ -103,6 +111,7 @@ class BrainzPlayerViewModel @Inject constructor( ) } + fun onSeek(seekTo: Float) { viewModelScope.launch { _progress.emit(seekTo) } } @@ -143,17 +152,24 @@ class BrainzPlayerViewModel @Inject constructor( return result } + @RequiresApi(Build.VERSION_CODES.O) fun playOrToggleSong(mediaItem: Song, toggle: Boolean = false) { val isPrepared = playbackState.value.isPrepared if (isPrepared && mediaItem.mediaID == currentlyPlayingSong.value.toSong.mediaID) { playbackState.value.let { playbackState -> when { playbackState.isPlaying -> if (toggle) brainzPlayerServiceConnection.transportControls.pause() - playbackState.isPlayEnabled -> brainzPlayerServiceConnection.transportControls.play() + playbackState.isPlayEnabled -> { + mediaItem.lastListenedTo = Instant.now().epochSecond + viewModelScope.launch { songRepository.updateSong(mediaItem) } + brainzPlayerServiceConnection.transportControls.play() + } else -> Unit } } } else { + mediaItem.lastListenedTo = Instant.now().epochSecond + viewModelScope.launch { songRepository.updateSong(mediaItem) } brainzPlayerServiceConnection.transportControls.playFromMediaId(mediaItem.mediaID.toString(), null) } } From 4eca379de5258eeb7a7037a968d348e10ccdbb27 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Fri, 8 Mar 2024 15:00:46 +0530 Subject: [PATCH 06/97] Add Playedtoday, PlayedThisWeek in Dao and recent_overview screen --- .../listenbrainz/android/model/dao/SongDao.kt | 8 ++ .../repository/brainzplayer/SongRepository.kt | 2 + .../brainzplayer/SongRepositoryImpl.kt | 18 +++ .../ui/components/BrainzPlayerActivityCard.kt | 107 ++++++++++++++++++ .../brainzplayer/BrainzPlayerScreen.kt | 89 +++------------ .../ui/screens/brainzplayer/OverviewScreen.kt | 54 +++++++-- .../brainzplayer/RecentPlaysOverviewScreen.kt | 64 +++++++++++ .../screens/brainzplayer/RecentPlaysScreen.kt | 9 -- .../navigation/BrainzPlayerNavigation.kt | 6 +- .../viewmodel/BrainzPlayerViewModel.kt | 6 +- 10 files changed, 265 insertions(+), 98 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerActivityCard.kt create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt delete mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysScreen.kt diff --git a/app/src/main/java/org/listenbrainz/android/model/dao/SongDao.kt b/app/src/main/java/org/listenbrainz/android/model/dao/SongDao.kt index 183e546e..368939ad 100644 --- a/app/src/main/java/org/listenbrainz/android/model/dao/SongDao.kt +++ b/app/src/main/java/org/listenbrainz/android/model/dao/SongDao.kt @@ -23,6 +23,14 @@ interface SongDao { @Query(value = "SELECT * FROM SONGS ORDER BY `lastListenedTo` DESC") fun getRecentlyPlayedSongs() : Flow> + @Query(value = "SELECT * FROM SONGS WHERE (:currentTime - lastListenedTo*1000) < 86400000 ORDER BY (:currentTime - lastListenedTo*1000) ASC") + fun getSongsPlayedToday( + currentTime : Long = System.currentTimeMillis() + ) : Flow> + @Query(value = "SELECT * FROM SONGS WHERE :currentTime - lastListenedTo > 86400000 AND :currentTime - lastListenedTo < 604800000 ORDER BY :currentTime - lastListenedTo ASC") + fun getSongsPlayedThisWeek( + currentTime : Long = System.currentTimeMillis() + ) : Flow> @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun addSongs(songEntities: List) diff --git a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepository.kt b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepository.kt index edc94557..6c9cbaaf 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepository.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepository.kt @@ -7,6 +7,8 @@ import org.listenbrainz.android.model.SongEntity interface SongRepository { fun getSongsStream() : Flow> fun getRecentlyPlayedSongs() : Flow> + fun getSongsPlayedToday() : Flow> + fun getSongsPlayedThisWeek() : Flow> suspend fun addSongs(userRequestedRefresh: Boolean = false): Boolean suspend fun updateSong(song : Song) diff --git a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepositoryImpl.kt index 2d0739aa..cc03c623 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepositoryImpl.kt @@ -1,5 +1,6 @@ package org.listenbrainz.android.repository.brainzplayer +import android.util.Log import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import org.listenbrainz.android.model.Song @@ -42,6 +43,7 @@ class SongRepositoryImpl @Inject constructor( override suspend fun updateSong(song : Song) { songDao.updateSong(song.toSongEntity()) + Log.v("pranav" , "Song updated!") } override fun getRecentlyPlayedSongs(): Flow> = @@ -52,4 +54,20 @@ class SongRepositoryImpl @Inject constructor( } } + override fun getSongsPlayedToday(): Flow> = + songDao.getSongsPlayedToday() + .map { it -> + it.map { + it.toSong() + } + } + + override fun getSongsPlayedThisWeek(): Flow> = + songDao.getSongsPlayedThisWeek() + .map { + it -> + it.map { + it.toSong() + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerActivityCard.kt b/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerActivityCard.kt new file mode 100644 index 00000000..bb163856 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerActivityCard.kt @@ -0,0 +1,107 @@ +package org.listenbrainz.android.ui.components + +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.fillMaxSize +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.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.drawscope.inset +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import org.listenbrainz.android.R + +@Composable +fun BrainzPlayerActivityCards(icon: String, errorIcon : Int, title: String, artist : String,modifier : Modifier = Modifier) { + Box( + modifier = modifier + .padding(4.dp) + .height(250.dp) + .width(180.dp) + .clip(RoundedCornerShape(10.dp)) + .clickable { }, + contentAlignment = Alignment.TopCenter + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box( + modifier = modifier + .padding(10.dp) + .clip(RoundedCornerShape(15.dp)) + .background(color = colorResource(id = R.color.bp_bottom_song_viewpager)) + .size(150.dp) + ) { + AsyncImage( + modifier = modifier + .fillMaxSize() + .align(Alignment.TopCenter), + model = icon, + contentDescription = "", + error = forwardingPainter( + painter = painterResource(id = errorIcon) + ) { info -> + inset(25f, 25f) { + with(info.painter) { + draw(size, info.alpha, info.colorFilter) + } + } + }, + contentScale = ContentScale.Crop + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 15.dp) + ) { + Text( + text = artist, + fontSize = 17.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Left, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.onSurface + ) + } + + Spacer(modifier = Modifier.height(6.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 15.dp) + ) { + Text( + text = title, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + textAlign = TextAlign.Left, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.onSurface + ) + } + + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index 61ae325c..3a17ce18 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -7,7 +7,7 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* +import androidx.compose.material3.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search @@ -63,13 +63,17 @@ fun BrainzPlayerScreen() { val songs = songsViewModel.songs.collectAsState(initial = listOf()).value val artists = artistViewModel.artists.collectAsState(initial = listOf()).value val playlists by playlistViewModel.playlists.collectAsState(initial = listOf()) - val recentlyPlayed = brainzPlayerViewModel.recentlyPlayed.collectAsState(initial = listOf()).value + val songsPlayedToday = brainzPlayerViewModel.songsPlayedToday.collectAsState(initial = listOf()).value + val recentlyPlayed = brainzPlayerViewModel.recentlyPlayed.collectAsState(initial = mutableListOf()).value + val topRecents = recentlyPlayed.subList(0, minOf(recentlyPlayed.size , 5)).toMutableList() + val songsPlayedThisWeek = brainzPlayerViewModel.songsPlayedThisWeek.collectAsState(initial = listOf()).value + topRecents.add(Song()) Column( modifier = Modifier .fillMaxSize() ) { - Navigation(albums, artists, playlists, recentlyPlayed, songs) + Navigation(albums, artists, playlists, songsPlayedToday, songsPlayedThisWeek ,topRecents ,songs) } } @@ -80,6 +84,8 @@ fun BrainzPlayerHomeScreen( albums: List, artists: List, playlists: List, + songsPlayedToday: List, + songsPlayedThisWeek: List, recentlyPlayedSongs: List, brainzPlayerViewModel: BrainzPlayerViewModel = hiltViewModel(), navigateToSongsScreen: () -> Unit, @@ -137,12 +143,17 @@ fun BrainzPlayerHomeScreen( } when (currentTab.value) { 0 -> OverviewScreen( + songsPlayedToday = songsPlayedToday, recentlyPlayedSongs = recentlyPlayedSongs, + goToRecentScreen = {currentTab.value = 1}, brainzPlayerViewModel = brainzPlayerViewModel, artists = artists, albums = albums ) - 1 -> RecentPlaysScreen() + 1 -> RecentPlaysScreen( + songsPlayedToday = songsPlayedToday, + songsPlayedThisWeek = songsPlayedThisWeek + ) 2 -> ArtistScreen(navigateToArtistScreen = {id -> navigateToArtistsScreen()}) } } @@ -361,73 +372,3 @@ fun BrainzPlayerHomeScreen( // } } -@Composable -fun BrainzPlayerActivityCards(icon: String, errorIcon : Int, title: String, artist : String,modifier : Modifier = Modifier) { - Box( - modifier = modifier - .padding(4.dp) - .height(250.dp) - .width(180.dp) - .clip(RoundedCornerShape(10.dp)) - .clickable { }, - contentAlignment = Alignment.TopCenter - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Box( - modifier = modifier - .padding(10.dp) - .clip(RoundedCornerShape(15.dp)) - .background(color = colorResource(id = R.color.bp_bottom_song_viewpager)) - .size(150.dp) - ) { - AsyncImage( - modifier = modifier - .fillMaxSize() - .align(Alignment.TopCenter), - model = icon, - contentDescription = "", - error = forwardingPainter( - painter = painterResource(id = errorIcon) - ) { info -> - inset(25f, 25f) { - with(info.painter) { - draw(size, info.alpha, info.colorFilter) - } - } - }, - contentScale = ContentScale.Crop - ) - } - Row(modifier = Modifier - .fillMaxWidth() - .padding(start = 15.dp)){ - Text( - text = artist, - fontSize = 17.sp, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Left, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurface - ) - } - - Spacer(modifier = Modifier.height(6.dp)) - Row(modifier = Modifier - .fillMaxWidth() - .padding(start = 15.dp)){ - Text( - text = title, - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - textAlign = TextAlign.Left, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurface - ) - } - - - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt index 120617c3..7eda9f65 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt @@ -1,20 +1,27 @@ package org.listenbrainz.android.ui.screens.brainzplayer +import android.util.Log 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.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -26,18 +33,21 @@ import org.listenbrainz.android.model.Artist import org.listenbrainz.android.model.PlayableType import org.listenbrainz.android.model.Playlist import org.listenbrainz.android.model.Song +import org.listenbrainz.android.ui.components.BrainzPlayerActivityCards import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel @Composable fun OverviewScreen ( + songsPlayedToday: List, + goToRecentScreen: () -> Unit, recentlyPlayedSongs: List, - brainzPlayerViewModel : BrainzPlayerViewModel = hiltViewModel(), + brainzPlayerViewModel: BrainzPlayerViewModel = hiltViewModel(), artists : List, albums: List ) { Column (modifier = Modifier.verticalScroll(rememberScrollState())) { - RecentlyPlayedOverview(recentlyPlayedSongs = recentlyPlayedSongs, brainzPlayerViewModel = brainzPlayerViewModel) + RecentlyPlayedOverview(recentlyPlayedSongs = recentlyPlayedSongs, goToRecentScreen = goToRecentScreen ,brainzPlayerViewModel = brainzPlayerViewModel) ArtistsOverview(artists = artists) AlbumsOverview(albums = albums) } @@ -48,7 +58,8 @@ fun OverviewScreen ( @Composable private fun RecentlyPlayedOverview( recentlyPlayedSongs: List, - brainzPlayerViewModel : BrainzPlayerViewModel + brainzPlayerViewModel : BrainzPlayerViewModel, + goToRecentScreen : () -> Unit ) { Text("Recently Played" , style = TextStyle( fontSize = 24.sp, @@ -65,16 +76,35 @@ private fun RecentlyPlayedOverview( .height(250.dp)){ items(items = recentlyPlayedSongs) { song -> - BrainzPlayerActivityCards(icon = song.albumArt, - errorIcon = R.drawable.ic_artist, - title = song.title, - artist = song.artist, - modifier = Modifier - .clickable { - brainzPlayerViewModel.changePlayable(recentlyPlayedSongs, PlayableType.ALL_SONGS, song.mediaID,recentlyPlayedSongs.sortedBy { it.discNumber }.indexOf(song),0L) - brainzPlayerViewModel.playOrToggleSong(song, true) + if(song.title == ""){ + Box( + modifier = Modifier + .padding(10.dp) + .clip(RoundedCornerShape(15.dp)) + .background(color = colorResource(id = R.color.bp_bottom_song_viewpager)) + .size(150.dp) + .clickable { + goToRecentScreen() + } + ){ + Column (modifier = Modifier.fillMaxSize().background(Color(0xFF1E1E1E)).padding(start = 5.dp , bottom = 20.dp) , verticalArrangement = Arrangement.Bottom) { + Text(" All \n Recently\n Played" , style = TextStyle(fontSize = 20.sp) , color = ListenBrainzTheme.colorScheme.lbSignature) } - ) + } + } + else{ + Log.v("pranav" , (song.lastListenedTo).toString()) + BrainzPlayerActivityCards(icon = song.albumArt, + errorIcon = R.drawable.ic_artist, + title = song.title, + artist = song.artist, + modifier = Modifier + .clickable { + brainzPlayerViewModel.changePlayable(recentlyPlayedSongs, PlayableType.ALL_SONGS, song.mediaID,recentlyPlayedSongs.sortedBy { it.discNumber }.indexOf(song),0L) + brainzPlayerViewModel.playOrToggleSong(song, true) + } + ) + } } } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt new file mode 100644 index 00000000..9c45b416 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt @@ -0,0 +1,64 @@ +package org.listenbrainz.android.ui.screens.brainzplayer + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.listenbrainz.android.model.Song +import org.listenbrainz.android.ui.components.ListenCardSmall +import org.listenbrainz.android.ui.theme.ListenBrainzTheme + +@Composable +fun RecentPlaysScreen( + songsPlayedToday: List, + songsPlayedThisWeek: List, +) { + Column (modifier = Modifier + .fillMaxSize() + .padding(start = 17.dp, end = 17.dp)) { + if(songsPlayedToday.isNotEmpty()){ + Text("Played Today" , color = ListenBrainzTheme.colorScheme.lbSignature , fontSize = 25.sp) + Spacer(modifier = Modifier.height(10.dp)) + PlayedToday(songsPlayedToday = songsPlayedToday) + } + if(songsPlayedThisWeek.isNotEmpty()) { + Text("Played This Week") + PlayedThisWeek(songsPlayedThisWeek = songsPlayedThisWeek) + } + } +} +@Composable +private fun PlayedToday( + songsPlayedToday: List +){ + LazyColumn (modifier = Modifier.height(250.dp)) { + items(songsPlayedToday){ + ListenCardSmall(trackName = it.title, artistName = it.artist, coverArtUrl = it.albumArt, enableDropdownIcon = true) { + Unit + } + Spacer(modifier = Modifier.height(5.dp)) + } + } +} + +@Composable +private fun PlayedThisWeek( + songsPlayedThisWeek: List +){ + LazyColumn (modifier = Modifier.height(250.dp)) { + items(songsPlayedThisWeek){ + ListenCardSmall(trackName = it.title, artistName = it.artist, coverArtUrl = it.albumArt, enableDropdownIcon = true) { + + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysScreen.kt deleted file mode 100644 index 4fb9cfa5..00000000 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysScreen.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.listenbrainz.android.ui.screens.brainzplayer - -import androidx.compose.material.Text -import androidx.compose.runtime.Composable - -@Composable -fun RecentPlaysScreen() { - Text(text = "Recent players screen!") -} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt index 50a0e6c2..8eb0c836 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt @@ -29,7 +29,9 @@ fun Navigation( albums: List, artists: List, playlists: List, - recentlyPlayedSongs: List, + songsPlayedToday: List, + songsPlayedThisWeek: List, + recentlyPlayedSongs : List, songs: List, navHostController: NavHostController = rememberNavController() ) { @@ -44,6 +46,8 @@ fun Navigation( albums = albums, artists = artists, playlists = playlists, + songsPlayedToday = songsPlayedToday, + songsPlayedThisWeek = songsPlayedThisWeek, recentlyPlayedSongs = recentlyPlayedSongs, navigateToSongsScreen = { goTo(BrainzPlayerNavigationItem.Songs) }, navigateToArtist = { id -> navHostController.navigate("onArtistClick/$id")}, diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt index b9bcf828..39d3f70c 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt @@ -58,6 +58,8 @@ class BrainzPlayerViewModel @Inject constructor( val songCurrentPosition = _songCurrentPosition.asStateFlow() val songs = songRepository.getSongsStream() val recentlyPlayed = songRepository.getRecentlyPlayedSongs() + val songsPlayedToday = songRepository.getSongsPlayedToday() + val songsPlayedThisWeek = songRepository.getSongsPlayedThisWeek() private val playbackState = brainzPlayerServiceConnection.playbackState val isShuffled = brainzPlayerServiceConnection.shuffleState val currentlyPlayingSong = brainzPlayerServiceConnection.currentPlayingSong @@ -160,7 +162,7 @@ class BrainzPlayerViewModel @Inject constructor( when { playbackState.isPlaying -> if (toggle) brainzPlayerServiceConnection.transportControls.pause() playbackState.isPlayEnabled -> { - mediaItem.lastListenedTo = Instant.now().epochSecond + mediaItem.lastListenedTo = Instant.now().epochSecond * 1000 viewModelScope.launch { songRepository.updateSong(mediaItem) } brainzPlayerServiceConnection.transportControls.play() } @@ -168,7 +170,7 @@ class BrainzPlayerViewModel @Inject constructor( } } } else { - mediaItem.lastListenedTo = Instant.now().epochSecond + mediaItem.lastListenedTo = Instant.now().epochSecond * 1000 viewModelScope.launch { songRepository.updateSong(mediaItem) } brainzPlayerServiceConnection.transportControls.playFromMediaId(mediaItem.mediaID.toString(), null) } From 2dec4c79d87543d54d1fbe0c721d9bfaeedffebd Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Thu, 21 Mar 2024 19:30:43 +0530 Subject: [PATCH 07/97] Fix display of artist's tracks --- .../android/model/ArtistEntity.kt | 4 +- .../android/model/dao/ArtistDao.kt | 2 +- .../listenbrainz/android/model/dao/SongDao.kt | 4 +- .../brainzplayer/ArtistRepositoryImpl.kt | 9 ++- .../brainzplayer/ArtistsOverviewScreen.kt | 69 +++++++++++++++++++ .../brainzplayer/BrainzPlayerScreen.kt | 5 +- .../brainzplayer/RecentPlaysOverviewScreen.kt | 18 +++-- .../viewmodel/BrainzPlayerViewModel.kt | 5 +- 8 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt diff --git a/app/src/main/java/org/listenbrainz/android/model/ArtistEntity.kt b/app/src/main/java/org/listenbrainz/android/model/ArtistEntity.kt index d9e0d213..1ff7ac50 100644 --- a/app/src/main/java/org/listenbrainz/android/model/ArtistEntity.kt +++ b/app/src/main/java/org/listenbrainz/android/model/ArtistEntity.kt @@ -8,6 +8,6 @@ data class ArtistEntity( @PrimaryKey(autoGenerate = true) val artistID: Long = 0, val name: String, - val songs: List, - val albums: List + var songs: List, + var albums: List ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/dao/ArtistDao.kt b/app/src/main/java/org/listenbrainz/android/model/dao/ArtistDao.kt index 1a3674a2..be3b274d 100644 --- a/app/src/main/java/org/listenbrainz/android/model/dao/ArtistDao.kt +++ b/app/src/main/java/org/listenbrainz/android/model/dao/ArtistDao.kt @@ -18,7 +18,7 @@ interface ArtistDao { @Query(value = "SELECT * FROM ARTISTS ORDER BY `name`") fun getArtistEntitiesAsList() : List - @Insert(onConflict = OnConflictStrategy.NONE) + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun addArtists(artistEntities: List)//: List /*@Query(value = "DELETE FROM ARTISTS WHERE name = :artistName") diff --git a/app/src/main/java/org/listenbrainz/android/model/dao/SongDao.kt b/app/src/main/java/org/listenbrainz/android/model/dao/SongDao.kt index 368939ad..5dbed754 100644 --- a/app/src/main/java/org/listenbrainz/android/model/dao/SongDao.kt +++ b/app/src/main/java/org/listenbrainz/android/model/dao/SongDao.kt @@ -23,11 +23,11 @@ interface SongDao { @Query(value = "SELECT * FROM SONGS ORDER BY `lastListenedTo` DESC") fun getRecentlyPlayedSongs() : Flow> - @Query(value = "SELECT * FROM SONGS WHERE (:currentTime - lastListenedTo*1000) < 86400000 ORDER BY (:currentTime - lastListenedTo*1000) ASC") + @Query(value = "SELECT * FROM SONGS WHERE (:currentTime - lastListenedTo) < 86400000 ORDER BY (:currentTime - lastListenedTo) ASC") fun getSongsPlayedToday( currentTime : Long = System.currentTimeMillis() ) : Flow> - @Query(value = "SELECT * FROM SONGS WHERE :currentTime - lastListenedTo > 86400000 AND :currentTime - lastListenedTo < 604800000 ORDER BY :currentTime - lastListenedTo ASC") + @Query(value = "SELECT * FROM SONGS WHERE (:currentTime - lastListenedTo) > 86400000 AND (:currentTime - lastListenedTo) < 604800000 ORDER BY (:currentTime - lastListenedTo) ASC") fun getSongsPlayedThisWeek( currentTime : Long = System.currentTimeMillis() ) : Flow> diff --git a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/ArtistRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/ArtistRepositoryImpl.kt index 98cb14a6..3941e8be 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/ArtistRepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/ArtistRepositoryImpl.kt @@ -55,18 +55,23 @@ class ArtistRepositoryImpl @Inject constructor( // Both jobs are being executed simultaneously. songsJob = async { for (artist in artists) { + val mutableSongs = artist.songs.toMutableList() // Here, if userRequestedRefresh is true, it will refresh songs cache which is what we expect from refreshing - artist.songs.toMutableList().addAll(addAllSongsOfArtist(artist.toArtist(), userRequestedRefresh).map { + mutableSongs.addAll(addAllSongsOfArtist(artist.toArtist(), userRequestedRefresh).map { it.toSongEntity() }) + + artist.songs = mutableSongs.toList() } } albumsJob = async { for (artist in artists) { + val mutableAlbums = artist.albums.toMutableList() // We do not need to refresh cache (songsListCache) here as it already got refreshed above when we created list of albums. - artist.albums.toMutableList().addAll(addAllAlbumsOfArtist(artist.toArtist()).map { + mutableAlbums.addAll(addAllAlbumsOfArtist(artist.toArtist()).map { it.toAlbumEntity() }) + artist.albums = mutableAlbums.toList() } } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt new file mode 100644 index 00000000..0d6a8b0b --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt @@ -0,0 +1,69 @@ +package org.listenbrainz.android.ui.screens.brainzplayer + +import android.util.Log +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.res.ResourcesCompat +import androidx.hilt.navigation.compose.hiltViewModel +import org.listenbrainz.android.R +import org.listenbrainz.android.model.Artist +import org.listenbrainz.android.ui.components.ListenCardSmall +import org.listenbrainz.android.ui.theme.ListenBrainzTheme +import org.listenbrainz.android.viewmodel.ArtistViewModel + +@Composable +fun ArtistsScreenOverview( + artists: List +) { + val artistsStarting: MutableMap> = mutableMapOf() + for (i in 0..25) { + artistsStarting['A' + i] = mutableListOf() + } + + for (i in 1..artists.size) { + artistsStarting[artists[i - 1].name[0]]?.add(artists[i - 1]) + } + + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + for (i in 0..25) { + val startingLetter: Char = ('A' + i) + if (artistsStarting[startingLetter]!!.size > 0) { + Text( + startingLetter.toString(), modifier = Modifier.padding(start = 10.dp , top = 15.dp , bottom = 15.dp) , style = TextStyle( + color = ListenBrainzTheme.colorScheme.lbSignature, + fontSize = 20.sp, + fontFamily = FontFamily(Font(R.font.roboto_bold)), + ) + ) + for (j in 1..artistsStarting[startingLetter]!!.size) { + var coverArt: String? = null + if (artistsStarting[startingLetter]!![j - 1].albums.isNotEmpty()) + coverArt = artistsStarting[startingLetter]!![j - 1].albums[0].albumArt + ListenCardSmall( + trackName = artistsStarting[startingLetter]!![j - 1].name, + artistName = "${artistsStarting[startingLetter]!![j - 1].songs.size} tracks", + coverArtUrl = coverArt, + modifier = Modifier.padding(10.dp) + ) { + + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index 3a17ce18..41373a04 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -1,6 +1,7 @@ package org.listenbrainz.android.ui.screens.brainzplayer +import android.util.Log import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow @@ -154,7 +155,9 @@ fun BrainzPlayerHomeScreen( songsPlayedToday = songsPlayedToday, songsPlayedThisWeek = songsPlayedThisWeek ) - 2 -> ArtistScreen(navigateToArtistScreen = {id -> navigateToArtistsScreen()}) + 2 -> ArtistsScreenOverview( + artists = artists + ) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt index 9c45b416..b31e3dcc 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt @@ -31,7 +31,8 @@ fun RecentPlaysScreen( PlayedToday(songsPlayedToday = songsPlayedToday) } if(songsPlayedThisWeek.isNotEmpty()) { - Text("Played This Week") + Text("Played This Week" , color = ListenBrainzTheme.colorScheme.lbSignature , fontSize = 25.sp) + Spacer(modifier = Modifier.height(10.dp)) PlayedThisWeek(songsPlayedThisWeek = songsPlayedThisWeek) } } @@ -40,7 +41,11 @@ fun RecentPlaysScreen( private fun PlayedToday( songsPlayedToday: List ){ - LazyColumn (modifier = Modifier.height(250.dp)) { + var heightConstraint = ListenBrainzTheme.sizes.listenCardHeight * songsPlayedToday.size + 20.dp + if(songsPlayedToday.size > 4) heightConstraint = 250.dp + LazyColumn (modifier = Modifier.height( + heightConstraint + )) { items(songsPlayedToday){ ListenCardSmall(trackName = it.title, artistName = it.artist, coverArtUrl = it.albumArt, enableDropdownIcon = true) { Unit @@ -54,11 +59,16 @@ private fun PlayedToday( private fun PlayedThisWeek( songsPlayedThisWeek: List ){ - LazyColumn (modifier = Modifier.height(250.dp)) { + var heightConstraint = ListenBrainzTheme.sizes.listenCardHeight * songsPlayedThisWeek.size + 20.dp + if(songsPlayedThisWeek.size > 4) heightConstraint = 250.dp + LazyColumn (modifier = Modifier.height( + heightConstraint + )) { items(songsPlayedThisWeek){ ListenCardSmall(trackName = it.title, artistName = it.artist, coverArtUrl = it.albumArt, enableDropdownIcon = true) { - + Unit } + Spacer(modifier = Modifier.height(5.dp)) } } } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt index 39d3f70c..bc64540c 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt @@ -7,7 +7,6 @@ import android.support.v4.media.session.PlaybackStateCompat.REPEAT_MODE_NONE import android.support.v4.media.session.PlaybackStateCompat.REPEAT_MODE_ONE import android.support.v4.media.session.PlaybackStateCompat.SHUFFLE_MODE_ALL import android.support.v4.media.session.PlaybackStateCompat.SHUFFLE_MODE_NONE -import android.util.Log import androidx.annotation.RequiresApi import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -162,7 +161,7 @@ class BrainzPlayerViewModel @Inject constructor( when { playbackState.isPlaying -> if (toggle) brainzPlayerServiceConnection.transportControls.pause() playbackState.isPlayEnabled -> { - mediaItem.lastListenedTo = Instant.now().epochSecond * 1000 + mediaItem.lastListenedTo = System.currentTimeMillis() viewModelScope.launch { songRepository.updateSong(mediaItem) } brainzPlayerServiceConnection.transportControls.play() } @@ -170,7 +169,7 @@ class BrainzPlayerViewModel @Inject constructor( } } } else { - mediaItem.lastListenedTo = Instant.now().epochSecond * 1000 + mediaItem.lastListenedTo = System.currentTimeMillis() viewModelScope.launch { songRepository.updateSong(mediaItem) } brainzPlayerServiceConnection.transportControls.playFromMediaId(mediaItem.mediaID.toString(), null) } From 0f64defcfde4165a9819b2d3ae8644f800cba5b8 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Thu, 21 Mar 2024 20:24:07 +0530 Subject: [PATCH 08/97] Add songs overview and albums overview screen Also added previews for albums and artists on main overview screen --- .../brainzplayer/AlbumsOverviewScreen.kt | 69 ++++++++++++++ .../brainzplayer/ArtistsOverviewScreen.kt | 18 ++-- .../brainzplayer/BrainzPlayerScreen.kt | 18 +++- .../ui/screens/brainzplayer/OverviewScreen.kt | 95 +++++++++++++------ .../brainzplayer/SongsOverviewScreen.kt | 70 ++++++++++++++ .../navigation/BrainzPlayerNavigation.kt | 4 + 6 files changed, 231 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt new file mode 100644 index 00000000..d77d2966 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt @@ -0,0 +1,69 @@ +package org.listenbrainz.android.ui.screens.brainzplayer + +import android.util.Log +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.res.ResourcesCompat +import androidx.hilt.navigation.compose.hiltViewModel +import org.listenbrainz.android.R +import org.listenbrainz.android.model.Album +import org.listenbrainz.android.model.Artist +import org.listenbrainz.android.ui.components.ListenCardSmall +import org.listenbrainz.android.ui.theme.ListenBrainzTheme +import org.listenbrainz.android.viewmodel.ArtistViewModel + +@Composable +fun AlbumsOverViewScreen( + albums : List +) { + val albumsStarting: MutableMap> = mutableMapOf() + for (i in 0..25) { + albumsStarting['A' + i] = mutableListOf() + } + + for (i in 1..albums.size) { + albumsStarting[albums[i - 1].title[0]]?.add(albums[i-1]) + } + + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + for (i in 0..25) { + val startingLetter: Char = ('A' + i) + if (albumsStarting[startingLetter]!!.size > 0) { + Text( + startingLetter.toString(), modifier = Modifier.padding(start = 10.dp , top = 10.dp , bottom = 5.dp) , style = TextStyle( + color = ListenBrainzTheme.colorScheme.lbSignature, + fontSize = 20.sp, + fontFamily = FontFamily(Font(R.font.roboto_bold)), + ) + ) + for (j in 1..albumsStarting[startingLetter]!!.size) { + var coverArt: String? = null + coverArt = albumsStarting[startingLetter]!![j - 1].albumArt + ListenCardSmall( + trackName = albumsStarting[startingLetter]!![j - 1].title, + artistName = albumsStarting[startingLetter]!![j-1].artist, + coverArtUrl = coverArt, + modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 3.dp, bottom = 3.dp) + ) { + + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt index 0d6a8b0b..63a05e75 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt @@ -1,33 +1,24 @@ package org.listenbrainz.android.ui.screens.brainzplayer -import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.content.res.ResourcesCompat -import androidx.hilt.navigation.compose.hiltViewModel import org.listenbrainz.android.R import org.listenbrainz.android.model.Artist import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.theme.ListenBrainzTheme -import org.listenbrainz.android.viewmodel.ArtistViewModel @Composable -fun ArtistsScreenOverview( +fun ArtistsOverviewScreen( artists: List ) { val artistsStarting: MutableMap> = mutableMapOf() @@ -56,9 +47,12 @@ fun ArtistsScreenOverview( coverArt = artistsStarting[startingLetter]!![j - 1].albums[0].albumArt ListenCardSmall( trackName = artistsStarting[startingLetter]!![j - 1].name, - artistName = "${artistsStarting[startingLetter]!![j - 1].songs.size} tracks", + artistName = when (artistsStarting[startingLetter]!![j - 1].songs.size) { + 1 -> "1 track" + else -> "${artistsStarting[startingLetter]!![j - 1].songs.size} tracks" + }, coverArtUrl = coverArt, - modifier = Modifier.padding(10.dp) + modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 3.dp, bottom = 3.dp) ) { } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index 41373a04..24e76079 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -67,14 +67,18 @@ fun BrainzPlayerScreen() { val songsPlayedToday = brainzPlayerViewModel.songsPlayedToday.collectAsState(initial = listOf()).value val recentlyPlayed = brainzPlayerViewModel.recentlyPlayed.collectAsState(initial = mutableListOf()).value val topRecents = recentlyPlayed.subList(0, minOf(recentlyPlayed.size , 5)).toMutableList() + val topArtists = artists.subList(0, minOf(artists.size,5)).toMutableList() + val topAlbums = albums.subList(0, minOf(songs.size,5)).toMutableList() val songsPlayedThisWeek = brainzPlayerViewModel.songsPlayedThisWeek.collectAsState(initial = listOf()).value topRecents.add(Song()) + topArtists.add(Artist()) + topAlbums.add(Album()) Column( modifier = Modifier .fillMaxSize() ) { - Navigation(albums, artists, playlists, songsPlayedToday, songsPlayedThisWeek ,topRecents ,songs) + Navigation(albums = albums, previewAlbums = topAlbums, artists = artists, previewArtists = topArtists, playlists, songsPlayedToday, songsPlayedThisWeek ,topRecents ,songs) } } @@ -83,7 +87,9 @@ fun BrainzPlayerScreen() { fun BrainzPlayerHomeScreen( songs : List, albums: List, + previewAlbums: List, artists: List, + previewArtists: List, playlists: List, songsPlayedToday: List, songsPlayedThisWeek: List, @@ -147,17 +153,21 @@ fun BrainzPlayerHomeScreen( songsPlayedToday = songsPlayedToday, recentlyPlayedSongs = recentlyPlayedSongs, goToRecentScreen = {currentTab.value = 1}, + goToArtistScreen = {currentTab.value = 2}, + goToAlbumScreen = {currentTab.value = 3}, brainzPlayerViewModel = brainzPlayerViewModel, - artists = artists, - albums = albums + artists = previewArtists, + albums = previewAlbums ) 1 -> RecentPlaysScreen( songsPlayedToday = songsPlayedToday, songsPlayedThisWeek = songsPlayedThisWeek ) - 2 -> ArtistsScreenOverview( + 2 -> ArtistsOverviewScreen( artists = artists ) + 3 -> AlbumsOverViewScreen(albums = albums) + 4 -> SongsOverviewScreen(songs = songs) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt index 7eda9f65..296929ea 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt @@ -41,6 +41,8 @@ import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel fun OverviewScreen ( songsPlayedToday: List, goToRecentScreen: () -> Unit, + goToArtistScreen: () -> Unit, + goToAlbumScreen: () -> Unit, recentlyPlayedSongs: List, brainzPlayerViewModel: BrainzPlayerViewModel = hiltViewModel(), artists : List, @@ -48,8 +50,8 @@ fun OverviewScreen ( ) { Column (modifier = Modifier.verticalScroll(rememberScrollState())) { RecentlyPlayedOverview(recentlyPlayedSongs = recentlyPlayedSongs, goToRecentScreen = goToRecentScreen ,brainzPlayerViewModel = brainzPlayerViewModel) - ArtistsOverview(artists = artists) - AlbumsOverview(albums = albums) + ArtistsOverview(artists = artists, goToArtistScreen = goToArtistScreen) + AlbumsOverview(albums = albums, goToAlbumScreen = goToAlbumScreen) } @@ -76,7 +78,19 @@ private fun RecentlyPlayedOverview( .height(250.dp)){ items(items = recentlyPlayedSongs) { song -> - if(song.title == ""){ + if(song.title != ""){ + BrainzPlayerActivityCards(icon = song.albumArt, + errorIcon = R.drawable.ic_artist, + title = song.title, + artist = song.artist, + modifier = Modifier + .clickable { + brainzPlayerViewModel.changePlayable(recentlyPlayedSongs, PlayableType.ALL_SONGS, song.mediaID,recentlyPlayedSongs.sortedBy { it.discNumber }.indexOf(song),0L) + brainzPlayerViewModel.playOrToggleSong(song, true) + } + ) + } + else{ Box( modifier = Modifier .padding(10.dp) @@ -92,26 +106,14 @@ private fun RecentlyPlayedOverview( } } } - else{ - Log.v("pranav" , (song.lastListenedTo).toString()) - BrainzPlayerActivityCards(icon = song.albumArt, - errorIcon = R.drawable.ic_artist, - title = song.title, - artist = song.artist, - modifier = Modifier - .clickable { - brainzPlayerViewModel.changePlayable(recentlyPlayedSongs, PlayableType.ALL_SONGS, song.mediaID,recentlyPlayedSongs.sortedBy { it.discNumber }.indexOf(song),0L) - brainzPlayerViewModel.playOrToggleSong(song, true) - } - ) - } } } } @Composable private fun ArtistsOverview( - artists : List + artists : List, + goToArtistScreen : () -> Unit ) { Text("Artists" , style = TextStyle( fontSize = 24.sp, @@ -128,11 +130,30 @@ private fun ArtistsOverview( .height(250.dp)){ items(items = artists) { artist -> - BrainzPlayerActivityCards(icon = "", - errorIcon = R.drawable.ic_artist, - title = "", - artist = artist.name, - ) + if(artist.name != ""){ + BrainzPlayerActivityCards(icon = "", + errorIcon = R.drawable.ic_artist, + title = "", + artist = artist.name, + ) + } + else{ + Box( + modifier = Modifier + .padding(10.dp) + .clip(RoundedCornerShape(15.dp)) + .background(color = colorResource(id = R.color.bp_bottom_song_viewpager)) + .size(150.dp) + .clickable { + goToArtistScreen() + } + ){ + Column (modifier = Modifier.fillMaxSize().background(Color(0xFF1E1E1E)).padding(start = 5.dp , bottom = 20.dp) , verticalArrangement = Arrangement.Bottom) { + Text(" All \n Artists" , style = TextStyle(fontSize = 20.sp) , color = ListenBrainzTheme.colorScheme.lbSignature) + } + } + } + } } } @@ -140,6 +161,7 @@ private fun ArtistsOverview( @Composable private fun AlbumsOverview( albums: List, + goToAlbumScreen: () -> Unit ){ Text("Albums" , style = TextStyle( fontSize = 24.sp, @@ -156,11 +178,30 @@ private fun AlbumsOverview( .height(250.dp)){ items(items = albums) { album -> - BrainzPlayerActivityCards(icon = album.albumArt, - errorIcon = R.drawable.ic_artist, - title = album.title, - artist = album.artist, - ) + if(album.title != ""){ + BrainzPlayerActivityCards(icon = album.albumArt, + errorIcon = R.drawable.ic_artist, + title = album.title, + artist = album.artist, + ) + } + else{ + Box( + modifier = Modifier + .padding(10.dp) + .clip(RoundedCornerShape(15.dp)) + .background(color = colorResource(id = R.color.bp_bottom_song_viewpager)) + .size(150.dp) + .clickable { + goToAlbumScreen() + } + ){ + Column (modifier = Modifier.fillMaxSize().background(Color(0xFF1E1E1E)).padding(start = 5.dp , bottom = 20.dp) , verticalArrangement = Arrangement.Bottom) { + Text(" All \n Albums" , style = TextStyle(fontSize = 20.sp) , color = ListenBrainzTheme.colorScheme.lbSignature) + } + } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt new file mode 100644 index 00000000..b23daa59 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt @@ -0,0 +1,70 @@ +package org.listenbrainz.android.ui.screens.brainzplayer + +import android.util.Log +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.res.ResourcesCompat +import androidx.hilt.navigation.compose.hiltViewModel +import org.listenbrainz.android.R +import org.listenbrainz.android.model.Album +import org.listenbrainz.android.model.Artist +import org.listenbrainz.android.model.Song +import org.listenbrainz.android.ui.components.ListenCardSmall +import org.listenbrainz.android.ui.theme.ListenBrainzTheme +import org.listenbrainz.android.viewmodel.ArtistViewModel + +@Composable +fun SongsOverviewScreen( + songs: List +) { + val songsStarting : MutableMap> = mutableMapOf() + for (i in 0..25) { + songsStarting['A' + i] = mutableListOf() + } + + for (i in 1..songs.size) { + songsStarting[songs[i - 1].title[0]]?.add(songs[i-1]) + } + + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + for (i in 0..25) { + val startingLetter: Char = ('A' + i) + if (songsStarting[startingLetter]!!.size > 0) { + Text( + startingLetter.toString(), modifier = Modifier.padding(start = 10.dp , top = 10.dp , bottom = 5.dp) , style = TextStyle( + color = ListenBrainzTheme.colorScheme.lbSignature, + fontSize = 20.sp, + fontFamily = FontFamily(Font(R.font.roboto_bold)), + ) + ) + for (j in 1..songsStarting[startingLetter]!!.size) { + var coverArt: String? = null + coverArt = songsStarting[startingLetter]!![j - 1].albumArt + ListenCardSmall( + trackName = songsStarting[startingLetter]!![j - 1].title, + artistName = songsStarting[startingLetter]!![j-1].artist, + coverArtUrl = coverArt, + modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 3.dp, bottom = 3.dp) + ) { + + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt index 8eb0c836..d5189328 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt @@ -27,7 +27,9 @@ import org.listenbrainz.android.ui.screens.brainzplayer.SongScreen @Composable fun Navigation( albums: List, + previewAlbums: List, artists: List, + previewArtists: List, playlists: List, songsPlayedToday: List, songsPlayedThisWeek: List, @@ -44,7 +46,9 @@ fun Navigation( BrainzPlayerHomeScreen( songs = songs, albums = albums, + previewAlbums = previewAlbums, artists = artists, + previewArtists = previewArtists, playlists = playlists, songsPlayedToday = songsPlayedToday, songsPlayedThisWeek = songsPlayedThisWeek, From a7451962e86c1b5f60c7538f37cbd62274386f03 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Fri, 22 Mar 2024 16:35:22 +0530 Subject: [PATCH 09/97] Add gradient to overview screens --- .../brainzplayer/AlbumsOverviewScreen.kt | 55 +++- .../brainzplayer/ArtistsOverviewScreen.kt | 66 +++-- .../brainzplayer/BrainzPlayerScreen.kt | 221 -------------- .../ui/screens/brainzplayer/OverviewScreen.kt | 274 +++++++++++------- .../brainzplayer/RecentPlaysOverviewScreen.kt | 53 +++- .../brainzplayer/SongsOverviewScreen.kt | 57 +++- .../navigation/BrainzPlayerNavigation.kt | 8 - 7 files changed, 346 insertions(+), 388 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt index d77d2966..03301a00 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt @@ -1,6 +1,7 @@ package org.listenbrainz.android.ui.screens.brainzplayer import android.util.Log +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -12,6 +13,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font @@ -44,23 +47,45 @@ fun AlbumsOverViewScreen( for (i in 0..25) { val startingLetter: Char = ('A' + i) if (albumsStarting[startingLetter]!!.size > 0) { - Text( - startingLetter.toString(), modifier = Modifier.padding(start = 10.dp , top = 10.dp , bottom = 5.dp) , style = TextStyle( - color = ListenBrainzTheme.colorScheme.lbSignature, - fontSize = 20.sp, - fontFamily = FontFamily(Font(R.font.roboto_bold)), + Column(modifier = Modifier.background( + brush = Brush.linearGradient( + start = Offset.Zero, + end = Offset(0f, Float.POSITIVE_INFINITY), + colors = listOf( + Color(0xFF111111), + Color(0xFF131313), + Color(0xFF151515), + Color(0xFF171717), + Color(0xFF272727), + Color(0xFF272E27) + ) ) - ) - for (j in 1..albumsStarting[startingLetter]!!.size) { - var coverArt: String? = null - coverArt = albumsStarting[startingLetter]!![j - 1].albumArt - ListenCardSmall( - trackName = albumsStarting[startingLetter]!![j - 1].title, - artistName = albumsStarting[startingLetter]!![j-1].artist, - coverArtUrl = coverArt, - modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 3.dp, bottom = 3.dp) - ) { + ).padding(top = 15.dp, bottom = 15.dp)) { + Text( + startingLetter.toString(), + modifier = Modifier.padding(start = 10.dp, top = 10.dp, bottom = 5.dp), + style = TextStyle( + color = ListenBrainzTheme.colorScheme.lbSignature, + fontSize = 20.sp, + fontFamily = FontFamily(Font(R.font.roboto_bold)), + ) + ) + for (j in 1..albumsStarting[startingLetter]!!.size) { + var coverArt: String? = null + coverArt = albumsStarting[startingLetter]!![j - 1].albumArt + ListenCardSmall( + trackName = albumsStarting[startingLetter]!![j - 1].title, + artistName = albumsStarting[startingLetter]!![j - 1].artist, + coverArtUrl = coverArt, + modifier = Modifier.padding( + start = 10.dp, + end = 10.dp, + top = 3.dp, + bottom = 3.dp + ) + ) { + } } } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt index 63a05e75..c5d2fb3b 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt @@ -1,5 +1,6 @@ package org.listenbrainz.android.ui.screens.brainzplayer +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -7,6 +8,9 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily @@ -34,27 +38,51 @@ fun ArtistsOverviewScreen( for (i in 0..25) { val startingLetter: Char = ('A' + i) if (artistsStarting[startingLetter]!!.size > 0) { - Text( - startingLetter.toString(), modifier = Modifier.padding(start = 10.dp , top = 15.dp , bottom = 15.dp) , style = TextStyle( - color = ListenBrainzTheme.colorScheme.lbSignature, - fontSize = 20.sp, - fontFamily = FontFamily(Font(R.font.roboto_bold)), + Column( + modifier = Modifier.background( + brush = Brush.linearGradient( + start = Offset.Zero, + end = Offset(0f, Float.POSITIVE_INFINITY), + colors = listOf( + Color(0xFF111111), + Color(0xFF131313), + Color(0xFF151515), + Color(0xFF171717), + Color(0xFF272727), + Color(0xFF272E27) + ) + ) + ).padding(top = 15.dp, bottom = 15.dp) + ) { + Text( + startingLetter.toString(), + modifier = Modifier.padding(start = 10.dp, top = 15.dp, bottom = 15.dp), + style = TextStyle( + color = ListenBrainzTheme.colorScheme.lbSignature, + fontSize = 20.sp, + fontFamily = FontFamily(Font(R.font.roboto_bold)), + ) ) - ) - for (j in 1..artistsStarting[startingLetter]!!.size) { - var coverArt: String? = null - if (artistsStarting[startingLetter]!![j - 1].albums.isNotEmpty()) - coverArt = artistsStarting[startingLetter]!![j - 1].albums[0].albumArt - ListenCardSmall( - trackName = artistsStarting[startingLetter]!![j - 1].name, - artistName = when (artistsStarting[startingLetter]!![j - 1].songs.size) { - 1 -> "1 track" - else -> "${artistsStarting[startingLetter]!![j - 1].songs.size} tracks" - }, - coverArtUrl = coverArt, - modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 3.dp, bottom = 3.dp) - ) { + for (j in 1..artistsStarting[startingLetter]!!.size) { + var coverArt: String? = null + if (artistsStarting[startingLetter]!![j - 1].albums.isNotEmpty()) + coverArt = artistsStarting[startingLetter]!![j - 1].albums[0].albumArt + ListenCardSmall( + trackName = artistsStarting[startingLetter]!![j - 1].name, + artistName = when (artistsStarting[startingLetter]!![j - 1].songs.size) { + 1 -> "1 track" + else -> "${artistsStarting[startingLetter]!![j - 1].songs.size} tracks" + }, + coverArtUrl = coverArt, + modifier = Modifier.padding( + start = 10.dp, + end = 10.dp, + top = 3.dp, + bottom = 3.dp + ) + ) { + } } } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index 24e76079..f4dba5c0 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -90,18 +90,10 @@ fun BrainzPlayerHomeScreen( previewAlbums: List, artists: List, previewArtists: List, - playlists: List, songsPlayedToday: List, songsPlayedThisWeek: List, recentlyPlayedSongs: List, brainzPlayerViewModel: BrainzPlayerViewModel = hiltViewModel(), - navigateToSongsScreen: () -> Unit, - navigateToArtistsScreen: () -> Unit, - navigateToAlbumsScreen: () -> Unit, - navigateToPlaylistsScreen: () -> Unit, - navigateToArtist: (id: Long) -> Unit, - navigateToAlbum: (id: Long) -> Unit, - navigateToPlaylist: (id: Long) -> Unit ) { val currentTab : MutableState = remember {mutableStateOf(0)} @@ -170,218 +162,5 @@ fun BrainzPlayerHomeScreen( 4 -> SongsOverviewScreen(songs = songs) } } - - -// Column(modifier = Modifier -// .padding(horizontal = 8.dp) -// .verticalScroll(rememberScrollState()) -// ) { -// // Recently Played -//// Text( -//// text = "Recently Played", -//// modifier = Modifier -//// .fillMaxWidth() -//// .padding(16.dp), -//// fontWeight = FontWeight.Bold, -//// fontSize = 24.sp, -//// textAlign = TextAlign.Start, -//// color = MaterialTheme.colorScheme.onSurface -//// ) -// LazyRow(modifier = Modifier.height(200.dp)) { -// items(items = recentlyPlayedSongs.items) { -// BrainzPlayerActivityCards(icon = it.albumArt, -// errorIcon = R.drawable.ic_artist, -// title = it.title, -// modifier = Modifier -// .clickable { -// brainzPlayerViewModel.changePlayable(recentlyPlayedSongs.items, PlayableType.ALL_SONGS, it.mediaID,recentlyPlayedSongs.items.sortedBy { it.discNumber }.indexOf(it),0L) -// brainzPlayerViewModel.playOrToggleSong(it, true) -// } -// ) -// } -// } -// -// // Songs button -// Card( -// modifier = Modifier -// .padding(16.dp) -// .clickable { -// navigateToSongsScreen() -// }, -// shape = RoundedCornerShape(16.dp), -// backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, -// elevation = 5.dp -// ) { -// Row(verticalAlignment = Alignment.CenterVertically) { -// Text( -// text = "Songs", -// modifier = Modifier -// .padding(top = 16.dp, bottom = 16.dp, start = 16.dp), -// fontWeight = FontWeight.Bold, -// fontSize = 24.sp, -// textAlign = TextAlign.Start, -// color = MaterialTheme.colorScheme.onSurface -// ) -// Icon( -// imageVector = Icons.Rounded.ArrowForwardIos, -// modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp, start = 8.dp), -// contentDescription = "Navigate to songs screen", -// tint = MaterialTheme.colorScheme.onSurface -// ) -// } -// } -// LazyRow(modifier = Modifier.height(200.dp)) { -// items(items = songs) { song -> -// BrainzPlayerActivityCards(icon = song.albumArt, -// errorIcon = R.drawable.ic_artist, -// title = song.title, -// modifier = Modifier -// .clickable { -// brainzPlayerViewModel.changePlayable( -// songs.sortedBy { it.discNumber }, -// PlayableType.ALL_SONGS, -// song.mediaID, -// songs -// .sortedBy { it.discNumber } -// .indexOf(song) -// ) -// brainzPlayerViewModel.playOrToggleSong(song, true) -// } -// ) -// } -// } -// -// // Artists -// Card( -// modifier = Modifier -// .padding(16.dp) -// .clickable { -// navigateToArtistsScreen() -// }, -// shape = RoundedCornerShape(16.dp), -// backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, -// elevation = 5.dp -// ) { -// Row(verticalAlignment = Alignment.CenterVertically) { -// Text( -// text = "Artists", -// modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, start = 16.dp), -// fontWeight = FontWeight.Bold, -// fontSize = 24.sp, -// textAlign = TextAlign.Start, -// color = MaterialTheme.colorScheme.onSurface -// ) -// Icon( -// imageVector = Icons.Rounded.ArrowForwardIos, -// modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp, start = 8.dp), -// contentDescription = "Navigate to artists screen", -// tint = MaterialTheme.colorScheme.onSurface -// ) -// } -// -// } -// LazyRow(modifier = Modifier.height(200.dp)) { -// items(items = artists) { -// BrainzPlayerActivityCards(icon = "", -// errorIcon = R.drawable.ic_artist, -// title = it.name, -// modifier = Modifier -// .clickable { -// navigateToArtist(it.id) -// } -// ) -// } -// } -// -// -// // Albums -// Card( -// modifier = Modifier -// .padding(16.dp) -// .clickable { -// navigateToAlbumsScreen() -// }, -// shape = RoundedCornerShape(16.dp), -// backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, -// elevation = 5.dp -// ) { -// Row(verticalAlignment = Alignment.CenterVertically) { -// Text( -// text = "Albums", -// modifier = Modifier -// .padding(top = 16.dp, bottom = 16.dp, start = 16.dp), -// fontWeight = FontWeight.Bold, -// fontSize = 24.sp, -// textAlign = TextAlign.Start, -// color = MaterialTheme.colorScheme.onSurface -// ) -// Icon( -// imageVector = Icons.Rounded.ArrowForwardIos, -// modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp, start = 8.dp), -// contentDescription = "Navigate to albums screen", -// tint = MaterialTheme.colorScheme.onSurface -// ) -// } -// -// } -// LazyRow(modifier = Modifier.height(200.dp)) { -// items(albums) { -// BrainzPlayerActivityCards(it.albumArt, -// R.drawable.ic_album, -// title = it.title, -// modifier = Modifier -// .clickable { -// navigateToAlbum(it.albumId) -// } -// ) -// } -// } -// -// -// // Playlists -// Card( -// modifier = Modifier -// .padding(16.dp) -// .clickable { -// navigateToPlaylistsScreen() -// }, -// shape = RoundedCornerShape(16.dp), -// backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, -// elevation = 5.dp -// ) { -// Row(verticalAlignment = Alignment.CenterVertically) { -// Text( -// text = "Playlists", -// modifier = Modifier -// .padding(top = 16.dp, bottom = 16.dp, start = 16.dp), -// fontWeight = FontWeight.Bold, -// fontSize = 24.sp, -// textAlign = TextAlign.Start, -// color = MaterialTheme.colorScheme.onSurface -// ) -// Icon( -// imageVector = Icons.Rounded.ArrowForwardIos, -// modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp, start = 8.dp), -// contentDescription = "Navigate to playlists screen", -// tint = MaterialTheme.colorScheme.onSurface -// ) -// } -// -// } -// LazyRow(modifier = Modifier.height(200.dp)) { -// items(playlists.filter { -// it.id != (-1).toLong() -// }) { -// BrainzPlayerActivityCards( -// icon = "", -// errorIcon = it.art, -// title = it.title, -// modifier = Modifier.clickable { navigateToPlaylist(it.id) } -// ) -// } -// } -// -// -// } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt index 296929ea..5a690466 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource @@ -63,46 +64,76 @@ private fun RecentlyPlayedOverview( brainzPlayerViewModel : BrainzPlayerViewModel, goToRecentScreen : () -> Unit ) { - Text("Recently Played" , style = TextStyle( - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - color = ListenBrainzTheme.colorScheme.lbSignature - ) , modifier = Modifier.padding(start = 17.dp)) - LazyRow( - modifier = Modifier - .background( - brush = Brush.verticalGradient( - colors = listOf(Color(1010), Color(1010)) - ) + Column(modifier = Modifier.background( + brush = Brush.linearGradient( + start = Offset.Zero, + end = Offset(0f, Float.POSITIVE_INFINITY), + colors = listOf( + Color(0xFF111111), + Color(0xFF131313), + Color(0xFF151515), + Color(0xFF171717), + Color(0xFF272727), + Color(0xFF272E27) ) - .height(250.dp)){ - items(items = recentlyPlayedSongs) { - song -> - if(song.title != ""){ - BrainzPlayerActivityCards(icon = song.albumArt, - errorIcon = R.drawable.ic_artist, - title = song.title, - artist = song.artist, - modifier = Modifier - .clickable { - brainzPlayerViewModel.changePlayable(recentlyPlayedSongs, PlayableType.ALL_SONGS, song.mediaID,recentlyPlayedSongs.sortedBy { it.discNumber }.indexOf(song),0L) - brainzPlayerViewModel.playOrToggleSong(song, true) - } + ) + ).padding(top = 15.dp, bottom = 15.dp)) { + Text( + "Recently Played", style = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = ListenBrainzTheme.colorScheme.lbSignature + ), modifier = Modifier.padding(start = 17.dp) + ) + LazyRow( + modifier = Modifier + .background( + brush = Brush.verticalGradient( + colors = listOf(Color(1010), Color(1010)) + ) ) - } - else{ - Box( - modifier = Modifier - .padding(10.dp) - .clip(RoundedCornerShape(15.dp)) - .background(color = colorResource(id = R.color.bp_bottom_song_viewpager)) - .size(150.dp) - .clickable { - goToRecentScreen() + .height(250.dp) + ) { + items(items = recentlyPlayedSongs) { song -> + if (song.title != "") { + BrainzPlayerActivityCards(icon = song.albumArt, + errorIcon = R.drawable.ic_artist, + title = song.title, + artist = song.artist, + modifier = Modifier + .clickable { + brainzPlayerViewModel.changePlayable( + recentlyPlayedSongs, + PlayableType.ALL_SONGS, + song.mediaID, + recentlyPlayedSongs.sortedBy { it.discNumber }.indexOf(song), + 0L + ) + brainzPlayerViewModel.playOrToggleSong(song, true) + } + ) + } else { + Box( + modifier = Modifier + .padding(10.dp) + .clip(RoundedCornerShape(15.dp)) + .background(color = colorResource(id = R.color.bp_bottom_song_viewpager)) + .size(150.dp) + .clickable { + goToRecentScreen() + } + ) { + Column( + modifier = Modifier.fillMaxSize().background(Color(0xFF1E1E1E)) + .padding(start = 5.dp, bottom = 20.dp), + verticalArrangement = Arrangement.Bottom + ) { + Text( + " All \n Recently\n Played", + style = TextStyle(fontSize = 20.sp), + color = ListenBrainzTheme.colorScheme.lbSignature + ) } - ){ - Column (modifier = Modifier.fillMaxSize().background(Color(0xFF1E1E1E)).padding(start = 5.dp , bottom = 20.dp) , verticalArrangement = Arrangement.Bottom) { - Text(" All \n Recently\n Played" , style = TextStyle(fontSize = 20.sp) , color = ListenBrainzTheme.colorScheme.lbSignature) } } } @@ -115,45 +146,70 @@ private fun ArtistsOverview( artists : List, goToArtistScreen : () -> Unit ) { - Text("Artists" , style = TextStyle( - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - color = ListenBrainzTheme.colorScheme.lbSignature - ) , modifier = Modifier.padding(start = 17.dp)) - LazyRow( - modifier = Modifier - .background( - brush = Brush.verticalGradient( - colors = listOf(Color(1010), Color(1010)) - ) + Column(modifier = Modifier.background( + brush = Brush.linearGradient( + start = Offset.Zero, + end = Offset(0f, Float.POSITIVE_INFINITY), + colors = listOf( + Color(0xFF111111), + Color(0xFF131313), + Color(0xFF151515), + Color(0xFF171717), + Color(0xFF272727), + Color(0xFF272E27) ) - .height(250.dp)){ - items(items = artists) { - artist -> - if(artist.name != ""){ - BrainzPlayerActivityCards(icon = "", - errorIcon = R.drawable.ic_artist, - title = "", - artist = artist.name, + ) + ).padding(top = 15.dp, bottom = 15.dp)) { + Text( + "Artists", style = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = ListenBrainzTheme.colorScheme.lbSignature + ), modifier = Modifier.padding(start = 17.dp) + ) + LazyRow( + modifier = Modifier + .background( + brush = Brush.verticalGradient( + colors = listOf(Color(1010), Color(1010)) + ) ) - } - else{ - Box( - modifier = Modifier - .padding(10.dp) - .clip(RoundedCornerShape(15.dp)) - .background(color = colorResource(id = R.color.bp_bottom_song_viewpager)) - .size(150.dp) - .clickable { - goToArtistScreen() + .height(250.dp) + ) { + items(items = artists) { artist -> + if (artist.name != "") { + BrainzPlayerActivityCards( + icon = "", + errorIcon = R.drawable.ic_artist, + title = "", + artist = artist.name, + ) + } else { + Box( + modifier = Modifier + .padding(10.dp) + .clip(RoundedCornerShape(15.dp)) + .background(color = colorResource(id = R.color.bp_bottom_song_viewpager)) + .size(150.dp) + .clickable { + goToArtistScreen() + } + ) { + Column( + modifier = Modifier.fillMaxSize().background(Color(0xFF1E1E1E)) + .padding(start = 5.dp, bottom = 20.dp), + verticalArrangement = Arrangement.Bottom + ) { + Text( + " All \n Artists", + style = TextStyle(fontSize = 20.sp), + color = ListenBrainzTheme.colorScheme.lbSignature + ) } - ){ - Column (modifier = Modifier.fillMaxSize().background(Color(0xFF1E1E1E)).padding(start = 5.dp , bottom = 20.dp) , verticalArrangement = Arrangement.Bottom) { - Text(" All \n Artists" , style = TextStyle(fontSize = 20.sp) , color = ListenBrainzTheme.colorScheme.lbSignature) } } - } + } } } } @@ -163,45 +219,55 @@ private fun AlbumsOverview( albums: List, goToAlbumScreen: () -> Unit ){ - Text("Albums" , style = TextStyle( - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - color = ListenBrainzTheme.colorScheme.lbSignature - ) , modifier = Modifier.padding(start = 17.dp)) - LazyRow( - modifier = Modifier - .background( - brush = Brush.verticalGradient( - colors = listOf(Color(1010), Color(1010)) - ) + Column(modifier = Modifier.background( + brush = Brush.linearGradient( + start = Offset.Zero, + end = Offset(0f, Float.POSITIVE_INFINITY), + colors = listOf( + Color(0xFF111111), + Color(0xFF131313), + Color(0xFF151515), + Color(0xFF171717), + Color(0xFF272727), + Color(0xFF272E27) ) - .height(250.dp)){ - items(items = albums) { - album -> - if(album.title != ""){ - BrainzPlayerActivityCards(icon = album.albumArt, - errorIcon = R.drawable.ic_artist, - title = album.title, - artist = album.artist, - ) - } - else{ - Box( - modifier = Modifier - .padding(10.dp) - .clip(RoundedCornerShape(15.dp)) - .background(color = colorResource(id = R.color.bp_bottom_song_viewpager)) - .size(150.dp) - .clickable { - goToAlbumScreen() + ) + ).padding(top = 15.dp, bottom = 15.dp)){ + Text("Albums" , style = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = ListenBrainzTheme.colorScheme.lbSignature + ) , modifier = Modifier.padding(start = 17.dp)) + LazyRow( + modifier = Modifier + .height(250.dp)){ + items(items = albums) { + album -> + if(album.title != ""){ + BrainzPlayerActivityCards(icon = album.albumArt, + errorIcon = R.drawable.ic_artist, + title = album.title, + artist = album.artist, + ) + } + else{ + Box( + modifier = Modifier + .padding(10.dp) + .clip(RoundedCornerShape(15.dp)) + .background(color = colorResource(id = R.color.bp_bottom_song_viewpager)) + .size(150.dp) + .clickable { + goToAlbumScreen() + } + ){ + Column (modifier = Modifier.fillMaxSize().background(Color(0xFF1E1E1E)).padding(start = 5.dp , bottom = 20.dp) , verticalArrangement = Arrangement.Bottom) { + Text(" All \n Albums" , style = TextStyle(fontSize = 20.sp) , color = ListenBrainzTheme.colorScheme.lbSignature) } - ){ - Column (modifier = Modifier.fillMaxSize().background(Color(0xFF1E1E1E)).padding(start = 5.dp , bottom = 20.dp) , verticalArrangement = Arrangement.Bottom) { - Text(" All \n Albums" , style = TextStyle(fontSize = 20.sp) , color = ListenBrainzTheme.colorScheme.lbSignature) } } - } + } } } } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt index b31e3dcc..cdf870ec 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt @@ -1,5 +1,6 @@ package org.listenbrainz.android.ui.screens.brainzplayer +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -10,6 +11,8 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -26,14 +29,52 @@ fun RecentPlaysScreen( .fillMaxSize() .padding(start = 17.dp, end = 17.dp)) { if(songsPlayedToday.isNotEmpty()){ - Text("Played Today" , color = ListenBrainzTheme.colorScheme.lbSignature , fontSize = 25.sp) - Spacer(modifier = Modifier.height(10.dp)) - PlayedToday(songsPlayedToday = songsPlayedToday) + Column(modifier = Modifier.background( + brush = Brush.linearGradient( + start = Offset.Zero, + end = Offset(0f, Float.POSITIVE_INFINITY), + colors = listOf( + Color(0xFF111111), + Color(0xFF131313), + Color(0xFF151515), + Color(0xFF171717), + Color(0xFF272727), + Color(0xFF272E27) + ) + ) + ).padding(top = 15.dp, bottom = 15.dp)) { + Text( + "Played Today", + color = ListenBrainzTheme.colorScheme.lbSignature, + fontSize = 25.sp + ) + Spacer(modifier = Modifier.height(10.dp)) + PlayedToday(songsPlayedToday = songsPlayedToday) + } } if(songsPlayedThisWeek.isNotEmpty()) { - Text("Played This Week" , color = ListenBrainzTheme.colorScheme.lbSignature , fontSize = 25.sp) - Spacer(modifier = Modifier.height(10.dp)) - PlayedThisWeek(songsPlayedThisWeek = songsPlayedThisWeek) + Column(modifier = Modifier.background( + brush = Brush.linearGradient( + start = Offset.Zero, + end = Offset(0f, Float.POSITIVE_INFINITY), + colors = listOf( + Color(0xFF111111), + Color(0xFF131313), + Color(0xFF151515), + Color(0xFF171717), + Color(0xFF272727), + Color(0xFF272E27) + ) + ) + ).padding(top = 15.dp, bottom = 15.dp)) { + Text( + "Played This Week", + color = ListenBrainzTheme.colorScheme.lbSignature, + fontSize = 25.sp + ) + Spacer(modifier = Modifier.height(10.dp)) + PlayedThisWeek(songsPlayedThisWeek = songsPlayedThisWeek) + } } } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt index b23daa59..81088fb4 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt @@ -1,6 +1,7 @@ package org.listenbrainz.android.ui.screens.brainzplayer import android.util.Log +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -12,6 +13,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font @@ -45,23 +48,47 @@ fun SongsOverviewScreen( for (i in 0..25) { val startingLetter: Char = ('A' + i) if (songsStarting[startingLetter]!!.size > 0) { - Text( - startingLetter.toString(), modifier = Modifier.padding(start = 10.dp , top = 10.dp , bottom = 5.dp) , style = TextStyle( - color = ListenBrainzTheme.colorScheme.lbSignature, - fontSize = 20.sp, - fontFamily = FontFamily(Font(R.font.roboto_bold)), + Column( + modifier = Modifier.background( + brush = Brush.linearGradient( + start = Offset.Zero, + end = Offset(0f, Float.POSITIVE_INFINITY), + colors = listOf( + Color(0xFF111111), + Color(0xFF131313), + Color(0xFF151515), + Color(0xFF171717), + Color(0xFF272727), + Color(0xFF272E27) + ) + ) + ).padding(top = 15.dp, bottom = 15.dp) + ) { + Text( + startingLetter.toString(), + modifier = Modifier.padding(start = 10.dp, top = 10.dp, bottom = 5.dp), + style = TextStyle( + color = ListenBrainzTheme.colorScheme.lbSignature, + fontSize = 20.sp, + fontFamily = FontFamily(Font(R.font.roboto_bold)), + ) ) - ) - for (j in 1..songsStarting[startingLetter]!!.size) { - var coverArt: String? = null - coverArt = songsStarting[startingLetter]!![j - 1].albumArt - ListenCardSmall( - trackName = songsStarting[startingLetter]!![j - 1].title, - artistName = songsStarting[startingLetter]!![j-1].artist, - coverArtUrl = coverArt, - modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 3.dp, bottom = 3.dp) - ) { + for (j in 1..songsStarting[startingLetter]!!.size) { + var coverArt: String? = null + coverArt = songsStarting[startingLetter]!![j - 1].albumArt + ListenCardSmall( + trackName = songsStarting[startingLetter]!![j - 1].title, + artistName = songsStarting[startingLetter]!![j - 1].artist, + coverArtUrl = coverArt, + modifier = Modifier.padding( + start = 10.dp, + end = 10.dp, + top = 3.dp, + bottom = 3.dp + ) + ) { + } } } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt index d5189328..ea3d451c 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt @@ -49,17 +49,9 @@ fun Navigation( previewAlbums = previewAlbums, artists = artists, previewArtists = previewArtists, - playlists = playlists, songsPlayedToday = songsPlayedToday, songsPlayedThisWeek = songsPlayedThisWeek, recentlyPlayedSongs = recentlyPlayedSongs, - navigateToSongsScreen = { goTo(BrainzPlayerNavigationItem.Songs) }, - navigateToArtist = { id -> navHostController.navigate("onArtistClick/$id")}, - navigateToAlbumsScreen = { goTo(BrainzPlayerNavigationItem.Albums) }, - navigateToArtistsScreen = { goTo(BrainzPlayerNavigationItem.Artists) }, - navigateToPlaylistsScreen = { goTo(BrainzPlayerNavigationItem.Playlists) }, - navigateToAlbum = { id -> navHostController.navigate("onAlbumClick/$id")}, - navigateToPlaylist = { id -> navHostController.navigate("onPlaylistClick/$id")} ) } composable(route = BrainzPlayerNavigationItem.Songs.route) { From 2e1cca421c7b0c036387d99321a307f6e0236fc6 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Mon, 25 Mar 2024 18:08:25 +0530 Subject: [PATCH 10/97] Completed Room Migration --- .../android/di/brainzplayer/BrainzPlayerDatabase.kt | 2 +- .../android/di/brainzplayer/DatabaseModule.kt | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/listenbrainz/android/di/brainzplayer/BrainzPlayerDatabase.kt b/app/src/main/java/org/listenbrainz/android/di/brainzplayer/BrainzPlayerDatabase.kt index eadc13a8..a4a862e1 100644 --- a/app/src/main/java/org/listenbrainz/android/di/brainzplayer/BrainzPlayerDatabase.kt +++ b/app/src/main/java/org/listenbrainz/android/di/brainzplayer/BrainzPlayerDatabase.kt @@ -20,7 +20,7 @@ import org.listenbrainz.android.util.TypeConverter ArtistEntity::class, PlaylistEntity::class ], - version = 1, + version = 2, exportSchema = false ) @TypeConverters(TypeConverter::class) diff --git a/app/src/main/java/org/listenbrainz/android/di/brainzplayer/DatabaseModule.kt b/app/src/main/java/org/listenbrainz/android/di/brainzplayer/DatabaseModule.kt index eb008cfd..56c6f821 100644 --- a/app/src/main/java/org/listenbrainz/android/di/brainzplayer/DatabaseModule.kt +++ b/app/src/main/java/org/listenbrainz/android/di/brainzplayer/DatabaseModule.kt @@ -2,6 +2,8 @@ package org.listenbrainz.android.di.brainzplayer import android.content.Context import androidx.room.Room +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -9,6 +11,15 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton + +val MIGRATION_1_2: Migration = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "ALTER TABLE 'SONGS' ADD COLUMN 'lastListenedTo' INTEGER NOT NULL DEFAULT 0" + ) + } +} + @Module @InstallIn(SingletonComponent::class) object DatabaseModule { @@ -21,6 +32,7 @@ object DatabaseModule { BrainzPlayerDatabase::class.java, "brainzplayer_database" ) + .addMigrations(MIGRATION_1_2) .build() @Provides From 0691fe04626bde8480da329bff1eab809b20d208 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Mon, 25 Mar 2024 18:47:45 +0530 Subject: [PATCH 11/97] Fixed DAO tests --- .../org/listenbrainz/android/data/dao/DaoTest.kt | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/java/org/listenbrainz/android/data/dao/DaoTest.kt b/app/src/androidTest/java/org/listenbrainz/android/data/dao/DaoTest.kt index f101caa7..6c258285 100644 --- a/app/src/androidTest/java/org/listenbrainz/android/data/dao/DaoTest.kt +++ b/app/src/androidTest/java/org/listenbrainz/android/data/dao/DaoTest.kt @@ -2,6 +2,8 @@ package org.listenbrainz.android.data.dao import android.content.Context import androidx.room.Room +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -24,6 +26,14 @@ import org.listenbrainz.android.model.dao.PlaylistDao import org.listenbrainz.android.model.dao.SongDao import org.listenbrainz.sharedtest.utils.CoroutineTestRule + +val MIGRATION_1_2: Migration = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "ALTER TABLE 'SONGS' ADD COLUMN 'lastListenedTo' INTEGER NOT NULL DEFAULT 0" + ) + } +} @ExperimentalCoroutinesApi @RunWith(AndroidJUnit4::class) @SmallTest @@ -61,7 +71,8 @@ class DaoTest { song.toLong(), song.toLong(), song.toLong(), - song.toLong() + song.toLong(), + 0 ) } @@ -83,6 +94,7 @@ class DaoTest { song.toLong(), song.toLong(), song.toLong(), + song.toLong(), song.toLong() ) } @@ -107,6 +119,7 @@ class DaoTest { song.toLong(), song.toLong(), song.toLong(), + song.toLong(), song.toLong() ) @@ -127,6 +140,7 @@ class DaoTest { val context = ApplicationProvider.getApplicationContext() brainzPlayerDatabase = Room .inMemoryDatabaseBuilder(context, BrainzPlayerDatabase::class.java) + .addMigrations(MIGRATION_1_2) .build() albumDao = brainzPlayerDatabase.albumDao() artistDao = brainzPlayerDatabase.artistDao() From c9ce74685a84ba89681d69458e2c54ac73a9674e Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Tue, 26 Mar 2024 22:04:16 +0530 Subject: [PATCH 12/97] Add conditional to migration --- .../android/di/brainzplayer/DatabaseModule.kt | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/di/brainzplayer/DatabaseModule.kt b/app/src/main/java/org/listenbrainz/android/di/brainzplayer/DatabaseModule.kt index 56c6f821..0f2abfab 100644 --- a/app/src/main/java/org/listenbrainz/android/di/brainzplayer/DatabaseModule.kt +++ b/app/src/main/java/org/listenbrainz/android/di/brainzplayer/DatabaseModule.kt @@ -14,9 +14,25 @@ import javax.inject.Singleton val MIGRATION_1_2: Migration = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL( - "ALTER TABLE 'SONGS' ADD COLUMN 'lastListenedTo' INTEGER NOT NULL DEFAULT 0" - ) + val cursor = db.query("PRAGMA table_info('SONGS')") + var columnExists = false + val columnNameIndex = cursor.getColumnIndex("name") + if (columnNameIndex != -1) { + while (cursor.moveToNext()) { + val columnName = cursor.getString(columnNameIndex) + if (columnName == "lastListenedTo") { + columnExists = true + break + } + } + } + cursor.close() + + if (!columnExists) { + db.execSQL( + "ALTER TABLE 'SONGS' ADD COLUMN 'lastListenedTo' INTEGER NOT NULL DEFAULT 0" + ) + } } } From 7199ea6a2bae02488f1cf24b90deeb0feef4400a Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Wed, 27 Mar 2024 16:30:20 +0530 Subject: [PATCH 13/97] Light Theme added --- .../android/di/brainzplayer/DatabaseModule.kt | 40 +++++++------ .../brainzplayer/AlbumsOverviewScreen.kt | 13 +---- .../brainzplayer/ArtistsOverviewScreen.kt | 13 +---- .../ui/screens/brainzplayer/OverviewScreen.kt | 58 ++----------------- .../brainzplayer/RecentPlaysOverviewScreen.kt | 26 +-------- .../brainzplayer/SongsOverviewScreen.kt | 13 +---- .../listenbrainz/android/ui/theme/Theme.kt | 44 +++++++++++++- 7 files changed, 73 insertions(+), 134 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/di/brainzplayer/DatabaseModule.kt b/app/src/main/java/org/listenbrainz/android/di/brainzplayer/DatabaseModule.kt index 0f2abfab..5e923c3b 100644 --- a/app/src/main/java/org/listenbrainz/android/di/brainzplayer/DatabaseModule.kt +++ b/app/src/main/java/org/listenbrainz/android/di/brainzplayer/DatabaseModule.kt @@ -11,31 +11,33 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton - -val MIGRATION_1_2: Migration = object : Migration(1, 2) { - override fun migrate(db: SupportSQLiteDatabase) { - val cursor = db.query("PRAGMA table_info('SONGS')") - var columnExists = false - val columnNameIndex = cursor.getColumnIndex("name") - if (columnNameIndex != -1) { - while (cursor.moveToNext()) { - val columnName = cursor.getString(columnNameIndex) - if (columnName == "lastListenedTo") { - columnExists = true - break +object Migrations { + val MIGRATION_1_2: Migration = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + val cursor = db.query("PRAGMA table_info('SONGS')") + var columnExists = false + val columnNameIndex = cursor.getColumnIndex("name") + if (columnNameIndex != -1) { + while (cursor.moveToNext()) { + val columnName = cursor.getString(columnNameIndex) + if (columnName == "lastListenedTo") { + columnExists = true + break + } } } - } - cursor.close() + cursor.close() - if (!columnExists) { - db.execSQL( - "ALTER TABLE 'SONGS' ADD COLUMN 'lastListenedTo' INTEGER NOT NULL DEFAULT 0" - ) + if (!columnExists) { + db.execSQL( + "ALTER TABLE 'SONGS' ADD COLUMN 'lastListenedTo' INTEGER NOT NULL DEFAULT 0" + ) + } } } } + @Module @InstallIn(SingletonComponent::class) object DatabaseModule { @@ -48,7 +50,7 @@ object DatabaseModule { BrainzPlayerDatabase::class.java, "brainzplayer_database" ) - .addMigrations(MIGRATION_1_2) + .addMigrations(Migrations.MIGRATION_1_2) .build() @Provides diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt index 03301a00..80397b2c 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt @@ -48,18 +48,7 @@ fun AlbumsOverViewScreen( val startingLetter: Char = ('A' + i) if (albumsStarting[startingLetter]!!.size > 0) { Column(modifier = Modifier.background( - brush = Brush.linearGradient( - start = Offset.Zero, - end = Offset(0f, Float.POSITIVE_INFINITY), - colors = listOf( - Color(0xFF111111), - Color(0xFF131313), - Color(0xFF151515), - Color(0xFF171717), - Color(0xFF272727), - Color(0xFF272E27) - ) - ) + brush = ListenBrainzTheme.colorScheme.gradientBrush ).padding(top = 15.dp, bottom = 15.dp)) { Text( startingLetter.toString(), diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt index c5d2fb3b..2067161d 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt @@ -40,18 +40,7 @@ fun ArtistsOverviewScreen( if (artistsStarting[startingLetter]!!.size > 0) { Column( modifier = Modifier.background( - brush = Brush.linearGradient( - start = Offset.Zero, - end = Offset(0f, Float.POSITIVE_INFINITY), - colors = listOf( - Color(0xFF111111), - Color(0xFF131313), - Color(0xFF151515), - Color(0xFF171717), - Color(0xFF272727), - Color(0xFF272E27) - ) - ) + brush = ListenBrainzTheme.colorScheme.gradientBrush ).padding(top = 15.dp, bottom = 15.dp) ) { Text( diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt index 5a690466..7d114efb 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt @@ -65,18 +65,7 @@ private fun RecentlyPlayedOverview( goToRecentScreen : () -> Unit ) { Column(modifier = Modifier.background( - brush = Brush.linearGradient( - start = Offset.Zero, - end = Offset(0f, Float.POSITIVE_INFINITY), - colors = listOf( - Color(0xFF111111), - Color(0xFF131313), - Color(0xFF151515), - Color(0xFF171717), - Color(0xFF272727), - Color(0xFF272E27) - ) - ) + brush = ListenBrainzTheme.colorScheme.gradientBrush ).padding(top = 15.dp, bottom = 15.dp)) { Text( "Recently Played", style = TextStyle( @@ -87,11 +76,6 @@ private fun RecentlyPlayedOverview( ) LazyRow( modifier = Modifier - .background( - brush = Brush.verticalGradient( - colors = listOf(Color(1010), Color(1010)) - ) - ) .height(250.dp) ) { items(items = recentlyPlayedSongs) { song -> @@ -117,14 +101,13 @@ private fun RecentlyPlayedOverview( modifier = Modifier .padding(10.dp) .clip(RoundedCornerShape(15.dp)) - .background(color = colorResource(id = R.color.bp_bottom_song_viewpager)) .size(150.dp) .clickable { goToRecentScreen() } ) { Column( - modifier = Modifier.fillMaxSize().background(Color(0xFF1E1E1E)) + modifier = Modifier.fillMaxSize().background(ListenBrainzTheme.colorScheme.placeHolderColor) .padding(start = 5.dp, bottom = 20.dp), verticalArrangement = Arrangement.Bottom ) { @@ -147,18 +130,7 @@ private fun ArtistsOverview( goToArtistScreen : () -> Unit ) { Column(modifier = Modifier.background( - brush = Brush.linearGradient( - start = Offset.Zero, - end = Offset(0f, Float.POSITIVE_INFINITY), - colors = listOf( - Color(0xFF111111), - Color(0xFF131313), - Color(0xFF151515), - Color(0xFF171717), - Color(0xFF272727), - Color(0xFF272E27) - ) - ) + brush = ListenBrainzTheme.colorScheme.gradientBrush ).padding(top = 15.dp, bottom = 15.dp)) { Text( "Artists", style = TextStyle( @@ -169,11 +141,6 @@ private fun ArtistsOverview( ) LazyRow( modifier = Modifier - .background( - brush = Brush.verticalGradient( - colors = listOf(Color(1010), Color(1010)) - ) - ) .height(250.dp) ) { items(items = artists) { artist -> @@ -189,14 +156,13 @@ private fun ArtistsOverview( modifier = Modifier .padding(10.dp) .clip(RoundedCornerShape(15.dp)) - .background(color = colorResource(id = R.color.bp_bottom_song_viewpager)) .size(150.dp) .clickable { goToArtistScreen() } ) { Column( - modifier = Modifier.fillMaxSize().background(Color(0xFF1E1E1E)) + modifier = Modifier.fillMaxSize().background(ListenBrainzTheme.colorScheme.placeHolderColor) .padding(start = 5.dp, bottom = 20.dp), verticalArrangement = Arrangement.Bottom ) { @@ -220,18 +186,7 @@ private fun AlbumsOverview( goToAlbumScreen: () -> Unit ){ Column(modifier = Modifier.background( - brush = Brush.linearGradient( - start = Offset.Zero, - end = Offset(0f, Float.POSITIVE_INFINITY), - colors = listOf( - Color(0xFF111111), - Color(0xFF131313), - Color(0xFF151515), - Color(0xFF171717), - Color(0xFF272727), - Color(0xFF272E27) - ) - ) + brush = ListenBrainzTheme.colorScheme.gradientBrush ).padding(top = 15.dp, bottom = 15.dp)){ Text("Albums" , style = TextStyle( fontSize = 24.sp, @@ -255,13 +210,12 @@ private fun AlbumsOverview( modifier = Modifier .padding(10.dp) .clip(RoundedCornerShape(15.dp)) - .background(color = colorResource(id = R.color.bp_bottom_song_viewpager)) .size(150.dp) .clickable { goToAlbumScreen() } ){ - Column (modifier = Modifier.fillMaxSize().background(Color(0xFF1E1E1E)).padding(start = 5.dp , bottom = 20.dp) , verticalArrangement = Arrangement.Bottom) { + Column (modifier = Modifier.fillMaxSize().background(ListenBrainzTheme.colorScheme.placeHolderColor).padding(start = 5.dp , bottom = 20.dp) , verticalArrangement = Arrangement.Bottom) { Text(" All \n Albums" , style = TextStyle(fontSize = 20.sp) , color = ListenBrainzTheme.colorScheme.lbSignature) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt index cdf870ec..7100b2cb 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt @@ -30,18 +30,7 @@ fun RecentPlaysScreen( .padding(start = 17.dp, end = 17.dp)) { if(songsPlayedToday.isNotEmpty()){ Column(modifier = Modifier.background( - brush = Brush.linearGradient( - start = Offset.Zero, - end = Offset(0f, Float.POSITIVE_INFINITY), - colors = listOf( - Color(0xFF111111), - Color(0xFF131313), - Color(0xFF151515), - Color(0xFF171717), - Color(0xFF272727), - Color(0xFF272E27) - ) - ) + brush = ListenBrainzTheme.colorScheme.gradientBrush ).padding(top = 15.dp, bottom = 15.dp)) { Text( "Played Today", @@ -54,18 +43,7 @@ fun RecentPlaysScreen( } if(songsPlayedThisWeek.isNotEmpty()) { Column(modifier = Modifier.background( - brush = Brush.linearGradient( - start = Offset.Zero, - end = Offset(0f, Float.POSITIVE_INFINITY), - colors = listOf( - Color(0xFF111111), - Color(0xFF131313), - Color(0xFF151515), - Color(0xFF171717), - Color(0xFF272727), - Color(0xFF272E27) - ) - ) + brush = ListenBrainzTheme.colorScheme.gradientBrush ).padding(top = 15.dp, bottom = 15.dp)) { Text( "Played This Week", diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt index 81088fb4..cf9ed1cf 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt @@ -50,18 +50,7 @@ fun SongsOverviewScreen( if (songsStarting[startingLetter]!!.size > 0) { Column( modifier = Modifier.background( - brush = Brush.linearGradient( - start = Offset.Zero, - end = Offset(0f, Float.POSITIVE_INFINITY), - colors = listOf( - Color(0xFF111111), - Color(0xFF131313), - Color(0xFF151515), - Color(0xFF171717), - Color(0xFF272727), - Color(0xFF272E27) - ) - ) + brush = ListenBrainzTheme.colorScheme.gradientBrush ).padding(top = 15.dp, bottom = 15.dp) ) { Text( diff --git a/app/src/main/java/org/listenbrainz/android/ui/theme/Theme.kt b/app/src/main/java/org/listenbrainz/android/ui/theme/Theme.kt index 6b47dd33..e598e0d7 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/theme/Theme.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/theme/Theme.kt @@ -17,6 +17,8 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.toArgb @@ -55,7 +57,36 @@ data class ColorScheme( val listenText: Color, /** Used for stars.*/ val golden: Color = Color(0xFFF9A825), - val hint: Color + val hint: Color, + /** Used for BP **/ + val gradientBrush: Brush, + val placeHolderColor: Color +) + + +private val brainzPlayerLightGradientsBrush = Brush.linearGradient( + start = Offset.Zero, + end = Offset(0f, Float.POSITIVE_INFINITY), + colors = listOf( + Color(0xFFF5F5F5), + Color(0xFFF7F7F7), + Color(0xFFF9F9F9), + Color(0xFFFBFBFB), + Color(0xFFFDFDFD) + ) +) + +private val brainzPlayerDarkGradientsBrush = Brush.linearGradient( + start = Offset.Zero, + end = Offset(0f, Float.POSITIVE_INFINITY), + colors = listOf( + Color(0xFF111111), + Color(0xFF131313), + Color(0xFF151515), + Color(0xFF171717), + Color(0xFF272727), + Color(0xFF272E27) + ) ) private val colorSchemeDark = ColorScheme( @@ -72,7 +103,9 @@ private val colorSchemeDark = ColorScheme( chipSelected = Color.Black, text = Color.White, listenText = Color.White, - hint = Color(0xFF8C8C8C) + hint = Color(0xFF8C8C8C), + gradientBrush = brainzPlayerDarkGradientsBrush, + placeHolderColor = Color(0xFF1E1E1E) ) private val colorSchemeLight = ColorScheme( @@ -89,11 +122,16 @@ private val colorSchemeLight = ColorScheme( chipSelected = Color(0xFFB6B6B6), text = Color.Black, listenText = lb_purple, - hint = Color(0xFF707070) + hint = Color(0xFF707070), + gradientBrush = brainzPlayerLightGradientsBrush, + placeHolderColor = Color(0xFFEBEBEB) ) + private var LocalColorScheme: ProvidableCompositionLocal = staticCompositionLocalOf { colorSchemeLight } + + private val DarkColorScheme = darkColorScheme( background = app_bg_dark, onBackground = app_bg_light, From a73178e7bbca9f3a40cf9281d084d027c738a40f Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Wed, 27 Mar 2024 16:37:07 +0530 Subject: [PATCH 14/97] Add conditional to migration in tests --- .../listenbrainz/android/data/dao/DaoTest.kt | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/app/src/androidTest/java/org/listenbrainz/android/data/dao/DaoTest.kt b/app/src/androidTest/java/org/listenbrainz/android/data/dao/DaoTest.kt index 6c258285..4c308c97 100644 --- a/app/src/androidTest/java/org/listenbrainz/android/data/dao/DaoTest.kt +++ b/app/src/androidTest/java/org/listenbrainz/android/data/dao/DaoTest.kt @@ -27,11 +27,29 @@ import org.listenbrainz.android.model.dao.SongDao import org.listenbrainz.sharedtest.utils.CoroutineTestRule -val MIGRATION_1_2: Migration = object : Migration(1, 2) { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL( - "ALTER TABLE 'SONGS' ADD COLUMN 'lastListenedTo' INTEGER NOT NULL DEFAULT 0" - ) +object Migrations { + val MIGRATION_1_2: Migration = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + val cursor = db.query("PRAGMA table_info('SONGS')") + var columnExists = false + val columnNameIndex = cursor.getColumnIndex("name") + if (columnNameIndex != -1) { + while (cursor.moveToNext()) { + val columnName = cursor.getString(columnNameIndex) + if (columnName == "lastListenedTo") { + columnExists = true + break + } + } + } + cursor.close() + + if (!columnExists) { + db.execSQL( + "ALTER TABLE 'SONGS' ADD COLUMN 'lastListenedTo' INTEGER NOT NULL DEFAULT 0" + ) + } + } } } @ExperimentalCoroutinesApi @@ -140,7 +158,7 @@ class DaoTest { val context = ApplicationProvider.getApplicationContext() brainzPlayerDatabase = Room .inMemoryDatabaseBuilder(context, BrainzPlayerDatabase::class.java) - .addMigrations(MIGRATION_1_2) + .addMigrations(Migrations.MIGRATION_1_2) .build() albumDao = brainzPlayerDatabase.albumDao() artistDao = brainzPlayerDatabase.artistDao() From c45459bf67749ae30fccd59c6760958d5294e24a Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Mon, 1 Apr 2024 18:59:24 +0530 Subject: [PATCH 15/97] Add listen card component for BP, integrate play button in songs screen --- .../ui/components/BrainzPlayerListenCard.kt | 153 ++++++++++++++++++ .../brainzplayer/AlbumsOverviewScreen.kt | 26 ++- .../brainzplayer/ArtistsOverviewScreen.kt | 29 ++-- .../brainzplayer/BrainzPlayerScreen.kt | 26 ++- .../brainzplayer/RecentPlaysOverviewScreen.kt | 36 +++-- .../brainzplayer/SongsOverviewScreen.kt | 20 +-- .../drawable/brainz_player_play_button.xml | 12 ++ 7 files changed, 238 insertions(+), 64 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerListenCard.kt create mode 100644 app/src/main/res/drawable/brainz_player_play_button.xml diff --git a/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerListenCard.kt b/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerListenCard.kt new file mode 100644 index 00000000..ff9ba490 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerListenCard.kt @@ -0,0 +1,153 @@ +package org.listenbrainz.android.ui.components + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.annotation.DrawableRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.layout.width +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import org.listenbrainz.android.R +import org.listenbrainz.android.ui.theme.ListenBrainzTheme + +@Composable +fun BrainzPlayerListenCard( + modifier: Modifier = Modifier, + title: String, + subTitle: String, + coverArtUrl: String?, + @DrawableRes errorAlbumArt: Int = R.drawable.ic_coverartarchive_logo_no_text, + onDropdownIconClick: () -> Unit = {}, + dropDown: @Composable () -> Unit = {}, + dropDownState: Boolean = false, + onPlayIconClick: () -> Unit +) { + Surface( + modifier = modifier + .fillMaxWidth(), + shape = ListenBrainzTheme.shapes.listenCardSmall, + shadowElevation = 4.dp, + color = ListenBrainzTheme.colorScheme.level1 + ) { + Column { + + Box( + modifier = Modifier + .fillMaxWidth() + .height(ListenBrainzTheme.sizes.listenCardHeight), + contentAlignment = Alignment.CenterStart + ) { + + Row( + modifier = Modifier.fillMaxWidth(0.7f), + verticalAlignment = Alignment.CenterVertically, + ) { + + AlbumArt(coverArtUrl, errorAlbumArt) + + Spacer(modifier = Modifier.width(ListenBrainzTheme.paddings.coverArtAndTextGap)) + + TitleAndSubtitle( + modifier = Modifier.padding(end = 6.dp), + title = title, + subtitle = subTitle + ) + } + Box(modifier = Modifier + .fillMaxWidth(0.275f) + .align(Alignment.CenterEnd)){ + DropdownButton (modifier = Modifier.align(Alignment.Center)) { + + } + PlayButton (modifier = Modifier.align(Alignment.CenterEnd)) { + onPlayIconClick() + } + } + + + + + + } + } + } +} + +@Composable +private fun DropdownButton(modifier: Modifier = Modifier, onDropdownIconClick: () -> Unit) { + + IconButton( + modifier = modifier, + onClick = onDropdownIconClick + ) { + Icon( + painter = painterResource(id = R.drawable.ic_options), + contentDescription = "", + tint = ListenBrainzTheme.colorScheme.hint, + modifier = Modifier.padding(horizontal = ListenBrainzTheme.paddings.insideCard) + ) + } +} + +@Composable +private fun PlayButton(modifier: Modifier = Modifier, onPlayIconClick: () -> Unit) { + + IconButton( + modifier = modifier, + onClick = onPlayIconClick + ) { + Icon( + painter = painterResource(id = R.drawable.brainz_player_play_button), + contentDescription = "", + tint = ListenBrainzTheme.colorScheme.hint, + modifier = Modifier.padding(horizontal = ListenBrainzTheme.paddings.insideCard) + ) + } +} + +@Composable +private fun AlbumArt( + coverArtUrl: String?, + errorAlbumArt: Int = R.drawable.ic_coverartarchive_logo_no_text +) { + // Use this for previews + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(coverArtUrl) + .build(), + fallback = painterResource(id = errorAlbumArt), + modifier = Modifier.size(ListenBrainzTheme.sizes.listenCardHeight), + contentScale = ContentScale.Fit, + placeholder = painterResource(id = errorAlbumArt), + filterQuality = FilterQuality.Low, + contentDescription = "Album Cover Art" + ) +} + + diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt index 80397b2c..e08abb9f 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt @@ -26,6 +26,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import org.listenbrainz.android.R import org.listenbrainz.android.model.Album import org.listenbrainz.android.model.Artist +import org.listenbrainz.android.ui.components.BrainzPlayerListenCard import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.viewmodel.ArtistViewModel @@ -47,9 +48,11 @@ fun AlbumsOverViewScreen( for (i in 0..25) { val startingLetter: Char = ('A' + i) if (albumsStarting[startingLetter]!!.size > 0) { - Column(modifier = Modifier.background( - brush = ListenBrainzTheme.colorScheme.gradientBrush - ).padding(top = 15.dp, bottom = 15.dp)) { + Column(modifier = Modifier + .background( + brush = ListenBrainzTheme.colorScheme.gradientBrush + ) + .padding(top = 15.dp, bottom = 15.dp)) { Text( startingLetter.toString(), modifier = Modifier.padding(start = 10.dp, top = 10.dp, bottom = 5.dp), @@ -62,17 +65,12 @@ fun AlbumsOverViewScreen( for (j in 1..albumsStarting[startingLetter]!!.size) { var coverArt: String? = null coverArt = albumsStarting[startingLetter]!![j - 1].albumArt - ListenCardSmall( - trackName = albumsStarting[startingLetter]!![j - 1].title, - artistName = albumsStarting[startingLetter]!![j - 1].artist, - coverArtUrl = coverArt, - modifier = Modifier.padding( - start = 10.dp, - end = 10.dp, - top = 3.dp, - bottom = 3.dp - ) - ) { + BrainzPlayerListenCard(title = albumsStarting[startingLetter]!![j - 1].title, subTitle = albumsStarting[startingLetter]!![j - 1].artist, coverArtUrl = coverArt,modifier = Modifier.padding( + start = 10.dp, + end = 10.dp, + top = 3.dp, + bottom = 3.dp + )){ } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt index 2067161d..06cd6ee9 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.listenbrainz.android.R import org.listenbrainz.android.model.Artist +import org.listenbrainz.android.ui.components.BrainzPlayerListenCard import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.theme.ListenBrainzTheme @@ -39,9 +40,11 @@ fun ArtistsOverviewScreen( val startingLetter: Char = ('A' + i) if (artistsStarting[startingLetter]!!.size > 0) { Column( - modifier = Modifier.background( - brush = ListenBrainzTheme.colorScheme.gradientBrush - ).padding(top = 15.dp, bottom = 15.dp) + modifier = Modifier + .background( + brush = ListenBrainzTheme.colorScheme.gradientBrush + ) + .padding(top = 15.dp, bottom = 15.dp) ) { Text( startingLetter.toString(), @@ -56,22 +59,10 @@ fun ArtistsOverviewScreen( var coverArt: String? = null if (artistsStarting[startingLetter]!![j - 1].albums.isNotEmpty()) coverArt = artistsStarting[startingLetter]!![j - 1].albums[0].albumArt - ListenCardSmall( - trackName = artistsStarting[startingLetter]!![j - 1].name, - artistName = when (artistsStarting[startingLetter]!![j - 1].songs.size) { - 1 -> "1 track" - else -> "${artistsStarting[startingLetter]!![j - 1].songs.size} tracks" - }, - coverArtUrl = coverArt, - modifier = Modifier.padding( - start = 10.dp, - end = 10.dp, - top = 3.dp, - bottom = 3.dp - ) - ) { - - } + BrainzPlayerListenCard(title = artistsStarting[startingLetter]!![j - 1].name, subTitle = when (artistsStarting[startingLetter]!![j - 1].songs.size) { + 1 -> "1 track" + else -> "${artistsStarting[startingLetter]!![j - 1].songs.size} tracks" + }, coverArtUrl = coverArt, onPlayIconClick = {}) } } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index f4dba5c0..34f90d84 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -153,13 +153,35 @@ fun BrainzPlayerHomeScreen( ) 1 -> RecentPlaysScreen( songsPlayedToday = songsPlayedToday, - songsPlayedThisWeek = songsPlayedThisWeek + songsPlayedThisWeek = songsPlayedThisWeek, + onPlayIconClick = { + song, newPlayables -> +// brainzPlayerViewModel.changePlayable( +// newPlayables, +// PlayableType.ALL_SONGS, +// song.mediaID, +// newPlayables.sortedBy { it.discNumber }.indexOf(song), +// 0L +// ) +// brainzPlayerViewModel.playOrToggleSong(song,true) +// Log.v("pranav",song.title) + } ) 2 -> ArtistsOverviewScreen( artists = artists ) 3 -> AlbumsOverViewScreen(albums = albums) - 4 -> SongsOverviewScreen(songs = songs) + 4 -> SongsOverviewScreen(songs = songs, onPlayIconClick = { + song , newPlayables -> + brainzPlayerViewModel.changePlayable( + newPlayables, + PlayableType.ALL_SONGS, + song.mediaID, + newPlayables.sortedBy { it.discNumber }.indexOf(song), + 0L + ) + brainzPlayerViewModel.playOrToggleSong(song,true) + }) } } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt index 7100b2cb..728688bb 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.listenbrainz.android.model.Song +import org.listenbrainz.android.ui.components.BrainzPlayerListenCard import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.theme.ListenBrainzTheme @@ -24,41 +25,47 @@ import org.listenbrainz.android.ui.theme.ListenBrainzTheme fun RecentPlaysScreen( songsPlayedToday: List, songsPlayedThisWeek: List, + onPlayIconClick: (Song, List) -> Unit ) { Column (modifier = Modifier .fillMaxSize() .padding(start = 17.dp, end = 17.dp)) { if(songsPlayedToday.isNotEmpty()){ - Column(modifier = Modifier.background( - brush = ListenBrainzTheme.colorScheme.gradientBrush - ).padding(top = 15.dp, bottom = 15.dp)) { + Column(modifier = Modifier + .background( + brush = ListenBrainzTheme.colorScheme.gradientBrush + ) + .padding(top = 15.dp, bottom = 15.dp)) { Text( "Played Today", color = ListenBrainzTheme.colorScheme.lbSignature, fontSize = 25.sp ) Spacer(modifier = Modifier.height(10.dp)) - PlayedToday(songsPlayedToday = songsPlayedToday) + PlayedToday(songsPlayedToday = songsPlayedToday, onPlayIconClick = onPlayIconClick) } } if(songsPlayedThisWeek.isNotEmpty()) { - Column(modifier = Modifier.background( - brush = ListenBrainzTheme.colorScheme.gradientBrush - ).padding(top = 15.dp, bottom = 15.dp)) { + Column(modifier = Modifier + .background( + brush = ListenBrainzTheme.colorScheme.gradientBrush + ) + .padding(top = 15.dp, bottom = 15.dp)) { Text( "Played This Week", color = ListenBrainzTheme.colorScheme.lbSignature, fontSize = 25.sp ) Spacer(modifier = Modifier.height(10.dp)) - PlayedThisWeek(songsPlayedThisWeek = songsPlayedThisWeek) + PlayedThisWeek(songsPlayedThisWeek = songsPlayedThisWeek, onPlayIconClick = onPlayIconClick) } } } } @Composable private fun PlayedToday( - songsPlayedToday: List + songsPlayedToday: List, + onPlayIconClick: (Song, List) -> Unit ){ var heightConstraint = ListenBrainzTheme.sizes.listenCardHeight * songsPlayedToday.size + 20.dp if(songsPlayedToday.size > 4) heightConstraint = 250.dp @@ -66,9 +73,7 @@ private fun PlayedToday( heightConstraint )) { items(songsPlayedToday){ - ListenCardSmall(trackName = it.title, artistName = it.artist, coverArtUrl = it.albumArt, enableDropdownIcon = true) { - Unit - } + BrainzPlayerListenCard(title = it.title, subTitle = it.artist, coverArtUrl = it.albumArt, onPlayIconClick = {onPlayIconClick(it,songsPlayedToday)}) Spacer(modifier = Modifier.height(5.dp)) } } @@ -76,7 +81,8 @@ private fun PlayedToday( @Composable private fun PlayedThisWeek( - songsPlayedThisWeek: List + songsPlayedThisWeek: List, + onPlayIconClick: (Song, List) -> Unit ){ var heightConstraint = ListenBrainzTheme.sizes.listenCardHeight * songsPlayedThisWeek.size + 20.dp if(songsPlayedThisWeek.size > 4) heightConstraint = 250.dp @@ -84,9 +90,7 @@ private fun PlayedThisWeek( heightConstraint )) { items(songsPlayedThisWeek){ - ListenCardSmall(trackName = it.title, artistName = it.artist, coverArtUrl = it.albumArt, enableDropdownIcon = true) { - Unit - } + BrainzPlayerListenCard(title = it.title, subTitle = it.artist, coverArtUrl = it.albumArt, onPlayIconClick = {onPlayIconClick(it,songsPlayedThisWeek)}) Spacer(modifier = Modifier.height(5.dp)) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt index cf9ed1cf..2810428f 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt @@ -27,13 +27,15 @@ import org.listenbrainz.android.R import org.listenbrainz.android.model.Album import org.listenbrainz.android.model.Artist import org.listenbrainz.android.model.Song +import org.listenbrainz.android.ui.components.BrainzPlayerListenCard import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.viewmodel.ArtistViewModel @Composable fun SongsOverviewScreen( - songs: List + songs: List, + onPlayIconClick: (Song, List) -> Unit, ) { val songsStarting : MutableMap> = mutableMapOf() for (i in 0..25) { @@ -63,20 +65,12 @@ fun SongsOverviewScreen( ) ) for (j in 1..songsStarting[startingLetter]!!.size) { + val song: Song = songsStarting[startingLetter]!![j-1] var coverArt: String? = null coverArt = songsStarting[startingLetter]!![j - 1].albumArt - ListenCardSmall( - trackName = songsStarting[startingLetter]!![j - 1].title, - artistName = songsStarting[startingLetter]!![j - 1].artist, - coverArtUrl = coverArt, - modifier = Modifier.padding( - start = 10.dp, - end = 10.dp, - top = 3.dp, - bottom = 3.dp - ) - ) { - + BrainzPlayerListenCard(title = songsStarting[startingLetter]!![j - 1].title, subTitle = songsStarting[startingLetter]!![j - 1].artist, coverArtUrl = coverArt){ + Log.v("pranav", song.title) + onPlayIconClick(song,songsStarting[startingLetter]!!) } } } diff --git a/app/src/main/res/drawable/brainz_player_play_button.xml b/app/src/main/res/drawable/brainz_player_play_button.xml new file mode 100644 index 00000000..b38c1852 --- /dev/null +++ b/app/src/main/res/drawable/brainz_player_play_button.xml @@ -0,0 +1,12 @@ + + + + From 29afe2e4c3dfd97745b3a182696180b64a720317 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:38:49 +0530 Subject: [PATCH 16/97] Fix out of bounds exception --- .../android/ui/screens/brainzplayer/BrainzPlayerScreen.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index 34f90d84..a36469dc 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -66,9 +66,9 @@ fun BrainzPlayerScreen() { val playlists by playlistViewModel.playlists.collectAsState(initial = listOf()) val songsPlayedToday = brainzPlayerViewModel.songsPlayedToday.collectAsState(initial = listOf()).value val recentlyPlayed = brainzPlayerViewModel.recentlyPlayed.collectAsState(initial = mutableListOf()).value - val topRecents = recentlyPlayed.subList(0, minOf(recentlyPlayed.size , 5)).toMutableList() - val topArtists = artists.subList(0, minOf(artists.size,5)).toMutableList() - val topAlbums = albums.subList(0, minOf(songs.size,5)).toMutableList() + val topRecents = recentlyPlayed.subList(0, minOf(recentlyPlayed.size-1 , 4)).toMutableList() + val topArtists = artists.subList(0, minOf(artists.size-1,4)).toMutableList() + val topAlbums = albums.subList(0, minOf(songs.size-1,4)).toMutableList() val songsPlayedThisWeek = brainzPlayerViewModel.songsPlayedThisWeek.collectAsState(initial = listOf()).value topRecents.add(Song()) topArtists.add(Artist()) From fb069ebc496a1b44945369e167c8c6cb7d783310 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:49:26 +0530 Subject: [PATCH 17/97] Fix wrong usage of songs instead of albums --- .../android/ui/screens/brainzplayer/BrainzPlayerScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index a36469dc..2d2a5cb6 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -68,7 +68,7 @@ fun BrainzPlayerScreen() { val recentlyPlayed = brainzPlayerViewModel.recentlyPlayed.collectAsState(initial = mutableListOf()).value val topRecents = recentlyPlayed.subList(0, minOf(recentlyPlayed.size-1 , 4)).toMutableList() val topArtists = artists.subList(0, minOf(artists.size-1,4)).toMutableList() - val topAlbums = albums.subList(0, minOf(songs.size-1,4)).toMutableList() + val topAlbums = albums.subList(0, minOf(albums.size-1,4)).toMutableList() val songsPlayedThisWeek = brainzPlayerViewModel.songsPlayedThisWeek.collectAsState(initial = listOf()).value topRecents.add(Song()) topArtists.add(Artist()) From e1c725feca107560f9f8dbf711383433360a9684 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Wed, 3 Apr 2024 08:41:27 +0530 Subject: [PATCH 18/97] Use take for getting first 5 items in list --- .../android/ui/screens/brainzplayer/BrainzPlayerScreen.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index 2d2a5cb6..e68bb54d 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -66,9 +66,9 @@ fun BrainzPlayerScreen() { val playlists by playlistViewModel.playlists.collectAsState(initial = listOf()) val songsPlayedToday = brainzPlayerViewModel.songsPlayedToday.collectAsState(initial = listOf()).value val recentlyPlayed = brainzPlayerViewModel.recentlyPlayed.collectAsState(initial = mutableListOf()).value - val topRecents = recentlyPlayed.subList(0, minOf(recentlyPlayed.size-1 , 4)).toMutableList() - val topArtists = artists.subList(0, minOf(artists.size-1,4)).toMutableList() - val topAlbums = albums.subList(0, minOf(albums.size-1,4)).toMutableList() + val topRecents = recentlyPlayed.take(5).toMutableList() + val topArtists = artists.take(5).toMutableList() + val topAlbums = albums.take(5).toMutableList() val songsPlayedThisWeek = brainzPlayerViewModel.songsPlayedThisWeek.collectAsState(initial = listOf()).value topRecents.add(Song()) topArtists.add(Artist()) From 4183f846b8a3aad643012e1c41b00e698d43fa24 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sun, 7 Apr 2024 17:16:21 +0530 Subject: [PATCH 19/97] Change Local player to player, make song primary text --- .../android/model/AppNavigationItem.kt | 2 +- .../ui/screens/brainzplayer/OverviewScreen.kt | 8 ++++---- .../screens/brainzplayer/SongsOverviewScreen.kt | 15 --------------- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt b/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt index 0d1fc3ce..63947019 100644 --- a/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt +++ b/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt @@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes import org.listenbrainz.android.R sealed class AppNavigationItem(val route: String, @DrawableRes val iconUnselected: Int, @DrawableRes val iconSelected: Int, val title: String) { - object BrainzPlayer : AppNavigationItem("brainzplayer", R.drawable.player_unselected, R.drawable.player_selected, "Local Player") + object BrainzPlayer : AppNavigationItem("brainzplayer", R.drawable.player_unselected, R.drawable.player_selected, "Player") object Explore : AppNavigationItem("explore", R.drawable.explore_unselected, R.drawable.explore_selected, "Explore") object Profile : AppNavigationItem("profile", R.drawable.profile_unselected, R.drawable.profile_selected, "Profile") object Feed : AppNavigationItem("feed", R.drawable.feed_unselected, R.drawable.feed_selected, "Feed") diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt index 7d114efb..c42c5b35 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt @@ -82,8 +82,8 @@ private fun RecentlyPlayedOverview( if (song.title != "") { BrainzPlayerActivityCards(icon = song.albumArt, errorIcon = R.drawable.ic_artist, - title = song.title, - artist = song.artist, + title = song.artist, + artist = song.title, modifier = Modifier .clickable { brainzPlayerViewModel.changePlayable( @@ -201,8 +201,8 @@ private fun AlbumsOverview( if(album.title != ""){ BrainzPlayerActivityCards(icon = album.albumArt, errorIcon = R.drawable.ic_artist, - title = album.title, - artist = album.artist, + title = album.artist, + artist = album.title, ) } else{ diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt index 2810428f..3ddb1714 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt @@ -1,36 +1,22 @@ package org.listenbrainz.android.ui.screens.brainzplayer -import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.content.res.ResourcesCompat -import androidx.hilt.navigation.compose.hiltViewModel import org.listenbrainz.android.R -import org.listenbrainz.android.model.Album -import org.listenbrainz.android.model.Artist import org.listenbrainz.android.model.Song import org.listenbrainz.android.ui.components.BrainzPlayerListenCard -import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.theme.ListenBrainzTheme -import org.listenbrainz.android.viewmodel.ArtistViewModel @Composable fun SongsOverviewScreen( @@ -69,7 +55,6 @@ fun SongsOverviewScreen( var coverArt: String? = null coverArt = songsStarting[startingLetter]!![j - 1].albumArt BrainzPlayerListenCard(title = songsStarting[startingLetter]!![j - 1].title, subTitle = songsStarting[startingLetter]!![j - 1].artist, coverArtUrl = coverArt){ - Log.v("pranav", song.title) onPlayIconClick(song,songsStarting[startingLetter]!!) } } From f84475d1f04283b3f223469878095357aeffced0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 00:48:06 +0000 Subject: [PATCH 20/97] Bump yutailang0119/action-android-lint from 3 to 4 Bumps [yutailang0119/action-android-lint](https://github.com/yutailang0119/action-android-lint) from 3 to 4. - [Release notes](https://github.com/yutailang0119/action-android-lint/releases) - [Commits](https://github.com/yutailang0119/action-android-lint/compare/v3...v4) --- updated-dependencies: - dependency-name: yutailang0119/action-android-lint dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 00c6a9b7..eec7e13d 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -32,7 +32,7 @@ jobs: run: ./gradlew lint - name: Check lint results - uses: yutailang0119/action-android-lint@v3 + uses: yutailang0119/action-android-lint@v4 with: report-path: build/reports/*.xml continue-on-error: false \ No newline at end of file From 6b3168385e7141e4735a2162a9bd0f596bb969ae Mon Sep 17 00:00:00 2001 From: Akshat Tiwari <51470769+akshaaatt@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:58:41 -0700 Subject: [PATCH 21/97] Bump dependencies --- app/build.gradle | 30 ++++++++++++------------ build.gradle | 6 ++--- gradle/wrapper/gradle-wrapper.properties | 2 +- sharedTest/build.gradle | 4 ++-- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 23556267..669b4a88 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,7 @@ plugins { id 'kotlin-kapt' id 'com.google.devtools.ksp' id 'dagger.hilt.android.plugin' - id "io.sentry.android.gradle" version "4.3.0" + id "io.sentry.android.gradle" version "4.4.0" } def keystorePropertiesFile = rootProject.file("keystore.properties") @@ -134,7 +134,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.7.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation 'androidx.browser:browser:1.7.0' + implementation 'androidx.browser:browser:1.8.0' implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.core:core-splashscreen:1.0.1' implementation "androidx.datastore:datastore-preferences:1.0.0" @@ -142,9 +142,9 @@ dependencies { //Web Service Setup implementation 'com.google.code.gson:gson:2.10.1' - implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:retrofit:2.11.0' implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12' - implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.11.0' implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12' implementation 'androidx.paging:paging-runtime-ktx:3.2.1' implementation "androidx.paging:paging-compose:3.2.1" @@ -153,7 +153,7 @@ dependencies { //Image downloading and Caching library implementation 'com.github.bumptech.glide:glide:4.16.0' implementation 'com.github.bumptech.glide:compose:1.0.0-beta01' - implementation 'io.coil-kt:coil-compose:2.5.0' + implementation 'io.coil-kt:coil-compose:2.6.0' implementation 'com.caverock:androidsvg-aar:1.4' ksp 'com.github.bumptech.glide:compiler:4.16.0' @@ -162,23 +162,23 @@ dependencies { //Design Setup implementation 'com.google.android.material:material:1.11.0' - implementation 'com.airbnb.android:lottie:6.3.0' + implementation 'com.airbnb.android:lottie:6.4.0' implementation 'com.github.akshaaatt:Onboarding:1.1.3' implementation 'com.github.akshaaatt:Share-Android:1.0.0' - implementation 'androidx.hilt:hilt-navigation-compose:1.1.0' - implementation 'com.airbnb.android:lottie-compose:6.3.0' + implementation 'androidx.hilt:hilt-navigation-compose:1.2.0' + implementation 'com.airbnb.android:lottie-compose:6.4.0' //Dagger-Hilt implementation("com.google.dagger:hilt-android:$hilt_version") kapt("com.google.dagger:hilt-android-compiler:$hilt_version") - kapt('androidx.hilt:hilt-compiler:1.1.0') - implementation 'androidx.hilt:hilt-work:1.1.0' + kapt('androidx.hilt:hilt-compiler:1.2.0') + implementation 'androidx.hilt:hilt-work:1.2.0' implementation "androidx.startup:startup-runtime:1.1.1" androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version" kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version" //Jetpack Compose - implementation platform('androidx.compose:compose-bom:2024.02.00') + implementation platform('androidx.compose:compose-bom:2024.04.00') implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-tooling' @@ -208,18 +208,18 @@ dependencies { //Test Setup implementation 'androidx.test.ext:junit-ktx:1.1.5' - implementation 'app.cash.turbine:turbine:1.0.0' + implementation 'app.cash.turbine:turbine:1.1.0' testImplementation 'junit:junit:4.13.2' testImplementation 'com.squareup.okhttp3:mockwebserver:5.0.0-alpha.12' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0' testImplementation 'androidx.arch.core:core-testing:2.2.0' // Mockito framework - testImplementation 'org.mockito:mockito-core:5.10.0' - testImplementation 'org.mockito.kotlin:mockito-kotlin:5.2.1' + testImplementation 'org.mockito:mockito-core:5.11.0' + testImplementation 'org.mockito.kotlin:mockito-kotlin:5.3.1' debugImplementation "androidx.test:monitor:1.6.1" // Solves "class PlatformTestStorageRegistery not found" error for ui tests. - debugImplementation 'androidx.compose.ui:ui-test-manifest:1.6.1' + debugImplementation 'androidx.compose.ui:ui-test-manifest:1.6.5' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" androidTestImplementation "androidx.work:work-testing:$work_version" diff --git a/build.gradle b/build.gradle index 84bc209d..4de9fdf7 100644 --- a/build.gradle +++ b/build.gradle @@ -4,8 +4,8 @@ buildscript { ext { kotlin_version = '1.9.22' navigationVersion = '2.7.7' - hilt_version = '2.50' - compose_version = '1.6.1' + hilt_version = '2.51.1' + compose_version = '1.6.5' room_version = '2.6.1' accompanist_version = '0.34.0' work_version = '2.9.0' @@ -17,7 +17,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.2.2' + classpath 'com.android.tools.build:gradle:8.3.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e393b529..71a11b57 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Aug 12 11:38:52 IST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/sharedTest/build.gradle b/sharedTest/build.gradle index e5cb41e0..e8e76f59 100644 --- a/sharedTest/build.gradle +++ b/sharedTest/build.gradle @@ -34,9 +34,9 @@ dependencies { //Web Service Setup implementation 'com.google.code.gson:gson:2.10.1' - implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:retrofit:2.11.0' implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12' - implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.11.0' //Test Setup implementation 'junit:junit:4.13.2' From 6c343e768d1563edb126d62f7a09eda20da56931 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Apr 2024 17:00:23 +0000 Subject: [PATCH 22/97] Bump io.sentry.android.gradle from 4.3.0 to 4.4.1 Bumps [io.sentry.android.gradle](https://github.com/getsentry/sentry-android-gradle-plugin) from 4.3.0 to 4.4.1. - [Release notes](https://github.com/getsentry/sentry-android-gradle-plugin/releases) - [Changelog](https://github.com/getsentry/sentry-android-gradle-plugin/blob/main/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-android-gradle-plugin/compare/4.3.0...4.4.1) --- updated-dependencies: - dependency-name: io.sentry.android.gradle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 669b4a88..4911a271 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,7 @@ plugins { id 'kotlin-kapt' id 'com.google.devtools.ksp' id 'dagger.hilt.android.plugin' - id "io.sentry.android.gradle" version "4.4.0" + id "io.sentry.android.gradle" version "4.4.1" } def keystorePropertiesFile = rootProject.file("keystore.properties") From 8b6f9a334b4a7364979f257c616588b1dfccaaec Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Tue, 23 Apr 2024 22:20:06 +0530 Subject: [PATCH 23/97] Rectified play button issue --- .../brainzplayer/BrainzPlayerScreen.kt | 49 +++---------------- .../ui/screens/brainzplayer/OverviewScreen.kt | 8 +-- 2 files changed, 9 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index e68bb54d..141122d8 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -1,50 +1,18 @@ package org.listenbrainz.android.ui.screens.brainzplayer -import android.util.Log import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.rounded.ArrowForwardIos -import androidx.compose.material3.DropdownMenu import androidx.compose.material3.ElevatedSuggestionChip import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SuggestionChipDefaults import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.inset -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.PopupProperties import androidx.hilt.navigation.compose.hiltViewModel -import coil.compose.AsyncImage -import kotlinx.coroutines.launch -import org.listenbrainz.android.R import org.listenbrainz.android.model.* -import org.listenbrainz.android.ui.components.forwardingPainter import org.listenbrainz.android.ui.screens.brainzplayer.navigation.Navigation import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.viewmodel.* @@ -156,15 +124,14 @@ fun BrainzPlayerHomeScreen( songsPlayedThisWeek = songsPlayedThisWeek, onPlayIconClick = { song, newPlayables -> -// brainzPlayerViewModel.changePlayable( -// newPlayables, -// PlayableType.ALL_SONGS, -// song.mediaID, -// newPlayables.sortedBy { it.discNumber }.indexOf(song), -// 0L -// ) -// brainzPlayerViewModel.playOrToggleSong(song,true) -// Log.v("pranav",song.title) + brainzPlayerViewModel.changePlayable( + newPlayables, + PlayableType.ALL_SONGS, + song.mediaID, + newPlayables.indexOf(song), + 0L + ) + brainzPlayerViewModel.playOrToggleSong(song,true) } ) 2 -> ArtistsOverviewScreen( diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt index c42c5b35..9e9613aa 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt @@ -1,6 +1,5 @@ package org.listenbrainz.android.ui.screens.brainzplayer -import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -19,10 +18,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.colorResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -32,7 +27,6 @@ import org.listenbrainz.android.R import org.listenbrainz.android.model.Album import org.listenbrainz.android.model.Artist import org.listenbrainz.android.model.PlayableType -import org.listenbrainz.android.model.Playlist import org.listenbrainz.android.model.Song import org.listenbrainz.android.ui.components.BrainzPlayerActivityCards import org.listenbrainz.android.ui.theme.ListenBrainzTheme @@ -90,7 +84,7 @@ private fun RecentlyPlayedOverview( recentlyPlayedSongs, PlayableType.ALL_SONGS, song.mediaID, - recentlyPlayedSongs.sortedBy { it.discNumber }.indexOf(song), + recentlyPlayedSongs.indexOf(song), 0L ) brainzPlayerViewModel.playOrToggleSong(song, true) From 8f9ca66b5beb6da35b9c9dc1a10ef0d0c099abe5 Mon Sep 17 00:00:00 2001 From: wileyfoxyx Date: Sat, 4 May 2024 20:09:32 +0000 Subject: [PATCH 24/97] Added translation using Weblate (Russian) Co-authored-by: wileyfoxyx --- app/src/main/res/values-ru/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-ru/strings.xml diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/app/src/main/res/values-ru/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From dcc496e0d575cf856fde0f22ef76de7b8beae2cc Mon Sep 17 00:00:00 2001 From: Fill read-only add-on Date: Sat, 4 May 2024 20:09:49 +0000 Subject: [PATCH 25/97] Translated using Weblate (Russian) Currently translated at 12.5% (2 of 16 strings) Co-authored-by: Fill read-only add-on Translate-URL: https://translations.metabrainz.org/projects/listenbrainz-android/store-metadata/ru/ Translation: ListenBrainz Android/store-metadata --- fastlane/metadata/android/ru-RU/title.txt | 1 + fastlane/metadata/android/ru-RU/video.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/ru-RU/title.txt create mode 100644 fastlane/metadata/android/ru-RU/video.txt diff --git a/fastlane/metadata/android/ru-RU/title.txt b/fastlane/metadata/android/ru-RU/title.txt new file mode 100644 index 00000000..337e7b46 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/title.txt @@ -0,0 +1 @@ +ListenBrainz diff --git a/fastlane/metadata/android/ru-RU/video.txt b/fastlane/metadata/android/ru-RU/video.txt new file mode 100644 index 00000000..c03d283b --- /dev/null +++ b/fastlane/metadata/android/ru-RU/video.txt @@ -0,0 +1 @@ +https://youtu.be/-CVNe9gmG6c From c6194e66b8f3438e780d9ef0dc52674ea58c1f85 Mon Sep 17 00:00:00 2001 From: wileyfoxyx Date: Sun, 5 May 2024 00:42:28 +0000 Subject: [PATCH 26/97] Translated using Weblate (Russian) Currently translated at 16.4% (31 of 188 strings) Co-authored-by: wileyfoxyx Translate-URL: https://translations.metabrainz.org/projects/listenbrainz-android/app/ru/ Translation: ListenBrainz Android/app --- app/src/main/res/values-ru/strings.xml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index a6b3daec..245d59af 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,2 +1,20 @@ - \ No newline at end of file + + Выполнен вход как + Треки + Релизы + ListenBrainz + Пожертвовать + О приложении + Добро пожаловать в ListenBrainz! + Исполнитель + Релиз + Лейбл + Запись + Размер изображения + Появляется в релизах + Поиск в ListenBrainz + Новости ListenBrainz + Сканировать + Коллекции + \ No newline at end of file From 8120e30eb465a48f022b1798b51c692c6242e679 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Tue, 7 May 2024 15:18:44 +0530 Subject: [PATCH 27/97] Added play buttons for album overview page --- .../brainzplayer/AlbumsOverviewScreen.kt | 10 +++--- .../brainzplayer/BrainzPlayerScreen.kt | 32 ++++++++++++++++--- .../navigation/BrainzPlayerNavigation.kt | 2 ++ 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt index e08abb9f..6a6612a9 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt @@ -26,6 +26,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import org.listenbrainz.android.R import org.listenbrainz.android.model.Album import org.listenbrainz.android.model.Artist +import org.listenbrainz.android.model.Song import org.listenbrainz.android.ui.components.BrainzPlayerListenCard import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.theme.ListenBrainzTheme @@ -33,7 +34,8 @@ import org.listenbrainz.android.viewmodel.ArtistViewModel @Composable fun AlbumsOverViewScreen( - albums : List + albums : List, + onPlayIconClick: (Album) -> Unit ) { val albumsStarting: MutableMap> = mutableMapOf() for (i in 0..25) { @@ -70,9 +72,9 @@ fun AlbumsOverViewScreen( end = 10.dp, top = 3.dp, bottom = 3.dp - )){ - - } + ), onPlayIconClick = { + onPlayIconClick(albumsStarting[startingLetter]!![j-1]) + }) } } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index 141122d8..ffd1b016 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -1,6 +1,7 @@ package org.listenbrainz.android.ui.screens.brainzplayer +import android.util.Log import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material3.ElevatedSuggestionChip @@ -37,6 +38,13 @@ fun BrainzPlayerScreen() { val topRecents = recentlyPlayed.take(5).toMutableList() val topArtists = artists.take(5).toMutableList() val topAlbums = albums.take(5).toMutableList() + val albumSongsMap : MutableMap> = mutableMapOf() + for(i in 1..albums.size){ + val albumSongs : List = albumViewModel.getAllSongsOfAlbum(albums[i-1].albumId).collectAsState( + initial = listOf() + ).value + albumSongsMap[albums[i-1].albumId] = albumSongs + } val songsPlayedThisWeek = brainzPlayerViewModel.songsPlayedThisWeek.collectAsState(initial = listOf()).value topRecents.add(Song()) topArtists.add(Artist()) @@ -46,7 +54,7 @@ fun BrainzPlayerScreen() { modifier = Modifier .fillMaxSize() ) { - Navigation(albums = albums, previewAlbums = topAlbums, artists = artists, previewArtists = topArtists, playlists, songsPlayedToday, songsPlayedThisWeek ,topRecents ,songs) + Navigation(albums = albums, previewAlbums = topAlbums, artists = artists, previewArtists = topArtists, playlists, songsPlayedToday, songsPlayedThisWeek ,topRecents ,songs, albumSongsMap) } } @@ -61,6 +69,7 @@ fun BrainzPlayerHomeScreen( songsPlayedToday: List, songsPlayedThisWeek: List, recentlyPlayedSongs: List, + albumSongsMap: MutableMap>, brainzPlayerViewModel: BrainzPlayerViewModel = hiltViewModel(), ) { @@ -137,7 +146,23 @@ fun BrainzPlayerHomeScreen( 2 -> ArtistsOverviewScreen( artists = artists ) - 3 -> AlbumsOverViewScreen(albums = albums) + 3 -> AlbumsOverViewScreen(albums = albums, onPlayIconClick = { + album -> + val albumSongs = albumSongsMap[album.albumId]!! + Log.v("pranav",album.albumId.toString()) + brainzPlayerViewModel.changePlayable( + albumSongs.sortedBy { it.trackNumber }, + PlayableType.ALBUM, + album.albumId, + albumSongs + .sortedBy { it.trackNumber } + .indexOf (albumSongs[0]), + 0L + ) + brainzPlayerViewModel.playOrToggleSong(albumSongs[0],true) + + + }) 4 -> SongsOverviewScreen(songs = songs, onPlayIconClick = { song , newPlayables -> brainzPlayerViewModel.changePlayable( @@ -151,5 +176,4 @@ fun BrainzPlayerHomeScreen( }) } } -} - +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt index ea3d451c..5bc4929a 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt @@ -35,6 +35,7 @@ fun Navigation( songsPlayedThisWeek: List, recentlyPlayedSongs : List, songs: List, + albumSongsMap: MutableMap>, navHostController: NavHostController = rememberNavController() ) { @@ -52,6 +53,7 @@ fun Navigation( songsPlayedToday = songsPlayedToday, songsPlayedThisWeek = songsPlayedThisWeek, recentlyPlayedSongs = recentlyPlayedSongs, + albumSongsMap = albumSongsMap ) } composable(route = BrainzPlayerNavigationItem.Songs.route) { From 512994cf2f829a70c74dde1ff95f32bc3adb7652 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Wed, 8 May 2024 13:50:02 +0530 Subject: [PATCH 28/97] Added play functionality in artist overview screen --- .../brainzplayer/ArtistsOverviewScreen.kt | 10 ++++-- .../brainzplayer/BrainzPlayerScreen.kt | 31 ++++++++++++++----- .../navigation/BrainzPlayerNavigation.kt | 6 ++-- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt index 06cd6ee9..87849d4d 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt @@ -2,6 +2,8 @@ package org.listenbrainz.android.ui.screens.brainzplayer import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -24,7 +26,8 @@ import org.listenbrainz.android.ui.theme.ListenBrainzTheme @Composable fun ArtistsOverviewScreen( - artists: List + artists: List, + onPlayClick : (Artist) -> Unit ) { val artistsStarting: MutableMap> = mutableMapOf() for (i in 0..25) { @@ -62,7 +65,10 @@ fun ArtistsOverviewScreen( BrainzPlayerListenCard(title = artistsStarting[startingLetter]!![j - 1].name, subTitle = when (artistsStarting[startingLetter]!![j - 1].songs.size) { 1 -> "1 track" else -> "${artistsStarting[startingLetter]!![j - 1].songs.size} tracks" - }, coverArtUrl = coverArt, onPlayIconClick = {}) + }, coverArtUrl = coverArt, onPlayIconClick = { + onPlayClick(artistsStarting[startingLetter]!![j-1]) + }, modifier = Modifier.padding(start = 10.dp, end = 10.dp)) + Spacer(modifier = Modifier.height(10.dp)) } } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index ffd1b016..aad9b9f4 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -38,12 +38,17 @@ fun BrainzPlayerScreen() { val topRecents = recentlyPlayed.take(5).toMutableList() val topArtists = artists.take(5).toMutableList() val topAlbums = albums.take(5).toMutableList() - val albumSongsMap : MutableMap> = mutableMapOf() + val albumSongsMap : MutableMap> = mutableMapOf() + val artistSongsMap : MutableMap> = mutableMapOf() for(i in 1..albums.size){ val albumSongs : List = albumViewModel.getAllSongsOfAlbum(albums[i-1].albumId).collectAsState( initial = listOf() ).value - albumSongsMap[albums[i-1].albumId] = albumSongs + albumSongsMap[albums[i-1]] = albumSongs + } + for(i in 1..artists.size){ + val artistSongs : List = artistViewModel.getAllSongsOfArtist(artists[i-1]).collectAsState(initial = listOf()).value.distinctBy { it.mediaID } + artistSongsMap[artists[i-1]] = artistSongs } val songsPlayedThisWeek = brainzPlayerViewModel.songsPlayedThisWeek.collectAsState(initial = listOf()).value topRecents.add(Song()) @@ -54,7 +59,7 @@ fun BrainzPlayerScreen() { modifier = Modifier .fillMaxSize() ) { - Navigation(albums = albums, previewAlbums = topAlbums, artists = artists, previewArtists = topArtists, playlists, songsPlayedToday, songsPlayedThisWeek ,topRecents ,songs, albumSongsMap) + Navigation(albums = albums, previewAlbums = topAlbums, artists = artists, previewArtists = topArtists, playlists, songsPlayedToday, songsPlayedThisWeek ,topRecents ,songs, albumSongsMap, artistSongsMap) } } @@ -69,7 +74,8 @@ fun BrainzPlayerHomeScreen( songsPlayedToday: List, songsPlayedThisWeek: List, recentlyPlayedSongs: List, - albumSongsMap: MutableMap>, + albumSongsMap: MutableMap>, + artistSongsMap: MutableMap>, brainzPlayerViewModel: BrainzPlayerViewModel = hiltViewModel(), ) { @@ -144,12 +150,23 @@ fun BrainzPlayerHomeScreen( } ) 2 -> ArtistsOverviewScreen( - artists = artists + artists = artists, + onPlayClick = { + artist -> + val artistSongs = artistSongsMap[artist]!! + brainzPlayerViewModel.changePlayable( + artistSongs, + PlayableType.ARTIST, + artist.id, + 0, + 0L + ) + brainzPlayerViewModel.playOrToggleSong(artistSongs[0], true) + } ) 3 -> AlbumsOverViewScreen(albums = albums, onPlayIconClick = { album -> - val albumSongs = albumSongsMap[album.albumId]!! - Log.v("pranav",album.albumId.toString()) + val albumSongs = albumSongsMap[album]!! brainzPlayerViewModel.changePlayable( albumSongs.sortedBy { it.trackNumber }, PlayableType.ALBUM, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt index 5bc4929a..1d01c51e 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt @@ -35,7 +35,8 @@ fun Navigation( songsPlayedThisWeek: List, recentlyPlayedSongs : List, songs: List, - albumSongsMap: MutableMap>, + albumSongsMap: MutableMap>, + artistSongsMap: MutableMap>, navHostController: NavHostController = rememberNavController() ) { @@ -53,7 +54,8 @@ fun Navigation( songsPlayedToday = songsPlayedToday, songsPlayedThisWeek = songsPlayedThisWeek, recentlyPlayedSongs = recentlyPlayedSongs, - albumSongsMap = albumSongsMap + albumSongsMap = albumSongsMap, + artistSongsMap = artistSongsMap ) } composable(route = BrainzPlayerNavigationItem.Songs.route) { From 5a1eb18d696cf1ebfa2de5c787c31c98d8b83cb8 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Wed, 8 May 2024 14:53:01 +0530 Subject: [PATCH 29/97] Completed UI for overview pages, have to implement 3dot menu --- .../brainzplayer/{ => overview}/AlbumsOverviewScreen.kt | 3 +++ .../brainzplayer/{ => overview}/ArtistsOverviewScreen.kt | 0 .../screens/brainzplayer/{ => overview}/OverviewScreen.kt | 0 .../{ => overview}/RecentPlaysOverviewScreen.kt | 7 +++---- .../brainzplayer/{ => overview}/SongsOverviewScreen.kt | 5 ++++- 5 files changed, 10 insertions(+), 5 deletions(-) rename app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/{ => overview}/AlbumsOverviewScreen.kt (95%) rename app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/{ => overview}/ArtistsOverviewScreen.kt (100%) rename app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/{ => overview}/OverviewScreen.kt (100%) rename app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/{ => overview}/RecentPlaysOverviewScreen.kt (95%) rename app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/{ => overview}/SongsOverviewScreen.kt (86%) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/AlbumsOverviewScreen.kt similarity index 95% rename from app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt rename to app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/AlbumsOverviewScreen.kt index 6a6612a9..ee25133c 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/AlbumsOverviewScreen.kt @@ -3,6 +3,8 @@ package org.listenbrainz.android.ui.screens.brainzplayer import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -75,6 +77,7 @@ fun AlbumsOverViewScreen( ), onPlayIconClick = { onPlayIconClick(albumsStarting[startingLetter]!![j-1]) }) + Spacer(modifier = Modifier.height(10.dp)) } } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/ArtistsOverviewScreen.kt similarity index 100% rename from app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistsOverviewScreen.kt rename to app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/ArtistsOverviewScreen.kt diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/OverviewScreen.kt similarity index 100% rename from app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/OverviewScreen.kt rename to app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/OverviewScreen.kt diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/RecentPlaysOverviewScreen.kt similarity index 95% rename from app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt rename to app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/RecentPlaysOverviewScreen.kt index 728688bb..e32279af 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/RecentPlaysOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/RecentPlaysOverviewScreen.kt @@ -28,14 +28,13 @@ fun RecentPlaysScreen( onPlayIconClick: (Song, List) -> Unit ) { Column (modifier = Modifier - .fillMaxSize() - .padding(start = 17.dp, end = 17.dp)) { + .fillMaxSize()) { if(songsPlayedToday.isNotEmpty()){ Column(modifier = Modifier .background( brush = ListenBrainzTheme.colorScheme.gradientBrush ) - .padding(top = 15.dp, bottom = 15.dp)) { + .padding(top = 15.dp, bottom = 15.dp, start = 10.dp)) { Text( "Played Today", color = ListenBrainzTheme.colorScheme.lbSignature, @@ -50,7 +49,7 @@ fun RecentPlaysScreen( .background( brush = ListenBrainzTheme.colorScheme.gradientBrush ) - .padding(top = 15.dp, bottom = 15.dp)) { + .padding(top = 15.dp, bottom = 15.dp , start = 10.dp)) { Text( "Played This Week", color = ListenBrainzTheme.colorScheme.lbSignature, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/SongsOverviewScreen.kt similarity index 86% rename from app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt rename to app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/SongsOverviewScreen.kt index 3ddb1714..b1aa0a8d 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/SongsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/SongsOverviewScreen.kt @@ -2,6 +2,8 @@ package org.listenbrainz.android.ui.screens.brainzplayer import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -54,9 +56,10 @@ fun SongsOverviewScreen( val song: Song = songsStarting[startingLetter]!![j-1] var coverArt: String? = null coverArt = songsStarting[startingLetter]!![j - 1].albumArt - BrainzPlayerListenCard(title = songsStarting[startingLetter]!![j - 1].title, subTitle = songsStarting[startingLetter]!![j - 1].artist, coverArtUrl = coverArt){ + BrainzPlayerListenCard(modifier = Modifier.padding(start= 10.dp, end = 10.dp),title = songsStarting[startingLetter]!![j - 1].title, subTitle = songsStarting[startingLetter]!![j - 1].artist, coverArtUrl = coverArt){ onPlayIconClick(song,songsStarting[startingLetter]!!) } + Spacer(modifier = Modifier.height(10.dp)) } } } From c3c2e2ca836051b5d666f1f53112191e0236ce6b Mon Sep 17 00:00:00 2001 From: Akshat Tiwari <51470769+akshaaatt@users.noreply.github.com> Date: Sun, 12 May 2024 23:33:13 +0530 Subject: [PATCH 30/97] Added Logout button and bumped dependencies - Users can now logout, if need be. --- app/build.gradle | 18 +-- .../repository/preferences/AppPreferences.kt | 2 +- .../preferences/AppPreferencesImpl.kt | 3 +- .../ui/screens/settings/SettingsScreen.kt | 105 ++++++++++++------ .../android/viewmodel/SettingsViewModel.kt | 21 +++- build.gradle | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- sharedTest/build.gradle | 8 +- 8 files changed, 107 insertions(+), 56 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4911a271..223dcd3d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -137,15 +137,15 @@ dependencies { implementation 'androidx.browser:browser:1.8.0' implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.core:core-splashscreen:1.0.1' - implementation "androidx.datastore:datastore-preferences:1.0.0" + implementation 'androidx.datastore:datastore-preferences:1.1.1' implementation "androidx.work:work-runtime-ktx:$work_version" //Web Service Setup implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.squareup.retrofit2:retrofit:2.11.0' - implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12' + implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.14' implementation 'com.squareup.retrofit2:converter-gson:2.11.0' - implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12' + implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14' implementation 'androidx.paging:paging-runtime-ktx:3.2.1' implementation "androidx.paging:paging-compose:3.2.1" @@ -161,7 +161,7 @@ dependencies { implementation "com.google.accompanist:accompanist-permissions:$accompanist_version" //Design Setup - implementation 'com.google.android.material:material:1.11.0' + implementation 'com.google.android.material:material:1.12.0' implementation 'com.airbnb.android:lottie:6.4.0' implementation 'com.github.akshaaatt:Onboarding:1.1.3' implementation 'com.github.akshaaatt:Share-Android:1.0.0' @@ -178,7 +178,7 @@ dependencies { kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version" //Jetpack Compose - implementation platform('androidx.compose:compose-bom:2024.04.00') + implementation platform('androidx.compose:compose-bom:2024.05.00') implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-tooling' @@ -210,16 +210,16 @@ dependencies { implementation 'androidx.test.ext:junit-ktx:1.1.5' implementation 'app.cash.turbine:turbine:1.1.0' testImplementation 'junit:junit:4.13.2' - testImplementation 'com.squareup.okhttp3:mockwebserver:5.0.0-alpha.12' - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0' + testImplementation 'com.squareup.okhttp3:mockwebserver:5.0.0-alpha.14' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1' testImplementation 'androidx.arch.core:core-testing:2.2.0' // Mockito framework - testImplementation 'org.mockito:mockito-core:5.11.0' + testImplementation 'org.mockito:mockito-core:5.12.0' testImplementation 'org.mockito.kotlin:mockito-kotlin:5.3.1' debugImplementation "androidx.test:monitor:1.6.1" // Solves "class PlatformTestStorageRegistery not found" error for ui tests. - debugImplementation 'androidx.compose.ui:ui-test-manifest:1.6.5' + debugImplementation 'androidx.compose.ui:ui-test-manifest:1.6.7' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" androidTestImplementation "androidx.work:work-testing:$work_version" diff --git a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt index f1c4bf27..7cba9661 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferences.kt @@ -28,7 +28,7 @@ interface AppPreferences { var onboardingCompleted: Boolean - suspend fun logoutUser() + suspend fun logoutUser(): Boolean val version: String diff --git a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt index 274b4877..dcf9e842 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/preferences/AppPreferencesImpl.kt @@ -253,12 +253,13 @@ class AppPreferencesImpl(private val context: Context): AppPreferences { get() = preferences.getBoolean(ONBOARDING, false) set(value) = setBoolean(ONBOARDING, value) - override suspend fun logoutUser() = withContext(Dispatchers.IO) { + override suspend fun logoutUser(): Boolean = withContext(Dispatchers.IO) { val editor = preferences.edit() editor.remove(REFRESH_TOKEN) editor.remove(USERNAME) editor.apply() lbAccessToken.set("") + return@withContext true } override var currentPlayable : Playable? diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt index 7c864460..da67c9f8 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt @@ -1,9 +1,11 @@ package org.listenbrainz.android.ui.screens.settings +import android.app.Activity import android.content.Intent import android.content.res.Configuration import android.os.Build import android.provider.Settings +import android.widget.Toast import androidx.appcompat.app.AppCompatDelegate import androidx.compose.foundation.Image import androidx.compose.foundation.clickable @@ -50,6 +52,7 @@ import androidx.lifecycle.Lifecycle import com.limurse.logger.Logger import com.limurse.logger.util.FileIntent import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.listenbrainz.android.BuildConfig @@ -58,8 +61,11 @@ import org.listenbrainz.android.model.UiMode import org.listenbrainz.android.ui.components.Switch import org.listenbrainz.android.ui.screens.listens.ListeningAppsList import org.listenbrainz.android.ui.screens.main.DonateActivity +import org.listenbrainz.android.ui.screens.main.MainActivity import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.util.Constants +import org.listenbrainz.android.util.Resource +import org.listenbrainz.android.util.Utils.getActivity import org.listenbrainz.android.viewmodel.ListensViewModel import org.listenbrainz.android.viewmodel.SettingsViewModel @@ -84,7 +90,8 @@ fun SettingsScreen( .getFlow().collectAsState(initial = true) val shouldScrobbleNewPlayers by viewModel.appPreferences .shouldListenNewPlayers.getFlow().collectAsState(initial = true) - + val logoutStatus = viewModel.logoutStatus.collectAsState(initial = null).value + val lifecycleOwner = LocalLifecycleOwner.current val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState() LaunchedEffect(lifecycleState) { @@ -98,6 +105,17 @@ fun SettingsScreen( } } + LaunchedEffect(logoutStatus) { + if (logoutStatus == true) { + val intent = Intent(context, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + (context as? Activity)?.finish() + } + } + Column(modifier = Modifier .fillMaxSize() .padding(horizontal = 8.dp) @@ -320,22 +338,42 @@ fun SettingsScreen( Logger.apply { compressLogsInZipFile { zipFile -> zipFile?.let { - FileIntent.fromFile( - context, - it, - BuildConfig.APPLICATION_ID - )?.let { intent -> - intent.putExtra(Intent.EXTRA_SUBJECT, "Log Files") - intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("mobile@metabrainz.org")) - intent.putExtra(Intent.EXTRA_TEXT, "Please find the attached log files.") - intent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", zipFile)) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - try { - context.startActivity(Intent.createChooser(intent, "Email logs...")) - } catch (e: java.lang.Exception) { - e(throwable = e) + FileIntent + .fromFile( + context, + it, + BuildConfig.APPLICATION_ID + ) + ?.let { intent -> + intent.putExtra(Intent.EXTRA_SUBJECT, "Log Files") + intent.putExtra( + Intent.EXTRA_EMAIL, + arrayOf("mobile@metabrainz.org") + ) + intent.putExtra( + Intent.EXTRA_TEXT, + "Please find the attached log files." + ) + intent.putExtra( + Intent.EXTRA_STREAM, + FileProvider.getUriForFile( + context, + "${BuildConfig.APPLICATION_ID}.provider", + zipFile + ) + ) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + try { + context.startActivity( + Intent.createChooser( + intent, + "Email logs..." + ) + ) + } catch (e: java.lang.Exception) { + e(throwable = e) + } } - } } } } @@ -367,6 +405,25 @@ fun SettingsScreen( Divider(thickness = 1.dp) + if(viewModel.appPreferences.getLoginStatusFlow().collectAsState(initial = Constants.Strings.STATUS_LOGGED_IN).value == Constants.Strings.STATUS_LOGGED_IN) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(18.dp) + .clickable { + viewModel.logout() + }, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "Logout", + color = MaterialTheme.colorScheme.onSurface, + ) + } + } + + Divider(thickness = 1.dp) + Row( modifier = Modifier .fillMaxWidth() @@ -503,22 +560,6 @@ fun SettingsScreen( ) } - // TODO: Decide whether we need a logout button or not - // Row( - // modifier = Modifier - // .fillMaxWidth() - // .padding(18.dp) - // , - // verticalAlignment = Alignment.CenterVertically, - // ) { - // Text( - // text = "Logout", - // color = MaterialTheme.colorScheme.onSurface, - // ) - // } - // - // Divider(thickness = 1.dp) - // BlackList Dialog val uiState by listensViewModel.preferencesUiState.collectAsState() if (showBlacklist) { diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/SettingsViewModel.kt index d9b93f08..9bd1050d 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/SettingsViewModel.kt @@ -3,6 +3,9 @@ package org.listenbrainz.android.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.listenbrainz.android.repository.preferences.AppPreferences import javax.inject.Inject @@ -11,14 +14,20 @@ import javax.inject.Inject class SettingsViewModel @Inject constructor( val appPreferences: AppPreferences, ): ViewModel() { - - fun version(): String { - return appPreferences.version - } - + + private val _logoutStatus = MutableStateFlow(null) + val logoutStatus: StateFlow = _logoutStatus.asStateFlow() + fun logout() { viewModelScope.launch { - appPreferences.logoutUser() + // Execute the logout operation and capture the result + val result = appPreferences.logoutUser() + // Emit the result through the StateFlow + _logoutStatus.emit(result) } } + + fun version(): String { + return appPreferences.version + } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4de9fdf7..9a954212 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { kotlin_version = '1.9.22' navigationVersion = '2.7.7' hilt_version = '2.51.1' - compose_version = '1.6.5' + compose_version = '1.6.7' room_version = '2.6.1' accompanist_version = '0.34.0' work_version = '2.9.0' @@ -17,7 +17,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.3.2' + classpath 'com.android.tools.build:gradle:8.4.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 71a11b57..a787e51d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Aug 12 11:38:52 IST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/sharedTest/build.gradle b/sharedTest/build.gradle index e8e76f59..683e531d 100644 --- a/sharedTest/build.gradle +++ b/sharedTest/build.gradle @@ -30,19 +30,19 @@ android { dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.11.0' + implementation 'com.google.android.material:material:1.12.0' //Web Service Setup implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.squareup.retrofit2:retrofit:2.11.0' - implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12' + implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.14' implementation 'com.squareup.retrofit2:converter-gson:2.11.0' //Test Setup implementation 'junit:junit:4.13.2' - implementation 'com.squareup.okhttp3:mockwebserver:5.0.0-alpha.12' + implementation 'com.squareup.okhttp3:mockwebserver:5.0.0-alpha.14' implementation 'androidx.arch.core:core-testing:2.2.0' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1' implementation 'androidx.room:room-testing:2.6.1' debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" From 159291ee4c5da355243171888e22858052d33f26 Mon Sep 17 00:00:00 2001 From: Akshat Tiwari <51470769+akshaaatt@users.noreply.github.com> Date: Mon, 13 May 2024 00:28:34 +0530 Subject: [PATCH 31/97] Fix tests --- .../org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt index d89af250..8adbbc53 100644 --- a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockAppPreferences.kt @@ -96,7 +96,7 @@ class MockAppPreferences( } - override suspend fun logoutUser() { + override suspend fun logoutUser(): Boolean { TODO("Not yet implemented") } From ea0c433cfad056702545de40a016e329fe54fc4c Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Fri, 17 May 2024 14:54:39 +0530 Subject: [PATCH 32/97] Added play next in artist overview --- .../listenbrainz/android/model/Playable.kt | 2 +- .../ui/components/BrainzPlayerActivityCard.kt | 4 +- .../ui/components/BrainzPlayerDropDownMenu.kt | 28 ++++++ .../ui/components/BrainzPlayerListenCard.kt | 15 +--- .../brainzplayer/BrainzPlayerScreen.kt | 88 +++++++++++++++---- .../navigation/BrainzPlayerNavigation.kt | 2 - .../overview/AlbumsOverviewScreen.kt | 17 +--- .../overview/ArtistsOverviewScreen.kt | 17 ++-- .../brainzplayer/overview/OverviewScreen.kt | 6 +- .../overview/RecentPlaysOverviewScreen.kt | 8 +- 10 files changed, 123 insertions(+), 64 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerDropDownMenu.kt diff --git a/app/src/main/java/org/listenbrainz/android/model/Playable.kt b/app/src/main/java/org/listenbrainz/android/model/Playable.kt index 4fde94ac..e27f2b34 100644 --- a/app/src/main/java/org/listenbrainz/android/model/Playable.kt +++ b/app/src/main/java/org/listenbrainz/android/model/Playable.kt @@ -3,7 +3,7 @@ package org.listenbrainz.android.model data class Playable( val type: PlayableType, val id: Long, - val songs: List = emptyList(), + var songs: List = emptyList(), var currentSongIndex : Int, var seekTo: Long = 0L ) diff --git a/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerActivityCard.kt b/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerActivityCard.kt index bb163856..d224225e 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerActivityCard.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerActivityCard.kt @@ -32,7 +32,7 @@ import coil.compose.AsyncImage import org.listenbrainz.android.R @Composable -fun BrainzPlayerActivityCards(icon: String, errorIcon : Int, title: String, artist : String,modifier : Modifier = Modifier) { +fun BrainzPlayerActivityCards(icon: String, errorIcon : Int, title: String, subtitle : String,modifier : Modifier = Modifier) { Box( modifier = modifier .padding(4.dp) @@ -74,7 +74,7 @@ fun BrainzPlayerActivityCards(icon: String, errorIcon : Int, title: String, arti .padding(start = 15.dp) ) { Text( - text = artist, + text = subtitle, fontSize = 17.sp, fontWeight = FontWeight.Bold, textAlign = TextAlign.Left, diff --git a/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerDropDownMenu.kt b/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerDropDownMenu.kt new file mode 100644 index 00000000..3ef53b62 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerDropDownMenu.kt @@ -0,0 +1,28 @@ +package org.listenbrainz.android.ui.components + +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable + + +@Composable +fun BrainzPlayerDropDownMenu( + expanded : Boolean , + onDismiss : () -> Unit = {}, + onAddToNewPlaylist : () -> Unit = {}, + onAddToExistingPlaylist : () -> Unit = {}, + onPlayNext : () -> Unit = {}, + onAddToQueue : () -> Unit = {}, +){ + DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) { + DropdownMenuItem(onClick = onAddToNewPlaylist, text = {Text(text = "Add to new playlist")}) + DropdownMenuItem(onClick = onAddToExistingPlaylist, text = {Text(text = "Add to existing playlist")}) + DropdownMenuItem(onClick = { + onPlayNext() + onDismiss() + }, text = {Text(text = "Play next")}) + DropdownMenuItem(onClick = onAddToQueue, text = {Text(text = "Add to queue")}) + } + +} diff --git a/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerListenCard.kt b/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerListenCard.kt index ff9ba490..2bb0abbb 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerListenCard.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerListenCard.kt @@ -1,35 +1,25 @@ package org.listenbrainz.android.ui.components -import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.annotation.DrawableRes -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize 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.layout.width -import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest @@ -82,9 +72,8 @@ fun BrainzPlayerListenCard( Box(modifier = Modifier .fillMaxWidth(0.275f) .align(Alignment.CenterEnd)){ - DropdownButton (modifier = Modifier.align(Alignment.Center)) { - - } + DropdownButton (modifier = Modifier.align(Alignment.Center), onDropdownIconClick = onDropdownIconClick) + if(dropDownState) dropDown() PlayButton (modifier = Modifier.align(Alignment.CenterEnd)) { onPlayIconClick() } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index aad9b9f4..c34b27fe 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -1,22 +1,45 @@ package org.listenbrainz.android.ui.screens.brainzplayer +import android.os.Build import android.util.Log -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* +import androidx.annotation.RequiresApi +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.ElevatedSuggestionChip import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SuggestionChipDefaults -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +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.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import org.listenbrainz.android.model.* +import org.listenbrainz.android.model.Album +import org.listenbrainz.android.model.Artist +import org.listenbrainz.android.model.PlayableType +import org.listenbrainz.android.model.Song import org.listenbrainz.android.ui.screens.brainzplayer.navigation.Navigation import org.listenbrainz.android.ui.theme.ListenBrainzTheme -import org.listenbrainz.android.viewmodel.* +import org.listenbrainz.android.util.BrainzPlayerExtensions.toSong +import org.listenbrainz.android.viewmodel.AlbumViewModel +import org.listenbrainz.android.viewmodel.ArtistViewModel +import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel +import org.listenbrainz.android.viewmodel.PlaylistViewModel +import org.listenbrainz.android.viewmodel.SongViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -39,17 +62,14 @@ fun BrainzPlayerScreen() { val topArtists = artists.take(5).toMutableList() val topAlbums = albums.take(5).toMutableList() val albumSongsMap : MutableMap> = mutableMapOf() - val artistSongsMap : MutableMap> = mutableMapOf() + for(i in 1..albums.size){ val albumSongs : List = albumViewModel.getAllSongsOfAlbum(albums[i-1].albumId).collectAsState( initial = listOf() ).value albumSongsMap[albums[i-1]] = albumSongs } - for(i in 1..artists.size){ - val artistSongs : List = artistViewModel.getAllSongsOfArtist(artists[i-1]).collectAsState(initial = listOf()).value.distinctBy { it.mediaID } - artistSongsMap[artists[i-1]] = artistSongs - } + val songsPlayedThisWeek = brainzPlayerViewModel.songsPlayedThisWeek.collectAsState(initial = listOf()).value topRecents.add(Song()) topArtists.add(Artist()) @@ -59,11 +79,12 @@ fun BrainzPlayerScreen() { modifier = Modifier .fillMaxSize() ) { - Navigation(albums = albums, previewAlbums = topAlbums, artists = artists, previewArtists = topArtists, playlists, songsPlayedToday, songsPlayedThisWeek ,topRecents ,songs, albumSongsMap, artistSongsMap) + Navigation(albums = albums, previewAlbums = topAlbums, artists = artists, previewArtists = topArtists, playlists, songsPlayedToday, songsPlayedThisWeek ,topRecents ,songs, albumSongsMap) } } +@RequiresApi(Build.VERSION_CODES.O) @Composable fun BrainzPlayerHomeScreen( songs : List, @@ -75,11 +96,14 @@ fun BrainzPlayerHomeScreen( songsPlayedThisWeek: List, recentlyPlayedSongs: List, albumSongsMap: MutableMap>, - artistSongsMap: MutableMap>, brainzPlayerViewModel: BrainzPlayerViewModel = hiltViewModel(), ) { val currentTab : MutableState = remember {mutableStateOf(0)} + val currentlyPlayingSong = + brainzPlayerViewModel.currentlyPlayingSong.collectAsState().value.toSong + val isPlaying = brainzPlayerViewModel.isPlaying + Log.v("pranav" , currentlyPlayingSong.title) Column { Row(modifier = Modifier .fillMaxWidth() @@ -153,15 +177,49 @@ fun BrainzPlayerHomeScreen( artists = artists, onPlayClick = { artist -> - val artistSongs = artistSongsMap[artist]!! brainzPlayerViewModel.changePlayable( - artistSongs, + artist.songs, PlayableType.ARTIST, artist.id, 0, 0L ) - brainzPlayerViewModel.playOrToggleSong(artistSongs[0], true) + brainzPlayerViewModel.playOrToggleSong(artist.songs[0], true) + }, + onPlayNext = { + artist -> + val currentSongIndex = + brainzPlayerViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID==currentlyPlayingSong.mediaID } + ?.plus(1) + if (isPlaying.value && currentSongIndex != null) { + val currentSongs = brainzPlayerViewModel.appPreferences.currentPlayable?.songs?.toMutableList() + currentSongs?.addAll(currentSongIndex, artist.songs) + brainzPlayerViewModel.appPreferences.currentPlayable = brainzPlayerViewModel.appPreferences.currentPlayable?.copy(songs = currentSongs ?: emptyList()) + brainzPlayerViewModel.appPreferences.currentPlayable?.songs?.let { + brainzPlayerViewModel.changePlayable( + it, + PlayableType.ALL_SONGS, + brainzPlayerViewModel.appPreferences.currentPlayable?.id ?: 0, + brainzPlayerViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID==currentlyPlayingSong.mediaID } ?: 0,brainzPlayerViewModel.songCurrentPosition.value + ) + } + brainzPlayerViewModel.queueChanged( + currentlyPlayingSong, + brainzPlayerViewModel.isPlaying.value + ) + } + else{ + brainzPlayerViewModel.changePlayable( + artist.songs, + PlayableType.ARTIST, + artist.id, + 0, + 0L + ) + brainzPlayerViewModel.playOrToggleSong(artist.songs[0], true) + } + + } ) 3 -> AlbumsOverViewScreen(albums = albums, onPlayIconClick = { diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt index 1d01c51e..36390324 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt @@ -36,7 +36,6 @@ fun Navigation( recentlyPlayedSongs : List, songs: List, albumSongsMap: MutableMap>, - artistSongsMap: MutableMap>, navHostController: NavHostController = rememberNavController() ) { @@ -55,7 +54,6 @@ fun Navigation( songsPlayedThisWeek = songsPlayedThisWeek, recentlyPlayedSongs = recentlyPlayedSongs, albumSongsMap = albumSongsMap, - artistSongsMap = artistSongsMap ) } composable(route = BrainzPlayerNavigationItem.Songs.route) { diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/AlbumsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/AlbumsOverviewScreen.kt index ee25133c..a0ba476a 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/AlbumsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/AlbumsOverviewScreen.kt @@ -1,38 +1,24 @@ package org.listenbrainz.android.ui.screens.brainzplayer -import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.content.res.ResourcesCompat -import androidx.hilt.navigation.compose.hiltViewModel import org.listenbrainz.android.R import org.listenbrainz.android.model.Album -import org.listenbrainz.android.model.Artist -import org.listenbrainz.android.model.Song import org.listenbrainz.android.ui.components.BrainzPlayerListenCard -import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.theme.ListenBrainzTheme -import org.listenbrainz.android.viewmodel.ArtistViewModel @Composable fun AlbumsOverViewScreen( @@ -67,8 +53,7 @@ fun AlbumsOverViewScreen( ) ) for (j in 1..albumsStarting[startingLetter]!!.size) { - var coverArt: String? = null - coverArt = albumsStarting[startingLetter]!![j - 1].albumArt + val coverArt = albumsStarting[startingLetter]!![j - 1].albumArt BrainzPlayerListenCard(title = albumsStarting[startingLetter]!![j - 1].title, subTitle = albumsStarting[startingLetter]!![j - 1].artist, coverArtUrl = coverArt,modifier = Modifier.padding( start = 10.dp, end = 10.dp, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/ArtistsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/ArtistsOverviewScreen.kt index 87849d4d..b4b8016f 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/ArtistsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/ArtistsOverviewScreen.kt @@ -9,10 +9,11 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily @@ -20,16 +21,20 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.listenbrainz.android.R import org.listenbrainz.android.model.Artist +import org.listenbrainz.android.ui.components.BrainzPlayerDropDownMenu import org.listenbrainz.android.ui.components.BrainzPlayerListenCard -import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.theme.ListenBrainzTheme @Composable fun ArtistsOverviewScreen( artists: List, - onPlayClick : (Artist) -> Unit + onPlayClick : (Artist) -> Unit, + onPlayNext : (Artist) -> Unit, ) { val artistsStarting: MutableMap> = mutableMapOf() + var dropdownState by remember { + mutableStateOf(Pair(-1,-1)) + } for (i in 0..25) { artistsStarting['A' + i] = mutableListOf() } @@ -67,7 +72,7 @@ fun ArtistsOverviewScreen( else -> "${artistsStarting[startingLetter]!![j - 1].songs.size} tracks" }, coverArtUrl = coverArt, onPlayIconClick = { onPlayClick(artistsStarting[startingLetter]!![j-1]) - }, modifier = Modifier.padding(start = 10.dp, end = 10.dp)) + }, modifier = Modifier.padding(start = 10.dp, end = 10.dp), dropDown = { BrainzPlayerDropDownMenu(onPlayNext = {onPlayNext(artistsStarting[startingLetter]!![j - 1])},expanded = dropdownState == Pair(i,j-1), onDismiss = {dropdownState = Pair(-1,-1)})}, onDropdownIconClick = {dropdownState = Pair(i,j-1)}, dropDownState = dropdownState == Pair(i,j-1)) Spacer(modifier = Modifier.height(10.dp)) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/OverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/OverviewScreen.kt index 9e9613aa..a6f3016d 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/OverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/OverviewScreen.kt @@ -77,7 +77,7 @@ private fun RecentlyPlayedOverview( BrainzPlayerActivityCards(icon = song.albumArt, errorIcon = R.drawable.ic_artist, title = song.artist, - artist = song.title, + subtitle = song.title, modifier = Modifier .clickable { brainzPlayerViewModel.changePlayable( @@ -143,7 +143,7 @@ private fun ArtistsOverview( icon = "", errorIcon = R.drawable.ic_artist, title = "", - artist = artist.name, + subtitle = artist.name, ) } else { Box( @@ -196,7 +196,7 @@ private fun AlbumsOverview( BrainzPlayerActivityCards(icon = album.albumArt, errorIcon = R.drawable.ic_artist, title = album.artist, - artist = album.title, + subtitle = album.title, ) } else{ diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/RecentPlaysOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/RecentPlaysOverviewScreen.kt index e32279af..cde6014f 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/RecentPlaysOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/RecentPlaysOverviewScreen.kt @@ -11,14 +11,10 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.listenbrainz.android.model.Song import org.listenbrainz.android.ui.components.BrainzPlayerListenCard -import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.theme.ListenBrainzTheme @Composable @@ -49,7 +45,7 @@ fun RecentPlaysScreen( .background( brush = ListenBrainzTheme.colorScheme.gradientBrush ) - .padding(top = 15.dp, bottom = 15.dp , start = 10.dp)) { + .padding(top = 15.dp, bottom = 15.dp, start = 10.dp)) { Text( "Played This Week", color = ListenBrainzTheme.colorScheme.lbSignature, @@ -72,7 +68,7 @@ private fun PlayedToday( heightConstraint )) { items(songsPlayedToday){ - BrainzPlayerListenCard(title = it.title, subTitle = it.artist, coverArtUrl = it.albumArt, onPlayIconClick = {onPlayIconClick(it,songsPlayedToday)}) + BrainzPlayerListenCard(title = it.title, subTitle = it.artist, coverArtUrl = it.albumArt, onPlayIconClick = {onPlayIconClick(it,songsPlayedToday)}, onDropdownIconClick = {}) Spacer(modifier = Modifier.height(5.dp)) } } From 59574bffab019bf460d90b50ca72a873c886842d Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sat, 18 May 2024 09:39:30 +0530 Subject: [PATCH 33/97] Dropdown added in recents screen --- .../ui/components/BrainzPlayerDropDownMenu.kt | 15 ++- .../brainzplayer/BrainzPlayerScreen.kt | 98 +++++++++++++++++-- .../navigation/BrainzPlayerNavigation.kt | 5 +- .../overview/AlbumsOverviewScreen.kt | 2 +- .../overview/ArtistsOverviewScreen.kt | 8 +- .../brainzplayer/overview/OverviewScreen.kt | 2 +- .../overview/RecentPlaysOverviewScreen.kt | 61 +++++++++--- .../overview/SongsOverviewScreen.kt | 2 +- 8 files changed, 166 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerDropDownMenu.kt b/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerDropDownMenu.kt index 3ef53b62..53ec4b12 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerDropDownMenu.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerDropDownMenu.kt @@ -16,13 +16,22 @@ fun BrainzPlayerDropDownMenu( onAddToQueue : () -> Unit = {}, ){ DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) { - DropdownMenuItem(onClick = onAddToNewPlaylist, text = {Text(text = "Add to new playlist")}) - DropdownMenuItem(onClick = onAddToExistingPlaylist, text = {Text(text = "Add to existing playlist")}) + DropdownMenuItem(onClick = { + onAddToNewPlaylist() + onDismiss() + }, text = {Text(text = "Add to new playlist")}) + DropdownMenuItem(onClick = { + onAddToExistingPlaylist() + onDismiss() + }, text = {Text(text = "Add to existing playlist")}) DropdownMenuItem(onClick = { onPlayNext() onDismiss() }, text = {Text(text = "Play next")}) - DropdownMenuItem(onClick = onAddToQueue, text = {Text(text = "Add to queue")}) + DropdownMenuItem(onClick = { + onAddToQueue() + onDismiss() + }, text = {Text(text = "Add to queue")}) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index c34b27fe..af3b9a09 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -2,7 +2,6 @@ package org.listenbrainz.android.ui.screens.brainzplayer import android.os.Build -import android.util.Log import androidx.annotation.RequiresApi import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll @@ -33,6 +32,11 @@ import org.listenbrainz.android.model.Artist import org.listenbrainz.android.model.PlayableType import org.listenbrainz.android.model.Song import org.listenbrainz.android.ui.screens.brainzplayer.navigation.Navigation +import org.listenbrainz.android.ui.screens.brainzplayer.overview.AlbumsOverViewScreen +import org.listenbrainz.android.ui.screens.brainzplayer.overview.ArtistsOverviewScreen +import org.listenbrainz.android.ui.screens.brainzplayer.overview.OverviewScreen +import org.listenbrainz.android.ui.screens.brainzplayer.overview.RecentPlaysScreen +import org.listenbrainz.android.ui.screens.brainzplayer.overview.SongsOverviewScreen import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.util.BrainzPlayerExtensions.toSong import org.listenbrainz.android.viewmodel.AlbumViewModel @@ -103,7 +107,6 @@ fun BrainzPlayerHomeScreen( val currentlyPlayingSong = brainzPlayerViewModel.currentlyPlayingSong.collectAsState().value.toSong val isPlaying = brainzPlayerViewModel.isPlaying - Log.v("pranav" , currentlyPlayingSong.title) Column { Row(modifier = Modifier .fillMaxWidth() @@ -162,15 +165,74 @@ fun BrainzPlayerHomeScreen( songsPlayedToday = songsPlayedToday, songsPlayedThisWeek = songsPlayedThisWeek, onPlayIconClick = { - song, newPlayables -> + song -> brainzPlayerViewModel.changePlayable( - newPlayables, + listOf(song), PlayableType.ALL_SONGS, song.mediaID, - newPlayables.indexOf(song), + 0, 0L ) brainzPlayerViewModel.playOrToggleSong(song,true) + }, + onAddToQueue = { + song -> + val currentSongs = brainzPlayerViewModel.appPreferences.currentPlayable?.songs?.toMutableList() + currentSongs?.add(currentSongs.size, song) + brainzPlayerViewModel.appPreferences.currentPlayable = brainzPlayerViewModel.appPreferences.currentPlayable?.copy(songs = currentSongs ?: emptyList()) + brainzPlayerViewModel.appPreferences.currentPlayable?.songs?.let { + brainzPlayerViewModel.changePlayable( + it, + PlayableType.ALL_SONGS, + brainzPlayerViewModel.appPreferences.currentPlayable?.id ?: 0, + brainzPlayerViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID==currentlyPlayingSong.mediaID } ?: 0,brainzPlayerViewModel.songCurrentPosition.value + ) + } + brainzPlayerViewModel.queueChanged( + currentlyPlayingSong, + brainzPlayerViewModel.isPlaying.value + ) + }, + onPlayNext = { + song -> + val currentSongIndex = + brainzPlayerViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID==currentlyPlayingSong.mediaID } + ?.plus(1) + if (isPlaying.value && currentSongIndex != null) { + val currentSongs = brainzPlayerViewModel.appPreferences.currentPlayable?.songs?.toMutableList() + currentSongs?.add(currentSongIndex, song) + brainzPlayerViewModel.appPreferences.currentPlayable = brainzPlayerViewModel.appPreferences.currentPlayable?.copy(songs = currentSongs ?: emptyList()) + brainzPlayerViewModel.appPreferences.currentPlayable?.songs?.let { + brainzPlayerViewModel.changePlayable( + it, + PlayableType.ALL_SONGS, + brainzPlayerViewModel.appPreferences.currentPlayable?.id ?: 0, + brainzPlayerViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID==currentlyPlayingSong.mediaID } ?: 0,brainzPlayerViewModel.songCurrentPosition.value + ) + } + brainzPlayerViewModel.queueChanged( + currentlyPlayingSong, + brainzPlayerViewModel.isPlaying.value + ) + } + else{ + // No song is playing, so start playing the selected song + brainzPlayerViewModel.changePlayable( + listOf(song), + PlayableType.SONG, + song.mediaID, + 0, + 0L + ) + brainzPlayerViewModel.playOrToggleSong(song, true) + } + + }, + onAddToExistingPlaylist = { + song -> + }, + onAddToNewPlaylist = { + song -> } ) 2 -> ArtistsOverviewScreen( @@ -218,8 +280,30 @@ fun BrainzPlayerHomeScreen( ) brainzPlayerViewModel.playOrToggleSong(artist.songs[0], true) } - - + }, + onAddToQueue = { + artist -> + val currentSongs = brainzPlayerViewModel.appPreferences.currentPlayable?.songs?.toMutableList() + currentSongs?.addAll(currentSongs.size, artist.songs) + brainzPlayerViewModel.appPreferences.currentPlayable = brainzPlayerViewModel.appPreferences.currentPlayable?.copy(songs = currentSongs ?: emptyList()) + brainzPlayerViewModel.appPreferences.currentPlayable?.songs?.let { + brainzPlayerViewModel.changePlayable( + it, + PlayableType.ALL_SONGS, + brainzPlayerViewModel.appPreferences.currentPlayable?.id ?: 0, + brainzPlayerViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID==currentlyPlayingSong.mediaID } ?: 0,brainzPlayerViewModel.songCurrentPosition.value + ) + } + brainzPlayerViewModel.queueChanged( + currentlyPlayingSong, + brainzPlayerViewModel.isPlaying.value + ) + }, + onAddToNewPlaylist = { + artist -> + }, + onAddToExistingPlaylist = { + artist -> } ) 3 -> AlbumsOverViewScreen(albums = albums, onPlayIconClick = { diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt index 36390324..64e5e906 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/navigation/BrainzPlayerNavigation.kt @@ -1,5 +1,7 @@ package org.listenbrainz.android.ui.screens.brainzplayer.navigation +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.navigation.NavHostController @@ -18,11 +20,12 @@ import org.listenbrainz.android.ui.screens.brainzplayer.BrainzPlayerHomeScreen import org.listenbrainz.android.ui.screens.brainzplayer.OnAlbumClickScreen import org.listenbrainz.android.ui.screens.brainzplayer.OnArtistClickScreen import org.listenbrainz.android.ui.screens.brainzplayer.OnPlaylistClickScreen -import org.listenbrainz.android.ui.screens.brainzplayer.OverviewScreen +import org.listenbrainz.android.ui.screens.brainzplayer.overview.OverviewScreen import org.listenbrainz.android.ui.screens.brainzplayer.PlaylistScreen import org.listenbrainz.android.ui.screens.brainzplayer.SongScreen +@RequiresApi(Build.VERSION_CODES.O) @ExperimentalMaterial3Api @Composable fun Navigation( diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/AlbumsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/AlbumsOverviewScreen.kt index a0ba476a..2cff3ade 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/AlbumsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/AlbumsOverviewScreen.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.ui.screens.brainzplayer +package org.listenbrainz.android.ui.screens.brainzplayer.overview import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/ArtistsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/ArtistsOverviewScreen.kt index b4b8016f..58e02478 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/ArtistsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/ArtistsOverviewScreen.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.ui.screens.brainzplayer +package org.listenbrainz.android.ui.screens.brainzplayer.overview import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -30,6 +30,9 @@ fun ArtistsOverviewScreen( artists: List, onPlayClick : (Artist) -> Unit, onPlayNext : (Artist) -> Unit, + onAddToQueue : (Artist) -> Unit, + onAddToExistingPlaylist : (Artist) -> Unit, + onAddToNewPlaylist : (Artist) -> Unit, ) { val artistsStarting: MutableMap> = mutableMapOf() var dropdownState by remember { @@ -65,6 +68,7 @@ fun ArtistsOverviewScreen( ) for (j in 1..artistsStarting[startingLetter]!!.size) { var coverArt: String? = null + val artist = artistsStarting[startingLetter]!![j-1] if (artistsStarting[startingLetter]!![j - 1].albums.isNotEmpty()) coverArt = artistsStarting[startingLetter]!![j - 1].albums[0].albumArt BrainzPlayerListenCard(title = artistsStarting[startingLetter]!![j - 1].name, subTitle = when (artistsStarting[startingLetter]!![j - 1].songs.size) { @@ -72,7 +76,7 @@ fun ArtistsOverviewScreen( else -> "${artistsStarting[startingLetter]!![j - 1].songs.size} tracks" }, coverArtUrl = coverArt, onPlayIconClick = { onPlayClick(artistsStarting[startingLetter]!![j-1]) - }, modifier = Modifier.padding(start = 10.dp, end = 10.dp), dropDown = { BrainzPlayerDropDownMenu(onPlayNext = {onPlayNext(artistsStarting[startingLetter]!![j - 1])},expanded = dropdownState == Pair(i,j-1), onDismiss = {dropdownState = Pair(-1,-1)})}, onDropdownIconClick = {dropdownState = Pair(i,j-1)}, dropDownState = dropdownState == Pair(i,j-1)) + }, modifier = Modifier.padding(start = 10.dp, end = 10.dp), dropDown = { BrainzPlayerDropDownMenu(onAddToNewPlaylist = {onAddToNewPlaylist(artist)}, onAddToExistingPlaylist = {onAddToExistingPlaylist(artist)},onAddToQueue = {onAddToQueue(artist)}, onPlayNext = {onPlayNext(artist)},expanded = dropdownState == Pair(i,j-1), onDismiss = {dropdownState = Pair(-1,-1)})}, onDropdownIconClick = {dropdownState = Pair(i,j-1)}, dropDownState = dropdownState == Pair(i,j-1)) Spacer(modifier = Modifier.height(10.dp)) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/OverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/OverviewScreen.kt index a6f3016d..4e85c601 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/OverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/OverviewScreen.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.ui.screens.brainzplayer +package org.listenbrainz.android.ui.screens.brainzplayer.overview import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/RecentPlaysOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/RecentPlaysOverviewScreen.kt index cde6014f..c7a13fda 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/RecentPlaysOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/RecentPlaysOverviewScreen.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.ui.screens.brainzplayer +package org.listenbrainz.android.ui.screens.brainzplayer.overview import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -7,13 +7,19 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.listenbrainz.android.model.Song +import org.listenbrainz.android.ui.components.BrainzPlayerDropDownMenu import org.listenbrainz.android.ui.components.BrainzPlayerListenCard import org.listenbrainz.android.ui.theme.ListenBrainzTheme @@ -21,9 +27,15 @@ import org.listenbrainz.android.ui.theme.ListenBrainzTheme fun RecentPlaysScreen( songsPlayedToday: List, songsPlayedThisWeek: List, - onPlayIconClick: (Song, List) -> Unit + onPlayIconClick: (Song) -> Unit, + onAddToQueue : (Song) -> Unit, + onPlayNext : (Song) -> Unit, + onAddToNewPlaylist : (Song) -> Unit, + onAddToExistingPlaylist : (Song) -> Unit ) { + val dropdownState : MutableState> = remember {mutableStateOf(Pair(-1,-1))} Column (modifier = Modifier + .verticalScroll(rememberScrollState()) .fillMaxSize()) { if(songsPlayedToday.isNotEmpty()){ Column(modifier = Modifier @@ -37,7 +49,7 @@ fun RecentPlaysScreen( fontSize = 25.sp ) Spacer(modifier = Modifier.height(10.dp)) - PlayedToday(songsPlayedToday = songsPlayedToday, onPlayIconClick = onPlayIconClick) + PlayedToday(songsPlayedToday = songsPlayedToday, onPlayIconClick = onPlayIconClick, dropDownState = dropdownState, onAddToQueue = onAddToQueue, onAddToNewPlaylist = onAddToNewPlaylist, onAddToExistingPlaylist = onAddToExistingPlaylist, onPlayNext = onPlayNext) } } if(songsPlayedThisWeek.isNotEmpty()) { @@ -52,7 +64,7 @@ fun RecentPlaysScreen( fontSize = 25.sp ) Spacer(modifier = Modifier.height(10.dp)) - PlayedThisWeek(songsPlayedThisWeek = songsPlayedThisWeek, onPlayIconClick = onPlayIconClick) + PlayedThisWeek(songsPlayedThisWeek = songsPlayedThisWeek, onPlayIconClick = onPlayIconClick, dropDownState = dropdownState, onAddToQueue = onAddToQueue, onAddToNewPlaylist = onAddToNewPlaylist, onAddToExistingPlaylist = onAddToExistingPlaylist, onPlayNext = onPlayNext) } } } @@ -60,32 +72,59 @@ fun RecentPlaysScreen( @Composable private fun PlayedToday( songsPlayedToday: List, - onPlayIconClick: (Song, List) -> Unit + onPlayIconClick: (Song) -> Unit, + dropDownState : MutableState>, + onAddToQueue: (Song) -> Unit, + onPlayNext: (Song) -> Unit, + onAddToExistingPlaylist: (Song) -> Unit, + onAddToNewPlaylist: (Song) -> Unit ){ var heightConstraint = ListenBrainzTheme.sizes.listenCardHeight * songsPlayedToday.size + 20.dp if(songsPlayedToday.size > 4) heightConstraint = 250.dp LazyColumn (modifier = Modifier.height( heightConstraint )) { - items(songsPlayedToday){ - BrainzPlayerListenCard(title = it.title, subTitle = it.artist, coverArtUrl = it.albumArt, onPlayIconClick = {onPlayIconClick(it,songsPlayedToday)}, onDropdownIconClick = {}) + itemsIndexed(songsPlayedToday){ + index, it -> + BrainzPlayerListenCard(title = it.title, subTitle = it.artist, coverArtUrl = it.albumArt, onPlayIconClick = {onPlayIconClick(it)}, onDropdownIconClick = {dropDownState.value = Pair(1,index)}, dropDownState = dropDownState.value == Pair(1,index),dropDown = {BrainzPlayerDropDownMenu( + expanded = dropDownState.value == Pair(1,index), + onDismiss = {dropDownState.value = Pair(-1,-1)}, + onAddToQueue = {onAddToQueue(it)}, + onPlayNext = {onPlayNext(it)}, + onAddToExistingPlaylist = {onAddToExistingPlaylist(it)}, + onAddToNewPlaylist = {onAddToNewPlaylist(it)} + )}) Spacer(modifier = Modifier.height(5.dp)) } + } } @Composable private fun PlayedThisWeek( songsPlayedThisWeek: List, - onPlayIconClick: (Song, List) -> Unit + onPlayIconClick: (Song) -> Unit, + dropDownState : MutableState>, + onAddToQueue: (Song) -> Unit, + onPlayNext: (Song) -> Unit, + onAddToExistingPlaylist: (Song) -> Unit, + onAddToNewPlaylist: (Song) -> Unit ){ var heightConstraint = ListenBrainzTheme.sizes.listenCardHeight * songsPlayedThisWeek.size + 20.dp if(songsPlayedThisWeek.size > 4) heightConstraint = 250.dp LazyColumn (modifier = Modifier.height( heightConstraint )) { - items(songsPlayedThisWeek){ - BrainzPlayerListenCard(title = it.title, subTitle = it.artist, coverArtUrl = it.albumArt, onPlayIconClick = {onPlayIconClick(it,songsPlayedThisWeek)}) + itemsIndexed(songsPlayedThisWeek){ + index, it -> + BrainzPlayerListenCard(title = it.title, subTitle = it.artist, coverArtUrl = it.albumArt, onPlayIconClick = {onPlayIconClick(it)}, onDropdownIconClick = {dropDownState.value = Pair(2,index)}, dropDownState = dropDownState.value == Pair(2,index),dropDown = {BrainzPlayerDropDownMenu( + expanded = dropDownState.value == Pair(2,index), + onDismiss = {dropDownState.value = Pair(-1,-1)}, + onAddToQueue = {onAddToQueue(it)}, + onPlayNext = {onPlayNext(it)}, + onAddToExistingPlaylist = {onAddToExistingPlaylist(it)}, + onAddToNewPlaylist = {onAddToNewPlaylist(it)} + )}) Spacer(modifier = Modifier.height(5.dp)) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/SongsOverviewScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/SongsOverviewScreen.kt index b1aa0a8d..9f145b5b 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/SongsOverviewScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/overview/SongsOverviewScreen.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.ui.screens.brainzplayer +package org.listenbrainz.android.ui.screens.brainzplayer.overview import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column From b59f3ca8c376f58601557462b6f2ede3f41fc1b3 Mon Sep 17 00:00:00 2001 From: Richard-Degenne Date: Mon, 3 Jun 2024 18:42:32 +0000 Subject: [PATCH 34/97] Translated using Weblate (French) Currently translated at 71.8% (135 of 188 strings) Co-authored-by: Richard-Degenne Translate-URL: https://translations.metabrainz.org/projects/listenbrainz-android/app/fr/ Translation: ListenBrainz Android/app --- app/src/main/res/values-fr/strings.xml | 81 ++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 7c23727e..d28ea92a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -43,4 +43,85 @@ Suivant Précédent Terminer + Artiste non reconnu + Aucun morceau ici pour le moment… + Nouvelle application + Aucune application n\'est activée + %1$d de plus + Morceau recommandé avec succès ! + Recommandation de morceau envoyée avec succès ! + Morceau épinglé avec succès ! + Une erreur s\'est produite ! Veuillez réessayer plus tard ! + Métadonnées + Sélectionner un fichier + Enregistrer les changements + Aucun fichier sélectionné + Chemin de dossier + Sélectionner + Haut + Apparence + Hey ! Vous êtes déjà connecté. + Statut de connexion inconnu. + La connexion a échoué, veuillez réessayer plus tard. + Erreur serveur, veuillez réessayer plus tard. + Impossible d\'obtenir un jeton d\'accès. + Jeton d\'accès récupéré avec succès + Impossible d\'obtenir les informations utilisateur + Informations utilisateur récupérées avec succès + Connexion refusée + Connexion approuvée + Connexion initiée + Pour vous identifier, connecter votre compte ListenBrainz avec notre application. Vous devrez vous identifier sur le site Web ListenBrainz. Suivez les instructions puis revenez ici pour terminer l\'authentification. + Bienvenue sur ListenBrainz ! Permettez-moi de vous montrer les bases ! + Explorer les données ListenBrainz + Explorer votre collection ListenBrainz + Rechercher avec un code-barres + Ouvrir sur le site Web + Jeton d\'utilisateur ListenBrainz + Saisissez le jeton d\'utilisateur ListenBrainz disponible à l\'adresse https://listenbrainz.org/profile/ + Spotify n\'est pas connecté. Utilisez l\'un des boutons \"Connecter\" + Une erreur s\'est produite. Consulter les journaux pour plus de détails. + Connecter + Connecter et autoriser + Connexion en cours + Connecté + Déconnecter + + Lire le balado + Lire l\'album + Lire l\'artiste + Lire la liste de lecture + Lire la piste + Ajouter à la collection + Retirer de la collection + Parcourir le contenu + Piste courante + Taille de l\'image + Icône bouton + Pistes + Faire un don + + À propos + Faire un don + À propos + ListenBrainz Android + Version + Rechercher + Réglages + Authentification + Sélectionnez le dossier dans lequel Picard peut trouver vos fichiers + Collections privées + Utiliser le thème de l\'appareil + Aucun client e-mail trouvé + Plus de liens + Historique de recherche supprimé + Vous devez vous connecter pour voir vos collections. + Passer + Ajouter un artiste + Ajouter une parution + Ajouter un évènement + Ajouter un groupe de parution + Ajouter un label + Ajouter un enregistrement + Chargement de votre bibliothèque musicale en cours \ No newline at end of file From 8d5bd89a95074a8613ccf422b8871de372be7487 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 3 Jun 2024 18:42:32 +0000 Subject: [PATCH 35/97] Update translation files Updated by "Remove blank strings" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://translations.metabrainz.org/projects/listenbrainz-android/app/ Translation: ListenBrainz Android/app --- app/src/main/res/values-fr/strings.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d28ea92a..2b525a90 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -86,7 +86,6 @@ Connexion en cours Connecté Déconnecter - Lire le balado Lire l\'album Lire l\'artiste @@ -100,7 +99,6 @@ Icône bouton Pistes Faire un don - À propos Faire un don À propos From e9a1b65f110b56335761a3c7a3f0de396c155170 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Jun 2024 00:05:54 +0000 Subject: [PATCH 36/97] Bump io.sentry.android.gradle from 4.4.1 to 4.7.0 Bumps [io.sentry.android.gradle](https://github.com/getsentry/sentry-android-gradle-plugin) from 4.4.1 to 4.7.0. - [Release notes](https://github.com/getsentry/sentry-android-gradle-plugin/releases) - [Changelog](https://github.com/getsentry/sentry-android-gradle-plugin/blob/main/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-android-gradle-plugin/compare/4.4.1...4.7.0) --- updated-dependencies: - dependency-name: io.sentry.android.gradle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 223dcd3d..2454564d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,7 @@ plugins { id 'kotlin-kapt' id 'com.google.devtools.ksp' id 'dagger.hilt.android.plugin' - id "io.sentry.android.gradle" version "4.4.1" + id "io.sentry.android.gradle" version "4.7.0" } def keystorePropertiesFile = rootProject.file("keystore.properties") From 79bd712ccec7ed153b3624f76d36804e3e0009d4 Mon Sep 17 00:00:00 2001 From: Akshat Tiwari Date: Wed, 5 Jun 2024 13:55:13 +0530 Subject: [PATCH 37/97] Bump dependencies, fix updates and optimize code --- app/build.gradle | 22 ++-- .../android/ui/screens/main/MainActivity.kt | 31 +++-- .../ui/screens/onboarding/FeaturesActivity.kt | 116 +++++++----------- .../android/ui/screens/profile/UserData.kt | 43 +++---- build.gradle | 4 +- sharedTest/build.gradle | 4 +- 6 files changed, 94 insertions(+), 126 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 223dcd3d..821d6732 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -127,12 +127,12 @@ android { dependencies { //AndroidX - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0' - implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.7.0' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0' + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.1' + implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.8.1' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.1' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.browser:browser:1.8.0' implementation 'androidx.preference:preference-ktx:1.2.1' @@ -141,13 +141,13 @@ dependencies { implementation "androidx.work:work-runtime-ktx:$work_version" //Web Service Setup - implementation 'com.google.code.gson:gson:2.10.1' + implementation 'com.google.code.gson:gson:2.11.0' implementation 'com.squareup.retrofit2:retrofit:2.11.0' implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.14' implementation 'com.squareup.retrofit2:converter-gson:2.11.0' implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14' - implementation 'androidx.paging:paging-runtime-ktx:3.2.1' - implementation "androidx.paging:paging-compose:3.2.1" + implementation 'androidx.paging:paging-runtime-ktx:3.3.0' + implementation 'androidx.paging:paging-compose:3.3.0' //Image downloading and Caching library @@ -162,11 +162,11 @@ dependencies { //Design Setup implementation 'com.google.android.material:material:1.12.0' - implementation 'com.airbnb.android:lottie:6.4.0' + implementation 'com.airbnb.android:lottie:6.4.1' implementation 'com.github.akshaaatt:Onboarding:1.1.3' implementation 'com.github.akshaaatt:Share-Android:1.0.0' implementation 'androidx.hilt:hilt-navigation-compose:1.2.0' - implementation 'com.airbnb.android:lottie-compose:6.4.0' + implementation 'com.airbnb.android:lottie-compose:6.4.1' //Dagger-Hilt implementation("com.google.dagger:hilt-android:$hilt_version") diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt index 3b8fba3e..5012eedd 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt @@ -18,7 +18,6 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.lifecycleScope import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController @@ -44,7 +43,7 @@ import org.listenbrainz.android.viewmodel.DashBoardViewModel @AndroidEntryPoint class MainActivity : ComponentActivity() { - + private lateinit var dashBoardViewModel: DashBoardViewModel @OptIn(ExperimentalMaterialApi::class) @@ -58,22 +57,22 @@ class MainActivity : ComponentActivity() { // TODO: Since this view-model will remain throughout the lifecycle of the app, // we can have tasks which require such lifecycle access or longevity. We can get this view-model's // instance anywhere when we initialize it as a hilt view-model. - + dashBoardViewModel.setUiMode() dashBoardViewModel.beginOnboarding(this) dashBoardViewModel.updatePermissionPreference() - - LifecycleStartEffect { + + DisposableEffect(Unit) { dashBoardViewModel.connectToSpotify() - onStopOrDispose { + onDispose { dashBoardViewModel.disconnectSpotify() } } - + var isGrantedPerms: String? by remember { mutableStateOf(null) } - + LaunchedEffect(Unit) { isGrantedPerms = dashBoardViewModel.getPermissionsPreference() } @@ -102,7 +101,7 @@ class MainActivity : ComponentActivity() { } } - LaunchedEffect(Unit) { + LaunchedEffect(isGrantedPerms) { if (isGrantedPerms == PermissionStatus.NOT_REQUESTED.name) { launcher.launch(dashBoardViewModel.neededPermissions) } @@ -136,7 +135,7 @@ class MainActivity : ComponentActivity() { ) } } - + val navController = rememberNavController() val backdropScaffoldState = rememberBackdropScaffoldState(initialValue = BackdropValue.Revealed) @@ -147,7 +146,7 @@ class MainActivity : ComponentActivity() { val scope = rememberCoroutineScope() val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination - + Scaffold( topBar = { TopBar(navController = navController, searchBarState = when (currentDestination?.route) { AppNavigationItem.BrainzPlayer.route -> brainzplayerSearchBarState @@ -173,11 +172,11 @@ class MainActivity : ComponentActivity() { }, containerColor = MaterialTheme.colorScheme.background, contentWindowInsets = WindowInsets.captionBar - + ) { - + if (isGrantedPerms == PermissionStatus.GRANTED.name) { - + BrainzPlayerBackDropScreen( backdropScaffoldState = backdropScaffoldState, paddingValues = it, @@ -210,7 +209,7 @@ class MainActivity : ComponentActivity() { ) } - + } } } @@ -225,4 +224,4 @@ class MainActivity : ComponentActivity() { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/onboarding/FeaturesActivity.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/onboarding/FeaturesActivity.kt index 55b11134..adffc359 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/onboarding/FeaturesActivity.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/onboarding/FeaturesActivity.kt @@ -31,98 +31,73 @@ class FeaturesActivity : OnboardAdvanced() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + showSignInButton = true isWizardMode = true showStatusBar(true) setStatusBarColorRes(R.color.app_bg) - when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> { - askForPermissions( - permissions = arrayOf(Manifest.permission.POST_NOTIFICATIONS, - Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_AUDIO - ), - slideNumber = 1, - required = true + askForPermissions( + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> arrayOf( + Manifest.permission.POST_NOTIFICATIONS, + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_AUDIO ) - } - Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { - askForPermissions( - permissions = arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), - slideNumber = 1, - required = true + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE ) - } - else -> { - askForPermissions( - permissions = arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE), - slideNumber = 1, - required = true + else -> arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE ) - } - } - - addSlide( - OnboardFragment.newInstance( - "Listens", - "Track your music listening habits ", - resourceId = R.raw.teen, - backgroundColor = ContextCompat.getColor(applicationContext, R.color.app_bg), - titleColor = ContextCompat.getColor(applicationContext, R.color.text), - descriptionColor = ContextCompat.getColor(applicationContext, R.color.text), - isLottie = true - ) + }, + slideNumber = 1, + required = true ) - addSlide( - OnboardFragment.newInstance( - "Critiques", - "Read and write about an album or event", - resourceId = R.raw.review, - backgroundColor = ContextCompat.getColor(applicationContext, R.color.app_bg), - titleColor = ContextCompat.getColor(applicationContext, R.color.text), - descriptionColor = ContextCompat.getColor(applicationContext, R.color.text), - isLottie = true - ) - ) + addSlides() - addSlide( - OnboardFragment.newInstance( - "BrainzPlayer", - "Listen to locally saved music", - resourceId = R.raw.music_player, - backgroundColor = ContextCompat.getColor(applicationContext, R.color.app_bg), - titleColor = ContextCompat.getColor(applicationContext, R.color.text), - descriptionColor = ContextCompat.getColor(applicationContext, R.color.text), - isLottie = true - ) + setTransformer(OnboardPageTransformerType.Parallax()) + } + + private fun addSlides() { + val slides = listOf( + SlideData("Listens", "Track your music listening habits", R.raw.teen), + SlideData("Critiques", "Read and write about an album or event", R.raw.review), + SlideData("BrainzPlayer", "Listen to locally saved music", R.raw.music_player) ) - setTransformer(OnboardPageTransformerType.Parallax()) + slides.forEach { slide -> + addSlide( + OnboardFragment.newInstance( + slide.title, + slide.description, + resourceId = slide.resourceId, + backgroundColor = ContextCompat.getColor(applicationContext, R.color.app_bg), + titleColor = ContextCompat.getColor(applicationContext, R.color.text), + descriptionColor = ContextCompat.getColor(applicationContext, R.color.text), + isLottie = true + ) + ) + } } override fun onDonePressed(currentFragment: Fragment?) { super.onDonePressed(currentFragment) Log.d("Onboarding completed") appPreferences.onboardingCompleted = true - val intent = Intent(this, MainActivity::class.java) - startActivity(intent) + startActivity(Intent(this, MainActivity::class.java)) finish() } override fun onNextPressed(currentFragment: Fragment?) { if (!appPreferences.isNotificationServiceAllowed) { Toast.makeText(this, "Allow notification access to submit listens", Toast.LENGTH_SHORT).show() - val intent: Intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { - Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS) - } else { - Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS") - } + val intent = Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS) startActivity(intent) - } - else { + } else { super.onNextPressed(currentFragment) } } @@ -136,15 +111,12 @@ class FeaturesActivity : OnboardAdvanced() { override fun onResume() { super.onResume() lifecycleScope.launch { - if ( - featuresViewModel.appPreferences.onboardingCompleted && - featuresViewModel.appPreferences.isUserLoggedIn() - ) { - val intent = Intent(this@FeaturesActivity, MainActivity::class.java) - startActivity(intent) + if (featuresViewModel.appPreferences.onboardingCompleted && featuresViewModel.appPreferences.isUserLoggedIn()) { + startActivity(Intent(this@FeaturesActivity, MainActivity::class.java)) finish() } } - } + + data class SlideData(val title: String, val description: String, val resourceId: Int) } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/UserData.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/UserData.kt index 4fba9c33..c38292ee 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/UserData.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/UserData.kt @@ -13,16 +13,18 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Card -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -43,14 +45,13 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.compose.LifecycleResumeEffect import kotlinx.coroutines.launch import org.listenbrainz.android.ui.screens.settings.PreferencesUiState import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.ui.theme.offWhite import org.listenbrainz.android.ui.theme.onScreenUiModeIsDark -@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable fun UserData( preferencesUiState: PreferencesUiState, @@ -64,9 +65,8 @@ fun UserData( var showDialog by remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() - LifecycleResumeEffect(Unit) { + LaunchedEffect(Unit) { updateNotificationServicePermissionStatus() - onPauseOrDispose {} } Card( @@ -74,8 +74,10 @@ fun UserData( .fillMaxWidth() .padding(8.dp) .clip(RoundedCornerShape(16.dp)), - elevation = 0.dp, - backgroundColor = if (onScreenUiModeIsDark()) Color.Black else offWhite, + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + colors = CardDefaults.cardColors( + containerColor = if (onScreenUiModeIsDark()) Color.Black else offWhite + ) ) { Column { Row( @@ -88,21 +90,21 @@ fun UserData( color = if (onScreenUiModeIsDark()) Color.White else Color.Black, fontWeight = FontWeight.Light, fontSize = 20.sp, - style = MaterialTheme.typography.subtitle1, + style = MaterialTheme.typography.titleSmall, textAlign = TextAlign.Center, ) } - + var tempAccessToken by remember { mutableStateOf(preferencesUiState.accessToken) } - if(preferencesUiState.accessToken.isEmpty()) { + if (preferencesUiState.accessToken.isEmpty()) { Row( modifier = Modifier .padding(16.dp) ) { OutlinedTextField( - value = preferencesUiState.accessToken, + value = tempAccessToken, onValueChange = { newText -> tempAccessToken = newText }, @@ -111,17 +113,13 @@ fun UserData( keyboardActions = KeyboardActions( onDone = { coroutineScope.launch { - val tokenValid = validateUserToken(preferencesUiState.accessToken) + val tokenValid = validateUserToken(tempAccessToken) if (tokenValid) { - coroutineScope.launch { - setToken(tempAccessToken) - } + setToken(tempAccessToken) keyboardController?.hide() focusManager.clearFocus() } else { - coroutineScope.launch { - setToken("") - } + setToken("") Toast.makeText( context, "Invalid token", @@ -152,7 +150,7 @@ fun UserData( } } - if(!preferencesUiState.isNotificationServiceAllowed) { + if (!preferencesUiState.isNotificationServiceAllowed) { Row( modifier = Modifier .padding(16.dp) @@ -196,7 +194,6 @@ fun UserData( dismissButton = { TextButton(onClick = { showDialog = false - // Your code to update preferences and switch state }) { Text("Cancel") } @@ -214,4 +211,4 @@ fun UserDataPreview() { { true }, {} ) -} \ No newline at end of file +} diff --git a/build.gradle b/build.gradle index 9a954212..56804c9d 100644 --- a/build.gradle +++ b/build.gradle @@ -10,14 +10,14 @@ buildscript { accompanist_version = '0.34.0' work_version = '2.9.0' exoplayer_version = '2.19.1' - paging_version = "3.2.1" + paging_version = '3.3.0' } repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.4.0' + classpath 'com.android.tools.build:gradle:8.4.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" } diff --git a/sharedTest/build.gradle b/sharedTest/build.gradle index 683e531d..a165eaf7 100644 --- a/sharedTest/build.gradle +++ b/sharedTest/build.gradle @@ -29,11 +29,11 @@ android { } dependencies { - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' //Web Service Setup - implementation 'com.google.code.gson:gson:2.10.1' + implementation 'com.google.code.gson:gson:2.11.0' implementation 'com.squareup.retrofit2:retrofit:2.11.0' implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.14' implementation 'com.squareup.retrofit2:converter-gson:2.11.0' From 572980d397c7abfdd7820ebe629027eada5d7a70 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:02:25 +0530 Subject: [PATCH 38/97] Made profile screen dynamic, initialized listenUIState flow --- .../listenbrainz/android/di/ServiceModule.kt | 7 +++ .../listenbrainz/android/di/UserRepository.kt | 16 ++++++ .../android/repository/user/UserRepository.kt | 9 +++ .../repository/user/UserRepositoryImpl.kt | 23 ++++++++ .../android/service/UserService.kt | 11 ++++ .../android/ui/navigation/AppNavigation.kt | 9 ++- .../ui/navigation/BottomNavigationBar.kt | 48 +++++++++++----- .../android/ui/screens/main/MainActivity.kt | 12 +++- .../ui/screens/profile/ProfileScreen.kt | 13 ++--- .../ui/screens/profile/ProfileUiState.kt | 22 +++++++ .../android/viewmodel/DashBoardViewModel.kt | 6 +- .../android/viewmodel/ProfileViewModel.kt | 57 ++++++++++++++++++- 12 files changed, 203 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/di/UserRepository.kt create mode 100644 app/src/main/java/org/listenbrainz/android/repository/user/UserRepository.kt create mode 100644 app/src/main/java/org/listenbrainz/android/repository/user/UserRepositoryImpl.kt create mode 100644 app/src/main/java/org/listenbrainz/android/service/UserService.kt create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileUiState.kt diff --git a/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt b/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt index 9a863864..d08587f7 100644 --- a/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt +++ b/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt @@ -18,6 +18,7 @@ import org.listenbrainz.android.service.BlogService import org.listenbrainz.android.service.FeedService import org.listenbrainz.android.service.ListensService import org.listenbrainz.android.service.SocialService +import org.listenbrainz.android.service.UserService import org.listenbrainz.android.service.Yim23Service import org.listenbrainz.android.service.YimService import org.listenbrainz.android.service.YouTubeApiService @@ -77,6 +78,12 @@ class ServiceModule { fun providesFeedService(appPreferences: AppPreferences): FeedService = constructRetrofit(appPreferences) .create(FeedService::class.java) + + @Singleton + @Provides + fun providesUserService(appPreferences: AppPreferences) : UserService = + constructRetrofit(appPreferences) + .create(UserService::class.java) @Singleton diff --git a/app/src/main/java/org/listenbrainz/android/di/UserRepository.kt b/app/src/main/java/org/listenbrainz/android/di/UserRepository.kt new file mode 100644 index 00000000..1ecf4ffd --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/di/UserRepository.kt @@ -0,0 +1,16 @@ +package org.listenbrainz.android.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.listenbrainz.android.repository.user.UserRepository +import org.listenbrainz.android.repository.user.UserRepositoryImpl + +@Module +@InstallIn(SingletonComponent::class) +abstract class UserRepositoryModule { + + @Binds + abstract fun bindsUserRepository (repository: UserRepositoryImpl?) : UserRepository? +} \ 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 new file mode 100644 index 00000000..47de456a --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/repository/user/UserRepository.kt @@ -0,0 +1,9 @@ +package org.listenbrainz.android.repository.user + +import org.listenbrainz.android.model.Listens +import org.listenbrainz.android.util.Resource + +interface UserRepository { + suspend fun fetchUserListenCount (username : String?) : Resource + suspend fun fetchListeningNow (username : String?) : 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 new file mode 100644 index 00000000..cf297915 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/repository/user/UserRepositoryImpl.kt @@ -0,0 +1,23 @@ +package org.listenbrainz.android.repository.user + +import org.listenbrainz.android.model.Listens +import org.listenbrainz.android.model.ResponseError +import org.listenbrainz.android.service.UserService +import org.listenbrainz.android.util.Resource +import org.listenbrainz.android.util.Utils.parseResponse +import javax.inject.Inject + +class UserRepositoryImpl @Inject constructor( + private val service: UserService +) : UserRepository { + override suspend fun fetchUserListenCount(username: String?): Resource = parseResponse { + if(username.isNullOrEmpty()) return ResponseError.DOES_NOT_EXIST.asResource() + service.getListenCount(username) + } + + override suspend fun fetchListeningNow(username: String?): Resource { + TODO("Not yet implemented") + } + + +} \ 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 new file mode 100644 index 00000000..a4513cbb --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/service/UserService.kt @@ -0,0 +1,11 @@ +package org.listenbrainz.android.service + +import org.listenbrainz.android.model.Listens +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Path + +interface UserService { + @GET("user/{user_name}/listen-count") + suspend fun getListenCount(@Path("user_name") username : String?) : Response +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt index bc13fcfd..c3c447e6 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt @@ -6,9 +6,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.navigation.NavController import androidx.navigation.NavHostController +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument import org.listenbrainz.android.model.AppNavigationItem import org.listenbrainz.android.ui.screens.brainzplayer.BrainzPlayerScreen import org.listenbrainz.android.ui.screens.explore.ExploreScreen @@ -37,7 +39,12 @@ fun AppNavigation( composable(route = AppNavigationItem.Explore.route){ ExploreScreen() } - composable(route = AppNavigationItem.Profile.route){ + composable(route = "${AppNavigationItem.Profile.route}/{username}" , arguments = listOf( + navArgument("username"){ + type = NavType.StringType + } + ) + ){ ProfileScreen( onScrollToTop = onScrollToTop, scrollRequestState = scrollRequestState, diff --git a/app/src/main/java/org/listenbrainz/android/ui/navigation/BottomNavigationBar.kt b/app/src/main/java/org/listenbrainz/android/ui/navigation/BottomNavigationBar.kt index 583ed33d..86deaf64 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/navigation/BottomNavigationBar.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/navigation/BottomNavigationBar.kt @@ -2,7 +2,14 @@ package org.listenbrainz.android.ui.navigation import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.* +import androidx.compose.material.BackdropScaffoldState +import androidx.compose.material.BackdropValue +import androidx.compose.material.BottomNavigation +import androidx.compose.material.BottomNavigationItem +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.rememberBackdropScaffoldState import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -26,7 +33,8 @@ import org.listenbrainz.android.ui.theme.ListenBrainzTheme fun BottomNavigationBar( navController: NavController = rememberNavController(), backdropScaffoldState: BackdropScaffoldState = rememberBackdropScaffoldState(initialValue = BackdropValue.Revealed), - scrollToTop: () -> Unit + scrollToTop: () -> Unit, + username : String?, ) { val items = listOf( AppNavigationItem.Feed, @@ -43,7 +51,6 @@ fun BottomNavigationBar( val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination val selected = currentDestination?.hierarchy?.any { it.route == item.route } == true - BottomNavigationItem( icon = { Icon( @@ -75,17 +82,30 @@ fun BottomNavigationBar( // A quick way to navigate to back layer content. backdropScaffoldState.reveal() - - navController.navigate(item.route){ - // Avoid building large backstack - popUpTo(AppNavigationItem.Feed.route){ - saveState = true + + when (item.route) { + AppNavigationItem.Profile.route -> navController.navigate("profile/${username}"){ + // Avoid building large backstack + popUpTo(AppNavigationItem.Feed.route){ + saveState = true + } + // Avoid copies + launchSingleTop = true + // Restore previous state + restoreState = true } - // Avoid copies - launchSingleTop = true - // Restore previous state - restoreState = true + else -> navController.navigate(item.route){ + // Avoid building large backstack + popUpTo(AppNavigationItem.Feed.route){ + saveState = true + } + // Avoid copies + launchSingleTop = true + // Restore previous state + restoreState = true + } } + } } @@ -99,7 +119,5 @@ fun BottomNavigationBar( @Preview @Composable fun BottomNavigationBarPreview() { - BottomNavigationBar(navController = rememberNavController()){ - - } + BottomNavigationBar(navController = rememberNavController() , scrollToTop = {} ,username = "pranavkonidena") } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt index 3b8fba3e..d89f1f16 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt @@ -15,7 +15,12 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.* +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.compose.LifecycleStartEffect @@ -147,7 +152,7 @@ class MainActivity : ComponentActivity() { val scope = rememberCoroutineScope() val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination - + val username = dashBoardViewModel.username Scaffold( topBar = { TopBar(navController = navController, searchBarState = when (currentDestination?.route) { AppNavigationItem.BrainzPlayer.route -> brainzplayerSearchBarState @@ -157,7 +162,8 @@ class MainActivity : ComponentActivity() { BottomNavigationBar( navController = navController, backdropScaffoldState = backdropScaffoldState, - scrollToTop = { scrollToTopState = true } + scrollToTop = { scrollToTopState = true }, + username = username ) }, snackbarHost = { diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt index f0206fd5..5ff7137b 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt @@ -36,7 +36,6 @@ import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.rememberLottieComposition import org.listenbrainz.android.R -import org.listenbrainz.android.ui.screens.listens.ListensScreen import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_IN import org.listenbrainz.android.viewmodel.ProfileViewModel @@ -49,7 +48,7 @@ fun ProfileScreen( snackbarState : SnackbarHostState ) { val scrollState = rememberScrollState() - + val uiState = viewModel.uiState.collectAsState() // Scroll to the top when shouldScrollToTop becomes true LaunchedEffect(scrollRequestState) { onScrollToTop { @@ -61,11 +60,11 @@ fun ProfileScreen( when(loginStatus) { STATUS_LOGGED_IN -> { - ListensScreen( - onScrollToTop = onScrollToTop, - scrollRequestState = scrollRequestState, - snackbarState = snackbarState - ) + Column { + Text(uiState.value.listensTabUiState?.listenCount.toString() , color = Color.White) + Text(uiState.value.listensTabUiState?.followersCount.toString() , color = Color.White) + } + } else -> { Column( 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 new file mode 100644 index 00000000..7fcaa896 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileUiState.kt @@ -0,0 +1,22 @@ +package org.listenbrainz.android.ui.screens.profile + +import org.listenbrainz.android.model.Listens +import org.listenbrainz.android.model.PinnedRecording +import org.listenbrainz.android.model.SimilarUser +import org.listenbrainz.android.ui.screens.listens.ListeningNowUiState + +data class ProfileUiState( + val listensTabUiState: ListensTabUiState? = null +) + +data class ListensTabUiState ( + val listenCount : Int? = null, + val followersCount : Int? = null, + val followingCount : Int? = null, + val listeningNow: ListeningNowUiState? = null, + val pinnedSong : PinnedRecording? = null, + val recentListens : List = emptyList(), + val followers : List? = emptyList(), + val following : List? = emptyList(), + val similarUsers : List = emptyList() +) diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/DashBoardViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/DashBoardViewModel.kt index ad259b05..a190a0a6 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/DashBoardViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/DashBoardViewModel.kt @@ -12,6 +12,7 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.listenbrainz.android.di.IoDispatcher @@ -31,6 +32,8 @@ class DashBoardViewModel @Inject constructor( @IoDispatcher private val ioDispatcher: CoroutineDispatcher ) : AndroidViewModel(application) { + var username = "" + val job = viewModelScope.launch { username = async {appPreferences.username.get() }.await() } // Sets Ui mode for XML layouts. fun setUiMode(){ viewModelScope.launch { @@ -123,7 +126,8 @@ class DashBoardViewModel @Inject constructor( && appPreferences.isListeningAllowed.get() } && appPreferences.lbAccessToken.get().isNotEmpty() } - + + fun connectToSpotify() { viewModelScope.launch { remotePlaybackHandler.connectToSpotify { 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 70d90c27..8f74e6e2 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt @@ -1,29 +1,43 @@ package org.listenbrainz.android.viewmodel -import androidx.lifecycle.ViewModel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.listenbrainz.android.di.IoDispatcher +import org.listenbrainz.android.repository.listens.ListensRepository import org.listenbrainz.android.repository.preferences.AppPreferences +import org.listenbrainz.android.repository.social.SocialRepository +import org.listenbrainz.android.repository.socket.SocketRepository +import org.listenbrainz.android.repository.user.UserRepository +import org.listenbrainz.android.ui.screens.profile.ListensTabUiState +import org.listenbrainz.android.ui.screens.profile.ProfileUiState import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_OUT import javax.inject.Inject @HiltViewModel class ProfileViewModel @Inject constructor( val appPreferences: AppPreferences, + val userRepository: UserRepository, + val listensRepository: ListensRepository, + val socketRepository: SocketRepository, + val socialRepository: SocialRepository, + private val savedStateHandle: SavedStateHandle, @IoDispatcher ioDispatcher: CoroutineDispatcher -) : ViewModel() { +) : BaseViewModel() { private val _loginStatusFlow: MutableStateFlow = MutableStateFlow(STATUS_LOGGED_OUT) val loginStatusFlow: StateFlow = _loginStatusFlow.asStateFlow() - + private val listenStateFlow : MutableStateFlow = MutableStateFlow(ListensTabUiState()) + private var username : String? = savedStateHandle.get("username") init { viewModelScope.launch(ioDispatcher) { appPreferences.getLoginStatusFlow() @@ -32,5 +46,42 @@ class ProfileViewModel @Inject constructor( _loginStatusFlow.emit(it) } } + viewModelScope.launch { + getUserListensData() + } + } + + + private suspend fun getUserListensData() { + if(username == null){ + username = appPreferences.username.get() + } + val listenCount = userRepository.fetchUserListenCount(username).data?.payload?.count + val followers = socialRepository.getFollowers(username).data?.followers + val followersCount = followers?.size + + val listensTabState = ListensTabUiState( + listenCount = listenCount, + followersCount = followersCount, + followers = followers + ) + listenStateFlow.emit(listensTabState) + + } + + override val uiState: StateFlow = createUiStateFlow() + + + override fun createUiStateFlow(): StateFlow { + return combine( + listenStateFlow + ) { + array -> + ProfileUiState(array[0]) + }.stateIn( + viewModelScope, + started = SharingStarted.Eagerly, + ProfileUiState() + ) } } \ No newline at end of file From 4a698cd57b05bf6371535334f07063374283b72f Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Thu, 13 Jun 2024 18:36:05 +0530 Subject: [PATCH 39/97] Initialized listens tab in user page --- .../android/model/feed/FeedEventType.kt | 27 +- .../android/model/user/UserSimilarity.kt | 12 + .../brainzplayer/SongRepositoryImpl.kt | 3 - .../android/repository/user/UserRepository.kt | 8 +- .../repository/user/UserRepositoryImpl.kt | 14 + .../android/service/UserService.kt | 8 + .../android/ui/navigation/AppNavigation.kt | 22 +- .../ui/navigation/BottomNavigationBar.kt | 25 +- .../android/ui/navigation/TopBar.kt | 2 +- .../brainzplayer/BrainzPlayerScreen.kt | 1 - .../android/ui/screens/feed/BaseFeedLayout.kt | 12 +- .../android/ui/screens/feed/FeedScreen.kt | 17 +- .../screens/feed/events/FollowFeedLayout.kt | 9 +- .../screens/feed/events/ListenFeedLayout.kt | 9 +- .../feed/events/ListenLikeFeedLayout.kt | 7 +- .../feed/events/NotificationFeedLayout.kt | 9 +- .../PersonalRecommendationFeedLayout.kt | 9 +- .../ui/screens/feed/events/PinFeedLayout.kt | 9 +- .../RecordingRecommendationFeedLayout.kt | 8 +- .../screens/feed/events/ReviewFeedLayout.kt | 7 +- .../screens/feed/events/UnknownFeedLayout.kt | 5 +- .../android/ui/screens/main/MainActivity.kt | 7 +- .../ui/screens/profile/BaseProfileScreen.kt | 203 ++++++++++++ .../ui/screens/profile/ProfileScreen.kt | 14 +- .../ui/screens/profile/ProfileScreenTab.kt | 9 + .../ui/screens/profile/ProfileUiState.kt | 36 ++- .../listens/ListeningAppsList.kt | 2 +- .../{ => profile}/listens/ListeningNowCard.kt | 2 +- .../listens/ListeningNowOnSpotify.kt | 2 +- .../{ => profile}/listens/ListensScreen.kt | 302 ++++++++++-------- .../{ => profile}/listens/ListensUiState.kt | 2 +- .../{ => profile}/listens/ProgressBar.kt | 2 +- .../{ => profile}/listens/TrackProgressBar.kt | 2 +- .../android/ui/screens/search/SearchScreen.kt | 16 +- .../ui/screens/settings/SettingsScreen.kt | 2 +- .../listenbrainz/android/ui/theme/Color.kt | 1 + .../android/viewmodel/ListensViewModel.kt | 4 +- .../android/viewmodel/ProfileViewModel.kt | 36 ++- app/src/main/res/drawable/follow_icon.xml | 9 + 39 files changed, 641 insertions(+), 233 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/UserSimilarity.kt create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/profile/BaseProfileScreen.kt create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreenTab.kt rename app/src/main/java/org/listenbrainz/android/ui/screens/{ => profile}/listens/ListeningAppsList.kt (99%) rename app/src/main/java/org/listenbrainz/android/ui/screens/{ => profile}/listens/ListeningNowCard.kt (98%) rename app/src/main/java/org/listenbrainz/android/ui/screens/{ => profile}/listens/ListeningNowOnSpotify.kt (98%) rename app/src/main/java/org/listenbrainz/android/ui/screens/{ => profile}/listens/ListensScreen.kt (55%) rename app/src/main/java/org/listenbrainz/android/ui/screens/{ => profile}/listens/ListensUiState.kt (92%) rename app/src/main/java/org/listenbrainz/android/ui/screens/{ => profile}/listens/ProgressBar.kt (98%) rename app/src/main/java/org/listenbrainz/android/ui/screens/{ => profile}/listens/TrackProgressBar.kt (95%) create mode 100644 app/src/main/res/drawable/follow_icon.xml diff --git a/app/src/main/java/org/listenbrainz/android/model/feed/FeedEventType.kt b/app/src/main/java/org/listenbrainz/android/model/feed/FeedEventType.kt index 74196779..a581c1e5 100644 --- a/app/src/main/java/org/listenbrainz/android/model/feed/FeedEventType.kt +++ b/app/src/main/java/org/listenbrainz/android/model/feed/FeedEventType.kt @@ -114,6 +114,7 @@ enum class FeedEventType ( onReview: () -> Unit, onPin: () -> Unit, onClick: () -> Unit, + goToUserPage: (String?) -> Unit ){ when (this){ RECORDING_RECOMMENDATION -> RecordingRecommendationFeedLayout( @@ -129,7 +130,8 @@ enum class FeedEventType ( onPin = onPin, onReview = onReview, onPersonallyRecommend = onPersonallyRecommend, - onRecommend = onRecommend + onRecommend = onRecommend, + goToUserPage = goToUserPage ) PERSONAL_RECORDING_RECOMMENDATION -> PersonalRecommendationFeedLayout( event = event, @@ -143,7 +145,8 @@ enum class FeedEventType ( onPin = onPin, onReview = onReview, onPersonallyRecommend = onPersonallyRecommend, - onRecommend = onRecommend + onRecommend = onRecommend, + goToUserPage = goToUserPage ) RECORDING_PIN -> PinFeedLayout( event = event, @@ -158,7 +161,8 @@ enum class FeedEventType ( onPin = onPin, onReview = onReview, onPersonallyRecommend = onPersonallyRecommend, - onRecommend = onRecommend + onRecommend = onRecommend, + goToUserPage = goToUserPage ) LIKE -> ListenLikeFeedLayout( event = event, @@ -172,7 +176,8 @@ enum class FeedEventType ( onPin = onPin, onReview = onReview, onPersonallyRecommend = onPersonallyRecommend, - onRecommend = onRecommend + onRecommend = onRecommend, + goToUserPage = goToUserPage ) LISTEN -> ListenFeedLayout( event = event, @@ -186,10 +191,11 @@ enum class FeedEventType ( onPin = onPin, onReview = onReview, onPersonallyRecommend = onPersonallyRecommend, - onRecommend = onRecommend + onRecommend = onRecommend, + goToUserPage = goToUserPage ) - FOLLOW -> FollowFeedLayout(event = event, parentUser = parentUser) - NOTIFICATION -> NotificationFeedLayout(event = event, onDeleteOrHide = onDeleteOrHide) + FOLLOW -> FollowFeedLayout(event = event, parentUser = parentUser, goToUserPage = goToUserPage) + NOTIFICATION -> NotificationFeedLayout(event = event, onDeleteOrHide = onDeleteOrHide, goToUserPage = goToUserPage) REVIEW -> ReviewFeedLayout( event = event, parentUser = parentUser, @@ -202,7 +208,8 @@ enum class FeedEventType ( onPin = onPin, onReview = onReview, onPersonallyRecommend = onPersonallyRecommend, - onRecommend = onRecommend + onRecommend = onRecommend, + goToUserPage = goToUserPage ) UNKNOWN -> UnknownFeedLayout(event = event) } @@ -212,7 +219,8 @@ enum class FeedEventType ( fun Tagline( modifier: Modifier = Modifier, event: FeedEvent, - parentUser: String + parentUser: String, + goToUserPage: (String?) -> Unit ) { val linkStyle = SpanStyle( color = ListenBrainzTheme.colorScheme.lbSignature, @@ -236,6 +244,7 @@ enum class FeedEventType ( text = annotatedString, ) { charOffset -> onClick(charOffset) + goToUserPage(event.username) } diff --git a/app/src/main/java/org/listenbrainz/android/model/user/UserSimilarity.kt b/app/src/main/java/org/listenbrainz/android/model/user/UserSimilarity.kt new file mode 100644 index 00000000..2af7d2f8 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/UserSimilarity.kt @@ -0,0 +1,12 @@ +package org.listenbrainz.android.model.user + +import com.google.gson.annotations.SerializedName + +data class UserSimilarityPayload( + @SerializedName("payload") val userSimilarity: UserSimilarity +) + +data class UserSimilarity( + @SerializedName("similarity") val similarity: Float, + @SerializedName("user_name") val username: String +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepositoryImpl.kt index cc03c623..10abcd57 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/SongRepositoryImpl.kt @@ -1,10 +1,8 @@ package org.listenbrainz.android.repository.brainzplayer -import android.util.Log import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import org.listenbrainz.android.model.Song -import org.listenbrainz.android.model.SongEntity import org.listenbrainz.android.model.dao.SongDao import org.listenbrainz.android.util.SongsData import org.listenbrainz.android.util.Transformer.toSong @@ -43,7 +41,6 @@ class SongRepositoryImpl @Inject constructor( override suspend fun updateSong(song : Song) { songDao.updateSong(song.toSongEntity()) - Log.v("pranav" , "Song updated!") } override fun getRecentlyPlayedSongs(): Flow> = 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 47de456a..e713414d 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 @@ -1,9 +1,13 @@ package org.listenbrainz.android.repository.user import org.listenbrainz.android.model.Listens +import org.listenbrainz.android.model.PinnedRecording +import org.listenbrainz.android.model.user.UserSimilarityPayload import org.listenbrainz.android.util.Resource interface UserRepository { - suspend fun fetchUserListenCount (username : String?) : Resource - suspend fun fetchListeningNow (username : String?) : Resource + suspend fun fetchUserListenCount (username: String?) : Resource + suspend fun fetchListeningNow (username: String?) : Resource + suspend fun fetchUserSimilarity(username: String? , otherUserName: String?) : Resource + suspend fun fetchUserCurrentPins(username: String?) : 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 cf297915..c09498fc 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 @@ -1,7 +1,10 @@ package org.listenbrainz.android.repository.user import org.listenbrainz.android.model.Listens +import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.ResponseError +import org.listenbrainz.android.model.user.UserSimilarity +import org.listenbrainz.android.model.user.UserSimilarityPayload import org.listenbrainz.android.service.UserService import org.listenbrainz.android.util.Resource import org.listenbrainz.android.util.Utils.parseResponse @@ -19,5 +22,16 @@ class UserRepositoryImpl @Inject constructor( TODO("Not yet implemented") } + override suspend fun fetchUserSimilarity(username: String?, otherUserName: String?) : Resource = parseResponse { + if(username.isNullOrEmpty() or otherUserName.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() + service.getUserSimilarity(username,otherUserName) + } + + override suspend fun fetchUserCurrentPins(username: String?): Resource = parseResponse { + if(username.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() + service.getUserCurrentPins(username) + } + + } \ 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 a4513cbb..03f55a30 100644 --- a/app/src/main/java/org/listenbrainz/android/service/UserService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/UserService.kt @@ -1,6 +1,8 @@ package org.listenbrainz.android.service import org.listenbrainz.android.model.Listens +import org.listenbrainz.android.model.PinnedRecording +import org.listenbrainz.android.model.user.UserSimilarityPayload import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path @@ -8,4 +10,10 @@ import retrofit2.http.Path interface UserService { @GET("user/{user_name}/listen-count") suspend fun getListenCount(@Path("user_name") username : String?) : Response + + @GET("user/{user_name}/similar-to/{other_user_name}") + suspend fun getUserSimilarity(@Path("user_name") username: String? , @Path("other_user_name") otherUserName: String?) : Response + + @GET("{user_name}/pins/current") + suspend fun getUserCurrentPins(@Path("user_name") username: String?) : Response } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt index c3c447e6..bd10ae09 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt @@ -31,7 +31,19 @@ fun AppNavigation( startDestination = AppNavigationItem.Feed.route ){ composable(route = AppNavigationItem.Feed.route){ - FeedScreen(scrollToTopState = scrollRequestState, onScrollToTop = onScrollToTop) + FeedScreen(scrollToTopState = scrollRequestState, onScrollToTop = onScrollToTop, goToUserPage = {username : String? -> + if(username != null) { + navController.navigate("${AppNavigationItem.Profile.route}/$username"){ + // Avoid building large backstack + popUpTo(AppNavigationItem.Feed.route){ + saveState = true + } + // Avoid copies + launchSingleTop = true + // Restore previous state + restoreState = true + } + } }) } composable(route = AppNavigationItem.BrainzPlayer.route){ BrainzPlayerScreen() @@ -39,15 +51,17 @@ fun AppNavigation( composable(route = AppNavigationItem.Explore.route){ ExploreScreen() } - composable(route = "${AppNavigationItem.Profile.route}/{username}" , arguments = listOf( + composable(route = "${AppNavigationItem.Profile.route}/{username}", arguments = listOf( navArgument("username"){ type = NavType.StringType } - ) - ){ + )) + { + val username = it.arguments?.getString("username") ProfileScreen( onScrollToTop = onScrollToTop, scrollRequestState = scrollRequestState, + username = username, snackbarState = snackbarState ) } diff --git a/app/src/main/java/org/listenbrainz/android/ui/navigation/BottomNavigationBar.kt b/app/src/main/java/org/listenbrainz/android/ui/navigation/BottomNavigationBar.kt index 86deaf64..5d19426f 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/navigation/BottomNavigationBar.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/navigation/BottomNavigationBar.kt @@ -84,23 +84,28 @@ fun BottomNavigationBar( backdropScaffoldState.reveal() when (item.route) { - AppNavigationItem.Profile.route -> navController.navigate("profile/${username}"){ - // Avoid building large backstack - popUpTo(AppNavigationItem.Feed.route){ - saveState = true + AppNavigationItem.Profile.route -> { + navController.navigate("profile/${username}"){ + // Avoid building large backstack + popUpTo(AppNavigationItem.Feed.route){ + saveState = true + } + // Avoid copies + launchSingleTop = true + // Restore previous state + restoreState = true } - // Avoid copies - launchSingleTop = true - // Restore previous state - restoreState = true } - else -> navController.navigate(item.route){ + AppNavigationItem.Feed.route -> { + navController.navigate(AppNavigationItem.Feed.route) + } + else -> navController.navigate(item.route){ // Avoid building large backstack popUpTo(AppNavigationItem.Feed.route){ saveState = true } // Avoid copies - launchSingleTop = true + launchSingleTop = false // Restore previous state restoreState = true } diff --git a/app/src/main/java/org/listenbrainz/android/ui/navigation/TopBar.kt b/app/src/main/java/org/listenbrainz/android/ui/navigation/TopBar.kt index 5921530c..634d8db5 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/navigation/TopBar.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/navigation/TopBar.kt @@ -38,7 +38,7 @@ fun TopBar( AppNavigationItem.Feed.route -> AppNavigationItem.Feed.title AppNavigationItem.BrainzPlayer.route -> AppNavigationItem.BrainzPlayer.title AppNavigationItem.Explore.route -> AppNavigationItem.Explore.title - AppNavigationItem.Profile.route -> AppNavigationItem.Profile.title + "${AppNavigationItem.Profile.route}/{username}" -> AppNavigationItem.Profile.title AppNavigationItem.Settings.route -> AppNavigationItem.Settings.title AppNavigationItem.About.route -> AppNavigationItem.About.title else -> "" diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index e68bb54d..a5891130 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -164,7 +164,6 @@ fun BrainzPlayerHomeScreen( // 0L // ) // brainzPlayerViewModel.playOrToggleSong(song,true) -// Log.v("pranav",song.title) } ) 2 -> ArtistsOverviewScreen( diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/BaseFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/BaseFeedLayout.kt index 56285765..9c69188b 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/BaseFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/BaseFeedLayout.kt @@ -3,7 +3,6 @@ package org.listenbrainz.android.ui.screens.feed import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.Canvas @@ -60,7 +59,8 @@ fun BaseFeedLayout( parentUser: String, onDeleteOrHide: () -> Unit, isHidden: Boolean = event.hidden == true, - content: @Composable () -> Unit + goToUserPage: (String?) -> Unit, + content: @Composable () -> Unit, ) { // Content that is to be measured for horizontal line. @@ -120,7 +120,10 @@ fun BaseFeedLayout( if (!isHidden){ eventType.Tagline( event = event, - parentUser = parentUser + parentUser = parentUser, + goToUserPage = { + username -> goToUserPage(username) + } ) } else { Text( @@ -300,7 +303,8 @@ private fun BaseFeedLayoutPreview() { hidden = false, metadata = Metadata(user1 = "JasjeetTest"), username = "Jasjeet" ), - onDeleteOrHide = {} + goToUserPage = {}, + onDeleteOrHide = {}, ) { Card(modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/FeedScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/FeedScreen.kt index 24b0303e..4c074349 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/FeedScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/FeedScreen.kt @@ -90,7 +90,8 @@ fun FeedScreen( viewModel: FeedViewModel = hiltViewModel(), socialViewModel: SocialViewModel = hiltViewModel(), scrollToTopState: Boolean, - onScrollToTop: (suspend () -> Unit) -> Unit + onScrollToTop: (suspend () -> Unit) -> Unit, + goToUserPage: (String?) -> Unit ) { val uiState by viewModel.uiState.collectAsState() @@ -121,7 +122,8 @@ fun FeedScreen( isCritiqueBrainzLinked = { viewModel.isCritiqueBrainzLinked() }, onPlay = { event -> viewModel.play(event) - } + }, + goToUserPage = goToUserPage ) } @@ -141,6 +143,7 @@ fun FeedScreen( searchFollower: (String) -> Unit, isCritiqueBrainzLinked: suspend () -> Boolean?, onPlay: (event: FeedEvent) -> Unit, + goToUserPage: (String?) -> Unit ) { val myFeedPagingData = uiState.myFeedState.eventList.collectAsLazyPagingItems() val myFeedListState = rememberLazyListState() @@ -241,7 +244,8 @@ fun FeedScreen( FeedDialogBundleKeys.feedDialogBundle(0, index) ) }, - onPlay = onPlay + onPlay = onPlay, + goToUserPage = goToUserPage ) 1 -> FollowListens( @@ -405,6 +409,7 @@ private fun MyFeed( review: (index: Int) -> Unit, pin: (index: Int) -> Unit, onPlay: (FeedEvent) -> Unit, + goToUserPage: (String?) -> Unit, uriHandler: UriHandler = LocalUriHandler.current ) { // Since, at most one drop down will be active at a time, then we only need to maintain one state variable. @@ -472,7 +477,8 @@ private fun MyFeed( onClick = { onPlay(event) dropdownItemIndex.value = null - } + }, + goToUserPage = goToUserPage ) } @@ -883,7 +889,8 @@ private fun FeedScreenPreview() { pin = {_,_ ->}, searchFollower = {}, isCritiqueBrainzLinked = {true}, - onPlay = {} + onPlay = {}, + goToUserPage = {} ) } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/FollowFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/FollowFeedLayout.kt index df9cb71a..4158a7bd 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/FollowFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/FollowFeedLayout.kt @@ -4,9 +4,9 @@ import android.content.res.Configuration import androidx.compose.material.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview +import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.model.feed.FeedEvent import org.listenbrainz.android.model.feed.FeedEventType -import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.ui.screens.feed.BaseFeedLayout import org.listenbrainz.android.ui.theme.ListenBrainzTheme @@ -14,13 +14,15 @@ import org.listenbrainz.android.ui.theme.ListenBrainzTheme fun FollowFeedLayout( event: FeedEvent, parentUser: String, + goToUserPage: (String?) -> Unit, ) { BaseFeedLayout( eventType = FeedEventType.FOLLOW, event = event, parentUser = parentUser, onDeleteOrHide = {}, - content = {} + content = {}, + goToUserPage = goToUserPage ) } @@ -42,7 +44,8 @@ private fun FollowFeedLayoutPreview() { ), username = "Jasjeet" ), - parentUser = "Jasjeet" + parentUser = "Jasjeet", + goToUserPage = {} ) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenFeedLayout.kt index f86da121..25f164bc 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenFeedLayout.kt @@ -27,13 +27,15 @@ fun ListenFeedLayout ( onPin: () -> Unit, onRecommend: () -> Unit, onPersonallyRecommend: () -> Unit, - onReview: () -> Unit + onReview: () -> Unit, + goToUserPage: (String?) -> Unit, ) { BaseFeedLayout( eventType = FeedEventType.LISTEN, event = event, parentUser = parentUser, - onDeleteOrHide = onDeleteOrHide + onDeleteOrHide = onDeleteOrHide, + goToUserPage = goToUserPage ) { ListenCardSmall( @@ -88,7 +90,8 @@ private fun ListenFeedLayoutPreview() { onPin = {}, onRecommend = {}, onPersonallyRecommend = {}, - onReview = {} + onReview = {}, + goToUserPage = {} ) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenLikeFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenLikeFeedLayout.kt index e34f2ee4..34490d99 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenLikeFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenLikeFeedLayout.kt @@ -27,13 +27,15 @@ fun ListenLikeFeedLayout( onPin: () -> Unit, onRecommend: () -> Unit, onPersonallyRecommend: () -> Unit, - onReview: () -> Unit + onReview: () -> Unit, + goToUserPage: (String?) -> Unit ) { BaseFeedLayout( eventType = FeedEventType.LIKE, event = event, parentUser = parentUser, onDeleteOrHide = onDeleteOrHide, + goToUserPage = goToUserPage ) { ListenCardSmall( trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", @@ -87,7 +89,8 @@ private fun ListenLikeFeedLayoutPreview() { onPin = {}, onRecommend = {}, onPersonallyRecommend = {}, - onReview = {} + onReview = {}, + goToUserPage = {} ) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/NotificationFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/NotificationFeedLayout.kt index 55e2e590..204a4a7f 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/NotificationFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/NotificationFeedLayout.kt @@ -4,9 +4,9 @@ import android.content.res.Configuration import androidx.compose.material.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview +import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.model.feed.FeedEvent import org.listenbrainz.android.model.feed.FeedEventType -import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.ui.screens.feed.BaseFeedLayout import org.listenbrainz.android.ui.theme.ListenBrainzTheme @@ -16,13 +16,15 @@ import org.listenbrainz.android.ui.theme.ListenBrainzTheme fun NotificationFeedLayout( event: FeedEvent, onDeleteOrHide: () -> Unit, + goToUserPage: (String?) -> Unit, ) { BaseFeedLayout( eventType = FeedEventType.NOTIFICATION, event = event, parentUser = "", onDeleteOrHide = onDeleteOrHide, - content = {} + content = {}, + goToUserPage = goToUserPage ) } @@ -41,7 +43,8 @@ private fun NotificationFeedLayoutPreview() { metadata = Metadata(), username = "Jasjeet" ), - onDeleteOrHide = {} + onDeleteOrHide = {}, + goToUserPage = {} ) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PersonalRecommendationFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PersonalRecommendationFeedLayout.kt index b63e2d66..2cef2b43 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PersonalRecommendationFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PersonalRecommendationFeedLayout.kt @@ -39,13 +39,15 @@ fun PersonalRecommendationFeedLayout( onPin: () -> Unit, onRecommend: () -> Unit, onPersonallyRecommend: () -> Unit, - onReview: () -> Unit + onReview: () -> Unit, + goToUserPage: (String?) -> Unit, ) { BaseFeedLayout( eventType = FeedEventType.PERSONAL_RECORDING_RECOMMENDATION, event = event, parentUser = parentUser, - onDeleteOrHide = onDeleteOrHide + onDeleteOrHide = onDeleteOrHide, + goToUserPage = goToUserPage ) { ListenCardSmall( trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", @@ -142,7 +144,8 @@ private fun PersonalRecommendationFeedLayoutPreview() { onPin = {}, onRecommend = {}, onPersonallyRecommend = {}, - onReview = {} + onReview = {}, + goToUserPage = {} ) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PinFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PinFeedLayout.kt index 75057159..51481550 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PinFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PinFeedLayout.kt @@ -30,14 +30,16 @@ fun PinFeedLayout( onPin: () -> Unit, onRecommend: () -> Unit, onPersonallyRecommend: () -> Unit, - onReview: () -> Unit + onReview: () -> Unit, + goToUserPage: (String?) -> Unit, ) { BaseFeedLayout( eventType = FeedEventType.RECORDING_PIN, event = event, isHidden = isHidden, parentUser = parentUser, - onDeleteOrHide = onDeleteOrHide + onDeleteOrHide = onDeleteOrHide, + goToUserPage = goToUserPage, ) { ListenCardSmall( @@ -106,7 +108,8 @@ fun PinFeedLayoutPreview() { onPin = {}, onRecommend = {}, onPersonallyRecommend = {}, - onReview = {} + onReview = {}, + goToUserPage = {}, ) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/RecordingRecommendationFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/RecordingRecommendationFeedLayout.kt index 5e6b19a6..2e3cc660 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/RecordingRecommendationFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/RecordingRecommendationFeedLayout.kt @@ -28,14 +28,15 @@ fun RecordingRecommendationFeedLayout( onPin: () -> Unit, onRecommend: () -> Unit, onPersonallyRecommend: () -> Unit, - onReview: () -> Unit + onReview: () -> Unit, + goToUserPage: (String?) -> Unit, ) { BaseFeedLayout( eventType = FeedEventType.RECORDING_RECOMMENDATION, event = event, isHidden = isHidden, parentUser = parentUser, - onDeleteOrHide = onDeleteOrHide,) { + onDeleteOrHide = onDeleteOrHide, goToUserPage = goToUserPage) { ListenCardSmall( trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", artistName = event.metadata.trackMetadata?.artistName ?: "Unknown", @@ -89,7 +90,8 @@ private fun RecordingRecommendationFeedCardPreview() { onPin = {}, onRecommend = {}, onPersonallyRecommend = {}, - onReview = {} + onReview = {}, + goToUserPage = {} ) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ReviewFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ReviewFeedLayout.kt index b05cd193..e7171706 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ReviewFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ReviewFeedLayout.kt @@ -49,13 +49,15 @@ fun ReviewFeedLayout( onRecommend: () -> Unit, onPersonallyRecommend: () -> Unit, onReview: () -> Unit, + goToUserPage: (String?) -> Unit, uriHandler: UriHandler = LocalUriHandler.current ) { BaseFeedLayout( eventType = FeedEventType.REVIEW, event = event, parentUser = parentUser, - onDeleteOrHide = onDeleteOrHide + onDeleteOrHide = onDeleteOrHide, + goToUserPage = goToUserPage ) { ListenCardSmall( @@ -173,7 +175,8 @@ private fun ReviewFeedLayoutPreview() { onPin = {}, onRecommend = {}, onPersonallyRecommend = {}, - onReview = {} + onReview = {}, + goToUserPage = {} ) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/UnknownFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/UnknownFeedLayout.kt index 475d7490..3d2dc76e 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/UnknownFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/UnknownFeedLayout.kt @@ -4,9 +4,9 @@ import android.content.res.Configuration import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview +import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.model.feed.FeedEvent import org.listenbrainz.android.model.feed.FeedEventType -import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.ui.screens.feed.BaseFeedLayout import org.listenbrainz.android.ui.theme.ListenBrainzTheme @@ -19,7 +19,8 @@ fun UnknownFeedLayout( event = event, parentUser = "", onDeleteOrHide = {}, - content = {} + content = {}, + goToUserPage = {} ) } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt index d89f1f16..2e7b2877 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt @@ -212,7 +212,12 @@ class MainActivity : ComponentActivity() { AppNavigationItem.BrainzPlayer.route -> BrainzPlayerSearchScreen(isActive = brainzplayerSearchBarState.isActive , deactivate = {brainzplayerSearchBarState.deactivate()} , brainzplayerQueryState = brainzplayerSearchTextState) else -> SearchScreen( isActive = searchBarState.isActive, - deactivate = {searchBarState.deactivate()} + deactivate = {searchBarState.deactivate()}, + goToUserPage = { + username -> + searchBarState.deactivate() + navController.navigate("${AppNavigationItem.Profile.route}/$username") + } ) } 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 new file mode 100644 index 00000000..a5f29e6b --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/BaseProfileScreen.kt @@ -0,0 +1,203 @@ +package org.listenbrainz.android.ui.screens.profile + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.ElevatedSuggestionChip +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SuggestionChipDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.listenbrainz.android.R +import org.listenbrainz.android.ui.components.LoadingAnimation +import org.listenbrainz.android.ui.screens.profile.listens.ListensScreen +import org.listenbrainz.android.ui.theme.ListenBrainzTheme +import org.listenbrainz.android.ui.theme.app_bg_light +import org.listenbrainz.android.ui.theme.lb_purple + +@Composable +fun BaseProfileScreen( + username: String?, + snackbarState: SnackbarHostState, + uiState: ProfileUiState, +){ + + val currentTab : MutableState = remember { mutableStateOf(ProfileScreenTab.LISTENS) } + val isLoggedInUser = uiState.listensTabUiState.isSelf + Box(modifier = Modifier.fillMaxSize()){ + AnimatedVisibility( + visible = uiState.listensTabUiState.isLoading, + modifier = Modifier.align(Alignment.Center), + enter = fadeIn(initialAlpha = 0.4f), + exit = fadeOut(animationSpec = tween(durationMillis = 250)) + ) { + LoadingAnimation() + } + AnimatedVisibility(visible = !uiState.listensTabUiState.isLoading) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .background( + Brush.verticalGradient( + listOf( + ListenBrainzTheme.colorScheme.background, + Color.Transparent + ) + ) + ) + ) { + Spacer(modifier = Modifier.width(ListenBrainzTheme.paddings.chipsHorizontal / 2)) + repeat(6) { position -> + ElevatedSuggestionChip( + modifier = Modifier.padding(ListenBrainzTheme.paddings.chipsHorizontal), + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + if (position == currentTab.value.index) { + ListenBrainzTheme.colorScheme.chipSelected + } else { + ListenBrainzTheme.colorScheme.chipUnselected + } + ), + shape = ListenBrainzTheme.shapes.chips, + elevation = SuggestionChipDefaults.elevatedSuggestionChipElevation(elevation = 4.dp), + label = { + Text( + text = when (position) { + 0 -> username ?: "" + 1 -> ProfileScreenTab.LISTENS.value + 2 -> ProfileScreenTab.STATS.value + 3 -> ProfileScreenTab.PLAYLISTS.value + 4 -> ProfileScreenTab.TASTE.value + 5 -> ProfileScreenTab.CREATED_FOR_YOU.value + else -> "" + }, + style = ListenBrainzTheme.textStyles.chips, + color = ListenBrainzTheme.colorScheme.text, + ) + }, + onClick = { currentTab.value = when (position) { + 1 -> ProfileScreenTab.LISTENS + 2 -> ProfileScreenTab.STATS + 3 -> ProfileScreenTab.PLAYLISTS + 4 -> ProfileScreenTab.TASTE + 5 -> ProfileScreenTab.CREATED_FOR_YOU + else -> ProfileScreenTab.LISTENS + } } + ) + } + } + + + Row (modifier = Modifier + .align(Alignment.End) + .padding(end = 20.dp)) { + when(isLoggedInUser) { + true -> AddListensButton() + false -> FollowButton() + null -> AddListensButton() + } + Spacer(modifier = Modifier.width(10.dp)) + MusicBrainzButton() + } + when(currentTab.value) { + ProfileScreenTab.LISTENS -> ListensScreen( + scrollRequestState = false, + onScrollToTop = {}, + snackbarState = snackbarState, + username = username + ) + else -> ListensScreen( + scrollRequestState = false, + onScrollToTop = {}, + snackbarState = snackbarState, + username = username + ) + } + + } + } + + } + +} + +@Composable +private fun FollowButton() { + IconButton(onClick = { /*TODO*/ }, modifier = Modifier + .background(lb_purple) + .width(90.dp) + .height(30.dp)) { + Row(modifier = Modifier.padding(all = 4.dp)) { + Icon(painter = painterResource(id = R.drawable.follow_icon), contentDescription = "", tint = app_bg_light, modifier = Modifier + .width(20.dp) + .height(20.dp)) + Spacer(modifier = Modifier.width(5.dp)) + Text("Follow", color = app_bg_light) + } + } +} + +@Composable +private fun AddListensButton() { + IconButton(onClick = { /*TODO*/ }, modifier = Modifier + .background(Color(0xFF353070)) + .width(90.dp) + .height(30.dp)) { + Row(modifier = Modifier.padding(all = 4.dp)) { + Icon(imageVector = Icons.Default.Add, contentDescription = "", tint = app_bg_light, modifier = Modifier + .width(10.dp) + .height(30.dp)) + Spacer(modifier = Modifier.width(5.dp)) + Text("Add Listens", color = Color.White, fontSize = 10.sp, ) + } + } +} + +@Composable +private fun MusicBrainzButton() { + IconButton(onClick = { /*TODO*/ }, modifier = Modifier + .background(Color(0xFF353070)) + .width(120.dp) + .height(30.dp)) { + Row(modifier = Modifier.padding(all = 4.dp)) { + Icon(painter = painterResource(id = R.drawable.ic_metabrainz_logo_no_text), contentDescription = "", modifier = Modifier + .width(20.dp) + .height(30.dp)) + Spacer(modifier = Modifier.width(5.dp)) + Text("MusicBrainz", color = Color.White, fontSize = 10.sp) + Spacer(modifier = Modifier.width(5.dp)) + Icon(imageVector = Icons.AutoMirrored.Filled.OpenInNew, contentDescription = "", tint = app_bg_light, modifier = Modifier + .width(10.dp) + .height(30.dp)) + } + } +} diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt index 5ff7137b..f90d8b33 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt @@ -45,7 +45,8 @@ fun ProfileScreen( viewModel: ProfileViewModel = hiltViewModel(), scrollRequestState: Boolean, onScrollToTop: (suspend () -> Unit) -> Unit, - snackbarState : SnackbarHostState + username: String?, + snackbarState: SnackbarHostState ) { val scrollState = rememberScrollState() val uiState = viewModel.uiState.collectAsState() @@ -60,11 +61,16 @@ fun ProfileScreen( when(loginStatus) { STATUS_LOGGED_IN -> { - Column { - Text(uiState.value.listensTabUiState?.listenCount.toString() , color = Color.White) - Text(uiState.value.listensTabUiState?.followersCount.toString() , color = Color.White) + LaunchedEffect(Unit) { + viewModel.getUserListensData(username) } + BaseProfileScreen( + username = username, + snackbarState = snackbarState, + uiState = uiState.value + ) + } else -> { Column( diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreenTab.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreenTab.kt new file mode 100644 index 00000000..27152f5c --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreenTab.kt @@ -0,0 +1,9 @@ +package org.listenbrainz.android.ui.screens.profile + +enum class ProfileScreenTab (val value : String, val index : Int,) { + LISTENS("Listens" , 1), + STATS("Stats", 2), + TASTE("Taste", 3), + PLAYLISTS("Playlists", 4), + CREATED_FOR_YOU("Created for you", 5), +} 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 7fcaa896..48321de8 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 @@ -1,22 +1,36 @@ package org.listenbrainz.android.ui.screens.profile -import org.listenbrainz.android.model.Listens +import com.spotify.protocol.types.PlayerState +import org.listenbrainz.android.model.Listen +import org.listenbrainz.android.model.ListenBitmap import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.SimilarUser -import org.listenbrainz.android.ui.screens.listens.ListeningNowUiState +import org.listenbrainz.android.ui.screens.profile.listens.ListeningNowUiState data class ProfileUiState( - val listensTabUiState: ListensTabUiState? = null + val listensTabUiState: ListensTabUiState = ListensTabUiState() ) data class ListensTabUiState ( - val listenCount : Int? = null, - val followersCount : Int? = null, - val followingCount : Int? = null, + val isLoading: Boolean = true, + val isSelf: Boolean = false, + val listenCount: Int? = null, + val followersCount: Int? = null, + val followingCount: Int? = null, val listeningNow: ListeningNowUiState? = null, - val pinnedSong : PinnedRecording? = null, - val recentListens : List = emptyList(), - val followers : List? = emptyList(), - val following : List? = emptyList(), - val similarUsers : List = emptyList() + val pinnedSong: PinnedRecording? = null, + val compatibility: Float? = null, + val recentListens: List? = emptyList(), + val followers: List? = emptyList(), + val following: List? = emptyList(), + val similarUsers: List? = emptyList() +) + +data class ListeningNowUiState( + val listeningNow: Listen? = null, + val listeningNowBitmap: ListenBitmap = ListenBitmap(), + val playerState: PlayerState? = null, + val songDuration: Long = 0L, + val songCurrentPosition: Long = 0L, + val progress: Float = 0f ) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/listens/ListeningAppsList.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListeningAppsList.kt similarity index 99% rename from app/src/main/java/org/listenbrainz/android/ui/screens/listens/ListeningAppsList.kt rename to app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListeningAppsList.kt index 74f57219..f7e14aa4 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/listens/ListeningAppsList.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListeningAppsList.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.ui.screens.listens +package org.listenbrainz.android.ui.screens.profile.listens import android.graphics.Canvas import android.graphics.ColorFilter diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/listens/ListeningNowCard.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListeningNowCard.kt similarity index 98% rename from app/src/main/java/org/listenbrainz/android/ui/screens/listens/ListeningNowCard.kt rename to app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListeningNowCard.kt index 7ceafb4e..3ba2e826 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/listens/ListeningNowCard.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListeningNowCard.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.ui.screens.listens +package org.listenbrainz.android.ui.screens.profile.listens import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/listens/ListeningNowOnSpotify.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListeningNowOnSpotify.kt similarity index 98% rename from app/src/main/java/org/listenbrainz/android/ui/screens/listens/ListeningNowOnSpotify.kt rename to app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListeningNowOnSpotify.kt index de0aabd3..278d7339 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/listens/ListeningNowOnSpotify.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListeningNowOnSpotify.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.ui.screens.listens +package org.listenbrainz.android.ui.screens.profile.listens import androidx.compose.foundation.Image import androidx.compose.foundation.clickable diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/listens/ListensScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt similarity index 55% rename from app/src/main/java/org/listenbrainz/android/ui/screens/listens/ListensScreen.kt rename to app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt index 74204df4..5b3c18bb 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/listens/ListensScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt @@ -1,22 +1,27 @@ -package org.listenbrainz.android.ui.screens.listens +package org.listenbrainz.android.ui.screens.profile.listens import android.os.Bundle import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +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.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -28,10 +33,14 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.UriHandler +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import kotlinx.coroutines.launch import org.listenbrainz.android.R @@ -42,7 +51,6 @@ import org.listenbrainz.android.model.TrackMetadata import org.listenbrainz.android.model.feed.ReviewEntityType import org.listenbrainz.android.ui.components.ErrorBar import org.listenbrainz.android.ui.components.ListenCardSmall -import org.listenbrainz.android.ui.components.LoadingAnimation import org.listenbrainz.android.ui.components.SuccessBar import org.listenbrainz.android.ui.components.dialogs.Dialog import org.listenbrainz.android.ui.components.dialogs.PersonalRecommendationDialog @@ -51,25 +59,30 @@ import org.listenbrainz.android.ui.components.dialogs.ReviewDialog import org.listenbrainz.android.ui.components.dialogs.rememberDialogsState import org.listenbrainz.android.ui.screens.feed.FeedUiState import org.listenbrainz.android.ui.screens.feed.SocialDropdown -import org.listenbrainz.android.ui.screens.profile.UserData +import org.listenbrainz.android.ui.screens.profile.ListensTabUiState import org.listenbrainz.android.ui.screens.settings.PreferencesUiState import org.listenbrainz.android.ui.theme.ListenBrainzTheme -import org.listenbrainz.android.util.Utils +import org.listenbrainz.android.ui.theme.app_bg_dark +import org.listenbrainz.android.ui.theme.app_bg_mid +import org.listenbrainz.android.util.Utils.getCoverArtUrl import org.listenbrainz.android.viewmodel.FeedViewModel import org.listenbrainz.android.viewmodel.ListensViewModel +import org.listenbrainz.android.viewmodel.ProfileViewModel import org.listenbrainz.android.viewmodel.SocialViewModel @Composable fun ListensScreen( viewModel: ListensViewModel = hiltViewModel(), + profileViewModel: ProfileViewModel = hiltViewModel(), socialViewModel: SocialViewModel = hiltViewModel(), feedViewModel : FeedViewModel = hiltViewModel(), scrollRequestState: Boolean, onScrollToTop: (suspend () -> Unit) -> Unit, - snackbarState : SnackbarHostState + snackbarState : SnackbarHostState, + username: String?, ) { - val uiState by viewModel.uiState.collectAsState() + val uiState by profileViewModel.uiState.collectAsState() val preferencesUiState by viewModel.preferencesUiState.collectAsState() val socialUiState by socialViewModel.uiState.collectAsState() val feedUiState by feedViewModel.uiState.collectAsState() @@ -80,7 +93,8 @@ fun ListensScreen( ListensScreen( scrollRequestState = scrollRequestState, onScrollToTop = onScrollToTop, - uiState = uiState, + username= username, + uiState = uiState.listensTabUiState, feedUiState = feedUiState, preferencesUiState = preferencesUiState, updateNotificationServicePermissionStatus = { @@ -147,7 +161,8 @@ private enum class ListenDialogBundleKeys { fun ListensScreen( scrollRequestState: Boolean, onScrollToTop: (suspend () -> Unit) -> Unit, - uiState: ListensUiState, + username: String?, + uiState: ListensTabUiState, feedUiState: FeedUiState, preferencesUiState: PreferencesUiState, updateNotificationServicePermissionStatus: () -> Unit, @@ -174,6 +189,10 @@ fun ListensScreen( val scope = rememberCoroutineScope() val context = LocalContext.current + + val recentListensCollapsibleState: MutableState = remember { + mutableStateOf(true) + } // Scroll to the top when shouldScrollToTop becomes true LaunchedEffect(scrollRequestState) { @@ -181,112 +200,109 @@ fun ListensScreen( listState.scrollToItem(0) } } - - Box(modifier = Modifier.fillMaxSize()) { - - LazyColumn(state = listState) { - item { - UserData( - preferencesUiState, - updateNotificationServicePermissionStatus, - validateUserToken, - setToken - ) - } - - item { - val pagerState = rememberPagerState { 1 } - - // TODO: Figure out the use of ListeningNowOnSpotify. It is hidden for now - HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page -> - when (page) { - 0 -> { - uiState.listeningNowUiState.listeningNow.let { listeningNow -> - ListeningNowCard( - listeningNow, - Utils.getCoverArtUrl( - caaReleaseMbid = listeningNow?.trackMetadata?.mbidMapping?.caaReleaseMbid, - caaId = listeningNow?.trackMetadata?.mbidMapping?.caaId - ) - ) { - listeningNow?.let { listen -> playListen(listen.trackMetadata) } - } - } - } - - 1 -> { - AnimatedVisibility( - visible = uiState.listeningNowUiState.playerState?.track?.name != null, - enter = slideInVertically(), - exit = slideOutVertically() - ) { - ListeningNowOnSpotify( - playerState = uiState.listeningNowUiState.playerState, - bitmap = uiState.listeningNowUiState.listeningNowBitmap - ) - } - } - } + + AnimatedVisibility(visible = !uiState.isLoading) { + LazyColumn(state = listState) { + item { + SongsListened(username = username, listenCount = uiState.listenCount) } - } - - itemsIndexed(items = uiState.listens) { index , listen -> - val metadata = Metadata(trackMetadata = listen.trackMetadata) - ListenCardSmall( - modifier = Modifier.padding( - horizontal = ListenBrainzTheme.paddings.horizontal, - vertical = ListenBrainzTheme.paddings.lazyListAdjacent - ), - trackName = listen.trackMetadata.trackName, - artistName = listen.trackMetadata.artistName, - coverArtUrl = Utils.getCoverArtUrl( - caaReleaseMbid = listen.trackMetadata.mbidMapping?.caaReleaseMbid, - caaId = listen.trackMetadata.mbidMapping?.caaId - ), - dropDown = { - SocialDropdown( - isExpanded = dropdownItemIndex.value == index, - onDismiss = { - dropdownItemIndex.value = null - }, - metadata = metadata, - onRecommend = { onRecommend(metadata) }, - onPersonallyRecommend = { - dialogsState.activateDialog(Dialog.PERSONAL_RECOMMENDATION , ListenDialogBundleKeys.listenDialogBundle(0, index)) - dropdownItemIndex.value = null - }, - onReview = { - dialogsState.activateDialog(Dialog.REVIEW , ListenDialogBundleKeys.listenDialogBundle(0, index)) - dropdownItemIndex.value = null - }, - onPin = { - dialogsState.activateDialog(Dialog.PIN , ListenDialogBundleKeys.listenDialogBundle(0, index)) - dropdownItemIndex.value = null - }, - onOpenInMusicBrainz = { - try { - uriHandler.openUri("https://musicbrainz.org/recording/${metadata.trackMetadata?.mbidMapping?.recordingMbid}") - } - catch(e : Error) { - scope.launch { - snackbarState.showSnackbar(context.getString(R.string.err_generic_toast)) + item{ + FollowersInformation(followersCount = uiState.followersCount, followingCount = uiState.followingCount) + } + item{ + Spacer(modifier = Modifier.height(30.dp)) + Text("Recent Listens", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), modifier = Modifier.padding(start = 16.dp)) + Spacer(modifier = Modifier.height(10.dp)) + } + itemsIndexed(items = (when(recentListensCollapsibleState.value){ + true -> uiState.recentListens?.take(5) ?: listOf() + false -> uiState.recentListens?.take(10) ?: listOf() + })) { index, listen -> + val metadata = Metadata(trackMetadata = listen.trackMetadata) + ListenCardSmall( + modifier = Modifier.padding( + horizontal = 16.dp, + vertical = ListenBrainzTheme.paddings.lazyListAdjacent + ), + trackName = listen.trackMetadata.trackName, + artistName = listen.trackMetadata.artistName, + coverArtUrl = getCoverArtUrl( + caaReleaseMbid = listen.trackMetadata.mbidMapping?.caaReleaseMbid, + caaId = listen.trackMetadata.mbidMapping?.caaId + ), + dropDown = { + SocialDropdown( + isExpanded = dropdownItemIndex.value == index, + onDismiss = { + dropdownItemIndex.value = null + }, + metadata = metadata, + onRecommend = { onRecommend(metadata) }, + onPersonallyRecommend = { + dialogsState.activateDialog(Dialog.PERSONAL_RECOMMENDATION , ListenDialogBundleKeys.listenDialogBundle(0, index)) + dropdownItemIndex.value = null + }, + onReview = { + dialogsState.activateDialog(Dialog.REVIEW , ListenDialogBundleKeys.listenDialogBundle(0, index)) + dropdownItemIndex.value = null + }, + onPin = { + dialogsState.activateDialog(Dialog.PIN , ListenDialogBundleKeys.listenDialogBundle(0, index)) + dropdownItemIndex.value = null + }, + onOpenInMusicBrainz = { + try { + uriHandler.openUri("https://musicbrainz.org/recording/${metadata.trackMetadata?.mbidMapping?.recordingMbid}") + } + catch(e : Error) { + scope.launch { + snackbarState.showSnackbar(context.getString(R.string.err_generic_toast)) + } } + dropdownItemIndex.value = null } - dropdownItemIndex.value = null - } - ) - }, - enableDropdownIcon = true, - onDropdownIconClick = { - dropdownItemIndex.value = index + ) + }, + enableDropdownIcon = true, + onDropdownIconClick = { + dropdownItemIndex.value = index + } + ) { + playListen(listen.trackMetadata) + } + } + item { + Spacer(modifier = Modifier.height(10.dp)) + Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + TextButton(onClick = { + recentListensCollapsibleState.value = !recentListensCollapsibleState.value + }, modifier = Modifier.border(border = BorderStroke(1.dp, + app_bg_mid), shape = RoundedCornerShape(7.dp) + )) { + Text(when(recentListensCollapsibleState.value){ + true -> "Load More" + false -> "Load Less" + }, color = app_bg_mid, style = MaterialTheme.typography.bodyMedium) + } } - ) { - playListen(listen.trackMetadata) + } + if(!uiState.isSelf){ + item { + Spacer(modifier = Modifier.height(30.dp)) + Text("Your Compatibility", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), modifier = Modifier.padding(start = 16.dp)) + Spacer(modifier = Modifier.height(10.dp)) + CompatibilityCard(compatibility = uiState.compatibility ?: 0f) + } + } + + } } + + ErrorBar(error = socialUiState.error, onErrorShown = onErrorShown ) SuccessBar(resId = socialUiState.successMsgId, onMessageShown = onMessageShown, snackbarState = snackbarState) @@ -296,7 +312,7 @@ fun ListensScreen( }, currentDialog = dialogsState.currentDialog, currentIndex = dialogsState.metadata?.getInt(ListenDialogBundleKeys.EVENT_INDEX.name), - listens = uiState.listens, + listens = uiState.recentListens ?: listOf(), onPin = {metadata, blurbContent -> onPin(metadata, blurbContent)}, searchUsers = { query -> searchUsers(query) }, feedUiState = feedUiState, @@ -304,19 +320,7 @@ fun ListensScreen( onReview = {type, blurbContent, rating, locale, metadata -> onReview(type, blurbContent, rating, locale, metadata) }, onPersonallyRecommend = {metadata, users, blurbContent -> onPersonallyRecommend(metadata, users, blurbContent)}, snackbarState = snackbarState, - socialUiState = socialUiState - ) - - // Loading Animation - AnimatedVisibility( - visible = uiState.isLoading, - modifier = Modifier.align(Alignment.Center), - enter = fadeIn(initialAlpha = 0.4f), - exit = fadeOut(animationSpec = tween(durationMillis = 250)) - ) { - LoadingAnimation() - } - } + socialUiState = socialUiState) } @Composable @@ -385,6 +389,43 @@ private fun Dialogs( } } +@Composable +private fun SongsListened(username: String? , listenCount: Int?){ + Column (horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(modifier = Modifier.height(30.dp)) + Text("$username has listened to", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Spacer(modifier = Modifier.height(10.dp)) + HorizontalDivider(color = app_bg_dark, modifier = Modifier.padding(start = 60.dp, end = 60.dp)) + Spacer(modifier = Modifier.height(10.dp)) + Text(listenCount.toString(), color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), textAlign = TextAlign.Center) + Text("songs so far", color = app_bg_mid, style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center) + } + +} + +@Composable +private fun FollowersInformation(followersCount: Int?, followingCount: Int?){ + Spacer(modifier = Modifier.height(30.dp)) + Row (horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth()) { + Column (horizontalAlignment = Alignment.CenterHorizontally) { + Text((followersCount ?:0).toString(), style = MaterialTheme.typography.bodyLarge, color = Color.White) + Text("Followers", style = MaterialTheme.typography.bodyLarge, color = Color.White) + } + Column (horizontalAlignment = Alignment.CenterHorizontally) { + Text((followingCount ?: 0).toString(), style = MaterialTheme.typography.bodyLarge, color = Color.White) + Text("Following", style = MaterialTheme.typography.bodyLarge, color = Color.White) + } + } +} + +@Composable +fun CompatibilityCard(compatibility: Float){ + LinearProgressIndicator(progress = { + compatibility + }) +} + + @Preview @Composable fun ListensScreenPreview() { @@ -392,7 +433,7 @@ fun ListensScreenPreview() { onScrollToTop = {}, scrollRequestState = false, updateNotificationServicePermissionStatus = {}, - uiState = ListensUiState(), + uiState = ListensTabUiState(), feedUiState = FeedUiState(), preferencesUiState = PreferencesUiState(), validateUserToken = { true }, @@ -408,6 +449,7 @@ fun ListensScreenPreview() { onReview = {_,_,_,_,_ ->}, onPersonallyRecommend = {_,_,_ ->}, dropdownItemIndex = remember { mutableStateOf(null) }, - snackbarState = remember { SnackbarHostState() } + snackbarState = remember { SnackbarHostState() }, + username = "pranavkonidena" ) } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/listens/ListensUiState.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensUiState.kt similarity index 92% rename from app/src/main/java/org/listenbrainz/android/ui/screens/listens/ListensUiState.kt rename to app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensUiState.kt index 1b760e7d..18120a93 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/listens/ListensUiState.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensUiState.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.ui.screens.listens +package org.listenbrainz.android.ui.screens.profile.listens import com.spotify.protocol.types.PlayerState import org.listenbrainz.android.model.Listen diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/listens/ProgressBar.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ProgressBar.kt similarity index 98% rename from app/src/main/java/org/listenbrainz/android/ui/screens/listens/ProgressBar.kt rename to app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ProgressBar.kt index 0fb4efb5..5eff914a 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/listens/ProgressBar.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ProgressBar.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.ui.screens.listens +package org.listenbrainz.android.ui.screens.profile.listens import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/listens/TrackProgressBar.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/TrackProgressBar.kt similarity index 95% rename from app/src/main/java/org/listenbrainz/android/ui/screens/listens/TrackProgressBar.kt rename to app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/TrackProgressBar.kt index 2db55c7c..1e225486 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/listens/TrackProgressBar.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/TrackProgressBar.kt @@ -1,4 +1,4 @@ -package org.listenbrainz.android.ui.screens.listens +package org.listenbrainz.android.ui.screens.profile.listens import android.os.Handler import android.widget.SeekBar diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/search/SearchScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/search/SearchScreen.kt index 800d2c71..733a5bab 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/search/SearchScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/search/SearchScreen.kt @@ -64,6 +64,7 @@ import org.listenbrainz.android.viewmodel.SearchViewModel fun SearchScreen( isActive: Boolean, viewModel: SearchViewModel = hiltViewModel(), + goToUserPage: (String) -> Unit, deactivate: () -> Unit ) { AnimatedVisibility( @@ -85,6 +86,7 @@ fun SearchScreen( }, onClear = { viewModel.clearUi() }, onErrorShown = { viewModel.clearErrorFlow() }, + goToUserPage = goToUserPage ) } @@ -105,6 +107,7 @@ private fun SearchScreen( }, onErrorShown: () -> Unit, focusRequester: FocusRequester = remember { FocusRequester() }, + goToUserPage: (String) -> Unit, window: WindowInfo = LocalWindowInfo.current ) { // Used for initial window focus. @@ -180,7 +183,7 @@ private fun SearchScreen( ErrorBar(uiState.error, onErrorShown) // Main Content - UserList(uiState, onFollowClick) + UserList(uiState, onFollowClick, goToUserPage) } } } @@ -188,7 +191,8 @@ private fun SearchScreen( @Composable private fun UserList( uiState: SearchUiState, - onFollowClick: (User, Int) -> Unit + onFollowClick: (User, Int) -> Unit, + goToUserPage: (String) -> Unit, ) { LazyColumn(contentPadding = PaddingValues(ListenBrainzTheme.paddings.lazyListAdjacent)) { @@ -215,7 +219,10 @@ private fun UserList( Text( text = user.username, color = ListenBrainzTheme.colorScheme.text, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, + modifier = Modifier.clickable { + goToUserPage(user.username) + } ) } @@ -255,7 +262,8 @@ private fun SearchScreenPreview() { onQueryChange = {}, onFollowClick = { _, _ -> flow { emit(true) }}, onClear = {}, - onErrorShown = {} + onErrorShown = {}, + goToUserPage = {} ) } } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt index da67c9f8..9d1f7782 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt @@ -59,7 +59,7 @@ import org.listenbrainz.android.BuildConfig import org.listenbrainz.android.R import org.listenbrainz.android.model.UiMode import org.listenbrainz.android.ui.components.Switch -import org.listenbrainz.android.ui.screens.listens.ListeningAppsList +import org.listenbrainz.android.ui.screens.profile.listens.ListeningAppsList import org.listenbrainz.android.ui.screens.main.DonateActivity import org.listenbrainz.android.ui.screens.main.MainActivity import org.listenbrainz.android.ui.theme.ListenBrainzTheme diff --git a/app/src/main/java/org/listenbrainz/android/ui/theme/Color.kt b/app/src/main/java/org/listenbrainz/android/ui/theme/Color.kt index ae6b7fd0..b4d54f21 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/theme/Color.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/theme/Color.kt @@ -20,6 +20,7 @@ val bp_lavender_dark = Color(0xFFE9DCFE) val app_bg_day = Color(0xFFFFFFFF) val app_bg_dark = Color(0xFF292929) val app_bg_light = Color(0xFF8FA3AD) +val app_bg_mid = Color(0xFF8D8D8D) val app_bottom_nav_dark = Color(0xFF000000) val app_bottom_nav_day = Color(0xFFFFFFFF) diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/ListensViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/ListensViewModel.kt index 2520d44c..a406bfe5 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ListensViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ListensViewModel.kt @@ -22,8 +22,8 @@ import org.listenbrainz.android.repository.listens.ListensRepository import org.listenbrainz.android.repository.preferences.AppPreferences import org.listenbrainz.android.repository.remoteplayer.RemotePlaybackHandler import org.listenbrainz.android.repository.socket.SocketRepository -import org.listenbrainz.android.ui.screens.listens.ListeningNowUiState -import org.listenbrainz.android.ui.screens.listens.ListensUiState +import org.listenbrainz.android.ui.screens.profile.listens.ListeningNowUiState +import org.listenbrainz.android.ui.screens.profile.listens.ListensUiState import org.listenbrainz.android.ui.screens.settings.PreferencesUiState import org.listenbrainz.android.util.LinkedService import org.listenbrainz.android.util.Resource.Status.FAILED 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 8f74e6e2..9ede1ae7 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.listenbrainz.android.di.IoDispatcher +import org.listenbrainz.android.model.Listen import org.listenbrainz.android.repository.listens.ListensRepository import org.listenbrainz.android.repository.preferences.AppPreferences import org.listenbrainz.android.repository.social.SocialRepository @@ -26,10 +27,10 @@ import javax.inject.Inject @HiltViewModel class ProfileViewModel @Inject constructor( val appPreferences: AppPreferences, - val userRepository: UserRepository, + private val userRepository: UserRepository, val listensRepository: ListensRepository, val socketRepository: SocketRepository, - val socialRepository: SocialRepository, + private val socialRepository: SocialRepository, private val savedStateHandle: SavedStateHandle, @IoDispatcher ioDispatcher: CoroutineDispatcher ) : BaseViewModel() { @@ -37,7 +38,6 @@ class ProfileViewModel @Inject constructor( private val _loginStatusFlow: MutableStateFlow = MutableStateFlow(STATUS_LOGGED_OUT) val loginStatusFlow: StateFlow = _loginStatusFlow.asStateFlow() private val listenStateFlow : MutableStateFlow = MutableStateFlow(ListensTabUiState()) - private var username : String? = savedStateHandle.get("username") init { viewModelScope.launch(ioDispatcher) { appPreferences.getLoginStatusFlow() @@ -46,25 +46,39 @@ class ProfileViewModel @Inject constructor( _loginStatusFlow.emit(it) } } - viewModelScope.launch { - getUserListensData() - } } - private suspend fun getUserListensData() { - if(username == null){ - username = appPreferences.username.get() + suspend fun getUserListensData(inputUsername: String?) { + val username = inputUsername ?: appPreferences.username.get() + var isLoggedInUser = false + if(inputUsername != null){ + isLoggedInUser = inputUsername == appPreferences.username.get() } val listenCount = userRepository.fetchUserListenCount(username).data?.payload?.count + val listens: List? = listensRepository.fetchUserListens(username).data?.payload?.listens val followers = socialRepository.getFollowers(username).data?.followers val followersCount = followers?.size - + val similarUsers = socialRepository.getSimilarUsers(username).data?.payload + val currentPins = userRepository.fetchUserCurrentPins(username).data + val compatibility = if (username != appPreferences.username.get()) + userRepository.fetchUserSimilarity( + appPreferences.username.get(), + username + ).data?.userSimilarity?.similarity + else 0f val listensTabState = ListensTabUiState( + isLoading = false, + isSelf = isLoggedInUser, listenCount = listenCount, followersCount = followersCount, - followers = followers + followers = followers, + recentListens = listens, + compatibility = compatibility, + similarUsers = similarUsers, + pinnedSong = currentPins ) + listenStateFlow.emit(listensTabState) } diff --git a/app/src/main/res/drawable/follow_icon.xml b/app/src/main/res/drawable/follow_icon.xml new file mode 100644 index 00000000..4e7ec211 --- /dev/null +++ b/app/src/main/res/drawable/follow_icon.xml @@ -0,0 +1,9 @@ + + + From 09e653dcd835ee47bc331ba430288e12fa9c7fba Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Fri, 14 Jun 2024 10:32:00 +0530 Subject: [PATCH 40/97] Adds compatibility card, similar artists --- .../listenbrainz/android/model/user/Artist.kt | 7 ++ .../android/model/user/TopArtists.kt | 5 ++ .../android/model/user/TopArtistsPayload.kt | 13 +++ .../android/repository/user/UserRepository.kt | 3 + .../repository/user/UserRepositoryImpl.kt | 8 +- .../android/service/UserService.kt | 4 + .../ui/screens/profile/ProfileUiState.kt | 3 +- .../screens/profile/listens/ListensScreen.kt | 84 +++++++++++++++++-- .../listenbrainz/android/ui/theme/Color.kt | 6 +- .../android/viewmodel/ProfileViewModel.kt | 21 ++++- 10 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/Artist.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/TopArtists.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/TopArtistsPayload.kt 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 new file mode 100644 index 00000000..131d879f --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/Artist.kt @@ -0,0 +1,7 @@ +package org.listenbrainz.android.model.user + +data class Artist( + val artist_mbid: String, + val artist_name: String, + val listen_count: Int +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/user/TopArtists.kt b/app/src/main/java/org/listenbrainz/android/model/user/TopArtists.kt new file mode 100644 index 00000000..574350d7 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/TopArtists.kt @@ -0,0 +1,5 @@ +package org.listenbrainz.android.model.user + +data class TopArtists( + val payload: TopArtistsPayload +) \ 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 new file mode 100644 index 00000000..bd67cc7a --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/TopArtistsPayload.kt @@ -0,0 +1,13 @@ +package org.listenbrainz.android.model.user + +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 +) \ 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 e713414d..76810b1e 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 @@ -2,6 +2,7 @@ package org.listenbrainz.android.repository.user import org.listenbrainz.android.model.Listens import org.listenbrainz.android.model.PinnedRecording +import org.listenbrainz.android.model.user.TopArtists import org.listenbrainz.android.model.user.UserSimilarityPayload import org.listenbrainz.android.util.Resource @@ -10,4 +11,6 @@ interface UserRepository { suspend fun fetchListeningNow (username: String?) : Resource suspend fun fetchUserSimilarity(username: String? , otherUserName: String?) : Resource suspend fun fetchUserCurrentPins(username: String?) : Resource + // Will move to artist VM once it is made + suspend fun getTopArtists(username: String?): 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 c09498fc..2c19134d 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 @@ -3,7 +3,7 @@ package org.listenbrainz.android.repository.user import org.listenbrainz.android.model.Listens import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.ResponseError -import org.listenbrainz.android.model.user.UserSimilarity +import org.listenbrainz.android.model.user.TopArtists import org.listenbrainz.android.model.user.UserSimilarityPayload import org.listenbrainz.android.service.UserService import org.listenbrainz.android.util.Resource @@ -32,6 +32,12 @@ class UserRepositoryImpl @Inject constructor( service.getUserCurrentPins(username) } + override suspend fun getTopArtists( + username: String? + ): Resource = parseResponse { + if(username.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() + service.getTopArtistsOfUser(username) + } } \ 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 03f55a30..faa6688e 100644 --- a/app/src/main/java/org/listenbrainz/android/service/UserService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/UserService.kt @@ -2,6 +2,7 @@ package org.listenbrainz.android.service import org.listenbrainz.android.model.Listens import org.listenbrainz.android.model.PinnedRecording +import org.listenbrainz.android.model.user.TopArtists import org.listenbrainz.android.model.user.UserSimilarityPayload import retrofit2.Response import retrofit2.http.GET @@ -16,4 +17,7 @@ interface UserService { @GET("{user_name}/pins/current") suspend fun getUserCurrentPins(@Path("user_name") username: String?) : Response + + @GET("stats/user/{user_name}/artists?count=100") + suspend fun getTopArtistsOfUser(@Path("user_name") username: String?) : Response } \ No newline at end of file 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 48321de8..77526c7b 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 @@ -23,7 +23,8 @@ data class ListensTabUiState ( val recentListens: List? = emptyList(), val followers: List? = emptyList(), val following: List? = emptyList(), - val similarUsers: List? = emptyList() + val similarUsers: List? = emptyList(), + val similarArtists: List = emptyList() ) data class ListeningNowUiState( diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt index 5b3c18bb..0ed0b4f6 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt @@ -12,6 +12,7 @@ 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 import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState @@ -34,10 +35,14 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.UriHandler +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -64,6 +69,8 @@ import org.listenbrainz.android.ui.screens.settings.PreferencesUiState import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.ui.theme.app_bg_dark import org.listenbrainz.android.ui.theme.app_bg_mid +import org.listenbrainz.android.ui.theme.compatibilityMeterColor +import org.listenbrainz.android.ui.theme.lb_purple_night import org.listenbrainz.android.util.Utils.getCoverArtUrl import org.listenbrainz.android.viewmodel.FeedViewModel import org.listenbrainz.android.viewmodel.ListensViewModel @@ -293,10 +300,19 @@ fun ListensScreen( Spacer(modifier = Modifier.height(30.dp)) Text("Your Compatibility", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), modifier = Modifier.padding(start = 16.dp)) Spacer(modifier = Modifier.height(10.dp)) - CompatibilityCard(compatibility = uiState.compatibility ?: 0f) + CompatibilityCard(compatibility = uiState.compatibility ?: 0f, uiState.similarArtists) } } + item { + Spacer(modifier = Modifier.height(30.dp)) + FollowersCard( + followersCount = uiState.followersCount, + followingCount = uiState.followingCount, + followers = uiState.followers ?: emptyList(), + following = uiState.following ?: emptyList() + ) + } } } @@ -323,6 +339,45 @@ fun ListensScreen( socialUiState = socialUiState) } +@Composable +private fun buildSimilarArtists(similarArtists: List) { + val white = Color.White + + when { + similarArtists.size > 5 -> { + val topSimilarArtists = similarArtists.take(5) + val artists = topSimilarArtists.joinToString(", ") + val text = buildAnnotatedString { + withStyle(style = SpanStyle(color = white)) { + append("You both listen to ") + } + withStyle(style = SpanStyle(color = lb_purple_night)) { + append(artists) + } + withStyle(style = SpanStyle(color = white)) { + append(" and more.") + } + } + Text(text = text, modifier = Modifier.padding(start=16.dp)) + } + similarArtists.isEmpty() -> { + Text("You have no common artists", color = white, modifier = Modifier.padding(start=16.dp)) + } + else -> { + val artists = similarArtists.joinToString(", ") + val text = buildAnnotatedString { + withStyle(style = SpanStyle(color = white)) { + append("You both listen to ") + } + withStyle(style = SpanStyle(color = lb_purple_night)) { + append(artists) + } + } + Text(text = text, modifier = Modifier.padding(start=16.dp)) + } + } +} + @Composable private fun Dialogs( deactivateDialog: () -> Unit, @@ -419,10 +474,29 @@ private fun FollowersInformation(followersCount: Int?, followingCount: Int?){ } @Composable -fun CompatibilityCard(compatibility: Float){ - LinearProgressIndicator(progress = { - compatibility - }) +fun CompatibilityCard(compatibility: Float, similarArtists: List){ + Row (modifier = Modifier.padding(start = 16.dp)) { + LinearProgressIndicator(progress = { + compatibility + }, color = compatibilityMeterColor, modifier = Modifier + .height(17.dp) + .fillMaxWidth(0.7f), strokeCap = StrokeCap.Round, trackColor = Color(0xFF1C1C1C)) + Spacer(modifier = Modifier.width(9.dp)) + Text("${(compatibility*100).toInt()} %", color = app_bg_mid, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + } + Spacer(modifier = Modifier.height(10.dp)) + buildSimilarArtists(similarArtists = similarArtists) +} + +@Composable +private fun FollowersCard(followersCount: Int?, followingCount: Int?, followers: List, following: List){ + Column (modifier = Modifier.padding(start=16.dp)) { + Text("Followers", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Row { + + } + } + } diff --git a/app/src/main/java/org/listenbrainz/android/ui/theme/Color.kt b/app/src/main/java/org/listenbrainz/android/ui/theme/Color.kt index b4d54f21..b8773184 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/theme/Color.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/theme/Color.kt @@ -27,6 +27,7 @@ val app_bottom_nav_day = Color(0xFFFFFFFF) val lb_orange = Color(0xFFEA743B) val lb_purple = Color(0xFF353070) val lb_yellow = Color(0xFFE59B2E) +val lb_purple_night = Color(0xFF9AABD1) val yimYellow = Color(0xFFFECB49) val yimRed = Color(0xFFFE0E25) @@ -41,4 +42,7 @@ val yim23DarkBlue = Color(0xFF354F53) val yim23DarkGray = Color(0xFF282423) val yim23Red = Color(0xFFBE4A55) val yim23Blue = Color(0xFF567276) -val yim23Grey = Color(0xFF4C4442) \ No newline at end of file +val yim23Grey = Color(0xFF4C4442) + +/* User Page Colors */ +val compatibilityMeterColor = Color(0xFFDB7544) \ 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 9ede1ae7..c4287dff 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt @@ -48,6 +48,23 @@ class ProfileViewModel @Inject constructor( } } + suspend fun getSimilarArtists(username: String?) : List { + val currentUsername = appPreferences.username.get() + val currentUserTopArtists = userRepository.getTopArtists(currentUsername) + val userTopArtists = userRepository.getTopArtists(username) + 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) + } + } + } + return similarArtists.distinct() + } + suspend fun getUserListensData(inputUsername: String?) { val username = inputUsername ?: appPreferences.username.get() @@ -67,6 +84,7 @@ class ProfileViewModel @Inject constructor( username ).data?.userSimilarity?.similarity else 0f + val similarArtists = getSimilarArtists(username) val listensTabState = ListensTabUiState( isLoading = false, isSelf = isLoggedInUser, @@ -76,7 +94,8 @@ class ProfileViewModel @Inject constructor( recentListens = listens, compatibility = compatibility, similarUsers = similarUsers, - pinnedSong = currentPins + pinnedSong = currentPins, + similarArtists = similarArtists ) listenStateFlow.emit(listensTabState) From 81a4a1ff5288e686b9f21fcfcbe8642b2f69c274 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:01:02 +0530 Subject: [PATCH 41/97] Added some pixel perfections, follow-unfollow functionality on user page load more buttons for all sections, gradients --- .../android/ui/components/SimilarUserCard.kt | 12 +- .../ui/screens/profile/BaseProfileScreen.kt | 75 +++- .../ui/screens/profile/ProfileScreen.kt | 8 +- .../ui/screens/profile/ProfileUiState.kt | 7 +- .../screens/profile/listens/ListensScreen.kt | 377 +++++++++++++++--- .../listenbrainz/android/ui/theme/Color.kt | 4 + .../android/viewmodel/ProfileViewModel.kt | 50 ++- .../main/res/drawable/musicbrainz_logo.xml | 12 + 8 files changed, 463 insertions(+), 82 deletions(-) create mode 100644 app/src/main/res/drawable/musicbrainz_logo.xml diff --git a/app/src/main/java/org/listenbrainz/android/ui/components/SimilarUserCard.kt b/app/src/main/java/org/listenbrainz/android/ui/components/SimilarUserCard.kt index aa119194..3b24bc41 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/components/SimilarUserCard.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/components/SimilarUserCard.kt @@ -44,12 +44,16 @@ fun SimilarUserCard( cardBackGround: Color = MaterialTheme.colorScheme.background, index: Int, userName: String, - similarity: Float + similarity: Float, + modifier: Modifier? = null ){ Surface( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), + modifier = when(modifier){ + null -> Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + else -> modifier + }, color = cardBackGround, shape = RoundedCornerShape(5.dp), shadowElevation = 5.dp, 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 a5f29e6b..39ed94ae 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 @@ -22,6 +22,7 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material3.ElevatedSuggestionChip import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SuggestionChipDefaults import androidx.compose.material3.Text @@ -35,19 +36,21 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import org.listenbrainz.android.R import org.listenbrainz.android.ui.components.LoadingAnimation import org.listenbrainz.android.ui.screens.profile.listens.ListensScreen import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.ui.theme.app_bg_light import org.listenbrainz.android.ui.theme.lb_purple +import org.listenbrainz.android.ui.theme.new_app_bg_light @Composable fun BaseProfileScreen( username: String?, snackbarState: SnackbarHostState, uiState: ProfileUiState, + onFollowClick: (String) -> Unit, + onUnfollowClick: (String) -> Unit, ){ val currentTab : MutableState = remember { mutableStateOf(ProfileScreenTab.LISTENS) } @@ -122,8 +125,14 @@ fun BaseProfileScreen( .padding(end = 20.dp)) { when(isLoggedInUser) { true -> AddListensButton() - false -> FollowButton() - null -> AddListensButton() + false -> when(uiState.listensTabUiState.isFollowing){ + true -> UnFollowButton(username = username, onUnFollowClick = { + onUnfollowClick(it) + }) + false -> FollowButton(username = username, onFollowClick = { + onFollowClick(it) + }) + } } Spacer(modifier = Modifier.width(10.dp)) MusicBrainzButton() @@ -151,17 +160,43 @@ fun BaseProfileScreen( } @Composable -private fun FollowButton() { - IconButton(onClick = { /*TODO*/ }, modifier = Modifier +private fun FollowButton(onFollowClick: (String) -> Unit, username: String?) { + IconButton(onClick = { + if(!username.isNullOrEmpty()){ + onFollowClick(username) + } + + }, modifier = Modifier .background(lb_purple) - .width(90.dp) + .width(100.dp) .height(30.dp)) { - Row(modifier = Modifier.padding(all = 4.dp)) { + Row(modifier = Modifier.padding(horizontal = 8.dp)) { Icon(painter = painterResource(id = R.drawable.follow_icon), contentDescription = "", tint = app_bg_light, modifier = Modifier .width(20.dp) .height(20.dp)) Spacer(modifier = Modifier.width(5.dp)) - Text("Follow", color = app_bg_light) + Text("Follow", color = new_app_bg_light, style = MaterialTheme.typography.bodyMedium) + } + } +} + +@Composable +private fun UnFollowButton(onUnFollowClick: (String) -> Unit, username: String?) { + IconButton(onClick = { + if(!username.isNullOrEmpty()){ + onUnFollowClick(username) + } + + }, modifier = Modifier + .background(lb_purple) + .width(100.dp) + .height(30.dp)) { + Row(modifier = Modifier.padding(horizontal = 8.dp)) { + Icon(painter = painterResource(id = R.drawable.follow_icon), contentDescription = "", tint = new_app_bg_light, modifier = Modifier + .width(20.dp) + .height(20.dp)) + Spacer(modifier = Modifier.width(5.dp)) + Text("Unfollow", color = new_app_bg_light, style = MaterialTheme.typography.bodyMedium) } } } @@ -170,14 +205,14 @@ private fun FollowButton() { private fun AddListensButton() { IconButton(onClick = { /*TODO*/ }, modifier = Modifier .background(Color(0xFF353070)) - .width(90.dp) + .width(110.dp) .height(30.dp)) { Row(modifier = Modifier.padding(all = 4.dp)) { - Icon(imageVector = Icons.Default.Add, contentDescription = "", tint = app_bg_light, modifier = Modifier + Icon(imageVector = Icons.Default.Add, contentDescription = "", tint = new_app_bg_light, modifier = Modifier .width(10.dp) - .height(30.dp)) + .height(20.dp)) Spacer(modifier = Modifier.width(5.dp)) - Text("Add Listens", color = Color.White, fontSize = 10.sp, ) + Text("Add Listens", color = new_app_bg_light, style = MaterialTheme.typography.bodyMedium) } } } @@ -186,18 +221,18 @@ private fun AddListensButton() { private fun MusicBrainzButton() { IconButton(onClick = { /*TODO*/ }, modifier = Modifier .background(Color(0xFF353070)) - .width(120.dp) + .width(140.dp) .height(30.dp)) { - Row(modifier = Modifier.padding(all = 4.dp)) { - Icon(painter = painterResource(id = R.drawable.ic_metabrainz_logo_no_text), contentDescription = "", modifier = Modifier + Row(modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp)) { + Icon(painter = painterResource(id = R.drawable.musicbrainz_logo), contentDescription = "", modifier = Modifier .width(20.dp) - .height(30.dp)) + .height(30.dp), tint = Color.Unspecified) Spacer(modifier = Modifier.width(5.dp)) - Text("MusicBrainz", color = Color.White, fontSize = 10.sp) + Text("MusicBrainz", color = new_app_bg_light, style = MaterialTheme.typography.bodyMedium) Spacer(modifier = Modifier.width(5.dp)) - Icon(imageVector = Icons.AutoMirrored.Filled.OpenInNew, contentDescription = "", tint = app_bg_light, modifier = Modifier - .width(10.dp) - .height(30.dp)) + Icon(imageVector = Icons.AutoMirrored.Filled.OpenInNew, contentDescription = "", tint = new_app_bg_light, modifier = Modifier + .width(30.dp) + .height(20.dp)) } } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt index f90d8b33..d53c2dc5 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt @@ -68,7 +68,13 @@ fun ProfileScreen( BaseProfileScreen( username = username, snackbarState = snackbarState, - uiState = uiState.value + uiState = uiState.value, + onFollowClick = { + viewModel.followUser(it) + }, + onUnfollowClick = { + viewModel.unfollowUser(it) + } ) } 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 77526c7b..39e124c9 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 @@ -21,10 +21,11 @@ data class ListensTabUiState ( val pinnedSong: PinnedRecording? = null, val compatibility: Float? = null, val recentListens: List? = emptyList(), - val followers: List? = emptyList(), - val following: List? = emptyList(), + val followers: List>? = emptyList(), + val following: List>? = emptyList(), val similarUsers: List? = emptyList(), - val similarArtists: List = emptyList() + val similarArtists: List = emptyList(), + val isFollowing: Boolean = false ) data class ListeningNowUiState( diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt index 0ed0b4f6..f32197b0 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt @@ -4,11 +4,15 @@ import android.os.Bundle import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.border +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.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -17,11 +21,14 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Text +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -34,6 +41,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.platform.LocalContext @@ -51,11 +60,13 @@ import kotlinx.coroutines.launch import org.listenbrainz.android.R import org.listenbrainz.android.model.Listen import org.listenbrainz.android.model.Metadata +import org.listenbrainz.android.model.SimilarUser import org.listenbrainz.android.model.SocialUiState import org.listenbrainz.android.model.TrackMetadata import org.listenbrainz.android.model.feed.ReviewEntityType import org.listenbrainz.android.ui.components.ErrorBar import org.listenbrainz.android.ui.components.ListenCardSmall +import org.listenbrainz.android.ui.components.SimilarUserCard import org.listenbrainz.android.ui.components.SuccessBar import org.listenbrainz.android.ui.components.dialogs.Dialog import org.listenbrainz.android.ui.components.dialogs.PersonalRecommendationDialog @@ -69,8 +80,11 @@ import org.listenbrainz.android.ui.screens.settings.PreferencesUiState import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.ui.theme.app_bg_dark import org.listenbrainz.android.ui.theme.app_bg_mid +import org.listenbrainz.android.ui.theme.app_bg_secondary_dark import org.listenbrainz.android.ui.theme.compatibilityMeterColor +import org.listenbrainz.android.ui.theme.lb_purple import org.listenbrainz.android.ui.theme.lb_purple_night +import org.listenbrainz.android.ui.theme.on_app_bg_dark import org.listenbrainz.android.util.Utils.getCoverArtUrl import org.listenbrainz.android.viewmodel.FeedViewModel import org.listenbrainz.android.viewmodel.ListensViewModel @@ -200,6 +214,18 @@ fun ListensScreen( val recentListensCollapsibleState: MutableState = remember { mutableStateOf(true) } + + val similarUsersCollapsibleState: MutableState = remember { + mutableStateOf(true) + } + + val followersMenuState: MutableState = remember { + mutableStateOf(true) + } + + val followersMenuCollapsibleState: MutableState = remember { + mutableStateOf(true) + } // Scroll to the top when shouldScrollToTop becomes true LaunchedEffect(scrollRequestState) { @@ -227,10 +253,12 @@ fun ListensScreen( })) { index, listen -> val metadata = Metadata(trackMetadata = listen.trackMetadata) ListenCardSmall( - modifier = Modifier.padding( - horizontal = 16.dp, - vertical = ListenBrainzTheme.paddings.lazyListAdjacent - ), + modifier = Modifier + .padding( + horizontal = 16.dp, + vertical = ListenBrainzTheme.paddings.lazyListAdjacent + ) + .background(app_bg_secondary_dark), trackName = listen.trackMetadata.trackName, artistName = listen.trackMetadata.artistName, coverArtUrl = getCoverArtUrl( @@ -282,36 +310,125 @@ fun ListensScreen( item { Spacer(modifier = Modifier.height(10.dp)) Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { - TextButton(onClick = { - recentListensCollapsibleState.value = !recentListensCollapsibleState.value - }, modifier = Modifier.border(border = BorderStroke(1.dp, - app_bg_mid), shape = RoundedCornerShape(7.dp) - )) { - Text(when(recentListensCollapsibleState.value){ - true -> "Load More" - false -> "Load Less" - }, color = app_bg_mid, style = MaterialTheme.typography.bodyMedium) - } + LoadMoreButton( + state = recentListensCollapsibleState.value, + onClick = { + recentListensCollapsibleState.value = !recentListensCollapsibleState.value + } + ) } } if(!uiState.isSelf){ item { - Spacer(modifier = Modifier.height(30.dp)) - Text("Your Compatibility", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), modifier = Modifier.padding(start = 16.dp)) - Spacer(modifier = Modifier.height(10.dp)) - CompatibilityCard(compatibility = uiState.compatibility ?: 0f, uiState.similarArtists) + Box(modifier = Modifier + .padding(top = 30.dp) + .clip(shape = RoundedCornerShape(20.dp)) + .background( + Brush.verticalGradient( + listOf( + Color(0xFF161616), + Color(0xFF1A1A1A), + Color(0xFF202020), + Color(0xFF242424), + Color.Transparent + ) + ) + )){ + Column { + Spacer(modifier = Modifier.height(30.dp)) + Text("Your Compatibility", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), modifier = Modifier.padding(start = 16.dp)) + Spacer(modifier = Modifier.height(10.dp)) + CompatibilityCard(compatibility = uiState.compatibility ?: 0f, uiState.similarArtists) + } + } + } } item { - Spacer(modifier = Modifier.height(30.dp)) - FollowersCard( - followersCount = uiState.followersCount, - followingCount = uiState.followingCount, - followers = uiState.followers ?: emptyList(), - following = uiState.following ?: emptyList() - ) + Box(modifier = Modifier + .padding(top = 30.dp) + .clip(shape = RoundedCornerShape(20.dp)) + .background( + Brush.verticalGradient( + listOf( + Color(0xFF161616), + Color(0xFF1A1A1A), + Color(0xFF202020), + Color(0xFF242424), + Color.Transparent + ) + ) + )){ + Column { + FollowersCard( + followersCount = uiState.followersCount, + followingCount = uiState.followingCount, + followers = when(followersMenuCollapsibleState.value){ + true -> uiState.followers?.take(5) ?: emptyList() + false -> uiState.followers ?: emptyList() + }, + following = when(followersMenuCollapsibleState.value){ + true -> uiState.following?.take(5) ?: emptyList() + false -> uiState.following ?: emptyList() + }, + followersState = followersMenuState.value, + onStateChange = { + newMenuState-> + followersMenuState.value = !newMenuState + } + ) + if((uiState.followersCount ?: 0) > 5 || ((uiState.followingCount ?: 0) > 5)){ + Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + LoadMoreButton( + state = followersMenuCollapsibleState.value, + onClick = { + followersMenuCollapsibleState.value = !followersMenuCollapsibleState.value + } + ) + } + } + } + + } + + } + + item { + Box(modifier = Modifier + .padding(top = 30.dp) + .clip(shape = RoundedCornerShape(20.dp)) + .background( + Brush.verticalGradient( + listOf( + + Color(0xFF202020), + Color(0xFF242424), + Color.Transparent + ) + ) + )){ + Column { + SimilarUsersCard(similarUsers = when(similarUsersCollapsibleState.value){ + true -> uiState.similarUsers?.take(5) ?: emptyList() + false -> uiState.similarUsers ?: emptyList() + }) + + if((uiState.similarUsers?.size ?: 0) > 5){ + Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + LoadMoreButton( + state = similarUsersCollapsibleState.value, + onClick = { + similarUsersCollapsibleState.value = !similarUsersCollapsibleState.value + } + ) + } + } + } + + } + } } @@ -340,7 +457,7 @@ fun ListensScreen( } @Composable -private fun buildSimilarArtists(similarArtists: List) { +private fun BuildSimilarArtists(similarArtists: List) { val white = Color.White when { @@ -358,10 +475,10 @@ private fun buildSimilarArtists(similarArtists: List) { append(" and more.") } } - Text(text = text, modifier = Modifier.padding(start=16.dp)) + Text(text = text, modifier = Modifier.padding(horizontal = 16.dp)) } similarArtists.isEmpty() -> { - Text("You have no common artists", color = white, modifier = Modifier.padding(start=16.dp)) + Text("You have no common artists", color = white, modifier = Modifier.padding(horizontal = 16.dp), style = MaterialTheme.typography.bodyMedium) } else -> { val artists = similarArtists.joinToString(", ") @@ -373,7 +490,7 @@ private fun buildSimilarArtists(similarArtists: List) { append(artists) } } - Text(text = text, modifier = Modifier.padding(start=16.dp)) + Text(text = text, modifier = Modifier.padding(horizontal = 16.dp), style = MaterialTheme.typography.bodyMedium) } } } @@ -444,33 +561,72 @@ private fun Dialogs( } } +@Composable +private fun LoadMoreButton( + state: Boolean, + onClick : () -> Unit, +){ + TextButton(onClick, modifier = Modifier.border(border = BorderStroke(1.dp, + app_bg_mid), shape = RoundedCornerShape(7.dp) + )) { + Text(when(state){ + true -> "Load More" + false -> "Load Less" + }, color = app_bg_mid, style = MaterialTheme.typography.bodyMedium) + } +} + + @Composable private fun SongsListened(username: String? , listenCount: Int?){ - Column (horizontalAlignment = Alignment.CenterHorizontally) { + Column (horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier + .clip(shape = RoundedCornerShape(100.dp)) + .background(color = app_bg_dark)) { Spacer(modifier = Modifier.height(30.dp)) Text("$username has listened to", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) - Spacer(modifier = Modifier.height(10.dp)) - HorizontalDivider(color = app_bg_dark, modifier = Modifier.padding(start = 60.dp, end = 60.dp)) - Spacer(modifier = Modifier.height(10.dp)) + Spacer(modifier = Modifier.height(15.dp)) + HorizontalDivider(color = on_app_bg_dark, modifier = Modifier.padding(start = 60.dp, end = 60.dp)) + Spacer(modifier = Modifier.height(15.dp)) Text(listenCount.toString(), color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), textAlign = TextAlign.Center) Text("songs so far", color = app_bg_mid, style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center) + Spacer(modifier = Modifier.height(30.dp)) } } @Composable private fun FollowersInformation(followersCount: Int?, followingCount: Int?){ - Spacer(modifier = Modifier.height(30.dp)) - Row (horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth()) { - Column (horizontalAlignment = Alignment.CenterHorizontally) { - Text((followersCount ?:0).toString(), style = MaterialTheme.typography.bodyLarge, color = Color.White) - Text("Followers", style = MaterialTheme.typography.bodyLarge, color = Color.White) - } - Column (horizontalAlignment = Alignment.CenterHorizontally) { - Text((followingCount ?: 0).toString(), style = MaterialTheme.typography.bodyLarge, color = Color.White) - Text("Following", style = MaterialTheme.typography.bodyLarge, color = Color.White) + Box(modifier = Modifier + .clip(shape = RoundedCornerShape(20.dp)) + .background( + Brush.verticalGradient( + listOf( + Color(0xFF161616), + Color(0xFF1A1A1A), + Color(0xFF202020), + Color(0xFF242424), + Color.Transparent + ) + ) + ) + ){ + Row (horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier + .fillMaxWidth() + .padding(top = 30.dp)) { + Column (horizontalAlignment = Alignment.CenterHorizontally) { + Text((followersCount ?:0).toString(), style = MaterialTheme.typography.bodyLarge, color = Color.White) + Spacer(modifier = Modifier.height(10.dp)) + Text("Followers", style = MaterialTheme.typography.bodyLarge, color = Color.White) + } + Column (horizontalAlignment = Alignment.CenterHorizontally) { + Text((followingCount ?: 0).toString(), style = MaterialTheme.typography.bodyLarge, color = Color.White) + Spacer(modifier = Modifier.height(10.dp)) + Text("Following", style = MaterialTheme.typography.bodyLarge, color = Color.White) + } } } + + } @Composable @@ -485,18 +641,143 @@ fun CompatibilityCard(compatibility: Float, similarArtists: List){ Text("${(compatibility*100).toInt()} %", color = app_bg_mid, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) } Spacer(modifier = Modifier.height(10.dp)) - buildSimilarArtists(similarArtists = similarArtists) + BuildSimilarArtists(similarArtists = similarArtists) } @Composable -private fun FollowersCard(followersCount: Int?, followingCount: Int?, followers: List, following: List){ - Column (modifier = Modifier.padding(start=16.dp)) { - Text("Followers", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) +private fun FollowersCard(followersCount: Int?, followingCount: Int?, followers: List>, following: List>, followersState: Boolean, onStateChange: (Boolean) -> Unit) { + Column(modifier = Modifier.padding(start = 16.dp , top = 30.dp)) { + Text( + "Followers", + color = Color.White, + style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp) + ) + Spacer(modifier = Modifier.height(10.dp)) Row { - + Card( + colors = CardDefaults.cardColors( + containerColor = when (followersState) { + true -> lb_purple_night + false -> app_bg_dark + }, + ), + border = when(followersState){ + true -> null + false -> BorderStroke(width = 1.dp, color = lb_purple_night) + }, + modifier = Modifier + .width(120.dp) + .height(30.dp) + .clickable { + onStateChange(followersState) + } + ) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + text = "Followers (${followersCount})", + color = when (followersState) { + true -> app_bg_dark + false -> lb_purple_night + }, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + ) + } + } + Spacer(modifier = Modifier.width(10.dp)) + Card( + colors = CardDefaults.cardColors( + containerColor = when (followersState) { + true -> app_bg_dark + false -> lb_purple_night + }, + ), + border = when(followersState){ + true -> BorderStroke(width = 1.dp, color = lb_purple_night) + false -> null + }, + modifier = Modifier + .width(120.dp) + .height(30.dp) + .clickable { + onStateChange(followersState) + } + ) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + text = "Following (${followingCount})", + color = when (followersState) { + true -> lb_purple_night + false -> app_bg_dark + }, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + ) + } + } + } + Spacer(modifier = Modifier.height(10.dp)) + when(followersState){ + true -> followers.map { + state -> + FollowCard(username = state.first, onButtonClick = Unit, followStatus = state.second) + Spacer(modifier = Modifier.height(10.dp)) + } + false -> following.map { + state -> + FollowCard(username = state.first, onButtonClick = Unit, followStatus = state.second) + Spacer(modifier = Modifier.height(10.dp)) + } } } +} +@Composable +private fun SimilarUsersCard(similarUsers: List){ + Spacer(modifier = Modifier.height(20.dp)) + Text("Similar Users", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), modifier = Modifier.padding(horizontal = 16.dp)) + Spacer(modifier = Modifier.height(20.dp)) + similarUsers.mapIndexed{ + index , item -> + SimilarUserCard(index = index, userName = item.username, similarity = item.similarity.toFloat(), modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp)) + } +} + +@Composable +private fun FollowCard(username: String?, onButtonClick: Unit, followStatus: Boolean) { + Card(colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E))) { + Row( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + .height(70.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + username ?: "", + color = lb_purple_night, + style = MaterialTheme.typography.bodyLarge + ) + TextButton( + onClick = { /*TODO*/ }, colors = ButtonDefaults.buttonColors( + containerColor = when (followStatus) { + true -> app_bg_dark + false -> lb_purple + } + ), modifier = Modifier + .width(90.dp) + .height(40.dp), shape = RoundedCornerShape(10.dp) + ) { + Text( + when (followStatus) { + true -> "Following" + false -> "Follow" + }, color = Color.White + ) + } + } + } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/theme/Color.kt b/app/src/main/java/org/listenbrainz/android/ui/theme/Color.kt index b8773184..f6ba8fd8 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/theme/Color.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/theme/Color.kt @@ -19,7 +19,11 @@ val bp_lavender_dark = Color(0xFFE9DCFE) /** background Colors */ val app_bg_day = Color(0xFFFFFFFF) val app_bg_dark = Color(0xFF292929) +val app_bg_secondary_dark = Color(0xFF1E1E1E) +val on_app_bg_dark = Color(0xFF101010) val app_bg_light = Color(0xFF8FA3AD) +//TODO: Change app_bg_light everywhere following approval from lead dev +val new_app_bg_light = Color(0xFFF5F5F5) val app_bg_mid = Color(0xFF8D8D8D) val app_bottom_nav_dark = Color(0xFF000000) val app_bottom_nav_day = Color(0xFFFFFFFF) 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 c4287dff..4f2ee330 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt @@ -1,5 +1,6 @@ package org.listenbrainz.android.viewmodel +import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -32,7 +33,8 @@ class ProfileViewModel @Inject constructor( val socketRepository: SocketRepository, private val socialRepository: SocialRepository, private val savedStateHandle: SavedStateHandle, - @IoDispatcher ioDispatcher: CoroutineDispatcher + @IoDispatcher val ioDispatcher: CoroutineDispatcher, + ) : BaseViewModel() { private val _loginStatusFlow: MutableStateFlow = MutableStateFlow(STATUS_LOGGED_OUT) @@ -48,7 +50,7 @@ class ProfileViewModel @Inject constructor( } } - suspend fun getSimilarArtists(username: String?) : List { + private suspend fun getSimilarArtists(username: String?) : List { val currentUsername = appPreferences.username.get() val currentUserTopArtists = userRepository.getTopArtists(currentUsername) val userTopArtists = userRepository.getTopArtists(username) @@ -65,6 +67,24 @@ class ProfileViewModel @Inject constructor( return similarArtists.distinct() } + fun followUser(username: String?){ + Log.v("pranav", "FOLLOW") + if(username.isNullOrEmpty()) return + viewModelScope.launch (ioDispatcher) { + socialRepository.followUser(username) + } + } + + fun unfollowUser(username: String?){ + Log.v("pranav", "UNFOLLOW") + if(username.isNullOrEmpty()) return + viewModelScope.launch(ioDispatcher) { + socialRepository.unfollowUser(username) + } + + } + + suspend fun getUserListensData(inputUsername: String?) { val username = inputUsername ?: appPreferences.username.get() @@ -75,7 +95,23 @@ class ProfileViewModel @Inject constructor( val listenCount = userRepository.fetchUserListenCount(username).data?.payload?.count val listens: List? = listensRepository.fetchUserListens(username).data?.payload?.listens val followers = socialRepository.getFollowers(username).data?.followers + val currentUserFollowing = socialRepository.getFollowing(appPreferences.username.get()).data?.following + val followersState : MutableList> = mutableListOf() + val followingState : MutableList> = mutableListOf() val followersCount = followers?.size + val following = socialRepository.getFollowing(username).data?.following + val currentUserFollowingSet = currentUserFollowing?.toSet() ?: emptySet() + viewModelScope.launch { + followers?.forEach { user -> + val isFollowing = currentUserFollowingSet.contains(user) + followersState.add(Pair(user, isFollowing)) + } + following?.forEach { user -> + val isFollowing = currentUserFollowingSet.contains(user) + followingState.add(Pair(user, isFollowing)) + } + } + val followingCount = following?.size val similarUsers = socialRepository.getSimilarUsers(username).data?.payload val currentPins = userRepository.fetchUserCurrentPins(username).data val compatibility = if (username != appPreferences.username.get()) @@ -85,21 +121,23 @@ class ProfileViewModel @Inject constructor( ).data?.userSimilarity?.similarity else 0f val similarArtists = getSimilarArtists(username) + val isFollowing = currentUserFollowingSet.contains(username) val listensTabState = ListensTabUiState( isLoading = false, isSelf = isLoggedInUser, listenCount = listenCount, followersCount = followersCount, - followers = followers, + followers = followersState, + followingCount = followingCount, + following = followingState, recentListens = listens, compatibility = compatibility, similarUsers = similarUsers, pinnedSong = currentPins, - similarArtists = similarArtists + similarArtists = similarArtists, + isFollowing = isFollowing ) - listenStateFlow.emit(listensTabState) - } override val uiState: StateFlow = createUiStateFlow() diff --git a/app/src/main/res/drawable/musicbrainz_logo.xml b/app/src/main/res/drawable/musicbrainz_logo.xml new file mode 100644 index 00000000..59619ce3 --- /dev/null +++ b/app/src/main/res/drawable/musicbrainz_logo.xml @@ -0,0 +1,12 @@ + + + + From 5419dfbbb64dc6f9709d263bdd882f1a6fed70b5 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:05:08 +0530 Subject: [PATCH 42/97] Remove log statements --- .../org/listenbrainz/android/viewmodel/ProfileViewModel.kt | 3 --- 1 file changed, 3 deletions(-) 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 4f2ee330..a11ed9e4 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt @@ -1,6 +1,5 @@ package org.listenbrainz.android.viewmodel -import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -68,7 +67,6 @@ class ProfileViewModel @Inject constructor( } fun followUser(username: String?){ - Log.v("pranav", "FOLLOW") if(username.isNullOrEmpty()) return viewModelScope.launch (ioDispatcher) { socialRepository.followUser(username) @@ -76,7 +74,6 @@ class ProfileViewModel @Inject constructor( } fun unfollowUser(username: String?){ - Log.v("pranav", "UNFOLLOW") if(username.isNullOrEmpty()) return viewModelScope.launch(ioDispatcher) { socialRepository.unfollowUser(username) From 0f0fe6d2631137389cd26a6beca596b5cc92c74f Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Wed, 26 Jun 2024 20:57:44 +0530 Subject: [PATCH 43/97] Minor tweaks Marked reqd comments as TODO, removed redundant function --- .../listenbrainz/android/repository/user/UserRepository.kt | 3 +-- .../android/repository/user/UserRepositoryImpl.kt | 3 --- .../org/listenbrainz/android/ui/components/SimilarUserCard.kt | 4 ++-- 3 files changed, 3 insertions(+), 7 deletions(-) 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 76810b1e..65963b49 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 @@ -8,9 +8,8 @@ import org.listenbrainz.android.util.Resource interface UserRepository { suspend fun fetchUserListenCount (username: String?) : Resource - suspend fun fetchListeningNow (username: String?) : Resource suspend fun fetchUserSimilarity(username: String? , otherUserName: String?) : Resource suspend fun fetchUserCurrentPins(username: String?) : Resource - // Will move to artist VM once it is made + //TODO: Move to artists VM once implemented suspend fun getTopArtists(username: String?): 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 2c19134d..ba4fa7d7 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 @@ -18,9 +18,6 @@ class UserRepositoryImpl @Inject constructor( service.getListenCount(username) } - override suspend fun fetchListeningNow(username: String?): Resource { - TODO("Not yet implemented") - } override suspend fun fetchUserSimilarity(username: String?, otherUserName: String?) : Resource = parseResponse { if(username.isNullOrEmpty() or otherUserName.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() diff --git a/app/src/main/java/org/listenbrainz/android/ui/components/SimilarUserCard.kt b/app/src/main/java/org/listenbrainz/android/ui/components/SimilarUserCard.kt index 3b24bc41..ed27e230 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/components/SimilarUserCard.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/components/SimilarUserCard.kt @@ -45,11 +45,11 @@ fun SimilarUserCard( index: Int, userName: String, similarity: Float, - modifier: Modifier? = null + modifier: Modifier = Modifier ){ Surface( modifier = when(modifier){ - null -> Modifier + Modifier -> Modifier .fillMaxWidth() .padding(vertical = 4.dp) else -> modifier From cb60eab96f12c21ae210b31ff24fc1a9ede2204a Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Wed, 26 Jun 2024 22:50:57 +0530 Subject: [PATCH 44/97] Fix default modifier on similar user card --- .../android/ui/components/SimilarUserCard.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/components/SimilarUserCard.kt b/app/src/main/java/org/listenbrainz/android/ui/components/SimilarUserCard.kt index ed27e230..c17dc41b 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/components/SimilarUserCard.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/components/SimilarUserCard.kt @@ -46,14 +46,11 @@ fun SimilarUserCard( userName: String, similarity: Float, modifier: Modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) ){ Surface( - modifier = when(modifier){ - Modifier -> Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - else -> modifier - }, + modifier = modifier, color = cardBackGround, shape = RoundedCornerShape(5.dp), shadowElevation = 5.dp, From cd0620f0c96abeeb70d069920b61607b21714177 Mon Sep 17 00:00:00 2001 From: Akshat Tiwari Date: Wed, 26 Jun 2024 23:12:54 +0530 Subject: [PATCH 45/97] Bump dependencies and update codebase --- .gitignore | 1 + app/build.gradle | 32 +++++---- .../android/ui/components/ListenCardSmall.kt | 21 +++--- .../android/ui/screens/feed/FeedScreen.kt | 72 +++++++++---------- .../PersonalRecommendationFeedLayout.kt | 18 ++--- .../profile/listens/ListeningAppsList.kt | 4 +- .../screens/profile/listens/ListensScreen.kt | 8 +-- build.gradle | 8 +-- gradle/wrapper/gradle-wrapper.properties | 2 +- sharedTest/build.gradle | 8 +-- 10 files changed, 88 insertions(+), 86 deletions(-) diff --git a/.gitignore b/.gitignore index 8812f584..4b8f8df0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /captures .externalNativeBuild play_config.json +.kotlin diff --git a/app/build.gradle b/app/build.gradle index 5ade18ca..28e48125 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,6 +5,7 @@ plugins { id 'com.google.devtools.ksp' id 'dagger.hilt.android.plugin' id "io.sentry.android.gradle" version "4.7.0" + id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" } def keystorePropertiesFile = rootProject.file("keystore.properties") @@ -95,7 +96,7 @@ android { } } composeOptions { - kotlinCompilerExtensionVersion '1.5.8' + kotlinCompilerExtensionVersion '1.5.14' } compileOptions { sourceCompatibility JavaVersion.VERSION_17 @@ -128,11 +129,11 @@ android { dependencies { //AndroidX implementation 'androidx.appcompat:appcompat:1.7.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.1' - implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.8.1' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.2' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.2' + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2' + implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.8.2' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.2' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.browser:browser:1.8.0' implementation 'androidx.preference:preference-ktx:1.2.1' @@ -178,7 +179,7 @@ dependencies { kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version" //Jetpack Compose - implementation platform('androidx.compose:compose-bom:2024.05.00') + implementation platform('androidx.compose:compose-bom:2024.06.00') implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-tooling' @@ -207,7 +208,7 @@ dependencies { } //Test Setup - implementation 'androidx.test.ext:junit-ktx:1.1.5' + implementation 'androidx.test.ext:junit-ktx:1.2.0' implementation 'app.cash.turbine:turbine:1.1.0' testImplementation 'junit:junit:4.13.2' testImplementation 'com.squareup.okhttp3:mockwebserver:5.0.0-alpha.14' @@ -218,16 +219,17 @@ dependencies { testImplementation 'org.mockito:mockito-core:5.12.0' testImplementation 'org.mockito.kotlin:mockito-kotlin:5.3.1' - debugImplementation "androidx.test:monitor:1.6.1" // Solves "class PlatformTestStorageRegistery not found" error for ui tests. - debugImplementation 'androidx.compose.ui:ui-test-manifest:1.6.7' + debugImplementation 'androidx.test:monitor:1.7.0' + // Solves "class PlatformTestStorageRegistery not found" error for ui tests. + debugImplementation 'androidx.compose.ui:ui-test-manifest:1.6.8' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" androidTestImplementation "androidx.work:work-testing:$work_version" - androidTestImplementation 'androidx.test:runner:1.5.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test:runner:1.6.0' + androidTestImplementation 'androidx.test.ext:junit:1.2.0' androidTestImplementation 'androidx.arch.core:core-testing:2.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.0' + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.0' //androidTestImplementation 'tools.fastlane:screengrab:2.1.1' // Fastlane ScreenGrab testImplementation project(path: ':sharedTest') @@ -257,5 +259,5 @@ dependencies { implementation 'com.github.akshaaatt:Logger-Android:1.0.0' //Charting Library (Vico) - implementation("com.patrykandpatrick.vico:compose:1.14.0") + implementation('com.patrykandpatrick.vico:compose:1.15.0') } 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 615896d5..71bf7f4a 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 @@ -55,11 +55,10 @@ fun ListenCardSmall( enableDropdownIcon: Boolean = false, onDropdownIconClick: () -> Unit = {}, dropDown: @Composable () -> Unit = {}, - dropDownState: Boolean = false, enableTrailingContent: Boolean = false, trailingContent: @Composable (modifier: Modifier) -> Unit = {}, enableBlurbContent: Boolean = false, - blurbContent: @Composable ColumnScope.(modifier: Modifier) -> Unit = {}, + blurbContent: @Composable (ColumnScope.(modifier: Modifier) -> Unit) = {}, onClick: () -> Unit, ) { Surface( @@ -111,7 +110,7 @@ fun ListenCardSmall( // Trailing content if (enableTrailingContent) { trailingContent( - modifier = Modifier + Modifier .fillMaxWidth(trailingContentFraction) .align(Alignment.CenterStart) .padding(end = 6.dp) @@ -137,7 +136,7 @@ fun ListenCardSmall( if (enableBlurbContent) { Divider() - blurbContent(modifier = Modifier.padding(ListenBrainzTheme.paddings.insideCard)) + blurbContent(Modifier.padding(ListenBrainzTheme.paddings.insideCard)) } } } @@ -217,14 +216,14 @@ private fun ListenCardSmallPreview() { trackName = "Title", artistName = "Artist", coverArtUrl = "", - enableTrailingContent = true, - enableBlurbContent = true, enableDropdownIcon = true, + enableTrailingContent = true, trailingContent = { modifier -> Column(modifier = modifier) { TitleAndSubtitle(title = "Userrrrrrrrrrrrrr", subtitle = "60%") } }, + enableBlurbContent = true, blurbContent = { Column(modifier = it) { Text(text = "Blurb Content", color = ListenBrainzTheme.colorScheme.text) @@ -243,14 +242,14 @@ private fun ListenCardSmallNoBlurbContentPreview() { trackName = "Title", artistName = "Artist", coverArtUrl = "", - enableTrailingContent = true, - enableBlurbContent = false, enableDropdownIcon = true, + enableTrailingContent = true, trailingContent = { modifier -> Column(modifier = modifier) { TitleAndSubtitle(title = "Userrrrrrrrrrrrrr", subtitle = "60%") } - } + }, + enableBlurbContent = false ) {} } } @@ -264,9 +263,9 @@ private fun ListenCardSmallSimplePreview() { trackName = "Title", artistName = "Artist", coverArtUrl = "", + enableDropdownIcon = true, enableTrailingContent = false, - enableBlurbContent = false, - enableDropdownIcon = true + enableBlurbContent = false ) {} } } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/FeedScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/FeedScreen.kt index 4c074349..002f9361 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/FeedScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/FeedScreen.kt @@ -534,20 +534,6 @@ fun FollowListens( caaId = event.metadata.trackMetadata?.mbidMapping?.caaId ), enableDropdownIcon = true, - enableTrailingContent = true, - trailingContent = { modifier -> - Column(modifier, horizontalAlignment = Alignment.End) { - TitleAndSubtitle( - title = event.username ?: "Unknown", - titleColor = ListenBrainzTheme.colorScheme.lbSignature - ) - Date( - event = event, - parentUser = parentUser, - eventType = eventType - ) - } - }, onDropdownIconClick = { dropdownItemIndex.value = if (dropdownItemIndex.value == null) index else null }, @@ -578,6 +564,20 @@ fun FollowListens( uriHandler.openUri("https://musicbrainz.org/recording/${event.metadata.trackMetadata?.mbidMapping?.recordingMbid ?: return@SocialDropdown}") } ) + }, + enableTrailingContent = true, + trailingContent = { modifier -> + Column(modifier, horizontalAlignment = Alignment.End) { + TitleAndSubtitle( + title = event.username ?: "Unknown", + titleColor = ListenBrainzTheme.colorScheme.lbSignature + ) + Date( + event = event, + parentUser = parentUser, + eventType = eventType + ) + } } ) { onPlay(event) @@ -634,28 +634,6 @@ fun SimilarListens( caaId = event.metadata.trackMetadata?.mbidMapping?.caaId ), enableDropdownIcon = true, - enableTrailingContent = true, - trailingContent = { modifier -> - /*TitleAndSubtitle( - modifier = modifier, - title = event.username ?: "Unknown", - subtitle = similarityToPercent(event.similarity), - alignment = Alignment.End, - titleColor = ListenBrainzTheme.colorScheme.lbSignature, - subtitleColor = ListenBrainzTheme.colorScheme.lbSignatureInverse - )*/ - Column(modifier, horizontalAlignment = Alignment.End) { - TitleAndSubtitle( - title = event.username ?: "Unknown", - titleColor = ListenBrainzTheme.colorScheme.lbSignature - ) - Date( - event = event, - parentUser = parentUser, - eventType = eventType - ) - } - }, onDropdownIconClick = { dropdownItemIndex.value = if (dropdownItemIndex.value == null){ index @@ -688,6 +666,28 @@ fun SimilarListens( uriHandler.openUri("https://musicbrainz.org/recording/${event.metadata.trackMetadata?.mbidMapping?.recordingMbid ?: return@SocialDropdown}") } ) + }, + enableTrailingContent = true, + trailingContent = { modifier -> + /*TitleAndSubtitle( + modifier = modifier, + title = event.username ?: "Unknown", + subtitle = similarityToPercent(event.similarity), + alignment = Alignment.End, + titleColor = ListenBrainzTheme.colorScheme.lbSignature, + subtitleColor = ListenBrainzTheme.colorScheme.lbSignatureInverse + )*/ + Column(modifier, horizontalAlignment = Alignment.End) { + TitleAndSubtitle( + title = event.username ?: "Unknown", + titleColor = ListenBrainzTheme.colorScheme.lbSignature + ) + Date( + event = event, + parentUser = parentUser, + eventType = eventType + ) + } } ) { onPlay(event) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PersonalRecommendationFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PersonalRecommendationFeedLayout.kt index 2cef2b43..2a8b0a9b 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PersonalRecommendationFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PersonalRecommendationFeedLayout.kt @@ -73,35 +73,34 @@ fun PersonalRecommendationFeedLayout( ) }, enableBlurbContent = true, - onClick = onClick, blurbContent = { modifier -> Column(modifier = modifier) { - + // Only show "Sent to:" text if user is the one who personally recommended. if (FeedEventType.isUserSelf(event, parentUser)){ - + Row( modifier = Modifier.padding(bottom = 6.dp), verticalAlignment = Alignment.CenterVertically ) { - + Text( text = "Sent to:", style = ListenBrainzTheme.textStyles.feedBlurbContent, color = ListenBrainzTheme.colorScheme.text ) - + LazyRow { - + items(items = event.metadata.usersList ?: emptyList()) { user -> Spacer(modifier = Modifier.width(6.dp)) - + UserTag(user) } } } } - + event.blurbContent?.let { Text( text = it, @@ -110,7 +109,8 @@ fun PersonalRecommendationFeedLayout( ) } } - } + }, + onClick = onClick ) } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListeningAppsList.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListeningAppsList.kt index f7e14aa4..22d66322 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListeningAppsList.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListeningAppsList.kt @@ -192,11 +192,11 @@ fun ListeningAppsListPreview(){ override fun setColorFilter(colorFilter: ColorFilter?) { TODO("Not yet implemented") } - + override fun getOpacity(): Int { TODO("Not yet implemented") } - + } }, { "Package Label" }, {} diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt index f32197b0..4b0db6eb 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt @@ -265,6 +265,10 @@ fun ListensScreen( caaReleaseMbid = listen.trackMetadata.mbidMapping?.caaReleaseMbid, caaId = listen.trackMetadata.mbidMapping?.caaId ), + enableDropdownIcon = true, + onDropdownIconClick = { + dropdownItemIndex.value = index + }, dropDown = { SocialDropdown( isExpanded = dropdownItemIndex.value == index, @@ -298,10 +302,6 @@ fun ListensScreen( } ) - }, - enableDropdownIcon = true, - onDropdownIconClick = { - dropdownItemIndex.value = index } ) { playListen(listen.trackMetadata) diff --git a/build.gradle b/build.gradle index 56804c9d..5f312a0e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,10 +2,10 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { ext { - kotlin_version = '1.9.22' + kotlin_version = '2.0.0' navigationVersion = '2.7.7' hilt_version = '2.51.1' - compose_version = '1.6.7' + compose_version = '1.6.8' room_version = '2.6.1' accompanist_version = '0.34.0' work_version = '2.9.0' @@ -17,14 +17,14 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.4.1' + classpath 'com.android.tools.build:gradle:8.5.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" } } plugins { - id 'com.google.devtools.ksp' version '1.9.22-1.0.17' apply false + id 'com.google.devtools.ksp' version '2.0.0-1.0.21' apply false } allprojects { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a787e51d..f234c2e4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Aug 12 11:38:52 IST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/sharedTest/build.gradle b/sharedTest/build.gradle index a165eaf7..979ee11e 100644 --- a/sharedTest/build.gradle +++ b/sharedTest/build.gradle @@ -46,11 +46,11 @@ dependencies { implementation 'androidx.room:room-testing:2.6.1' debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" - implementation 'androidx.test:runner:1.5.2' - implementation 'androidx.test.ext:junit:1.1.5' + implementation 'androidx.test:runner:1.6.0' + implementation 'androidx.test.ext:junit:1.2.0' implementation 'androidx.arch.core:core-testing:2.2.0' - implementation 'androidx.test.espresso:espresso-core:3.5.1' - implementation 'androidx.test.espresso:espresso-intents:3.5.1' + implementation 'androidx.test.espresso:espresso-core:3.6.0' + implementation 'androidx.test.espresso:espresso-intents:3.6.0' implementation "androidx.compose.ui:ui-test-junit4:$compose_version" implementation project(path: ':app') From c3a18445caa86545e93d4895bc83e6660d743a51 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Thu, 27 Jun 2024 15:28:04 +0530 Subject: [PATCH 46/97] Added go to dashboard in user page --- .../android/ui/navigation/AppNavigation.kt | 6 +- .../android/ui/screens/main/MainActivity.kt | 5 +- .../ui/screens/profile/BaseProfileScreen.kt | 60 +++++++++++++++++-- .../ui/screens/profile/ProfileScreen.kt | 6 +- .../listenbrainz/android/util/Constants.kt | 1 + app/src/main/res/drawable/home_icon.xml | 9 +++ 6 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 app/src/main/res/drawable/home_icon.xml diff --git a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt index bd10ae09..434bf758 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt @@ -23,7 +23,8 @@ fun AppNavigation( navController: NavController = rememberNavController(), scrollRequestState: Boolean, onScrollToTop: (suspend () -> Unit) -> Unit, - snackbarState : SnackbarHostState + snackbarState : SnackbarHostState, + goToUserProfile: () -> Unit, ) { NavHost( navController = navController as NavHostController, @@ -62,7 +63,8 @@ fun AppNavigation( onScrollToTop = onScrollToTop, scrollRequestState = scrollRequestState, username = username, - snackbarState = snackbarState + snackbarState = snackbarState, + goToUserProfile = goToUserProfile ) } composable(route = AppNavigationItem.Settings.route){ diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt index dd381ab4..939c0e6c 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt @@ -199,7 +199,10 @@ class MainActivity : ComponentActivity() { } } }, - snackbarState = snackbarState + snackbarState = snackbarState, + goToUserProfile = { + navController.navigate("${AppNavigationItem.Profile.route}/${username}") + } ) } } 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 39ed94ae..ab2bbc0f 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 @@ -5,6 +5,7 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -19,14 +20,17 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Home import androidx.compose.material3.ElevatedSuggestionChip import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState 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.mutableStateOf import androidx.compose.runtime.remember @@ -34,6 +38,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import org.listenbrainz.android.R @@ -41,8 +46,10 @@ import org.listenbrainz.android.ui.components.LoadingAnimation import org.listenbrainz.android.ui.screens.profile.listens.ListensScreen import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.ui.theme.app_bg_light +import org.listenbrainz.android.ui.theme.lb_orange import org.listenbrainz.android.ui.theme.lb_purple import org.listenbrainz.android.ui.theme.new_app_bg_light +import org.listenbrainz.android.util.Constants @Composable fun BaseProfileScreen( @@ -51,10 +58,15 @@ fun BaseProfileScreen( uiState: ProfileUiState, onFollowClick: (String) -> Unit, onUnfollowClick: (String) -> Unit, + goToUserProfile: () -> Unit, ){ val currentTab : MutableState = remember { mutableStateOf(ProfileScreenTab.LISTENS) } val isLoggedInUser = uiState.listensTabUiState.isSelf + val uriHandler = LocalUriHandler.current + val mbOpeningErrorState = remember { + mutableStateOf(null) + } Box(modifier = Modifier.fillMaxSize()){ AnimatedVisibility( visible = uiState.listensTabUiState.isLoading, @@ -80,18 +92,41 @@ fun BaseProfileScreen( ) ) { Spacer(modifier = Modifier.width(ListenBrainzTheme.paddings.chipsHorizontal / 2)) - repeat(6) { position -> + repeat(5) { position -> ElevatedSuggestionChip( modifier = Modifier.padding(ListenBrainzTheme.paddings.chipsHorizontal), colors = SuggestionChipDefaults.elevatedSuggestionChipColors( if (position == currentTab.value.index) { ListenBrainzTheme.colorScheme.chipSelected } else { - ListenBrainzTheme.colorScheme.chipUnselected + if(position == 0){ + if(uiState.listensTabUiState.isSelf){ + lb_purple + } + else{ + lb_orange + } + } + else{ + ListenBrainzTheme.colorScheme.chipUnselected + } + } ), shape = ListenBrainzTheme.shapes.chips, elevation = SuggestionChipDefaults.elevatedSuggestionChipElevation(elevation = 4.dp), + icon = { + if(position == 0 && !uiState.listensTabUiState.isSelf){ + Box (modifier = Modifier + .background(lb_purple) + .padding(4.dp)) { + Icon(Icons.Default.Home, contentDescription = "", tint = new_app_bg_light, modifier = Modifier.clickable { + goToUserProfile() + }) + } + + } + }, label = { Text( text = when (position) { @@ -135,7 +170,17 @@ fun BaseProfileScreen( } } Spacer(modifier = Modifier.width(10.dp)) - MusicBrainzButton() + MusicBrainzButton(){ + try { + uriHandler.openUri(Constants.MB_BASE_URL + "user/${username}") + } + catch (e: RuntimeException) { + mbOpeningErrorState.value = e.message; + } + catch (e: Exception){ + mbOpeningErrorState.value = e.message; + } + } } when(currentTab.value) { ProfileScreenTab.LISTENS -> ListensScreen( @@ -154,6 +199,11 @@ fun BaseProfileScreen( } } + if(mbOpeningErrorState.value != null){ + LaunchedEffect(mbOpeningErrorState.value) { + snackbarState.showSnackbar("Some Error Occoued", duration = SnackbarDuration.Short) + } + } } @@ -218,8 +268,8 @@ private fun AddListensButton() { } @Composable -private fun MusicBrainzButton() { - IconButton(onClick = { /*TODO*/ }, modifier = Modifier +private fun MusicBrainzButton(onClick: () -> Unit) { + IconButton(onClick = onClick, modifier = Modifier .background(Color(0xFF353070)) .width(140.dp) .height(30.dp)) { diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt index d53c2dc5..b087e57c 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt @@ -46,7 +46,8 @@ fun ProfileScreen( scrollRequestState: Boolean, onScrollToTop: (suspend () -> Unit) -> Unit, username: String?, - snackbarState: SnackbarHostState + snackbarState: SnackbarHostState, + goToUserProfile: () -> Unit ) { val scrollState = rememberScrollState() val uiState = viewModel.uiState.collectAsState() @@ -74,7 +75,8 @@ fun ProfileScreen( }, onUnfollowClick = { viewModel.unfollowUser(it) - } + }, + goToUserProfile = goToUserProfile ) } diff --git a/app/src/main/java/org/listenbrainz/android/util/Constants.kt b/app/src/main/java/org/listenbrainz/android/util/Constants.kt index 16deebdd..7ed02df2 100644 --- a/app/src/main/java/org/listenbrainz/android/util/Constants.kt +++ b/app/src/main/java/org/listenbrainz/android/util/Constants.kt @@ -16,6 +16,7 @@ object Constants { const val ONBOARDING = "onboarding_lb" const val ABOUT_URL = "https://listenbrainz.org/about" const val LB_BASE_URL = "https://listenbrainz.org/" + const val MB_BASE_URL = "https://musicbrainz.org/" object Strings { const val TIMESTAMP = "timestamp" diff --git a/app/src/main/res/drawable/home_icon.xml b/app/src/main/res/drawable/home_icon.xml new file mode 100644 index 00000000..152346a1 --- /dev/null +++ b/app/src/main/res/drawable/home_icon.xml @@ -0,0 +1,9 @@ + + + From 32f3b3b2f2140469b82ff0490b8b2a9b85721c03 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sun, 30 Jun 2024 17:44:38 +0530 Subject: [PATCH 47/97] Added light theme in listens tab --- .../ui/screens/profile/BaseProfileScreen.kt | 82 +++++--- .../ui/screens/profile/ProfileScreen.kt | 2 +- .../ui/screens/profile/ProfileUiState.kt | 15 +- .../screens/profile/listens/ListensScreen.kt | 198 ++++++++---------- .../ui/screens/profile/stats/StatsScreen.kt | 15 ++ .../screens/profile/taste/LovedHatedEnum.kt | 4 + .../ui/screens/profile/taste/TasteScreen.kt | 29 +++ .../listenbrainz/android/ui/theme/Color.kt | 1 + .../listenbrainz/android/ui/theme/Theme.kt | 61 +++++- .../android/viewmodel/ProfileViewModel.kt | 46 +++- 10 files changed, 299 insertions(+), 154 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/LovedHatedEnum.kt create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt 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 ab2bbc0f..135dd41b 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 @@ -41,20 +41,25 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import org.listenbrainz.android.R import org.listenbrainz.android.ui.components.LoadingAnimation import org.listenbrainz.android.ui.screens.profile.listens.ListensScreen +import org.listenbrainz.android.ui.screens.profile.stats.StatsScreen +import org.listenbrainz.android.ui.screens.profile.taste.TasteScreen import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.ui.theme.app_bg_light import org.listenbrainz.android.ui.theme.lb_orange import org.listenbrainz.android.ui.theme.lb_purple import org.listenbrainz.android.ui.theme.new_app_bg_light import org.listenbrainz.android.util.Constants +import org.listenbrainz.android.viewmodel.ProfileViewModel @Composable fun BaseProfileScreen( username: String?, snackbarState: SnackbarHostState, + viewModel: ProfileViewModel = hiltViewModel(), uiState: ProfileUiState, onFollowClick: (String) -> Unit, onUnfollowClick: (String) -> Unit, @@ -62,7 +67,7 @@ fun BaseProfileScreen( ){ val currentTab : MutableState = remember { mutableStateOf(ProfileScreenTab.LISTENS) } - val isLoggedInUser = uiState.listensTabUiState.isSelf + val isLoggedInUser = uiState.isSelf val uriHandler = LocalUriHandler.current val mbOpeningErrorState = remember { mutableStateOf(null) @@ -100,7 +105,7 @@ fun BaseProfileScreen( ListenBrainzTheme.colorScheme.chipSelected } else { if(position == 0){ - if(uiState.listensTabUiState.isSelf){ + if(uiState.isSelf){ lb_purple } else{ @@ -116,7 +121,7 @@ fun BaseProfileScreen( shape = ListenBrainzTheme.shapes.chips, elevation = SuggestionChipDefaults.elevatedSuggestionChipElevation(elevation = 4.dp), icon = { - if(position == 0 && !uiState.listensTabUiState.isSelf){ + if(position == 0 && !uiState.isSelf){ Box (modifier = Modifier .background(lb_purple) .padding(4.dp)) { @@ -133,20 +138,23 @@ fun BaseProfileScreen( 0 -> username ?: "" 1 -> ProfileScreenTab.LISTENS.value 2 -> ProfileScreenTab.STATS.value - 3 -> ProfileScreenTab.PLAYLISTS.value - 4 -> ProfileScreenTab.TASTE.value + 3 -> ProfileScreenTab.TASTE.value + 4 -> ProfileScreenTab.PLAYLISTS.value 5 -> ProfileScreenTab.CREATED_FOR_YOU.value else -> "" }, style = ListenBrainzTheme.textStyles.chips, - color = ListenBrainzTheme.colorScheme.text, + color = when (position){ + 0 -> Color.White + else -> ListenBrainzTheme.colorScheme.textColor + }, ) }, onClick = { currentTab.value = when (position) { 1 -> ProfileScreenTab.LISTENS 2 -> ProfileScreenTab.STATS - 3 -> ProfileScreenTab.PLAYLISTS - 4 -> ProfileScreenTab.TASTE + 3 -> ProfileScreenTab.TASTE + 4 -> ProfileScreenTab.PLAYLISTS 5 -> ProfileScreenTab.CREATED_FOR_YOU else -> ProfileScreenTab.LISTENS } } @@ -158,42 +166,54 @@ fun BaseProfileScreen( Row (modifier = Modifier .align(Alignment.End) .padding(end = 20.dp)) { - when(isLoggedInUser) { - true -> AddListensButton() - false -> when(uiState.listensTabUiState.isFollowing){ - true -> UnFollowButton(username = username, onUnFollowClick = { - onUnfollowClick(it) - }) - false -> FollowButton(username = username, onFollowClick = { - onFollowClick(it) - }) + if(currentTab.value == ProfileScreenTab.LISTENS){ + when(isLoggedInUser) { + true -> AddListensButton() + false -> when(uiState.listensTabUiState.isFollowing){ + true -> UnFollowButton(username = username, onUnFollowClick = { + onUnfollowClick(it) + }) + false -> FollowButton(username = username, onFollowClick = { + onFollowClick(it) + }) + } + } + Spacer(modifier = Modifier.width(10.dp)) + MusicBrainzButton{ + try { + uriHandler.openUri(Constants.MB_BASE_URL + "user/${username}") + } + catch (e: RuntimeException) { + mbOpeningErrorState.value = e.message + } + catch (e: Exception){ + mbOpeningErrorState.value = e.message + } } - } - Spacer(modifier = Modifier.width(10.dp)) - MusicBrainzButton(){ - try { - uriHandler.openUri(Constants.MB_BASE_URL + "user/${username}") - } - catch (e: RuntimeException) { - mbOpeningErrorState.value = e.message; - } - catch (e: Exception){ - mbOpeningErrorState.value = e.message; - } } } when(currentTab.value) { ProfileScreenTab.LISTENS -> ListensScreen( scrollRequestState = false, + profileViewModel = viewModel, onScrollToTop = {}, snackbarState = snackbarState, username = username ) + ProfileScreenTab.STATS -> StatsScreen( + viewModel = viewModel, + uiState = uiState, + ) + ProfileScreenTab.TASTE -> TasteScreen( + viewModel = viewModel, + uiState = uiState, + ) else -> ListensScreen( scrollRequestState = false, + profileViewModel = viewModel, onScrollToTop = {}, snackbarState = snackbarState, - username = username + username = username, ) } @@ -201,7 +221,7 @@ fun BaseProfileScreen( } if(mbOpeningErrorState.value != null){ LaunchedEffect(mbOpeningErrorState.value) { - snackbarState.showSnackbar("Some Error Occoued", duration = SnackbarDuration.Short) + snackbarState.showSnackbar("Some Error Occurred", duration = SnackbarDuration.Short) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt index b087e57c..d0515be7 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt @@ -63,7 +63,7 @@ fun ProfileScreen( when(loginStatus) { STATUS_LOGGED_IN -> { LaunchedEffect(Unit) { - viewModel.getUserListensData(username) + viewModel.getUserDataFromRemote(username) } BaseProfileScreen( 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 39e124c9..1e493f0c 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 @@ -8,12 +8,14 @@ import org.listenbrainz.android.model.SimilarUser import org.listenbrainz.android.ui.screens.profile.listens.ListeningNowUiState data class ProfileUiState( - val listensTabUiState: ListensTabUiState = ListensTabUiState() + val isSelf: Boolean = false, + val listensTabUiState: ListensTabUiState = ListensTabUiState(), + val statsTabUIState: StatsTabUIState = StatsTabUIState(), + val tasteTabUIState: TasteTabUIState = TasteTabUIState(), ) data class ListensTabUiState ( val isLoading: Boolean = true, - val isSelf: Boolean = false, val listenCount: Int? = null, val followersCount: Int? = null, val followingCount: Int? = null, @@ -28,6 +30,15 @@ data class ListensTabUiState ( val isFollowing: Boolean = false ) +data class TasteTabUIState ( + val isLoading: Boolean = true, + + ) + +data class StatsTabUIState( + val isLoading: Boolean = true, +) + data class ListeningNowUiState( val listeningNow: Listen? = null, val listeningNowBitmap: ListenBitmap = ListenBitmap(), diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt index 4b0db6eb..a5fc3c85 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt @@ -3,7 +3,6 @@ package org.listenbrainz.android.ui.screens.profile.listens import android.os.Bundle import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -42,7 +41,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.platform.LocalContext @@ -75,16 +73,13 @@ import org.listenbrainz.android.ui.components.dialogs.ReviewDialog import org.listenbrainz.android.ui.components.dialogs.rememberDialogsState import org.listenbrainz.android.ui.screens.feed.FeedUiState import org.listenbrainz.android.ui.screens.feed.SocialDropdown -import org.listenbrainz.android.ui.screens.profile.ListensTabUiState +import org.listenbrainz.android.ui.screens.profile.ProfileUiState import org.listenbrainz.android.ui.screens.settings.PreferencesUiState import org.listenbrainz.android.ui.theme.ListenBrainzTheme -import org.listenbrainz.android.ui.theme.app_bg_dark import org.listenbrainz.android.ui.theme.app_bg_mid -import org.listenbrainz.android.ui.theme.app_bg_secondary_dark import org.listenbrainz.android.ui.theme.compatibilityMeterColor import org.listenbrainz.android.ui.theme.lb_purple import org.listenbrainz.android.ui.theme.lb_purple_night -import org.listenbrainz.android.ui.theme.on_app_bg_dark import org.listenbrainz.android.util.Utils.getCoverArtUrl import org.listenbrainz.android.viewmodel.FeedViewModel import org.listenbrainz.android.viewmodel.ListensViewModel @@ -94,7 +89,7 @@ import org.listenbrainz.android.viewmodel.SocialViewModel @Composable fun ListensScreen( viewModel: ListensViewModel = hiltViewModel(), - profileViewModel: ProfileViewModel = hiltViewModel(), + profileViewModel: ProfileViewModel, socialViewModel: SocialViewModel = hiltViewModel(), feedViewModel : FeedViewModel = hiltViewModel(), scrollRequestState: Boolean, @@ -115,7 +110,7 @@ fun ListensScreen( scrollRequestState = scrollRequestState, onScrollToTop = onScrollToTop, username= username, - uiState = uiState.listensTabUiState, + uiState = uiState, feedUiState = feedUiState, preferencesUiState = preferencesUiState, updateNotificationServicePermissionStatus = { @@ -158,6 +153,17 @@ fun ListensScreen( }, onPersonallyRecommend = { metadata, users, blurbContent -> socialViewModel.personallyRecommend(metadata, users, blurbContent) + }, + onFollowButtonClick = { + it, status -> + if(!username.isNullOrEmpty()) { + if(!status){ + profileViewModel.followUser(it) + } + else{ + profileViewModel.unfollowUser(it) + } + } } ) } @@ -177,13 +183,12 @@ private enum class ListenDialogBundleKeys { -@OptIn(ExperimentalFoundationApi::class) @Composable fun ListensScreen( scrollRequestState: Boolean, onScrollToTop: (suspend () -> Unit) -> Unit, username: String?, - uiState: ListensTabUiState, + uiState: ProfileUiState, feedUiState: FeedUiState, preferencesUiState: PreferencesUiState, updateNotificationServicePermissionStatus: () -> Unit, @@ -201,7 +206,8 @@ fun ListensScreen( searchUsers: (String) -> Unit, isCritiqueBrainzLinked: suspend () -> Boolean?, onReview: (type: ReviewEntityType, blurbContent: String, rating: Int?, locale: String, metadata: Metadata) -> Unit, - onPersonallyRecommend: (metadata: Metadata, users: List, blurbContent: String) -> Unit + onPersonallyRecommend: (metadata: Metadata, users: List, blurbContent: String) -> Unit, + onFollowButtonClick: (String?, Boolean) -> Unit, ) { val listState = rememberLazyListState() @@ -234,22 +240,22 @@ fun ListensScreen( } } - AnimatedVisibility(visible = !uiState.isLoading) { + AnimatedVisibility(visible = !uiState.listensTabUiState.isLoading) { LazyColumn(state = listState) { item { - SongsListened(username = username, listenCount = uiState.listenCount) + SongsListened(username = username, listenCount = uiState.listensTabUiState.listenCount, isSelf = uiState.isSelf) } item{ - FollowersInformation(followersCount = uiState.followersCount, followingCount = uiState.followingCount) + FollowersInformation(followersCount = uiState.listensTabUiState.followersCount, followingCount = uiState.listensTabUiState.followingCount) } item{ Spacer(modifier = Modifier.height(30.dp)) - Text("Recent Listens", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), modifier = Modifier.padding(start = 16.dp)) + Text("Recent Listens", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), modifier = Modifier.padding(start = 16.dp)) Spacer(modifier = Modifier.height(10.dp)) } itemsIndexed(items = (when(recentListensCollapsibleState.value){ - true -> uiState.recentListens?.take(5) ?: listOf() - false -> uiState.recentListens?.take(10) ?: listOf() + true -> uiState.listensTabUiState.recentListens?.take(5) ?: listOf() + false -> uiState.listensTabUiState.recentListens?.take(10) ?: listOf() })) { index, listen -> val metadata = Metadata(trackMetadata = listen.trackMetadata) ListenCardSmall( @@ -257,8 +263,7 @@ fun ListensScreen( .padding( horizontal = 16.dp, vertical = ListenBrainzTheme.paddings.lazyListAdjacent - ) - .background(app_bg_secondary_dark), + ), trackName = listen.trackMetadata.trackName, artistName = listen.trackMetadata.artistName, coverArtUrl = getCoverArtUrl( @@ -308,7 +313,7 @@ fun ListensScreen( } } item { - Spacer(modifier = Modifier.height(10.dp)) + Spacer(modifier = Modifier.height(20.dp)) Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { LoadMoreButton( state = recentListensCollapsibleState.value, @@ -323,23 +328,14 @@ fun ListensScreen( item { Box(modifier = Modifier .padding(top = 30.dp) - .clip(shape = RoundedCornerShape(20.dp)) .background( - Brush.verticalGradient( - listOf( - Color(0xFF161616), - Color(0xFF1A1A1A), - Color(0xFF202020), - Color(0xFF242424), - Color.Transparent - ) - ) + ListenBrainzTheme.colorScheme.songsListenedToBG )){ Column { Spacer(modifier = Modifier.height(30.dp)) - Text("Your Compatibility", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), modifier = Modifier.padding(start = 16.dp)) + Text("Your Compatibility", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), modifier = Modifier.padding(start = 16.dp)) Spacer(modifier = Modifier.height(10.dp)) - CompatibilityCard(compatibility = uiState.compatibility ?: 0f, uiState.similarArtists) + CompatibilityCard(compatibility = uiState.listensTabUiState.compatibility ?: 0f, uiState.listensTabUiState.similarArtists) } } @@ -349,37 +345,30 @@ fun ListensScreen( item { Box(modifier = Modifier .padding(top = 30.dp) - .clip(shape = RoundedCornerShape(20.dp)) .background( - Brush.verticalGradient( - listOf( - Color(0xFF161616), - Color(0xFF1A1A1A), - Color(0xFF202020), - Color(0xFF242424), - Color.Transparent - ) - ) + ListenBrainzTheme.colorScheme.songsListenedToBG )){ Column { FollowersCard( - followersCount = uiState.followersCount, - followingCount = uiState.followingCount, + followersCount = uiState.listensTabUiState.followersCount, + followingCount = uiState.listensTabUiState.followingCount, followers = when(followersMenuCollapsibleState.value){ - true -> uiState.followers?.take(5) ?: emptyList() - false -> uiState.followers ?: emptyList() + true -> uiState.listensTabUiState.followers?.take(5) ?: emptyList() + false -> uiState.listensTabUiState.followers ?: emptyList() }, following = when(followersMenuCollapsibleState.value){ - true -> uiState.following?.take(5) ?: emptyList() - false -> uiState.following ?: emptyList() + true -> uiState.listensTabUiState.following?.take(5) ?: emptyList() + false -> uiState.listensTabUiState.following ?: emptyList() }, followersState = followersMenuState.value, onStateChange = { newMenuState-> followersMenuState.value = !newMenuState - } + }, + onFollowButtonClick = onFollowButtonClick ) - if((uiState.followersCount ?: 0) > 5 || ((uiState.followingCount ?: 0) > 5)){ + if((uiState.listensTabUiState.followersCount ?: 0) > 5 || ((uiState.listensTabUiState.followingCount ?: 0) > 5)){ + Spacer(modifier = Modifier.height(20.dp)) Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { LoadMoreButton( state = followersMenuCollapsibleState.value, @@ -400,22 +389,16 @@ fun ListensScreen( .padding(top = 30.dp) .clip(shape = RoundedCornerShape(20.dp)) .background( - Brush.verticalGradient( - listOf( - - Color(0xFF202020), - Color(0xFF242424), - Color.Transparent - ) - ) + ListenBrainzTheme.colorScheme.songsListenedToBG )){ Column { SimilarUsersCard(similarUsers = when(similarUsersCollapsibleState.value){ - true -> uiState.similarUsers?.take(5) ?: emptyList() - false -> uiState.similarUsers ?: emptyList() + true -> uiState.listensTabUiState.similarUsers?.take(5) ?: emptyList() + false -> uiState.listensTabUiState.similarUsers ?: emptyList() }) - if((uiState.similarUsers?.size ?: 0) > 5){ + if((uiState.listensTabUiState.similarUsers?.size ?: 0) > 5){ + Spacer(modifier = Modifier.height(20.dp)) Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { LoadMoreButton( state = similarUsersCollapsibleState.value, @@ -445,7 +428,7 @@ fun ListensScreen( }, currentDialog = dialogsState.currentDialog, currentIndex = dialogsState.metadata?.getInt(ListenDialogBundleKeys.EVENT_INDEX.name), - listens = uiState.recentListens ?: listOf(), + listens = uiState.listensTabUiState.recentListens ?: listOf(), onPin = {metadata, blurbContent -> onPin(metadata, blurbContent)}, searchUsers = { query -> searchUsers(query) }, feedUiState = feedUiState, @@ -578,16 +561,20 @@ private fun LoadMoreButton( @Composable -private fun SongsListened(username: String? , listenCount: Int?){ +private fun SongsListened(username: String? , listenCount: Int?, isSelf: Boolean){ Column (horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier - .clip(shape = RoundedCornerShape(100.dp)) - .background(color = app_bg_dark)) { + .background(ListenBrainzTheme.colorScheme.songsListenedToBG)) { Spacer(modifier = Modifier.height(30.dp)) - Text("$username has listened to", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Text( + when(isSelf){ + true -> "$username has listened to" + false -> "You have listened to" + } + , color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) Spacer(modifier = Modifier.height(15.dp)) - HorizontalDivider(color = on_app_bg_dark, modifier = Modifier.padding(start = 60.dp, end = 60.dp)) + HorizontalDivider(color = ListenBrainzTheme.colorScheme.dividerColor, modifier = Modifier.padding(start = 60.dp, end = 60.dp)) Spacer(modifier = Modifier.height(15.dp)) - Text(listenCount.toString(), color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), textAlign = TextAlign.Center) + Text(listenCount.toString(), color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), textAlign = TextAlign.Center) Text("songs so far", color = app_bg_mid, style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center) Spacer(modifier = Modifier.height(30.dp)) } @@ -597,33 +584,25 @@ private fun SongsListened(username: String? , listenCount: Int?){ @Composable private fun FollowersInformation(followersCount: Int?, followingCount: Int?){ Box(modifier = Modifier - .clip(shape = RoundedCornerShape(20.dp)) .background( - Brush.verticalGradient( - listOf( - Color(0xFF161616), - Color(0xFF1A1A1A), - Color(0xFF202020), - Color(0xFF242424), - Color.Transparent - ) - ) + ListenBrainzTheme.colorScheme.userPageGradient ) ){ Row (horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier .fillMaxWidth() - .padding(top = 30.dp)) { + .padding(top = 30.dp, bottom = 30.dp)) { Column (horizontalAlignment = Alignment.CenterHorizontally) { - Text((followersCount ?:0).toString(), style = MaterialTheme.typography.bodyLarge, color = Color.White) + Text((followersCount ?:0).toString(), style = MaterialTheme.typography.bodyLarge, color = ListenBrainzTheme.colorScheme.textColor) Spacer(modifier = Modifier.height(10.dp)) - Text("Followers", style = MaterialTheme.typography.bodyLarge, color = Color.White) + Text("Followers", style = MaterialTheme.typography.bodyLarge, color = ListenBrainzTheme.colorScheme.textColor) } Column (horizontalAlignment = Alignment.CenterHorizontally) { - Text((followingCount ?: 0).toString(), style = MaterialTheme.typography.bodyLarge, color = Color.White) + Text((followingCount ?: 0).toString(), style = MaterialTheme.typography.bodyLarge, color = ListenBrainzTheme.colorScheme.textColor) Spacer(modifier = Modifier.height(10.dp)) - Text("Following", style = MaterialTheme.typography.bodyLarge, color = Color.White) + Text("Following", style = MaterialTheme.typography.bodyLarge, color = ListenBrainzTheme.colorScheme.textColor) } } + Spacer(modifier = Modifier.height(10.dp)) } @@ -645,11 +624,11 @@ fun CompatibilityCard(compatibility: Float, similarArtists: List){ } @Composable -private fun FollowersCard(followersCount: Int?, followingCount: Int?, followers: List>, following: List>, followersState: Boolean, onStateChange: (Boolean) -> Unit) { +private fun FollowersCard(followersCount: Int?, followingCount: Int?, followers: List>, following: List>, followersState: Boolean, onStateChange: (Boolean) -> Unit, onFollowButtonClick: (String?, Boolean) -> Unit) { Column(modifier = Modifier.padding(start = 16.dp , top = 30.dp)) { Text( "Followers", - color = Color.White, + color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp) ) Spacer(modifier = Modifier.height(10.dp)) @@ -657,8 +636,8 @@ private fun FollowersCard(followersCount: Int?, followingCount: Int?, followers: Card( colors = CardDefaults.cardColors( containerColor = when (followersState) { - true -> lb_purple_night - false -> app_bg_dark + true -> ListenBrainzTheme.colorScheme.followerChipSelected + false -> ListenBrainzTheme.colorScheme.followerChipUnselected }, ), border = when(followersState){ @@ -676,8 +655,8 @@ private fun FollowersCard(followersCount: Int?, followingCount: Int?, followers: Text( text = "Followers (${followersCount})", color = when (followersState) { - true -> app_bg_dark - false -> lb_purple_night + true -> ListenBrainzTheme.colorScheme.followerChipUnselected + false -> ListenBrainzTheme.colorScheme.followerChipSelected }, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, @@ -688,8 +667,8 @@ private fun FollowersCard(followersCount: Int?, followingCount: Int?, followers: Card( colors = CardDefaults.cardColors( containerColor = when (followersState) { - true -> app_bg_dark - false -> lb_purple_night + true -> ListenBrainzTheme.colorScheme.followerChipUnselected + false -> ListenBrainzTheme.colorScheme.followerChipSelected }, ), border = when(followersState){ @@ -707,8 +686,8 @@ private fun FollowersCard(followersCount: Int?, followingCount: Int?, followers: Text( text = "Following (${followingCount})", color = when (followersState) { - true -> lb_purple_night - false -> app_bg_dark + true -> ListenBrainzTheme.colorScheme.followerChipSelected + false -> ListenBrainzTheme.colorScheme.followerChipUnselected }, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, @@ -720,12 +699,12 @@ private fun FollowersCard(followersCount: Int?, followingCount: Int?, followers: when(followersState){ true -> followers.map { state -> - FollowCard(username = state.first, onButtonClick = Unit, followStatus = state.second) + FollowCard(username = state.first, onFollowButtonClick = onFollowButtonClick, followStatus = state.second) Spacer(modifier = Modifier.height(10.dp)) } false -> following.map { state -> - FollowCard(username = state.first, onButtonClick = Unit, followStatus = state.second) + FollowCard(username = state.first, onFollowButtonClick = onFollowButtonClick, followStatus = state.second) Spacer(modifier = Modifier.height(10.dp)) } } @@ -735,7 +714,7 @@ private fun FollowersCard(followersCount: Int?, followingCount: Int?, followers: @Composable private fun SimilarUsersCard(similarUsers: List){ Spacer(modifier = Modifier.height(20.dp)) - Text("Similar Users", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), modifier = Modifier.padding(horizontal = 16.dp)) + Text("Similar Users", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), modifier = Modifier.padding(horizontal = 16.dp)) Spacer(modifier = Modifier.height(20.dp)) similarUsers.mapIndexed{ index , item -> @@ -744,36 +723,42 @@ private fun SimilarUsersCard(similarUsers: List){ } @Composable -private fun FollowCard(username: String?, onButtonClick: Unit, followStatus: Boolean) { - Card(colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E))) { +private fun FollowCard(username: String?, onFollowButtonClick: (String?, Boolean) -> Unit, followStatus: Boolean) { + Card(colors = CardDefaults.cardColors(containerColor = ListenBrainzTheme.colorScheme.followerCardColor)) { Row( modifier = Modifier .padding(horizontal = 16.dp) .fillMaxWidth() - .height(70.dp), + .height(60.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( username ?: "", - color = lb_purple_night, + color = ListenBrainzTheme.colorScheme.followerCardTextColor, style = MaterialTheme.typography.bodyLarge ) TextButton( - onClick = { /*TODO*/ }, colors = ButtonDefaults.buttonColors( + onClick = { + onFollowButtonClick(username, followStatus) + }, colors = ButtonDefaults.buttonColors( containerColor = when (followStatus) { - true -> app_bg_dark + true -> ListenBrainzTheme.colorScheme.followingButtonColor false -> lb_purple } ), modifier = Modifier .width(90.dp) - .height(40.dp), shape = RoundedCornerShape(10.dp) + .height(40.dp), shape = RoundedCornerShape(10.dp), + border = ListenBrainzTheme.colorScheme.followingButtonBorder ) { Text( when (followStatus) { true -> "Following" false -> "Follow" - }, color = Color.White + }, color = when(followStatus){ + true -> ListenBrainzTheme.colorScheme.followerCardTextColor + false -> Color.White + } ) } } @@ -788,7 +773,7 @@ fun ListensScreenPreview() { onScrollToTop = {}, scrollRequestState = false, updateNotificationServicePermissionStatus = {}, - uiState = ListensTabUiState(), + uiState = ProfileUiState(), feedUiState = FeedUiState(), preferencesUiState = PreferencesUiState(), validateUserToken = { true }, @@ -805,6 +790,7 @@ fun ListensScreenPreview() { onPersonallyRecommend = {_,_,_ ->}, dropdownItemIndex = remember { mutableStateOf(null) }, snackbarState = remember { SnackbarHostState() }, - username = "pranavkonidena" + username = "pranavkonidena", + onFollowButtonClick = {_,_ -> } ) } 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 new file mode 100644 index 00000000..4b0f11a6 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt @@ -0,0 +1,15 @@ +package org.listenbrainz.android.ui.screens.profile.stats + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import org.listenbrainz.android.ui.screens.profile.ProfileUiState +import org.listenbrainz.android.viewmodel.ProfileViewModel + +@Composable +fun StatsScreen( + viewModel: ProfileViewModel, + uiState: ProfileUiState, +) { + Text(text = uiState.listensTabUiState.listenCount.toString(), color = Color.White) +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/LovedHatedEnum.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/LovedHatedEnum.kt new file mode 100644 index 00000000..4589767b --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/LovedHatedEnum.kt @@ -0,0 +1,4 @@ +enum class LovedHated { + loved, + hated, +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt new file mode 100644 index 00000000..df28bc68 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt @@ -0,0 +1,29 @@ +package org.listenbrainz.android.ui.screens.profile.taste + +import LovedHated +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.ElevatedSuggestionChip +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import org.listenbrainz.android.ui.screens.profile.ProfileUiState +import org.listenbrainz.android.viewmodel.ProfileViewModel + +@Composable +fun TasteScreen( + viewModel: ProfileViewModel, + uiState: ProfileUiState, +) { + val lovedHatedState: MutableState = remember { mutableStateOf(LovedHated.loved) } + LazyColumn { + item { + Row { + ElevatedSuggestionChip(onClick = { lovedHatedState.value = LovedHated.loved }, label = { Text("Loved") }) + ElevatedSuggestionChip(onClick = { lovedHatedState.value = LovedHated.hated }, label = { Text("Hated") }) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/theme/Color.kt b/app/src/main/java/org/listenbrainz/android/ui/theme/Color.kt index f6ba8fd8..1a0132a5 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/theme/Color.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/theme/Color.kt @@ -21,6 +21,7 @@ val app_bg_day = Color(0xFFFFFFFF) val app_bg_dark = Color(0xFF292929) val app_bg_secondary_dark = Color(0xFF1E1E1E) val on_app_bg_dark = Color(0xFF101010) +val app_bg_secondary_light = Color(0xFFD9D9D9) val app_bg_light = Color(0xFF8FA3AD) //TODO: Change app_bg_light everywhere following approval from lead dev val new_app_bg_light = Color(0xFFF5F5F5) diff --git a/app/src/main/java/org/listenbrainz/android/ui/theme/Theme.kt b/app/src/main/java/org/listenbrainz/android/ui/theme/Theme.kt index e598e0d7..f8b06715 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/theme/Theme.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/theme/Theme.kt @@ -2,6 +2,7 @@ package org.listenbrainz.android.ui.theme import android.app.Activity import android.content.Context +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme @@ -60,7 +61,19 @@ data class ColorScheme( val hint: Color, /** Used for BP **/ val gradientBrush: Brush, - val placeHolderColor: Color + val placeHolderColor: Color, + /** Used for User Pages **/ + val dividerColor: Color, + val textColor: Color, + val songsListenedToBG: Color, + val userPageGradient: Brush, + val followerChipSelected: Color, + val followerChipUnselected: Color, + val followerCardColor: Color, + val followerCardTextColor: Color, + val followingButtonColor: Color, + val followingButtonTextColor: Color, + val followingButtonBorder: BorderStroke?, ) @@ -105,7 +118,26 @@ private val colorSchemeDark = ColorScheme( listenText = Color.White, hint = Color(0xFF8C8C8C), gradientBrush = brainzPlayerDarkGradientsBrush, - placeHolderColor = Color(0xFF1E1E1E) + placeHolderColor = Color(0xFF1E1E1E), + dividerColor = app_bg_secondary_dark, + textColor = new_app_bg_light, + songsListenedToBG = app_bg_dark, + userPageGradient = Brush.verticalGradient( + listOf( + Color(0xFF161616), + Color(0xFF1A1A1A), + Color(0xFF202020), + Color(0xFF242424), + Color.Transparent + ) + ), + followerChipSelected = lb_purple_night, + followerChipUnselected = app_bg_dark, + followerCardColor = app_bg_secondary_dark, + followerCardTextColor = lb_purple_night, + followingButtonColor = app_bg_dark, + followingButtonTextColor = Color.White, + followingButtonBorder = null, ) private val colorSchemeLight = ColorScheme( @@ -124,7 +156,30 @@ private val colorSchemeLight = ColorScheme( listenText = lb_purple, hint = Color(0xFF707070), gradientBrush = brainzPlayerLightGradientsBrush, - placeHolderColor = Color(0xFFEBEBEB) + placeHolderColor = Color(0xFFEBEBEB), + dividerColor = app_bg_secondary_light, + textColor = app_bg_dark, + songsListenedToBG = new_app_bg_light, + userPageGradient = Brush.verticalGradient( + listOf( + Color(0xFFEAEAEA), + Color(0xFFEBEBEB), + Color(0xFFF0F0F0), + Color(0xFFF1F1F1), + Color(0xFFF2F2F2), + Color(0xFFF3F3F3), + Color(0xFFF4F4F4), + Color(0xFFF5F5F5), + Color.Transparent + ) + ), + followerChipSelected = lb_purple, + followerChipUnselected = Color.White, + followerCardColor = Color.White, + followerCardTextColor = lb_purple, + followingButtonColor = Color.White, + followingButtonTextColor = lb_purple, + followingButtonBorder = BorderStroke(width = 1.dp, color = lb_purple) ) 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 a11ed9e4..0db694d7 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt @@ -1,6 +1,5 @@ package org.listenbrainz.android.viewmodel -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher @@ -17,10 +16,11 @@ import org.listenbrainz.android.model.Listen import org.listenbrainz.android.repository.listens.ListensRepository import org.listenbrainz.android.repository.preferences.AppPreferences import org.listenbrainz.android.repository.social.SocialRepository -import org.listenbrainz.android.repository.socket.SocketRepository import org.listenbrainz.android.repository.user.UserRepository 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.util.Constants.Strings.STATUS_LOGGED_OUT import javax.inject.Inject @@ -28,17 +28,19 @@ import javax.inject.Inject class ProfileViewModel @Inject constructor( val appPreferences: AppPreferences, private val userRepository: UserRepository, - val listensRepository: ListensRepository, - val socketRepository: SocketRepository, + private val listensRepository: ListensRepository, private val socialRepository: SocialRepository, - private val savedStateHandle: SavedStateHandle, @IoDispatcher val ioDispatcher: CoroutineDispatcher, ) : BaseViewModel() { private val _loginStatusFlow: MutableStateFlow = MutableStateFlow(STATUS_LOGGED_OUT) + private var isLoggedInUser = false val loginStatusFlow: StateFlow = _loginStatusFlow.asStateFlow() private val listenStateFlow : MutableStateFlow = MutableStateFlow(ListensTabUiState()) + private val statsStateFlow : MutableStateFlow = MutableStateFlow(StatsTabUIState()) + private val tasteStateFlow : MutableStateFlow = MutableStateFlow(TasteTabUIState()) + init { viewModelScope.launch(ioDispatcher) { appPreferences.getLoginStatusFlow() @@ -81,11 +83,18 @@ class ProfileViewModel @Inject constructor( } + suspend fun getUserDataFromRemote( + inputUsername: String? + ){ + getUserListensData(inputUsername) + getUserStatsData(inputUsername) + getUserTasteData(inputUsername) + } + - suspend fun getUserListensData(inputUsername: String?) { + private suspend fun getUserListensData(inputUsername: String?) { val username = inputUsername ?: appPreferences.username.get() - var isLoggedInUser = false if(inputUsername != null){ isLoggedInUser = inputUsername == appPreferences.username.get() } @@ -121,7 +130,6 @@ class ProfileViewModel @Inject constructor( val isFollowing = currentUserFollowingSet.contains(username) val listensTabState = ListensTabUiState( isLoading = false, - isSelf = isLoggedInUser, listenCount = listenCount, followersCount = followersCount, followers = followersState, @@ -137,15 +145,31 @@ class ProfileViewModel @Inject constructor( listenStateFlow.emit(listensTabState) } + suspend fun getUserStatsData(inputUsername: String?) { + + } + + suspend fun getUserTasteData(inputUsername: String?) { + + } + + override val uiState: StateFlow = createUiStateFlow() override fun createUiStateFlow(): StateFlow { return combine( - listenStateFlow + listenStateFlow, + statsStateFlow, + tasteStateFlow, ) { - array -> - ProfileUiState(array[0]) + listensUIState, statsUIState, tasteUIState -> + ProfileUiState( + isSelf = isLoggedInUser, + listensTabUiState = listensUIState, + statsTabUIState = statsUIState, + tasteTabUIState = tasteUIState, + ) }.stateIn( viewModelScope, started = SharingStarted.Eagerly, From f208674884e5bdd3d971e869760994584a6093fc Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sun, 30 Jun 2024 21:07:57 +0530 Subject: [PATCH 48/97] Fixed first suggestion chip in scrollable tabs section in user pages --- .../ui/screens/profile/BaseProfileScreen.kt | 122 +++++++++--------- .../ui/screens/profile/ProfileUiState.kt | 1 - .../screens/profile/listens/ListensScreen.kt | 4 +- 3 files changed, 66 insertions(+), 61 deletions(-) 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 135dd41b..064a81f8 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 @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.filled.Add @@ -36,6 +37,7 @@ 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.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalUriHandler @@ -94,71 +96,75 @@ fun BaseProfileScreen( Color.Transparent ) ) - ) + ), + verticalAlignment = Alignment.CenterVertically ) { Spacer(modifier = Modifier.width(ListenBrainzTheme.paddings.chipsHorizontal / 2)) repeat(5) { position -> - ElevatedSuggestionChip( - modifier = Modifier.padding(ListenBrainzTheme.paddings.chipsHorizontal), - colors = SuggestionChipDefaults.elevatedSuggestionChipColors( - if (position == currentTab.value.index) { - ListenBrainzTheme.colorScheme.chipSelected - } else { - if(position == 0){ - if(uiState.isSelf){ - lb_purple - } - else{ - lb_orange + when(position){ + 0 -> Box(modifier = Modifier.padding(ListenBrainzTheme.paddings.chipsHorizontal,) .clip(shape = RoundedCornerShape(4.dp)).background( + when(uiState.isSelf){ + true -> lb_purple + false -> lb_orange + } + )) { + Row (modifier = Modifier.padding(end = 8.dp, top = when(uiState.isSelf){ + true -> 4.dp + false -> 0.dp + }, bottom = when(uiState.isSelf){ + true -> 4.dp + false -> 0.dp + }), verticalAlignment = Alignment.CenterVertically) { + if(!uiState.isSelf){ + Box (modifier = Modifier + .background(lb_purple) + .padding(4.dp)) { + Icon(Icons.Default.Home, contentDescription = "", tint = new_app_bg_light, modifier = Modifier.clickable { + goToUserProfile() + }) } } - else{ - ListenBrainzTheme.colorScheme.chipUnselected - } - + Text(username ?: "", color = when(uiState.isSelf){ + true -> Color.White + false -> Color.Black + }, modifier = Modifier.padding(start = 8.dp)) } - ), - shape = ListenBrainzTheme.shapes.chips, - elevation = SuggestionChipDefaults.elevatedSuggestionChipElevation(elevation = 4.dp), - icon = { - if(position == 0 && !uiState.isSelf){ - Box (modifier = Modifier - .background(lb_purple) - .padding(4.dp)) { - Icon(Icons.Default.Home, contentDescription = "", tint = new_app_bg_light, modifier = Modifier.clickable { - goToUserProfile() - }) + } + else -> ElevatedSuggestionChip( + modifier = Modifier.padding(ListenBrainzTheme.paddings.chipsHorizontal), + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + if (position == currentTab.value.index) { + ListenBrainzTheme.colorScheme.chipSelected + } else { + ListenBrainzTheme.colorScheme.chipUnselected } - - } - }, - label = { - Text( - text = when (position) { - 0 -> username ?: "" - 1 -> ProfileScreenTab.LISTENS.value - 2 -> ProfileScreenTab.STATS.value - 3 -> ProfileScreenTab.TASTE.value - 4 -> ProfileScreenTab.PLAYLISTS.value - 5 -> ProfileScreenTab.CREATED_FOR_YOU.value - else -> "" - }, - style = ListenBrainzTheme.textStyles.chips, - color = when (position){ - 0 -> Color.White - else -> ListenBrainzTheme.colorScheme.textColor - }, - ) - }, - onClick = { currentTab.value = when (position) { - 1 -> ProfileScreenTab.LISTENS - 2 -> ProfileScreenTab.STATS - 3 -> ProfileScreenTab.TASTE - 4 -> ProfileScreenTab.PLAYLISTS - 5 -> ProfileScreenTab.CREATED_FOR_YOU - else -> ProfileScreenTab.LISTENS - } } - ) + ), + shape = ListenBrainzTheme.shapes.chips, + elevation = SuggestionChipDefaults.elevatedSuggestionChipElevation(elevation = 4.dp), + label = { + Text( + text = when (position) { + 1 -> ProfileScreenTab.LISTENS.value + 2 -> ProfileScreenTab.STATS.value + 3 -> ProfileScreenTab.TASTE.value + 4 -> ProfileScreenTab.PLAYLISTS.value + 5 -> ProfileScreenTab.CREATED_FOR_YOU.value + else -> "" + }, + style = ListenBrainzTheme.textStyles.chips, + color = ListenBrainzTheme.colorScheme.textColor + ) + }, + onClick = { currentTab.value = when (position) { + 1 -> ProfileScreenTab.LISTENS + 2 -> ProfileScreenTab.STATS + 3 -> ProfileScreenTab.TASTE + 4 -> ProfileScreenTab.PLAYLISTS + 5 -> ProfileScreenTab.CREATED_FOR_YOU + else -> ProfileScreenTab.LISTENS + } } + ) + } } } 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 1e493f0c..a2fe212a 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 @@ -32,7 +32,6 @@ data class ListensTabUiState ( data class TasteTabUIState ( val isLoading: Boolean = true, - ) data class StatsTabUIState( diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt index a5fc3c85..2fde3dba 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt @@ -567,8 +567,8 @@ private fun SongsListened(username: String? , listenCount: Int?, isSelf: Boolean Spacer(modifier = Modifier.height(30.dp)) Text( when(isSelf){ - true -> "$username has listened to" - false -> "You have listened to" + true -> "You have listened to" + false -> "$username has listened to" } , color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) Spacer(modifier = Modifier.height(15.dp)) From ac735e7cb0ff0c5ae7cd40a6bef6956bc588a5c5 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Wed, 3 Jul 2024 11:16:51 +0530 Subject: [PATCH 49/97] Added loved and hated songs of user --- .../android/model/user/UserFeedback.kt | 8 + .../android/model/user/UserFeedbackEntry.kt | 13 ++ .../android/repository/user/UserRepository.kt | 2 + .../repository/user/UserRepositoryImpl.kt | 6 + .../android/service/UserService.kt | 5 + .../ui/screens/profile/ProfileUiState.kt | 3 + .../screens/profile/listens/ListensScreen.kt | 2 +- .../ui/screens/profile/taste/TasteScreen.kt | 138 +++++++++++++++++- .../android/viewmodel/ProfileViewModel.kt | 26 +++- app/src/main/res/drawable/heart.xml | 9 ++ 10 files changed, 201 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/UserFeedback.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/UserFeedbackEntry.kt create mode 100644 app/src/main/res/drawable/heart.xml diff --git a/app/src/main/java/org/listenbrainz/android/model/user/UserFeedback.kt b/app/src/main/java/org/listenbrainz/android/model/user/UserFeedback.kt new file mode 100644 index 00000000..c9233499 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/UserFeedback.kt @@ -0,0 +1,8 @@ +package org.listenbrainz.android.model.user + +import com.google.gson.annotations.SerializedName + +data class UserFeedback( + @SerializedName("count") val count: Int? = null, + @SerializedName("feedback") val feedback: List? = null, +) diff --git a/app/src/main/java/org/listenbrainz/android/model/user/UserFeedbackEntry.kt b/app/src/main/java/org/listenbrainz/android/model/user/UserFeedbackEntry.kt new file mode 100644 index 00000000..33b77a96 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/UserFeedbackEntry.kt @@ -0,0 +1,13 @@ +package org.listenbrainz.android.model.user + +import com.google.gson.annotations.SerializedName +import org.listenbrainz.android.model.TrackMetadata + +data class UserFeedbackEntry( + val created: Int? = 0, + @SerializedName ("recording_mbid") val recordingMBID: String? = null, + @SerializedName ("recording_msid") val recordingMSID: String? = null, + val score: Int? = null, + @SerializedName("track_metadata") val trackMetadata: TrackMetadata? = null, + @SerializedName("user_id") val userId: String? = 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 65963b49..c92cefc1 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,6 +3,7 @@ package org.listenbrainz.android.repository.user import org.listenbrainz.android.model.Listens import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.user.TopArtists +import org.listenbrainz.android.model.user.UserFeedback import org.listenbrainz.android.model.user.UserSimilarityPayload import org.listenbrainz.android.util.Resource @@ -12,4 +13,5 @@ interface UserRepository { suspend fun fetchUserCurrentPins(username: String?) : Resource //TODO: Move to artists VM once implemented suspend fun getTopArtists(username: String?): Resource + suspend fun getUserFeedback(username: String?, score: Int?): 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 ba4fa7d7..93f949e8 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,6 +4,7 @@ import org.listenbrainz.android.model.Listens import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.ResponseError import org.listenbrainz.android.model.user.TopArtists +import org.listenbrainz.android.model.user.UserFeedback import org.listenbrainz.android.model.user.UserSimilarityPayload import org.listenbrainz.android.service.UserService import org.listenbrainz.android.util.Resource @@ -36,5 +37,10 @@ class UserRepositoryImpl @Inject constructor( service.getTopArtistsOfUser(username) } + override suspend fun getUserFeedback(username: String?, score: Int?): Resource = parseResponse { + if(username.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() + service.getUserFeedback(username, score) + } + } \ 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 faa6688e..ac3a163b 100644 --- a/app/src/main/java/org/listenbrainz/android/service/UserService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/UserService.kt @@ -3,10 +3,12 @@ package org.listenbrainz.android.service import org.listenbrainz.android.model.Listens import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.user.TopArtists +import org.listenbrainz.android.model.user.UserFeedback import org.listenbrainz.android.model.user.UserSimilarityPayload import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path +import retrofit2.http.Query interface UserService { @GET("user/{user_name}/listen-count") @@ -20,4 +22,7 @@ interface UserService { @GET("stats/user/{user_name}/artists?count=100") suspend fun getTopArtistsOfUser(@Path("user_name") username: String?) : Response + + @GET("feedback/user/{user_name}/get-feedback?metadata=true") + suspend fun getUserFeedback(@Path("user_name") username: String?, @Query("score") score: Int?) : Response } \ No newline at end of file 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 a2fe212a..44cb9b44 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 @@ -5,6 +5,7 @@ import org.listenbrainz.android.model.Listen import org.listenbrainz.android.model.ListenBitmap import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.SimilarUser +import org.listenbrainz.android.model.user.UserFeedback import org.listenbrainz.android.ui.screens.profile.listens.ListeningNowUiState data class ProfileUiState( @@ -32,6 +33,8 @@ data class ListensTabUiState ( data class TasteTabUIState ( val isLoading: Boolean = true, + val lovedSongs: UserFeedback? = null, + val hatedSongs: UserFeedback? = null, ) data class StatsTabUIState( diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt index 2fde3dba..ff56c0c1 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt @@ -545,7 +545,7 @@ private fun Dialogs( } @Composable -private fun LoadMoreButton( +fun LoadMoreButton( state: Boolean, onClick : () -> Unit, ){ diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt index df28bc68..f2c6066d 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt @@ -1,15 +1,40 @@ package org.listenbrainz.android.ui.screens.profile.taste import LovedHated +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement 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 import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.HeartBroken import androidx.compose.material3.ElevatedSuggestionChip +import androidx.compose.material3.Icon +import androidx.compose.material3.SuggestionChipDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +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.lb_purple_night +import org.listenbrainz.android.util.Utils.getCoverArtUrl import org.listenbrainz.android.viewmodel.ProfileViewModel @Composable @@ -18,11 +43,118 @@ fun TasteScreen( uiState: ProfileUiState, ) { val lovedHatedState: MutableState = remember { mutableStateOf(LovedHated.loved) } + val lovedHatedCollapsibleState: MutableState = remember { + mutableStateOf(true) + } + val dropdownItemIndex: MutableState = rememberSaveable { + mutableStateOf(null) + } + LazyColumn { item { - Row { - ElevatedSuggestionChip(onClick = { lovedHatedState.value = LovedHated.loved }, label = { Text("Loved") }) - ElevatedSuggestionChip(onClick = { lovedHatedState.value = LovedHated.hated }, label = { Text("Hated") }) + Row (modifier = Modifier.padding(start = 16.dp)) { + ElevatedSuggestionChip( + onClick = { lovedHatedState.value = LovedHated.loved }, + label = { + Row (verticalAlignment = Alignment.CenterVertically) { + Text("Loved", color = when(lovedHatedState.value == LovedHated.loved){ + true -> Color.Black + false -> lb_purple_night + }) + Spacer(modifier = Modifier.width(5.dp)) + Icon( + painter = painterResource(id = R.drawable.heart), + contentDescription = "", + modifier = Modifier.height(15.dp), + tint = when(lovedHatedState.value == LovedHated.loved){ + true -> Color.Black + false -> lb_purple_night + } + ) + } + }, + shape = RoundedCornerShape(10.dp), + border = when(lovedHatedState.value == LovedHated.loved){ + true -> null + false -> BorderStroke(1.dp, lb_purple_night) + }, + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + if (lovedHatedState.value == LovedHated.loved) { + ListenBrainzTheme.colorScheme.followerChipSelected + } else { + ListenBrainzTheme.colorScheme.followerChipUnselected + } + ), + ) + Spacer(modifier = Modifier.width(10.dp)) + ElevatedSuggestionChip( + onClick = { lovedHatedState.value = LovedHated.hated }, + label = { Row(verticalAlignment = Alignment.CenterVertically) { + Text("Hated", color = when(lovedHatedState.value == LovedHated.hated){ + true -> Color.Black + false -> lb_purple_night + }) + Spacer(modifier = Modifier.width(5.dp)) + Icon(Icons.Default.HeartBroken, contentDescription = "", modifier = Modifier.height(15.dp), tint = when(lovedHatedState.value == LovedHated.hated){ + true -> Color.Black + false -> lb_purple_night + }) + } }, + shape = RoundedCornerShape(10.dp), + border = when(lovedHatedState.value == LovedHated.hated){ + true -> null + false -> BorderStroke(1.dp, lb_purple_night) + }, + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + if (lovedHatedState.value == LovedHated.hated) { + ListenBrainzTheme.colorScheme.followerChipSelected + } else { + ListenBrainzTheme.colorScheme.followerChipUnselected + } + ), + ) + } + } + itemsIndexed(items = when(lovedHatedState.value){ + LovedHated.loved -> when(lovedHatedCollapsibleState.value){ + true -> uiState.tasteTabUIState.lovedSongs?.feedback?.take(5) ?: listOf() + false -> uiState.tasteTabUIState.lovedSongs?.feedback ?: listOf() + } + LovedHated.hated -> when(lovedHatedCollapsibleState.value){ + true -> uiState.tasteTabUIState.hatedSongs?.feedback?.take(5) ?: listOf() + false -> uiState.tasteTabUIState.hatedSongs?.feedback ?: listOf() + } + }){ + index, feedback -> + ListenCardSmall( + modifier = Modifier + .padding( + horizontal = 16.dp, + vertical = ListenBrainzTheme.paddings.lazyListAdjacent + ), + trackName = feedback.trackMetadata?.trackName ?: "", artistName = feedback.trackMetadata + ?.artistName ?: "", coverArtUrl = getCoverArtUrl( + caaReleaseMbid = feedback.trackMetadata?.mbidMapping?.caaReleaseMbid, + caaId = feedback.trackMetadata?.mbidMapping?.caaId + ), + enableDropdownIcon = true, + onDropdownIconClick = { + dropdownItemIndex.value = index + }, + ) { + } + } + item{ + if((uiState.tasteTabUIState.lovedSongs?.count + ?: 0) > 5 || (uiState.tasteTabUIState.hatedSongs?.count ?: 0) > 5 + ){ + Spacer(modifier = Modifier.height(20.dp)) + Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + LoadMoreButton(state = lovedHatedCollapsibleState.value) { + lovedHatedCollapsibleState.value = !lovedHatedCollapsibleState.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 0db694d7..99d7807a 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt @@ -3,6 +3,8 @@ package org.listenbrainz.android.viewmodel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -85,10 +87,13 @@ class ProfileViewModel @Inject constructor( suspend fun getUserDataFromRemote( inputUsername: String? - ){ - getUserListensData(inputUsername) - getUserStatsData(inputUsername) - getUserTasteData(inputUsername) + ) = coroutineScope{ + val listensTabData = async{ getUserListensData(inputUsername) } + val statsTabData = async {getUserStatsData(inputUsername)} + val tasteTabData = async {getUserTasteData(inputUsername)} + listensTabData.await() + statsTabData.await() + tasteTabData.await() } @@ -145,12 +150,19 @@ class ProfileViewModel @Inject constructor( listenStateFlow.emit(listensTabState) } - suspend fun getUserStatsData(inputUsername: String?) { + private suspend fun getUserStatsData(inputUsername: String?) { } - suspend fun getUserTasteData(inputUsername: String?) { - + private suspend fun getUserTasteData(inputUsername: String?) { + val lovedSongs = userRepository.getUserFeedback(inputUsername, 1).data + val hatedSongs = userRepository.getUserFeedback(inputUsername, -1).data + val tastesTabState = TasteTabUIState( + isLoading = false, + lovedSongs = lovedSongs, + hatedSongs = hatedSongs, + ) + tasteStateFlow.emit(tastesTabState) } diff --git a/app/src/main/res/drawable/heart.xml b/app/src/main/res/drawable/heart.xml new file mode 100644 index 00000000..a4a0346f --- /dev/null +++ b/app/src/main/res/drawable/heart.xml @@ -0,0 +1,9 @@ + + + From 3c6d4beed0cfae8fafc5c89d2a988ba2ed8c601f Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:28:08 +0530 Subject: [PATCH 50/97] Added feed features such as pin,review in taste listen cards --- .../ui/screens/profile/BaseProfileScreen.kt | 6 +- .../screens/profile/listens/ListensScreen.kt | 4 +- .../ui/screens/profile/stats/StatsScreen.kt | 8 +- .../ui/screens/profile/taste/TasteScreen.kt | 313 +++++++++++++----- 4 files changed, 248 insertions(+), 83 deletions(-) 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 064a81f8..2cc83352 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,12 +207,10 @@ fun BaseProfileScreen( username = username ) ProfileScreenTab.STATS -> StatsScreen( - viewModel = viewModel, - uiState = uiState, + ) ProfileScreenTab.TASTE -> TasteScreen( - viewModel = viewModel, - uiState = uiState, + snackbarState = snackbarState, ) else -> ListensScreen( scrollRequestState = false, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt index ff56c0c1..1c1278f0 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt @@ -168,7 +168,7 @@ fun ListensScreen( ) } -private enum class ListenDialogBundleKeys { +enum class ListenDialogBundleKeys { PAGE, EVENT_INDEX; companion object { @@ -479,7 +479,7 @@ private fun BuildSimilarArtists(similarArtists: List) { } @Composable -private fun Dialogs( +fun Dialogs( deactivateDialog: () -> Unit, currentDialog: Dialog, feedUiState: FeedUiState, 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 4b0f11a6..b9e8677c 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,14 +2,16 @@ package org.listenbrainz.android.ui.screens.profile.stats import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.graphics.Color -import org.listenbrainz.android.ui.screens.profile.ProfileUiState +import androidx.hilt.navigation.compose.hiltViewModel import org.listenbrainz.android.viewmodel.ProfileViewModel @Composable fun StatsScreen( - viewModel: ProfileViewModel, - uiState: ProfileUiState, + viewModel: ProfileViewModel = hiltViewModel(), ) { + val uiState by viewModel.uiState.collectAsState() Text(text = uiState.listensTabUiState.listenCount.toString(), color = Color.White) } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt index f2c6066d..5eafe587 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt @@ -16,104 +16,142 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.HeartBroken import androidx.compose.material3.ElevatedSuggestionChip import androidx.compose.material3.Icon +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SuggestionChipDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import kotlinx.coroutines.launch import org.listenbrainz.android.R +import org.listenbrainz.android.model.Metadata +import org.listenbrainz.android.model.SocialUiState +import org.listenbrainz.android.model.TrackMetadata +import org.listenbrainz.android.model.feed.ReviewEntityType +import org.listenbrainz.android.ui.components.ErrorBar import org.listenbrainz.android.ui.components.ListenCardSmall +import org.listenbrainz.android.ui.components.SuccessBar +import org.listenbrainz.android.ui.components.dialogs.Dialog +import org.listenbrainz.android.ui.components.dialogs.rememberDialogsState +import org.listenbrainz.android.ui.screens.feed.FeedUiState +import org.listenbrainz.android.ui.screens.feed.SocialDropdown import org.listenbrainz.android.ui.screens.profile.ProfileUiState +import org.listenbrainz.android.ui.screens.profile.listens.Dialogs +import org.listenbrainz.android.ui.screens.profile.listens.ListenDialogBundleKeys import org.listenbrainz.android.ui.screens.profile.listens.LoadMoreButton import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.ui.theme.lb_purple_night import org.listenbrainz.android.util.Utils.getCoverArtUrl +import org.listenbrainz.android.viewmodel.FeedViewModel import org.listenbrainz.android.viewmodel.ProfileViewModel +import org.listenbrainz.android.viewmodel.SocialViewModel @Composable fun TasteScreen( - viewModel: ProfileViewModel, - uiState: ProfileUiState, + viewModel: ProfileViewModel = hiltViewModel(), + socialViewModel: SocialViewModel = hiltViewModel(), + feedViewModel : FeedViewModel = hiltViewModel(), + snackbarState : SnackbarHostState, ) { + val uiState by viewModel.uiState.collectAsState() + val socialUiState by socialViewModel.uiState.collectAsState() + val feedUiState by feedViewModel.uiState.collectAsState() + + val dropdownItemIndex: MutableState = rememberSaveable { + mutableStateOf(null) + } + TasteScreen( + uiState = uiState, + socialUiState = socialUiState, + feedUiState = feedUiState, + snackbarState = snackbarState, + dropdownItemIndex = dropdownItemIndex, + playListen = { + socialViewModel.playListen(it) + }, + onPin = { + metadata, blurbContent -> socialViewModel.pin(metadata , blurbContent) + dropdownItemIndex.value = null + }, + onRecommend = {metadata -> + socialViewModel.recommend(metadata) + dropdownItemIndex.value = null + }, + searchUsers = { + query -> feedViewModel.searchUser(query) + }, + isCritiqueBrainzLinked = { + feedViewModel.isCritiqueBrainzLinked() + }, + onReview = { + type, blurbContent, rating, locale, metadata -> socialViewModel.review(metadata , type , blurbContent , rating , locale) + }, + onPersonallyRecommend = { + metadata, users, blurbContent -> socialViewModel.personallyRecommend(metadata, users, blurbContent) + }, + onErrorShown = { + socialViewModel.clearErrorFlow() + }, + onMessageShown = { + socialViewModel.clearMsgFlow() + } + ) +} + +@Composable +fun TasteScreen( + uiState: ProfileUiState, + socialUiState: SocialUiState, + feedUiState: FeedUiState, + snackbarState: SnackbarHostState, + uriHandler: UriHandler = LocalUriHandler.current, + dropdownItemIndex : MutableState, + playListen: (TrackMetadata) -> Unit, + onPin : (metadata : Metadata, blurbContent : String) -> Unit, + onRecommend : (metadata : Metadata) -> Unit, + searchUsers: (String) -> Unit, + isCritiqueBrainzLinked: suspend () -> Boolean?, + onReview: (type: ReviewEntityType, blurbContent: String, rating: Int?, locale: String, metadata: Metadata) -> Unit, + onPersonallyRecommend: (metadata: Metadata, users: List, blurbContent: String) -> Unit, + onErrorShown : () -> Unit, + onMessageShown : () -> Unit, +){ val lovedHatedState: MutableState = remember { mutableStateOf(LovedHated.loved) } + val lovedHatedCollapsibleState: MutableState = remember { mutableStateOf(true) } - val dropdownItemIndex: MutableState = rememberSaveable { - mutableStateOf(null) - } + + val dialogsState = rememberDialogsState() + + val scope = rememberCoroutineScope() + + val context = LocalContext.current LazyColumn { item { - Row (modifier = Modifier.padding(start = 16.dp)) { - ElevatedSuggestionChip( - onClick = { lovedHatedState.value = LovedHated.loved }, - label = { - Row (verticalAlignment = Alignment.CenterVertically) { - Text("Loved", color = when(lovedHatedState.value == LovedHated.loved){ - true -> Color.Black - false -> lb_purple_night - }) - Spacer(modifier = Modifier.width(5.dp)) - Icon( - painter = painterResource(id = R.drawable.heart), - contentDescription = "", - modifier = Modifier.height(15.dp), - tint = when(lovedHatedState.value == LovedHated.loved){ - true -> Color.Black - false -> lb_purple_night - } - ) - } - }, - shape = RoundedCornerShape(10.dp), - border = when(lovedHatedState.value == LovedHated.loved){ - true -> null - false -> BorderStroke(1.dp, lb_purple_night) - }, - colors = SuggestionChipDefaults.elevatedSuggestionChipColors( - if (lovedHatedState.value == LovedHated.loved) { - ListenBrainzTheme.colorScheme.followerChipSelected - } else { - ListenBrainzTheme.colorScheme.followerChipUnselected - } - ), - ) - Spacer(modifier = Modifier.width(10.dp)) - ElevatedSuggestionChip( - onClick = { lovedHatedState.value = LovedHated.hated }, - label = { Row(verticalAlignment = Alignment.CenterVertically) { - Text("Hated", color = when(lovedHatedState.value == LovedHated.hated){ - true -> Color.Black - false -> lb_purple_night - }) - Spacer(modifier = Modifier.width(5.dp)) - Icon(Icons.Default.HeartBroken, contentDescription = "", modifier = Modifier.height(15.dp), tint = when(lovedHatedState.value == LovedHated.hated){ - true -> Color.Black - false -> lb_purple_night - }) - } }, - shape = RoundedCornerShape(10.dp), - border = when(lovedHatedState.value == LovedHated.hated){ - true -> null - false -> BorderStroke(1.dp, lb_purple_night) - }, - colors = SuggestionChipDefaults.elevatedSuggestionChipColors( - if (lovedHatedState.value == LovedHated.hated) { - ListenBrainzTheme.colorScheme.followerChipSelected - } else { - ListenBrainzTheme.colorScheme.followerChipUnselected - } - ), - ) - } + LovedHatedBar( + lovedHatedState = lovedHatedState.value, + onLovedClick = { + lovedHatedState.value = LovedHated.loved + }, + onHatedClick = { + lovedHatedState.value = LovedHated.hated + } + ) } itemsIndexed(items = when(lovedHatedState.value){ LovedHated.loved -> when(lovedHatedCollapsibleState.value){ @@ -125,7 +163,8 @@ fun TasteScreen( false -> uiState.tasteTabUIState.hatedSongs?.feedback ?: listOf() } }){ - index, feedback -> + index, feedback -> + val metadata = Metadata(trackMetadata = feedback.trackMetadata) ListenCardSmall( modifier = Modifier .padding( @@ -133,15 +172,52 @@ fun TasteScreen( vertical = ListenBrainzTheme.paddings.lazyListAdjacent ), trackName = feedback.trackMetadata?.trackName ?: "", artistName = feedback.trackMetadata - ?.artistName ?: "", coverArtUrl = getCoverArtUrl( - caaReleaseMbid = feedback.trackMetadata?.mbidMapping?.caaReleaseMbid, - caaId = feedback.trackMetadata?.mbidMapping?.caaId - ), + ?.artistName ?: "", coverArtUrl = getCoverArtUrl( + caaReleaseMbid = feedback.trackMetadata?.mbidMapping?.caaReleaseMbid, + caaId = feedback.trackMetadata?.mbidMapping?.caaId + ), enableDropdownIcon = true, onDropdownIconClick = { dropdownItemIndex.value = index }, - ) { + dropDown = { + SocialDropdown( + isExpanded = dropdownItemIndex.value == index, + onDismiss = { + dropdownItemIndex.value = null + }, + metadata = metadata, + onRecommend = { onRecommend(metadata) }, + onPersonallyRecommend = { + dialogsState.activateDialog(Dialog.PERSONAL_RECOMMENDATION , ListenDialogBundleKeys.listenDialogBundle(0, index)) + dropdownItemIndex.value = null + }, + onReview = { + dialogsState.activateDialog(Dialog.REVIEW , ListenDialogBundleKeys.listenDialogBundle(0, index)) + dropdownItemIndex.value = null + }, + onPin = { + dialogsState.activateDialog(Dialog.PIN , ListenDialogBundleKeys.listenDialogBundle(0, index)) + dropdownItemIndex.value = null + }, + onOpenInMusicBrainz = { + try { + uriHandler.openUri("https://musicbrainz.org/recording/${metadata.trackMetadata?.mbidMapping?.recordingMbid}") + } + catch(e : Error) { + scope.launch { + snackbarState.showSnackbar(context.getString(R.string.err_generic_toast)) + } + } + dropdownItemIndex.value = null + } + + ) + } + ) { + if(feedback.trackMetadata != null){ + playListen(feedback.trackMetadata) + } } } item{ @@ -158,4 +234,93 @@ fun TasteScreen( } } } + + ErrorBar(error = socialUiState.error, onErrorShown = onErrorShown ) + SuccessBar(resId = socialUiState.successMsgId, onMessageShown = onMessageShown, snackbarState = snackbarState) + + Dialogs( + deactivateDialog = { + dialogsState.deactivateDialog() + }, + currentDialog = dialogsState.currentDialog, + currentIndex = dialogsState.metadata?.getInt(ListenDialogBundleKeys.EVENT_INDEX.name), + listens = uiState.listensTabUiState.recentListens ?: listOf(), + onPin = {metadata, blurbContent -> onPin(metadata, blurbContent)}, + searchUsers = { query -> searchUsers(query) }, + feedUiState = feedUiState, + isCritiqueBrainzLinked = isCritiqueBrainzLinked, + onReview = {type, blurbContent, rating, locale, metadata -> onReview(type, blurbContent, rating, locale, metadata) }, + onPersonallyRecommend = {metadata, users, blurbContent -> onPersonallyRecommend(metadata, users, blurbContent)}, + snackbarState = snackbarState, + socialUiState = socialUiState) +} + +@Composable +private fun LovedHatedBar( + lovedHatedState: LovedHated, + onLovedClick: () -> Unit, + onHatedClick: () -> Unit +){ + Row (modifier = Modifier.padding(start = 16.dp)) { + ElevatedSuggestionChip( + onClick = onLovedClick, + label = { + Row (verticalAlignment = Alignment.CenterVertically) { + Text("Loved", color = when(lovedHatedState == LovedHated.loved){ + true -> ListenBrainzTheme.colorScheme.followerChipUnselected + false -> ListenBrainzTheme.colorScheme.followerChipSelected + }) + Spacer(modifier = Modifier.width(5.dp)) + Icon( + painter = painterResource(id = R.drawable.heart), + contentDescription = "", + modifier = Modifier.height(15.dp), + tint = when(lovedHatedState == LovedHated.loved){ + true -> ListenBrainzTheme.colorScheme.followerChipUnselected + false -> ListenBrainzTheme.colorScheme.followerChipSelected + } + ) + } + }, + shape = RoundedCornerShape(10.dp), + border = when(lovedHatedState == LovedHated.loved){ + true -> null + false -> BorderStroke(1.dp, lb_purple_night) + }, + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + if (lovedHatedState == LovedHated.loved) { + ListenBrainzTheme.colorScheme.followerChipSelected + } else { + ListenBrainzTheme.colorScheme.followerChipUnselected + } + ), + ) + Spacer(modifier = Modifier.width(10.dp)) + ElevatedSuggestionChip( + onClick = onHatedClick, + label = { Row(verticalAlignment = Alignment.CenterVertically) { + Text("Hated", color = when(lovedHatedState == LovedHated.hated){ + true -> ListenBrainzTheme.colorScheme.followerChipUnselected + false -> ListenBrainzTheme.colorScheme.followerChipSelected + }) + Spacer(modifier = Modifier.width(5.dp)) + Icon(Icons.Default.HeartBroken, contentDescription = "", modifier = Modifier.height(15.dp), tint = when(lovedHatedState == LovedHated.hated){ + true -> ListenBrainzTheme.colorScheme.followerChipUnselected + false -> ListenBrainzTheme.colorScheme.followerChipSelected + }) + } }, + shape = RoundedCornerShape(10.dp), + border = when(lovedHatedState == LovedHated.hated){ + true -> null + false -> BorderStroke(1.dp, lb_purple_night) + }, + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + if (lovedHatedState == LovedHated.hated) { + ListenBrainzTheme.colorScheme.followerChipSelected + } else { + ListenBrainzTheme.colorScheme.followerChipUnselected + } + ), + ) + } } \ No newline at end of file From c2fa269716b1a877fd16719138c9ae6997b8dcf2 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:05:31 +0530 Subject: [PATCH 51/97] Added all user pins in taste tab --- .../android/model/user/AllPinnedRecordings.kt | 12 ++ .../android/repository/user/UserRepository.kt | 2 + .../repository/user/UserRepositoryImpl.kt | 6 + .../android/service/UserService.kt | 4 + .../ui/screens/profile/ProfileUiState.kt | 2 + .../ui/screens/profile/taste/TasteScreen.kt | 116 ++++++++++++++++++ .../android/viewmodel/ProfileViewModel.kt | 2 + 7 files changed, 144 insertions(+) create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/AllPinnedRecordings.kt diff --git a/app/src/main/java/org/listenbrainz/android/model/user/AllPinnedRecordings.kt b/app/src/main/java/org/listenbrainz/android/model/user/AllPinnedRecordings.kt new file mode 100644 index 00000000..ae6e1973 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/AllPinnedRecordings.kt @@ -0,0 +1,12 @@ +package org.listenbrainz.android.model.user + +import com.google.gson.annotations.SerializedName +import org.listenbrainz.android.model.PinnedRecording + +data class AllPinnedRecordings( + @SerializedName("pinned_recordings") val pinnedRecordings: List? = listOf(), + @SerializedName("total_count") val totalCount: Int? = 0, + @SerializedName("user_name") val userName: String? = "", + val count: Int? = 0, + val offset: Int? = 0, +) \ 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 c92cefc1..70ab20ad 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 @@ -2,6 +2,7 @@ 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.TopArtists import org.listenbrainz.android.model.user.UserFeedback import org.listenbrainz.android.model.user.UserSimilarityPayload @@ -11,6 +12,7 @@ interface UserRepository { suspend fun fetchUserListenCount (username: String?) : Resource suspend fun fetchUserSimilarity(username: String? , otherUserName: String?) : Resource 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 getUserFeedback(username: String?, score: Int?): Resource 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 93f949e8..27d01bd4 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 @@ -3,6 +3,7 @@ package org.listenbrainz.android.repository.user 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.TopArtists import org.listenbrainz.android.model.user.UserFeedback import org.listenbrainz.android.model.user.UserSimilarityPayload @@ -30,6 +31,11 @@ class UserRepositoryImpl @Inject constructor( service.getUserCurrentPins(username) } + override suspend fun fetchUserPins(username: String?): Resource = parseResponse{ + if(username.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() + service.getUserPins(username) + } + override suspend fun getTopArtists( username: String? ): Resource = parseResponse { 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 ac3a163b..17364a08 100644 --- a/app/src/main/java/org/listenbrainz/android/service/UserService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/UserService.kt @@ -2,6 +2,7 @@ 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.TopArtists import org.listenbrainz.android.model.user.UserFeedback import org.listenbrainz.android.model.user.UserSimilarityPayload @@ -20,6 +21,9 @@ interface UserService { @GET("{user_name}/pins/current") suspend fun getUserCurrentPins(@Path("user_name") username: String?) : Response + @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 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 44cb9b44..aa18aafd 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 @@ -5,6 +5,7 @@ import org.listenbrainz.android.model.Listen 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.UserFeedback import org.listenbrainz.android.ui.screens.profile.listens.ListeningNowUiState @@ -35,6 +36,7 @@ data class TasteTabUIState ( val isLoading: Boolean = true, val lovedSongs: UserFeedback? = null, val hatedSongs: UserFeedback? = null, + val pins: AllPinnedRecordings? = null, ) data class StatsTabUIState( diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt index 5eafe587..45617c08 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt @@ -3,6 +3,8 @@ package org.listenbrainz.android.ui.screens.profile.taste import LovedHated import androidx.compose.foundation.BorderStroke 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.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -16,6 +18,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.HeartBroken import androidx.compose.material3.ElevatedSuggestionChip import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SuggestionChipDefaults import androidx.compose.material3.Text @@ -34,10 +37,12 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.UriHandler 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 kotlinx.coroutines.launch import org.listenbrainz.android.R import org.listenbrainz.android.model.Metadata +import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.SocialUiState import org.listenbrainz.android.model.TrackMetadata import org.listenbrainz.android.model.feed.ReviewEntityType @@ -135,6 +140,10 @@ fun TasteScreen( mutableStateOf(true) } + val pinsCollapsibleState: MutableState = remember { + mutableStateOf(true) + } + val dialogsState = rememberDialogsState() val scope = rememberCoroutineScope() @@ -230,7 +239,114 @@ fun TasteScreen( lovedHatedCollapsibleState.value = !lovedHatedCollapsibleState.value } } + Spacer(modifier = Modifier.height(20.dp)) + } + } + item { + val pinnedRecordings = when(pinsCollapsibleState.value){ + true -> uiState.tasteTabUIState.pins?.pinnedRecordings?.take(5) ?: listOf() + false -> uiState.tasteTabUIState.pins?.pinnedRecordings ?: listOf() + } + Box( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp) + ) { + Column { + Text( + text = "Pins", + color = ListenBrainzTheme.colorScheme.textColor, + style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp) + ) + Spacer(modifier = Modifier.height(20.dp)) + pinnedRecordings.mapIndexed { index, recording: PinnedRecording -> + val metadata = Metadata(trackMetadata = recording.trackMetadata) + ListenCardSmall( + enableBlurbContent = true, + blurbContent = { + Text( + ('"' + (recording.blurbContent + ?: "No content specified") + '"'), + color = ListenBrainzTheme.colorScheme.textColor, + modifier = Modifier.padding(8.dp) + ) + }, + modifier = Modifier + .padding( + + vertical = ListenBrainzTheme.paddings.lazyListAdjacent + ), + trackName = recording.trackMetadata?.trackName ?: "", + artistName = recording.trackMetadata + ?.artistName ?: "", + coverArtUrl = getCoverArtUrl( + caaReleaseMbid = recording.trackMetadata?.mbidMapping?.caaReleaseMbid, + caaId = recording.trackMetadata?.mbidMapping?.caaId + ), + enableDropdownIcon = true, + onDropdownIconClick = { + dropdownItemIndex.value = index + }, + dropDown = { + SocialDropdown( + isExpanded = dropdownItemIndex.value == index, + onDismiss = { + dropdownItemIndex.value = null + }, + metadata = metadata, + onRecommend = { onRecommend(metadata) }, + onPersonallyRecommend = { + dialogsState.activateDialog( + Dialog.PERSONAL_RECOMMENDATION, + ListenDialogBundleKeys.listenDialogBundle(0, index) + ) + dropdownItemIndex.value = null + }, + onReview = { + dialogsState.activateDialog( + Dialog.REVIEW, + ListenDialogBundleKeys.listenDialogBundle(0, index) + ) + dropdownItemIndex.value = null + }, + onPin = { + dialogsState.activateDialog( + Dialog.PIN, + ListenDialogBundleKeys.listenDialogBundle(0, index) + ) + dropdownItemIndex.value = null + }, + onOpenInMusicBrainz = { + try { + uriHandler.openUri("https://musicbrainz.org/recording/${metadata.trackMetadata?.mbidMapping?.recordingMbid}") + } catch (e: Error) { + scope.launch { + snackbarState.showSnackbar(context.getString(R.string.err_generic_toast)) + } + } + dropdownItemIndex.value = null + } + + ) + } + ) { + if (recording.trackMetadata != null) { + playListen(recording.trackMetadata) + } + } + } + } + } + } + item { + if((uiState.tasteTabUIState.pins?.count ?: 0) > 5){ + Spacer(modifier = Modifier.height(20.dp)) + Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + LoadMoreButton(state = pinsCollapsibleState.value) { + pinsCollapsibleState.value = !pinsCollapsibleState.value + } + } + Spacer(modifier = Modifier.height(20.dp)) } } } 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 99d7807a..56d5d8f7 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt @@ -157,10 +157,12 @@ class ProfileViewModel @Inject constructor( private suspend fun getUserTasteData(inputUsername: String?) { val lovedSongs = userRepository.getUserFeedback(inputUsername, 1).data val hatedSongs = userRepository.getUserFeedback(inputUsername, -1).data + val userPins = userRepository.fetchUserPins(inputUsername).data val tastesTabState = TasteTabUIState( isLoading = false, lovedSongs = lovedSongs, hatedSongs = hatedSongs, + pins = userPins, ) tasteStateFlow.emit(tastesTabState) } From 5cf6160aed883bf085ab46302471a95e8f18f175 Mon Sep 17 00:00:00 2001 From: Akshat Tiwari Date: Sun, 7 Jul 2024 15:26:27 +0530 Subject: [PATCH 52/97] Update gradle.properties --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ed39821c..c3d3da7e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,4 +4,4 @@ android.enableJetifier=false android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false -org.gradle.configuration-cache=true +org.gradle.configuration-cache=true \ No newline at end of file From 91a7f13477c7d30d198e90aae7b647cf6343ae40 Mon Sep 17 00:00:00 2001 From: Akshat Tiwari Date: Mon, 8 Jul 2024 09:29:54 +0530 Subject: [PATCH 53/97] Migrate to KTS - Dependencies updated - Code migrations --- app/build.gradle | 263 ------------------ app/build.gradle.kts | 236 ++++++++++++++++ app/proguard-rules.pro | 20 -- .../android/di/Yim23RepositoryModule.kt | 2 +- .../repository/yim23/Yim23RepositoryImpl.kt | 5 - .../ui/screens/yim23/YearInMusic23Activity.kt | 3 - build.gradle | 61 ---- build.gradle.kts | 12 + gradle.properties | 1 - gradle/libs.versions.toml | 148 ++++++++++ settings.gradle | 21 +- sharedTest/build.gradle | 57 ---- sharedTest/build.gradle.kts | 69 +++++ 13 files changed, 485 insertions(+), 413 deletions(-) delete mode 100644 app/build.gradle create mode 100644 app/build.gradle.kts delete mode 100644 build.gradle create mode 100644 build.gradle.kts create mode 100644 gradle/libs.versions.toml delete mode 100644 sharedTest/build.gradle create mode 100644 sharedTest/build.gradle.kts diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 28e48125..00000000 --- a/app/build.gradle +++ /dev/null @@ -1,263 +0,0 @@ -plugins { - id 'com.android.application' - id 'kotlin-android' - id 'kotlin-kapt' - id 'com.google.devtools.ksp' - id 'dagger.hilt.android.plugin' - id "io.sentry.android.gradle" version "4.7.0" - id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" -} - -def keystorePropertiesFile = rootProject.file("keystore.properties") -def localPropertiesFile = rootProject.file('local.properties') - -android { - namespace 'org.listenbrainz.android' - compileSdk 34 - - signingConfigs { - release { - if (keystorePropertiesFile.exists()) { - def keystoreProperties = new Properties() - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) - storeFile file(keystoreProperties['storeFile']) - storePassword keystoreProperties['storePassword'] - keyAlias keystoreProperties['keyAlias'] - keyPassword keystoreProperties['keyPassword'] - } - } - } - - defaultConfig { - applicationId 'org.listenbrainz.android' - minSdk 21 - targetSdk 34 - versionCode 51 - versionName "2.6.1" - multiDexEnabled true - testInstrumentationRunner "org.listenbrainz.android.di.CustomTestRunner" // "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables { - useSupportLibrary true - } - } - - buildFeatures { - compose true - viewBinding true - } - - buildTypes { - debug { - if (localPropertiesFile.exists()) { - def localProperties = new Properties() - localProperties.load(new FileInputStream(localPropertiesFile)) - - if(localProperties.getProperty('youtubeApiKey') != null && !localProperties.getProperty('youtubeApiKey').isEmpty()){ - resValue("string", "youtubeApiKey", localProperties['youtubeApiKey']) - } - else{ - resValue("string", "youtubeApiKey", "test") - } - - if (localProperties.getProperty('spotifyClientId') != null && !localProperties.getProperty('spotifyClientId').isEmpty()){ - resValue("string", "spotifyClientId", localProperties['spotifyClientId']) - } - else{ - resValue("string", "spotifyClientId", "test") - } - } - else{ - resValue("string", "youtubeApiKey", "test") - resValue("string", "spotifyClientId", "test") - } - resValue("string", "sentryDsn", "") - - applicationIdSuffix ".debug" - versionNameSuffix ".debug" - } - release { - if (keystorePropertiesFile.exists()) { - def keystoreProperties = new Properties() - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) - resValue("string", "youtubeApiKey", keystoreProperties['youtubeApiKey']) - resValue("string", "spotifyClientId", keystoreProperties['spotifyClientId']) - resValue("string", "sentryDsn", keystoreProperties['sentryDsn']) - - signingConfig signingConfigs.release - } - else{ - resValue("string", "youtubeApiKey", "") - resValue("string", "spotifyClientId", "") - resValue("string", "sentryDsn", "") - } - minifyEnabled false - //shrinkResources true - //proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - composeOptions { - kotlinCompilerExtensionVersion '1.5.14' - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = '17' - } - lint { - abortOnError false - } - kapt { - correctErrorTypes true - } - packagingOptions { - resources { - excludes += '/META-INF/{AL2.0,LGPL2.1}' - } - } - dependenciesInfo { - // Disables dependency metadata when building APKs. - // This is for the signed .apk that we post to GitHub, so the dependency metadata isn't relevant. - includeInApk = false - // Disables dependency metadata when building Android App Bundles. - // This is for the Google Play Store, so we'll want the metadata. - includeInBundle = true - } -} - -dependencies { - //AndroidX - implementation 'androidx.appcompat:appcompat:1.7.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.2' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.2' - implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2' - implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.8.2' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.2' - implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation 'androidx.browser:browser:1.8.0' - implementation 'androidx.preference:preference-ktx:1.2.1' - implementation 'androidx.core:core-splashscreen:1.0.1' - implementation 'androidx.datastore:datastore-preferences:1.1.1' - implementation "androidx.work:work-runtime-ktx:$work_version" - - //Web Service Setup - implementation 'com.google.code.gson:gson:2.11.0' - implementation 'com.squareup.retrofit2:retrofit:2.11.0' - implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.14' - implementation 'com.squareup.retrofit2:converter-gson:2.11.0' - implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14' - implementation 'androidx.paging:paging-runtime-ktx:3.3.0' - implementation 'androidx.paging:paging-compose:3.3.0' - - - //Image downloading and Caching library - implementation 'com.github.bumptech.glide:glide:4.16.0' - implementation 'com.github.bumptech.glide:compose:1.0.0-beta01' - implementation 'io.coil-kt:coil-compose:2.6.0' - implementation 'com.caverock:androidsvg-aar:1.4' - ksp 'com.github.bumptech.glide:compiler:4.16.0' - - //Permissions - implementation "com.google.accompanist:accompanist-permissions:$accompanist_version" - - //Design Setup - implementation 'com.google.android.material:material:1.12.0' - implementation 'com.airbnb.android:lottie:6.4.1' - implementation 'com.github.akshaaatt:Onboarding:1.1.3' - implementation 'com.github.akshaaatt:Share-Android:1.0.0' - implementation 'androidx.hilt:hilt-navigation-compose:1.2.0' - implementation 'com.airbnb.android:lottie-compose:6.4.1' - - //Dagger-Hilt - implementation("com.google.dagger:hilt-android:$hilt_version") - kapt("com.google.dagger:hilt-android-compiler:$hilt_version") - kapt('androidx.hilt:hilt-compiler:1.2.0') - implementation 'androidx.hilt:hilt-work:1.2.0' - implementation "androidx.startup:startup-runtime:1.1.1" - androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version" - kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version" - - //Jetpack Compose - implementation platform('androidx.compose:compose-bom:2024.06.00') - implementation 'androidx.compose.ui:ui-graphics' - implementation 'androidx.compose.ui:ui' - implementation 'androidx.compose.ui:ui-tooling' - implementation 'androidx.compose.ui:ui-util' - implementation 'androidx.compose.material:material' - implementation 'androidx.compose.material:material-icons-extended' - implementation 'androidx.compose.material3:material3' - implementation 'androidx.compose.material3:material3-window-size-class' - implementation 'androidx.compose.animation:animation' - implementation 'androidx.compose.ui:ui-tooling-preview' - implementation 'androidx.compose.foundation:foundation' - implementation 'androidx.activity:activity-compose' - - // Compose Navigation - implementation "androidx.navigation:navigation-compose:$navigationVersion" - - //Spotify - implementation files('./lib/spotify-app-remote-release-0.7.2.aar') - - // HTML Parser for retrieving token - implementation 'org.jsoup:jsoup:1.17.2' - - //Socket IO - implementation ('io.socket:socket.io-client:2.1.0') { - exclude group: 'org.json', module: 'json' - } - - //Test Setup - implementation 'androidx.test.ext:junit-ktx:1.2.0' - implementation 'app.cash.turbine:turbine:1.1.0' - testImplementation 'junit:junit:4.13.2' - testImplementation 'com.squareup.okhttp3:mockwebserver:5.0.0-alpha.14' - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1' - testImplementation 'androidx.arch.core:core-testing:2.2.0' - - // Mockito framework - testImplementation 'org.mockito:mockito-core:5.12.0' - testImplementation 'org.mockito.kotlin:mockito-kotlin:5.3.1' - - debugImplementation 'androidx.test:monitor:1.7.0' - // Solves "class PlatformTestStorageRegistery not found" error for ui tests. - debugImplementation 'androidx.compose.ui:ui-test-manifest:1.6.8' - - androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" - androidTestImplementation "androidx.work:work-testing:$work_version" - androidTestImplementation 'androidx.test:runner:1.6.0' - androidTestImplementation 'androidx.test.ext:junit:1.2.0' - androidTestImplementation 'androidx.arch.core:core-testing:2.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.0' - androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.0' - //androidTestImplementation 'tools.fastlane:screengrab:2.1.1' // Fastlane ScreenGrab - - testImplementation project(path: ':sharedTest') - androidTestImplementation project(path: ':sharedTest') - - //Exoplayer - api "com.google.android.exoplayer:exoplayer-core:$exoplayer_version" - api "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version" - api "com.google.android.exoplayer:extension-mediasession:$exoplayer_version" - - // Room db - implementation "androidx.room:room-runtime:$room_version" - ksp "androidx.room:room-compiler:$room_version" - implementation "androidx.room:room-ktx:$room_version" - testImplementation "androidx.room:room-testing:$room_version" - - // Paging - implementation "androidx.paging:paging-runtime-ktx:$paging_version" - implementation "androidx.paging:paging-compose:$paging_version" - testImplementation "androidx.paging:paging-common-ktx:$paging_version" - - //Jetpack Compose accompanists (https://github.com/google/accompanist) - implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version" - - // Third party libraries - implementation 'com.github.a914-gowtham:compose-ratingbar:1.3.4' - implementation 'com.github.akshaaatt:Logger-Android:1.0.0' - - //Charting Library (Vico) - implementation('com.patrykandpatrick.vico:compose:1.15.0') -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 00000000..8b1283d2 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,236 @@ +import java.util.Properties +import java.io.FileInputStream + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt) + alias(libs.plugins.compose.compiler) + id("io.sentry.android.gradle") version "4.7.0" +} + +val keystorePropertiesFile = rootProject.file("keystore.properties") +val localPropertiesFile = rootProject.file("local.properties") + +android { + namespace = "org.listenbrainz.android" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + applicationId = "org.listenbrainz.android" + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + versionCode = 51 + versionName = "2.6.1" + multiDexEnabled = true + testInstrumentationRunner = "org.listenbrainz.android.di.CustomTestRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + signingConfigs { + create("release") { + if (keystorePropertiesFile.exists()) { + val keystoreProperties = Properties() + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) + storeFile = file(keystoreProperties["storeFile"] as String) + storePassword = keystoreProperties["storePassword"] as String + keyAlias = keystoreProperties["keyAlias"] as String + keyPassword = keystoreProperties["keyPassword"] as String + } + } + } + + buildTypes { + debug { + if (localPropertiesFile.exists()) { + val localProperties = Properties() + localProperties.load(FileInputStream(localPropertiesFile)) + + if (localProperties.getProperty("youtubeApiKey") != null && localProperties.getProperty("youtubeApiKey").isNotEmpty()) { + resValue("string", "youtubeApiKey", localProperties.getProperty("youtubeApiKey")) + } else { + resValue("string", "youtubeApiKey", "test") + } + + if (localProperties.getProperty("spotifyClientId") != null && localProperties.getProperty("spotifyClientId").isNotEmpty()) { + resValue("string", "spotifyClientId", localProperties.getProperty("spotifyClientId")) + } else { + resValue("string", "spotifyClientId", "test") + } + } else { + resValue("string", "youtubeApiKey", "test") + resValue("string", "spotifyClientId", "test") + } + resValue("string", "sentryDsn", "") + + applicationIdSuffix = ".debug" + versionNameSuffix = ".debug" + } + + release { + if (keystorePropertiesFile.exists()) { + val keystoreProperties = Properties() + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) + resValue("string", "youtubeApiKey", keystoreProperties.getProperty("youtubeApiKey")) + resValue("string", "spotifyClientId", keystoreProperties.getProperty("spotifyClientId")) + resValue("string", "sentryDsn", keystoreProperties.getProperty("sentryDsn")) + + signingConfig = signingConfigs.getByName("release") + } else { + resValue("string", "youtubeApiKey", "") + resValue("string", "spotifyClientId", "") + resValue("string", "sentryDsn", "") + } + isMinifyEnabled = false + // isShrinkResources = true + // proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + + buildFeatures { + compose = true + viewBinding = true + buildConfig = true + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.compose.get() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + + lint { + abortOnError = false + } + + dependenciesInfo { + includeInApk = false + includeInBundle = true + } +} + +dependencies { + implementation(libs.androidx.appcompat) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.androidx.lifecycle.extensions) + implementation(libs.androidx.browser) + implementation(libs.androidx.preference.ktx) + implementation(libs.androidx.core.splashscreen) + implementation(libs.androidx.datastore.preferences) + implementation(libs.androidx.work.runtime.ktx) + + implementation(libs.gson) + implementation(libs.retrofit) + implementation(libs.okhttp) + implementation(libs.retrofit.converter.gson) + implementation(libs.okhttp.logging.interceptor) + implementation(libs.androidx.paging.runtime) + implementation(libs.androidx.paging.compose) + + implementation(libs.glide) + implementation(libs.glide.compose) + implementation(libs.coil.compose) + implementation(libs.androidsvg) + ksp(libs.glide.compiler) + + implementation(libs.google.accompanist.permissions) + + implementation(libs.material) + implementation(libs.lottie) + implementation(libs.onboarding) + implementation(libs.share.android) + implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.lottie.compose) + + // Dependency Injection with Hilt + implementation(libs.hilt.android) + ksp(libs.hilt.android.compiler) + implementation(libs.androidx.hilt.work) + implementation(libs.androidx.startup.runtime) + androidTestImplementation(libs.hilt.android) + + val composeBom = platform(libs.androidx.compose.bom) + implementation(composeBom) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.tooling) + implementation(libs.androidx.compose.ui.util) + implementation(libs.androidx.compose.material) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material3.window.size) + implementation(libs.androidx.compose.animation) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.activity.compose) + + implementation(libs.androidx.navigation.compose) + + //Spotify + implementation(files("./lib/spotify-app-remote-release-0.7.2.aar")) + + implementation(libs.jsoup) + + implementation(libs.socket.io) { + exclude(group = "org.json", module = "json") + } + + implementation(libs.androidx.test.ext.junit.ktx) + implementation(libs.turbine) + testImplementation(libs.junit) + testImplementation(libs.mockwebserver) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.androidx.arch.core.testing) + + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.kotlin) + + debugImplementation(libs.androidx.test.monitor) + debugImplementation(libs.androidx.compose.ui.test.manifest) + + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + androidTestImplementation(libs.androidx.work.testing) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.arch.core.testing) + androidTestImplementation(libs.androidx.test.espresso.core) + androidTestImplementation(libs.androidx.test.espresso.intents) + + testImplementation(project(":sharedTest")) + androidTestImplementation(project(":sharedTest")) + + implementation(libs.google.exoplayer.core) + implementation(libs.google.exoplayer.ui) + implementation(libs.google.exoplayer.mediasession) + + implementation(libs.androidx.room.runtime) + ksp(libs.androidx.room.compiler) + implementation(libs.androidx.room.ktx) + + implementation(libs.google.accompanist.systemuicontroller) + + implementation(libs.compose.ratingbar) + implementation(libs.logger.android) + + implementation(libs.vico.compose) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f1b42451..8b137891 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,21 +1 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/app/src/main/java/org/listenbrainz/android/di/Yim23RepositoryModule.kt b/app/src/main/java/org/listenbrainz/android/di/Yim23RepositoryModule.kt index c86ad98a..7846a146 100644 --- a/app/src/main/java/org/listenbrainz/android/di/Yim23RepositoryModule.kt +++ b/app/src/main/java/org/listenbrainz/android/di/Yim23RepositoryModule.kt @@ -12,5 +12,5 @@ import org.listenbrainz.android.repository.yim23.Yim23RepositoryImpl @InstallIn(ActivityRetainedComponent::class) abstract class Yim23RepositoryModule { @Binds - abstract fun bindsYim23Repository(repository: Yim23RepositoryImpl?) : Yim23Repository + abstract fun bindsYim23Repository(repository: Yim23RepositoryImpl?) : Yim23Repository? } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/repository/yim23/Yim23RepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/yim23/Yim23RepositoryImpl.kt index 1c015d19..681e3828 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/yim23/Yim23RepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/yim23/Yim23RepositoryImpl.kt @@ -1,15 +1,10 @@ package org.listenbrainz.android.repository.yim23 -import androidx.annotation.WorkerThread import org.listenbrainz.android.model.ResponseError import org.listenbrainz.android.model.yimdata.Yim23Payload -import org.listenbrainz.android.model.yimdata.YimPayload import org.listenbrainz.android.service.Yim23Service -import org.listenbrainz.android.service.YimService -import org.listenbrainz.android.util.Log import org.listenbrainz.android.util.Resource import org.listenbrainz.android.util.Utils -import retrofit2.http.GET import javax.inject.Inject // TODO: TO BE REMOVED WHEN YIM GOES LIVE diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/yim23/YearInMusic23Activity.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/yim23/YearInMusic23Activity.kt index f641f483..11dbfeb9 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/yim23/YearInMusic23Activity.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/yim23/YearInMusic23Activity.kt @@ -1,7 +1,6 @@ package org.listenbrainz.android.ui.screens.yim23 import android.os.Bundle -import android.util.Log import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -15,9 +14,7 @@ import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_IN import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_OUT import org.listenbrainz.android.util.connectivityobserver.NetworkConnectivityViewModel import org.listenbrainz.android.util.connectivityobserver.NetworkConnectivityViewModelImpl -import org.listenbrainz.android.viewmodel.SocialViewModel import org.listenbrainz.android.viewmodel.Yim23ViewModel -import org.listenbrainz.android.viewmodel.YimViewModel @AndroidEntryPoint class YearInMusic23Activity : ComponentActivity() { diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 5f312a0e..00000000 --- a/build.gradle +++ /dev/null @@ -1,61 +0,0 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -buildscript { - ext { - kotlin_version = '2.0.0' - navigationVersion = '2.7.7' - hilt_version = '2.51.1' - compose_version = '1.6.8' - room_version = '2.6.1' - accompanist_version = '0.34.0' - work_version = '2.9.0' - exoplayer_version = '2.19.1' - paging_version = '3.3.0' - } - repositories { - google() - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:8.5.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" - } -} - -plugins { - id 'com.google.devtools.ksp' version '2.0.0-1.0.21' apply false -} - -allprojects { - repositories { - google() - mavenCentral() - maven {url "https://jitpack.io"} - } -} - -tasks.register('clean', Delete) { - delete rootProject.buildDir -} - -subprojects { - tasks.withType(KotlinCompile).configureEach { - kotlinOptions { - if (project.findProperty("composeCompilerReports") == "true") { - freeCompilerArgs += [ - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + - project.buildDir.absolutePath + "/compose_compiler" - ] - } - if (project.findProperty("composeCompilerMetrics") == "true") { - freeCompilerArgs += [ - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + - project.buildDir.absolutePath + "/compose_compiler" - ] - } - } - } -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..d6be785a --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.hilt) apply false + alias(libs.plugins.compose.compiler) apply false +} + +tasks.register("clean") { + delete(layout.buildDirectory) +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index c3d3da7e..35d10e57 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,6 @@ org.gradle.jvmargs=-Xmx4096m android.useAndroidX=true android.enableJetifier=false -android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false org.gradle.configuration-cache=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..59ff59a7 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,148 @@ +[versions] +kotlin = "2.0.0" +navigation = "2.7.7" +hilt = "2.51.1" +compose = "1.6.8" +room = "2.6.1" +accompanist = "0.34.0" +work = "2.9.0" +exoplayer = "2.19.1" +paging = "3.3.0" +androidGradlePlugin = "8.5.0" +ksp = "2.0.0-1.0.22" +composeBom = "2024.06.00" +appcompat = "1.7.0" +lifecycle = "2.8.3" +browser = "1.8.0" +preference = "1.2.1" +coreSplashscreen = "1.0.1" +datastorePreferences = "1.1.1" +gson = "2.11.0" +retrofit = "2.11.0" +okhttp = "5.0.0-alpha.14" +glide = "4.16.0" +glideCompose = "1.0.0-beta01" +coil = "2.6.0" +androidsvg = "1.4" +material = "1.12.0" +lottie = "6.4.1" +onboarding = "1.1.3" +shareAndroid = "1.0.0" +hiltNavigationCompose = "1.2.0" +startup = "1.1.1" +jsoup = "1.17.2" +socketIo = "2.1.0" +junit = "4.13.2" +archCoreTesting = "2.2.0" +mockito = "5.12.0" +mockitoKotlin = "5.3.1" +testMonitor = "1.7.1" +testRunner = "1.6.1" +testExtJunit = "1.2.1" +espresso = "3.6.1" +turbine = "1.1.0" +vicoCompose = "1.15.0" +composeRatingbar = "1.3.4" +loggerAndroid = "1.0.0" +compileSdk = "34" +targetSdk = "34" +minSdk = "21" + +[libraries] +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } +androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } +androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" } +androidx-lifecycle-extensions = { module = "androidx.lifecycle:lifecycle-extensions", version = "2.2.0" } +androidx-browser = { module = "androidx.browser:browser", version.ref = "browser" } +androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preference" } +androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } +androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "work" } +androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } +androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hiltNavigationCompose" } +androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "startup" } + +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" } +androidx-compose-material = { group = "androidx.compose.material", name = "material" } +androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-material3-window-size = { group = "androidx.compose.material3", name = "material3-window-size-class" } +androidx-compose-animation = { group = "androidx.compose.animation", name = "animation" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose" } + +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" } + +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } + +androidx-paging-runtime = { module = "androidx.paging:paging-runtime-ktx", version.ref = "paging" } +androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging" } + +google-accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } +google-accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } + +hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } +hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } + +google-exoplayer-core = { module = "com.google.android.exoplayer:exoplayer-core", version.ref = "exoplayer" } +google-exoplayer-ui = { module = "com.google.android.exoplayer:exoplayer-ui", version.ref = "exoplayer" } +google-exoplayer-mediasession = { module = "com.google.android.exoplayer:extension-mediasession", version.ref = "exoplayer" } + +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } + +glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } +glide-compose = { module = "com.github.bumptech.glide:compose", version.ref = "glideCompose" } +glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" } +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } +androidsvg = { module = "com.caverock:androidsvg-aar", version.ref = "androidsvg" } + +material = { module = "com.google.android.material:material", version.ref = "material" } +lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" } +lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottie" } +onboarding = { module = "com.github.akshaaatt:Onboarding", version.ref = "onboarding" } +share-android = { module = "com.github.akshaaatt:Share-Android", version.ref = "shareAndroid" } + +jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } +socket-io = { module = "io.socket:socket.io-client", version.ref = "socketIo" } +junit = { module = "junit:junit", version.ref = "junit" } +androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "testExtJunit" } +androidx-test-ext-junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "testExtJunit" } +turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } +mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version = "1.8.1" } +androidx-arch-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "archCoreTesting" } +mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } +mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" } +androidx-test-monitor = { module = "androidx.test:monitor", version.ref = "testMonitor" } +androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" } +androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" } +androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "work" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "testRunner" } +androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } +androidx-test-espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espresso" } + +vico-compose = { module = "com.patrykandpatrick.vico:compose", version.ref = "vicoCompose" } +compose-ratingbar = { module = "com.github.a914-gowtham:compose-ratingbar", version.ref = "composeRatingbar" } +logger-android = { module = "com.github.akshaaatt:Logger-Android", version.ref = "loggerAndroid" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 20251183..479c0fef 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,19 @@ -include ':app' -include ':sharedTest' +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { url = uri("https://jitpack.io") } + } +} + +include(":app") +include(":sharedTest") diff --git a/sharedTest/build.gradle b/sharedTest/build.gradle deleted file mode 100644 index 979ee11e..00000000 --- a/sharedTest/build.gradle +++ /dev/null @@ -1,57 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' -} - -android { - namespace 'org.listenbrainz.sharedtest' - compileSdk 34 - - defaultConfig { - minSdk 21 - targetSdk 34 - } - - buildTypes { - release { - minifyEnabled false - } - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = '17' - } -} - -dependencies { - implementation 'androidx.appcompat:appcompat:1.7.0' - implementation 'com.google.android.material:material:1.12.0' - - //Web Service Setup - implementation 'com.google.code.gson:gson:2.11.0' - implementation 'com.squareup.retrofit2:retrofit:2.11.0' - implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.14' - implementation 'com.squareup.retrofit2:converter-gson:2.11.0' - - //Test Setup - implementation 'junit:junit:4.13.2' - implementation 'com.squareup.okhttp3:mockwebserver:5.0.0-alpha.14' - implementation 'androidx.arch.core:core-testing:2.2.0' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1' - implementation 'androidx.room:room-testing:2.6.1' - debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" - - implementation 'androidx.test:runner:1.6.0' - implementation 'androidx.test.ext:junit:1.2.0' - implementation 'androidx.arch.core:core-testing:2.2.0' - implementation 'androidx.test.espresso:espresso-core:3.6.0' - implementation 'androidx.test.espresso:espresso-intents:3.6.0' - implementation "androidx.compose.ui:ui-test-junit4:$compose_version" - - implementation project(path: ':app') -} \ No newline at end of file diff --git a/sharedTest/build.gradle.kts b/sharedTest/build.gradle.kts new file mode 100644 index 00000000..26da68db --- /dev/null +++ b/sharedTest/build.gradle.kts @@ -0,0 +1,69 @@ +plugins { + id("com.android.library") + id("kotlin-android") +} + +android { + namespace = "org.listenbrainz.sharedtest" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } + + lint { + targetSdk = libs.versions.compileSdk.get().toInt() + } +} + +dependencies { + implementation(libs.androidx.appcompat) + implementation(libs.material) + + // Web Service Setup + implementation(libs.gson) + implementation(libs.retrofit) + implementation(libs.okhttp) + implementation(libs.retrofit.converter.gson) + + // Test Setup + implementation(libs.junit) + implementation(libs.mockwebserver) + implementation(libs.androidx.arch.core.testing) + implementation(libs.kotlinx.coroutines.test) + + implementation(libs.androidx.test.runner) + implementation(libs.androidx.test.ext.junit) + implementation(libs.androidx.test.espresso.core) + implementation(libs.androidx.test.espresso.intents) + + implementation(project(":app")) +} \ No newline at end of file From 70123df8a64fcfae2384bf9234d83431d1bd7dcd Mon Sep 17 00:00:00 2001 From: Akshat Tiwari Date: Mon, 8 Jul 2024 13:13:22 +0530 Subject: [PATCH 54/97] Cleanup and fix version issues --- app/build.gradle.kts | 92 ++++++++++++++++++++----------------- build.gradle.kts | 1 + gradle/libs.versions.toml | 7 ++- sharedTest/build.gradle.kts | 18 +++----- 4 files changed, 62 insertions(+), 56 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8b1283d2..6ba5adf2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,7 +8,7 @@ plugins { alias(libs.plugins.ksp) alias(libs.plugins.hilt) alias(libs.plugins.compose.compiler) - id("io.sentry.android.gradle") version "4.7.0" + alias(libs.plugins.sentry) } val keystorePropertiesFile = rootProject.file("keystore.properties") @@ -126,6 +126,7 @@ android { } dependencies { + // AndroidX libraries implementation(libs.androidx.appcompat) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.viewmodel.ktx) @@ -138,37 +139,26 @@ dependencies { implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.work.runtime.ktx) + implementation(libs.androidx.paging.runtime) + implementation(libs.androidx.paging.compose) + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + // Networking implementation(libs.gson) implementation(libs.retrofit) implementation(libs.okhttp) implementation(libs.retrofit.converter.gson) implementation(libs.okhttp.logging.interceptor) - implementation(libs.androidx.paging.runtime) - implementation(libs.androidx.paging.compose) + // Image loading and processing implementation(libs.glide) implementation(libs.glide.compose) implementation(libs.coil.compose) implementation(libs.androidsvg) ksp(libs.glide.compiler) - implementation(libs.google.accompanist.permissions) - - implementation(libs.material) - implementation(libs.lottie) - implementation(libs.onboarding) - implementation(libs.share.android) - implementation(libs.androidx.hilt.navigation.compose) - implementation(libs.lottie.compose) - - // Dependency Injection with Hilt - implementation(libs.hilt.android) - ksp(libs.hilt.android.compiler) - implementation(libs.androidx.hilt.work) - implementation(libs.androidx.startup.runtime) - androidTestImplementation(libs.hilt.android) - + // Compose val composeBom = platform(libs.androidx.compose.bom) implementation(composeBom) implementation(libs.androidx.compose.ui.graphics) @@ -183,30 +173,55 @@ dependencies { implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.foundation) implementation(libs.androidx.activity.compose) - implementation(libs.androidx.navigation.compose) - //Spotify + // Dependency Injection + implementation(libs.hilt.android) + ksp(libs.hilt.android.compiler) + implementation(libs.androidx.hilt.work) + implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.androidx.startup.runtime) + + // UI Components + implementation(libs.material) + implementation(libs.lottie) + implementation(libs.lottie.compose) + implementation(libs.onboarding) + implementation(libs.share.android) + implementation(libs.compose.ratingbar) + + // Accompanist + implementation(libs.google.accompanist.permissions) + implementation(libs.google.accompanist.systemuicontroller) + + // Media playback + implementation(libs.google.exoplayer.core) + implementation(libs.google.exoplayer.ui) + implementation(libs.google.exoplayer.mediasession) + + // Spotify SDK implementation(files("./lib/spotify-app-remote-release-0.7.2.aar")) + // Networking and parsing implementation(libs.jsoup) - implementation(libs.socket.io) { exclude(group = "org.json", module = "json") } - implementation(libs.androidx.test.ext.junit.ktx) - implementation(libs.turbine) + // Logging + implementation(libs.logger.android) + + // Charts + implementation(libs.vico.compose) + + // Testing testImplementation(libs.junit) testImplementation(libs.mockwebserver) testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.androidx.arch.core.testing) - testImplementation(libs.mockito.core) testImplementation(libs.mockito.kotlin) - - debugImplementation(libs.androidx.test.monitor) - debugImplementation(libs.androidx.compose.ui.test.manifest) + testImplementation(project(":sharedTest")) androidTestImplementation(libs.androidx.compose.ui.test.junit4) androidTestImplementation(libs.androidx.work.testing) @@ -215,22 +230,15 @@ dependencies { androidTestImplementation(libs.androidx.arch.core.testing) androidTestImplementation(libs.androidx.test.espresso.core) androidTestImplementation(libs.androidx.test.espresso.intents) - - testImplementation(project(":sharedTest")) + androidTestImplementation(libs.hilt.android) + androidTestImplementation(libs.hilt.android.testing) androidTestImplementation(project(":sharedTest")) - implementation(libs.google.exoplayer.core) - implementation(libs.google.exoplayer.ui) - implementation(libs.google.exoplayer.mediasession) - - implementation(libs.androidx.room.runtime) - ksp(libs.androidx.room.compiler) - implementation(libs.androidx.room.ktx) - - implementation(libs.google.accompanist.systemuicontroller) + kspAndroidTest(libs.hilt.android.compiler) - implementation(libs.compose.ratingbar) - implementation(libs.logger.android) + debugImplementation(libs.androidx.test.monitor) + debugImplementation(libs.androidx.compose.ui.test.manifest) - implementation(libs.vico.compose) + implementation(libs.androidx.test.ext.junit.ktx) + implementation(libs.turbine) } \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index d6be785a..7e1eccd5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.ksp) apply false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 59ff59a7..ea9ac2af 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ work = "2.9.0" exoplayer = "2.19.1" paging = "3.3.0" androidGradlePlugin = "8.5.0" +sentry = "4.7.0" ksp = "2.0.0-1.0.22" composeBom = "2024.06.00" appcompat = "1.7.0" @@ -83,7 +84,6 @@ androidx-navigation-compose = { module = "androidx.navigation:navigation-compose androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } -androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } androidx-paging-runtime = { module = "androidx.paging:paging-runtime-ktx", version.ref = "paging" } androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging" } @@ -93,6 +93,7 @@ google-accompanist-systemuicontroller = { module = "com.google.accompanist:accom hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } +hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } google-exoplayer-core = { module = "com.google.android.exoplayer:exoplayer-core", version.ref = "exoplayer" } google-exoplayer-ui = { module = "com.google.android.exoplayer:exoplayer-ui", version.ref = "exoplayer" } @@ -145,4 +146,6 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } \ No newline at end of file +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } +sentry = { id = "io.sentry.android.gradle", version.ref = "sentry" } \ No newline at end of file diff --git a/sharedTest/build.gradle.kts b/sharedTest/build.gradle.kts index 26da68db..988a96db 100644 --- a/sharedTest/build.gradle.kts +++ b/sharedTest/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("com.android.library") - id("kotlin-android") + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) } android { @@ -9,7 +9,6 @@ android { defaultConfig { minSdk = libs.versions.minSdk.get().toInt() - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") } @@ -33,37 +32,32 @@ android { jvmTarget = "17" } - testOptions { - unitTests { - isIncludeAndroidResources = true - } - } - lint { targetSdk = libs.versions.compileSdk.get().toInt() } } dependencies { + // AndroidX and UI implementation(libs.androidx.appcompat) implementation(libs.material) - // Web Service Setup + // Networking implementation(libs.gson) implementation(libs.retrofit) implementation(libs.okhttp) implementation(libs.retrofit.converter.gson) - // Test Setup + // Testing implementation(libs.junit) implementation(libs.mockwebserver) implementation(libs.androidx.arch.core.testing) implementation(libs.kotlinx.coroutines.test) - implementation(libs.androidx.test.runner) implementation(libs.androidx.test.ext.junit) implementation(libs.androidx.test.espresso.core) implementation(libs.androidx.test.espresso.intents) + // App module dependency implementation(project(":app")) } \ No newline at end of file From c887f8c05ecfb093185cc7e946b202e7851417f3 Mon Sep 17 00:00:00 2001 From: Akshat Tiwari Date: Mon, 8 Jul 2024 13:46:10 +0530 Subject: [PATCH 55/97] Update build.gradle.kts --- app/build.gradle.kts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6ba5adf2..94eeb030 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -97,12 +97,6 @@ android { buildConfig = true } - buildTypes { - release { - isMinifyEnabled = false - } - } - composeOptions { kotlinCompilerExtensionVersion = libs.versions.compose.get() } @@ -111,6 +105,7 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } + kotlinOptions { jvmTarget = "17" } From b2389a5832f1faccbb106fbe6e7031b8f4d5e27c Mon Sep 17 00:00:00 2001 From: Akshat Tiwari Date: Mon, 8 Jul 2024 14:25:36 +0530 Subject: [PATCH 56/97] Add missing dependency --- app/build.gradle.kts | 3 +++ gradle/libs.versions.toml | 1 + 2 files changed, 4 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 94eeb030..e76e6c65 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -136,7 +136,10 @@ dependencies { implementation(libs.androidx.work.runtime.ktx) implementation(libs.androidx.paging.runtime) implementation(libs.androidx.paging.compose) + + //Room DB implementation(libs.androidx.room.runtime) + ksp(libs.androidx.room.compiler) implementation(libs.androidx.room.ktx) // Networking diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ea9ac2af..e275fdaf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -83,6 +83,7 @@ androidx-activity-compose = { group = "androidx.activity", name = "activity-comp androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } androidx-paging-runtime = { module = "androidx.paging:paging-runtime-ktx", version.ref = "paging" } From 92c583445b05e1137cdd4f72b79cecd6e3f511dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 09:08:19 +0000 Subject: [PATCH 57/97] Bump io.sentry.android.gradle from 4.7.0 to 4.9.0 Bumps [io.sentry.android.gradle](https://github.com/getsentry/sentry-android-gradle-plugin) from 4.7.0 to 4.9.0. - [Release notes](https://github.com/getsentry/sentry-android-gradle-plugin/releases) - [Changelog](https://github.com/getsentry/sentry-android-gradle-plugin/blob/main/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-android-gradle-plugin/compare/4.7.0...4.9.0) --- updated-dependencies: - dependency-name: io.sentry.android.gradle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e275fdaf..50f62279 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ work = "2.9.0" exoplayer = "2.19.1" paging = "3.3.0" androidGradlePlugin = "8.5.0" -sentry = "4.7.0" +sentry = "4.9.0" ksp = "2.0.0-1.0.22" composeBom = "2024.06.00" appcompat = "1.7.0" From ae0d940eca6f7f130af01672d4bb4ea1ccf3a34d Mon Sep 17 00:00:00 2001 From: Akshat Tiwari Date: Wed, 10 Jul 2024 00:03:59 +0530 Subject: [PATCH 58/97] Cleanup --- app/src/main/AndroidManifest.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a3203f71..a540fc26 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,8 +26,7 @@ android:icon="@mipmap/ic_launcher_round" android:label="@string/app_name" android:supportsRtl="true" - android:theme="@style/Theme.App.Starting" - tools:targetApi="32"> + android:theme="@style/Theme.App.Starting"> Date: Wed, 10 Jul 2024 00:26:06 +0000 Subject: [PATCH 59/97] Bump com.github.a914-gowtham:compose-ratingbar from 1.3.4 to 1.3.12 Bumps com.github.a914-gowtham:compose-ratingbar from 1.3.4 to 1.3.12. --- updated-dependencies: - dependency-name: com.github.a914-gowtham:compose-ratingbar dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e275fdaf..df0b0e13 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ testExtJunit = "1.2.1" espresso = "3.6.1" turbine = "1.1.0" vicoCompose = "1.15.0" -composeRatingbar = "1.3.4" +composeRatingbar = "1.3.12" loggerAndroid = "1.0.0" compileSdk = "34" targetSdk = "34" From 0e1b80b22f150537d07a2ee54eb7feb3b2287c09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 00:26:21 +0000 Subject: [PATCH 60/97] Bump com.github.bumptech.glide:compose from 1.0.0-beta01 to 4.14.0 Bumps [com.github.bumptech.glide:compose](https://github.com/bumptech/glide) from 1.0.0-beta01 to 4.14.0. - [Release notes](https://github.com/bumptech/glide/releases) - [Commits](https://github.com/bumptech/glide/commits/v4.14.0) --- updated-dependencies: - dependency-name: com.github.bumptech.glide:compose dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e275fdaf..4d72225f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ gson = "2.11.0" retrofit = "2.11.0" okhttp = "5.0.0-alpha.14" glide = "4.16.0" -glideCompose = "1.0.0-beta01" +glideCompose = "4.14.0" coil = "2.6.0" androidsvg = "1.4" material = "1.12.0" 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 61/97] 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 62/97] 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 28d3766e5d9b5564204a8596d555603a964f4592 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jul 2024 00:26:13 +0000 Subject: [PATCH 63/97] Bump org.jsoup:jsoup from 1.17.2 to 1.18.1 Bumps [org.jsoup:jsoup](https://github.com/jhy/jsoup) from 1.17.2 to 1.18.1. - [Release notes](https://github.com/jhy/jsoup/releases) - [Changelog](https://github.com/jhy/jsoup/blob/master/CHANGES.md) - [Commits](https://github.com/jhy/jsoup/compare/jsoup-1.17.2...jsoup-1.18.1) --- updated-dependencies: - dependency-name: org.jsoup:jsoup dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 50f62279..623591fb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ onboarding = "1.1.3" shareAndroid = "1.0.0" hiltNavigationCompose = "1.2.0" startup = "1.1.1" -jsoup = "1.17.2" +jsoup = "1.18.1" socketIo = "2.1.0" junit = "4.13.2" archCoreTesting = "2.2.0" From 65a3ef2a30156d5ef6cfed1488acf4303b12a94e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jul 2024 00:26:19 +0000 Subject: [PATCH 64/97] Bump io.sentry.android.gradle from 4.9.0 to 4.10.0 Bumps [io.sentry.android.gradle](https://github.com/getsentry/sentry-android-gradle-plugin) from 4.9.0 to 4.10.0. - [Release notes](https://github.com/getsentry/sentry-android-gradle-plugin/releases) - [Changelog](https://github.com/getsentry/sentry-android-gradle-plugin/blob/main/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-android-gradle-plugin/compare/4.9.0...4.10.0) --- updated-dependencies: - dependency-name: io.sentry.android.gradle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 50f62279..07a711b9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ work = "2.9.0" exoplayer = "2.19.1" paging = "3.3.0" androidGradlePlugin = "8.5.0" -sentry = "4.9.0" +sentry = "4.10.0" ksp = "2.0.0-1.0.22" composeBom = "2024.06.00" appcompat = "1.7.0" From b1c753672e3a0e4a7b11dee8e1cf724c3781349a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jul 2024 06:58:22 +0000 Subject: [PATCH 65/97] Bump com.google.devtools.ksp from 2.0.0-1.0.22 to 2.0.0-1.0.23 Bumps [com.google.devtools.ksp](https://github.com/google/ksp) from 2.0.0-1.0.22 to 2.0.0-1.0.23. - [Release notes](https://github.com/google/ksp/releases) - [Commits](https://github.com/google/ksp/compare/2.0.0-1.0.22...2.0.0-1.0.23) --- updated-dependencies: - dependency-name: com.google.devtools.ksp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2ed8d65c..a6594eca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ exoplayer = "2.19.1" paging = "3.3.0" androidGradlePlugin = "8.5.0" sentry = "4.10.0" -ksp = "2.0.0-1.0.22" +ksp = "2.0.0-1.0.23" composeBom = "2024.06.00" appcompat = "1.7.0" lifecycle = "2.8.3" From 399f00d9860e960ed416b589ed06ff9149cce562 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jul 2024 06:58:27 +0000 Subject: [PATCH 66/97] Bump androidGradlePlugin from 8.5.0 to 8.5.1 Bumps `androidGradlePlugin` from 8.5.0 to 8.5.1. Updates `com.android.application` from 8.5.0 to 8.5.1 Updates `com.android.library` from 8.5.0 to 8.5.1 --- updated-dependencies: - dependency-name: com.android.application dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.android.library dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2ed8d65c..9450db05 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ accompanist = "0.34.0" work = "2.9.0" exoplayer = "2.19.1" paging = "3.3.0" -androidGradlePlugin = "8.5.0" +androidGradlePlugin = "8.5.1" sentry = "4.10.0" ksp = "2.0.0-1.0.22" composeBom = "2024.06.00" From 5acdfe3b047d49612378ee6a1f376ee4c5571971 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jul 2024 06:58:53 +0000 Subject: [PATCH 67/97] Bump io.socket:socket.io-client from 2.1.0 to 2.1.1 Bumps [io.socket:socket.io-client](https://github.com/socketio/socket.io-client-java) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/socketio/socket.io-client-java/releases) - [Changelog](https://github.com/socketio/socket.io-client-java/blob/main/History.md) - [Commits](https://github.com/socketio/socket.io-client-java/compare/socket.io-client-2.1.0...socket.io-client-2.1.1) --- updated-dependencies: - dependency-name: io.socket:socket.io-client dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2ed8d65c..d8259854 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,7 +32,7 @@ shareAndroid = "1.0.0" hiltNavigationCompose = "1.2.0" startup = "1.1.1" jsoup = "1.18.1" -socketIo = "2.1.0" +socketIo = "2.1.1" junit = "4.13.2" archCoreTesting = "2.2.0" mockito = "5.12.0" From 1088578da579648d814f895b0f079fee0d609c9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jul 2024 08:34:33 +0000 Subject: [PATCH 68/97] Bump org.mockito.kotlin:mockito-kotlin from 5.3.1 to 5.4.0 Bumps [org.mockito.kotlin:mockito-kotlin](https://github.com/mockito/mockito-kotlin) from 5.3.1 to 5.4.0. - [Release notes](https://github.com/mockito/mockito-kotlin/releases) - [Commits](https://github.com/mockito/mockito-kotlin/compare/5.3.1...5.4.0) --- updated-dependencies: - dependency-name: org.mockito.kotlin:mockito-kotlin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 31a61401..f0a8b78e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,7 +36,7 @@ socketIo = "2.1.1" junit = "4.13.2" archCoreTesting = "2.2.0" mockito = "5.12.0" -mockitoKotlin = "5.3.1" +mockitoKotlin = "5.4.0" testMonitor = "1.7.1" testRunner = "1.6.1" testExtJunit = "1.2.1" 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 69/97] 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 70/97] 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 71/97] 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?) { From 06e0e451a7441c048d5af5f3dbad230297e197cc Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sun, 11 Aug 2024 12:43:08 +0530 Subject: [PATCH 72/97] Added listen counts in listen card component --- .../android/ui/components/ListenCardSmall.kt | 22 +- .../ui/screens/profile/BaseProfileScreen.kt | 3 +- .../ui/screens/profile/stats/StatsScreen.kt | 233 ++++++++++++++---- 3 files changed, 204 insertions(+), 54 deletions(-) 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 e8e27015..328e473d 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 @@ -2,6 +2,7 @@ package org.listenbrainz.android.ui.components import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.annotation.DrawableRes +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -13,6 +14,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -22,6 +24,7 @@ import androidx.compose.runtime.Composable 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.FilterQuality import androidx.compose.ui.layout.ContentScale @@ -51,6 +54,7 @@ fun ListenCardSmall( trackName: String, artistName: String, coverArtUrl: String?, + listenCount: Int? = null, @DrawableRes errorAlbumArt: Int = R.drawable.ic_coverartarchive_logo_no_text, enableDropdownIcon: Boolean = false, onDropdownIconClick: () -> Unit = {}, @@ -100,16 +104,28 @@ fun ListenCardSmall( Spacer(modifier = Modifier.width(ListenBrainzTheme.paddings.coverArtAndTextGap)) TitleAndSubtitle(modifier = Modifier.padding(end = 6.dp), title = trackName, subtitle = artistName, titleColor = titleColor, subtitleColor = subtitleColor) - } - + Box( modifier = modifier .fillMaxWidth(1f - mainContentFraction) .align(Alignment.CenterEnd), contentAlignment = Alignment.Center ) { - + if(listenCount != null){ + Box( + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .background(ListenBrainzTheme.colorScheme.followerChipSelected) + .padding(6.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = listenCount.toString(), + color = Color.Black + ) + } + } // Trailing content if (enableTrailingContent) { trailingContent( 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 669f4f27..3de228aa 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,8 @@ fun BaseProfileScreen( username = username ) ProfileScreenTab.STATS -> StatsScreen( - username = username + username = username, + snackbarState = snackbarState, ) ProfileScreenTab.TASTE -> TasteScreen( snackbarState = snackbarState, 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 421a48c8..65e39447 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 @@ -20,6 +20,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ElevatedSuggestionChip import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SuggestionChipDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -30,10 +31,15 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable 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.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -51,23 +57,48 @@ 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.launch import kotlinx.coroutines.withContext import org.listenbrainz.android.R +import org.listenbrainz.android.model.AdditionalInfo +import org.listenbrainz.android.model.MbidMapping +import org.listenbrainz.android.model.Metadata +import org.listenbrainz.android.model.SocialUiState +import org.listenbrainz.android.model.TrackMetadata +import org.listenbrainz.android.model.feed.ReviewEntityType +import org.listenbrainz.android.ui.components.ErrorBar import org.listenbrainz.android.ui.components.ListenCardSmall +import org.listenbrainz.android.ui.components.SuccessBar +import org.listenbrainz.android.ui.components.dialogs.Dialog +import org.listenbrainz.android.ui.components.dialogs.rememberDialogsState +import org.listenbrainz.android.ui.screens.feed.FeedUiState +import org.listenbrainz.android.ui.screens.feed.SocialDropdown import org.listenbrainz.android.ui.screens.profile.ProfileUiState +import org.listenbrainz.android.ui.screens.profile.listens.Dialogs +import org.listenbrainz.android.ui.screens.profile.listens.ListenDialogBundleKeys 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.FeedViewModel import org.listenbrainz.android.viewmodel.ProfileViewModel +import org.listenbrainz.android.viewmodel.SocialViewModel @Composable fun StatsScreen( username: String?, viewModel: ProfileViewModel = hiltViewModel(), + socialViewModel: SocialViewModel = hiltViewModel(), + feedViewModel : FeedViewModel = hiltViewModel(), + snackbarState : SnackbarHostState, ) { val uiState by viewModel.uiState.collectAsState() + val socialUiState by socialViewModel.uiState.collectAsState() + val feedUiState by feedViewModel.uiState.collectAsState() + val dropdownItemIndex: MutableState = rememberSaveable { + mutableStateOf(null) + } val statsRangeState: MutableState = remember { mutableStateOf(StatsRange.THIS_WEEK) } @@ -94,53 +125,48 @@ fun StatsScreen( }, fetchTopSongs = { viewModel.getUserTopSongs(it) + }, + dropdownItemIndex = dropdownItemIndex, + feedUiState = feedUiState, + playListen = { + socialViewModel.playListen(it) + }, + snackbarState = snackbarState, + socialUiState = socialUiState, + onRecommend = {metadata -> + socialViewModel.recommend(metadata) + dropdownItemIndex.value = null + }, + onErrorShown = { + socialViewModel.clearErrorFlow() + }, + onMessageShown = { + socialViewModel.clearMsgFlow() + }, + onPin = { + metadata, blurbContent -> socialViewModel.pin(metadata , blurbContent) + dropdownItemIndex.value = null + }, + searchUsers = { + query -> feedViewModel.searchUser(query) + }, + isCritiqueBrainzLinked = { + feedViewModel.isCritiqueBrainzLinked() + }, + onReview = { + type, blurbContent, rating, locale, metadata -> socialViewModel.review(metadata , type , blurbContent , rating , locale) + }, + onPersonallyRecommend = { + metadata, users, blurbContent -> socialViewModel.personallyRecommend(metadata, users, blurbContent) } ) } - -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, + uriHandler: UriHandler = LocalUriHandler.current, statsRangeState: StatsRange, setStatsRange: (StatsRange) -> Unit, userGlobalState: UserGlobal, @@ -148,24 +174,37 @@ fun StatsScreen( fetchTopArtists: suspend (String?) -> Unit, fetchTopAlbums: suspend (String?) -> Unit, fetchTopSongs: suspend (String?) -> Unit, + dropdownItemIndex : MutableState, + feedUiState: FeedUiState, + playListen: (TrackMetadata) -> Unit, + snackbarState: SnackbarHostState, + socialUiState: SocialUiState, + onRecommend : (metadata : Metadata) -> Unit, + onErrorShown : () -> Unit, + onMessageShown : () -> Unit, + onPin : (metadata : Metadata , blurbContent : String) -> Unit, + searchUsers: (String) -> Unit, + isCritiqueBrainzLinked: suspend () -> Boolean?, + onReview: (type: ReviewEntityType, blurbContent: String, rating: Int?, locale: String, metadata: Metadata) -> Unit, + onPersonallyRecommend: (metadata: Metadata, users: List, blurbContent: String) -> Unit, ) { - val currentTabSelection: MutableState = remember { mutableStateOf(CategoryState.ARTISTS) } - val artistsCollapseState: MutableState = remember { mutableStateOf(true) } - val albumsCollapseState: MutableState = remember { mutableStateOf(true) } - val songsCollapseState: MutableState = remember { mutableStateOf(true) } + val dialogsState = rememberDialogsState() + val scope = rememberCoroutineScope() + val context = LocalContext.current + when(currentTabSelection.value){ CategoryState.ARTISTS -> { if(uiState.statsTabUIState.topArtists == null){ @@ -398,9 +437,21 @@ fun StatsScreen( } 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)) { + topAlbums.mapIndexed { + index, 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), + enableTrailingContent = true, + listenCount = topAlbum.listenCount, + enableDropdownIcon = true + ) + { } } @@ -419,10 +470,73 @@ fun StatsScreen( } 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),) { - + topSongs.mapIndexed { + index, topSong -> + val metadata = Metadata(trackMetadata = TrackMetadata( + artistName = topSong.artistName ?: "", + releaseName = topSong.releaseName, + trackName = topSong.trackName ?: "", + mbidMapping = MbidMapping( + artistMbids = topSong.artistMbids ?: listOf(), + recordingName = topSong.releaseName ?: "", + caaId = topSong.caaId, + caaReleaseMbid = topSong.caaReleaseMbid, + recordingMbid = topSong.recordingMbid + ), + additionalInfo = AdditionalInfo() + )) + 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), + enableDropdownIcon = true, + onDropdownIconClick = { + dropdownItemIndex.value = index + }, + dropDown = { + SocialDropdown( + isExpanded = dropdownItemIndex.value == index, + onDismiss = { + dropdownItemIndex.value = null + }, + metadata = metadata, + onRecommend = { onRecommend(metadata) }, + onPersonallyRecommend = { + dialogsState.activateDialog(Dialog.PERSONAL_RECOMMENDATION , ListenDialogBundleKeys.listenDialogBundle(0, index)) + dropdownItemIndex.value = null + }, + onReview = { + dialogsState.activateDialog(Dialog.REVIEW , ListenDialogBundleKeys.listenDialogBundle(0, index)) + dropdownItemIndex.value = null + }, + onPin = { + dialogsState.activateDialog(Dialog.PIN , ListenDialogBundleKeys.listenDialogBundle(0, index)) + dropdownItemIndex.value = null + }, + onOpenInMusicBrainz = { + try { + uriHandler.openUri("https://musicbrainz.org/recording/${metadata.trackMetadata?.mbidMapping?.recordingMbid}") + } + catch(e : Error) { + scope.launch { + snackbarState.showSnackbar(context.getString(R.string.err_generic_toast)) + } + } + dropdownItemIndex.value = null + } + ) + }, + enableTrailingContent = true, + listenCount = topSong.listenCount + ) { + val trackMetadata = metadata.trackMetadata + if(trackMetadata != null){ + playListen(trackMetadata) + } } } Spacer(modifier = Modifier.height(10.dp)) @@ -438,6 +552,25 @@ fun StatsScreen( } } } + + ErrorBar(error = socialUiState.error, onErrorShown = onErrorShown ) + SuccessBar(resId = socialUiState.successMsgId, onMessageShown = onMessageShown, snackbarState = snackbarState) + + Dialogs( + deactivateDialog = { + dialogsState.deactivateDialog() + }, + currentDialog = dialogsState.currentDialog, + currentIndex = dialogsState.metadata?.getInt(ListenDialogBundleKeys.EVENT_INDEX.name), + listens = uiState.listensTabUiState.recentListens ?: listOf(), + onPin = {metadata, blurbContent -> onPin(metadata, blurbContent)}, + searchUsers = { query -> searchUsers(query) }, + feedUiState = feedUiState, + isCritiqueBrainzLinked = isCritiqueBrainzLinked, + onReview = {type, blurbContent, rating, locale, metadata -> onReview(type, blurbContent, rating, locale, metadata) }, + onPersonallyRecommend = {metadata, users, blurbContent -> onPersonallyRecommend(metadata, users, blurbContent)}, + snackbarState = snackbarState, + socialUiState = socialUiState) } @Composable From 0bd74ba90a31149c6874a444da7fe07f3d1df746 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Mon, 12 Aug 2024 08:05:52 +0530 Subject: [PATCH 73/97] Added mock data for User Repository --- .../android/model/PinnedRecording.kt | 4 + .../android/repository/user/UserRepository.kt | 3 +- .../repository/user/UserRepositoryImpl.kt | 3 +- .../android/service/UserService.kt | 4 +- .../android/viewmodel/ProfileViewModel.kt | 2 +- .../sharedtest/mocks/MockUserRepository.kt | 80 +++++++++++++++++++ .../testdata/UserRepositoryTestData.kt | 64 +++++++++++++++ .../sharedtest/utils/ResourceString.kt | 32 ++++++++ .../src/main/resources/current_pins.json | 1 + .../resources/global_listening_activity.json | 1 + .../main/resources/listening_activity.json | 1 + .../src/main/resources/loved_hated_songs.json | 1 + sharedTest/src/main/resources/pins.json | 1 + sharedTest/src/main/resources/top_albums.json | 1 + .../src/main/resources/top_artists.json | 1 + sharedTest/src/main/resources/top_songs.json | 1 + 16 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockUserRepository.kt create mode 100644 sharedTest/src/main/java/org/listenbrainz/sharedtest/testdata/UserRepositoryTestData.kt create mode 100644 sharedTest/src/main/resources/current_pins.json create mode 100644 sharedTest/src/main/resources/global_listening_activity.json create mode 100644 sharedTest/src/main/resources/listening_activity.json create mode 100644 sharedTest/src/main/resources/loved_hated_songs.json create mode 100644 sharedTest/src/main/resources/pins.json create mode 100644 sharedTest/src/main/resources/top_albums.json create mode 100644 sharedTest/src/main/resources/top_artists.json create mode 100644 sharedTest/src/main/resources/top_songs.json diff --git a/app/src/main/java/org/listenbrainz/android/model/PinnedRecording.kt b/app/src/main/java/org/listenbrainz/android/model/PinnedRecording.kt index 2aa06bf5..2b596fe2 100644 --- a/app/src/main/java/org/listenbrainz/android/model/PinnedRecording.kt +++ b/app/src/main/java/org/listenbrainz/android/model/PinnedRecording.kt @@ -2,6 +2,10 @@ package org.listenbrainz.android.model import com.google.gson.annotations.SerializedName +data class CurrentPins( + @SerializedName("pinned_recording") val pinnedRecording: PinnedRecording? = null +) + data class PinnedRecording( @SerializedName("created" ) val created: Float? = null, @SerializedName("row_id" ) val rowId: Int? = null, 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 3d625c65..571486df 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 @@ -1,5 +1,6 @@ package org.listenbrainz.android.repository.user +import org.listenbrainz.android.model.CurrentPins import org.listenbrainz.android.model.Listens import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.user.AllPinnedRecordings @@ -14,7 +15,7 @@ import org.listenbrainz.android.util.Resource interface UserRepository { suspend fun fetchUserListenCount (username: String?) : Resource suspend fun fetchUserSimilarity(username: String? , otherUserName: String?) : Resource - suspend fun fetchUserCurrentPins(username: String?) : Resource + suspend fun fetchUserCurrentPins(username: String?) : Resource suspend fun fetchUserPins(username: String?) : Resource //TODO: Move to artists VM once implemented suspend fun getTopArtists(username: String?, rangeString: String = "all_time", count: Int = 25): Resource 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 45b67023..1b96747f 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 @@ -1,5 +1,6 @@ package org.listenbrainz.android.repository.user +import org.listenbrainz.android.model.CurrentPins import org.listenbrainz.android.model.Listens import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.ResponseError @@ -29,7 +30,7 @@ class UserRepositoryImpl @Inject constructor( service.getUserSimilarity(username,otherUserName) } - override suspend fun fetchUserCurrentPins(username: String?): Resource = parseResponse { + override suspend fun fetchUserCurrentPins(username: String?): Resource = parseResponse { if(username.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() service.getUserCurrentPins(username) } 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 82f01a2d..641932a7 100644 --- a/app/src/main/java/org/listenbrainz/android/service/UserService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/UserService.kt @@ -1,7 +1,7 @@ package org.listenbrainz.android.service +import org.listenbrainz.android.model.CurrentPins 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 @@ -22,7 +22,7 @@ interface UserService { suspend fun getUserSimilarity(@Path("user_name") username: String? , @Path("other_user_name") otherUserName: String?) : Response @GET("{user_name}/pins/current") - suspend fun getUserCurrentPins(@Path("user_name") username: String?) : Response + suspend fun getUserCurrentPins(@Path("user_name") username: String?) : Response @GET("{user_name}/pins") suspend fun getUserPins(@Path("user_name") username: String?) : Response 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 8b09f7bd..f7509344 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt @@ -126,7 +126,7 @@ class ProfileViewModel @Inject constructor( } val followingCount = following?.size val similarUsers = socialRepository.getSimilarUsers(username).data?.payload - val currentPins = userRepository.fetchUserCurrentPins(username).data + val currentPins = userRepository.fetchUserCurrentPins(username).data?.pinnedRecording val compatibility = if (username != appPreferences.username.get()) userRepository.fetchUserSimilarity( appPreferences.username.get(), diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockUserRepository.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockUserRepository.kt new file mode 100644 index 00000000..3e01e1b2 --- /dev/null +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockUserRepository.kt @@ -0,0 +1,80 @@ +package org.listenbrainz.sharedtest.mocks + +import org.listenbrainz.android.model.CurrentPins +import org.listenbrainz.android.model.Listens +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 +import org.listenbrainz.android.repository.user.UserRepository +import org.listenbrainz.android.util.Resource +import org.listenbrainz.sharedtest.testdata.UserRepositoryTestData.allPinsTestData +import org.listenbrainz.sharedtest.testdata.UserRepositoryTestData.currentPinsTestData +import org.listenbrainz.sharedtest.testdata.UserRepositoryTestData.globalListeningActivityTestData +import org.listenbrainz.sharedtest.testdata.UserRepositoryTestData.listenCountTestData +import org.listenbrainz.sharedtest.testdata.UserRepositoryTestData.listeningActivityTestData +import org.listenbrainz.sharedtest.testdata.UserRepositoryTestData.lovedHatedSongsTestData +import org.listenbrainz.sharedtest.testdata.UserRepositoryTestData.topAlbumsTestData +import org.listenbrainz.sharedtest.testdata.UserRepositoryTestData.topArtistsTestData +import org.listenbrainz.sharedtest.testdata.UserRepositoryTestData.topSongsTestData +import org.listenbrainz.sharedtest.testdata.UserRepositoryTestData.userSimilarityTestData + +class MockUserRepository : UserRepository { + override suspend fun fetchUserListenCount(username: String?): Resource { + return Resource(Resource.Status.SUCCESS, listenCountTestData) + } + + override suspend fun fetchUserSimilarity( + username: String?, + otherUserName: String? + ): Resource { + return Resource(Resource.Status.SUCCESS, userSimilarityTestData) + } + + override suspend fun fetchUserCurrentPins(username: String?): Resource { + return Resource(Resource.Status.SUCCESS, currentPinsTestData) + } + + override suspend fun fetchUserPins(username: String?): Resource { + return Resource(Resource.Status.SUCCESS, allPinsTestData) + } + + override suspend fun getTopArtists( + username: String?, + rangeString: String, + count: Int + ): Resource { + return Resource(Resource.Status.SUCCESS, topArtistsTestData) + } + + override suspend fun getUserFeedback(username: String?, score: Int?): Resource { + return Resource(Resource.Status.SUCCESS, lovedHatedSongsTestData) + } + + override suspend fun getUserListeningActivity( + username: String?, + rangeString: String + ): Resource { + return Resource(Resource.Status.SUCCESS, listeningActivityTestData) + } + + override suspend fun getGlobalListeningActivity(rangeString: String): Resource { + return Resource(Resource.Status.SUCCESS, globalListeningActivityTestData) + } + + override suspend fun getTopAlbums( + username: String?, + rangeString: String, + count: Int + ): Resource { + return Resource(Resource.Status.SUCCESS, topAlbumsTestData) + } + + override suspend fun getTopSongs(username: String?, rangeString: String): Resource { + return Resource(Resource.Status.SUCCESS, topSongsTestData) + } + +} \ No newline at end of file diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/testdata/UserRepositoryTestData.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/testdata/UserRepositoryTestData.kt new file mode 100644 index 00000000..20a68c33 --- /dev/null +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/testdata/UserRepositoryTestData.kt @@ -0,0 +1,64 @@ +package org.listenbrainz.sharedtest.testdata + +import com.google.gson.Gson +import org.listenbrainz.android.model.CurrentPins +import org.listenbrainz.android.model.Listens +import org.listenbrainz.android.model.Payload +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.UserSimilarity +import org.listenbrainz.android.model.user.UserSimilarityPayload +import org.listenbrainz.sharedtest.utils.ResourceString.all_pins +import org.listenbrainz.sharedtest.utils.ResourceString.current_pins +import org.listenbrainz.sharedtest.utils.ResourceString.globalListeningActivity +import org.listenbrainz.sharedtest.utils.ResourceString.loved_hated_songs +import org.listenbrainz.sharedtest.utils.ResourceString.topAlbums +import org.listenbrainz.sharedtest.utils.ResourceString.topSongs +import org.listenbrainz.sharedtest.utils.ResourceString.top_artists +import org.listenbrainz.sharedtest.utils.ResourceString.userListeningActivity + +object UserRepositoryTestData { + val listenCountTestData: Listens = Listens( + payload = Payload( + count = 34946, + latest_listen_ts = 0, + listens = listOf(), + user_id = "akshaaatt" + ) + ) + + val userSimilarityTestData: UserSimilarityPayload = UserSimilarityPayload( + userSimilarity = UserSimilarity( + similarity = 0.2580655f, + username = "Shubhi" + ) + ) + + val currentPinsTestData: CurrentPins? + get() = Gson().fromJson(current_pins, CurrentPins::class.java) + + val allPinsTestData: AllPinnedRecordings? + get() = Gson().fromJson(all_pins, AllPinnedRecordings::class.java) + + val topArtistsTestData: TopArtists? + get() = Gson().fromJson(top_artists, TopArtists::class.java) + + val lovedHatedSongsTestData: UserFeedback? + get() = Gson().fromJson(loved_hated_songs, UserFeedback::class.java) + + val listeningActivityTestData: UserListeningActivity? + get() = Gson().fromJson(userListeningActivity, UserListeningActivity::class.java) + + val globalListeningActivityTestData: UserListeningActivity? + get() = Gson().fromJson(globalListeningActivity, UserListeningActivity::class.java) + + val topAlbumsTestData: TopAlbums? + get() = Gson().fromJson(topAlbums, TopAlbums::class.java) + + val topSongsTestData: TopSongs? + get() = Gson().fromJson(topSongs, TopSongs::class.java) +} \ No newline at end of file diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/utils/ResourceString.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/utils/ResourceString.kt index b261f2b4..1a237663 100644 --- a/sharedTest/src/main/java/org/listenbrainz/sharedtest/utils/ResourceString.kt +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/utils/ResourceString.kt @@ -76,6 +76,38 @@ object ResourceString { val yim_data by lazy { EntityTestUtils.loadResourceAsString("yim_data.json") } + + val current_pins by lazy { + EntityTestUtils.loadResourceAsString("current_pins.json") + } + + val all_pins by lazy { + EntityTestUtils.loadResourceAsString("pins.json") + } + + val top_artists by lazy { + EntityTestUtils.loadResourceAsString("top_artists.json") + } + + val loved_hated_songs by lazy { + EntityTestUtils.loadResourceAsString("loved_hated_songs.json") + } + + val userListeningActivity by lazy { + EntityTestUtils.loadResourceAsString("listening_activity.json") + } + + val globalListeningActivity by lazy { + EntityTestUtils.loadResourceAsString("global_listening_activity.json") + } + + val topAlbums by lazy { + EntityTestUtils.loadResourceAsString("top_albums.json") + } + + val topSongs by lazy { + EntityTestUtils.loadResourceAsString("top_songs.json") + } fun String.toClass(): T { return Gson().fromJson(this, object: TypeToken() {}.type) diff --git a/sharedTest/src/main/resources/current_pins.json b/sharedTest/src/main/resources/current_pins.json new file mode 100644 index 00000000..f809c450 --- /dev/null +++ b/sharedTest/src/main/resources/current_pins.json @@ -0,0 +1 @@ +{"pinned_recording":{"blurb_content":"Noice","created":1723356490,"pinned_until":1723961286,"recording_mbid":null,"recording_msid":"cbf5a3fb-6823-4a94-a211-cb811d99e59d","row_id":2270,"track_metadata":{"additional_info":{"recording_msid":"cbf5a3fb-6823-4a94-a211-cb811d99e59d"},"artist_name":"*NSYNC","release_name":"No Strings Attached","track_name":"Bye Bye Bye - From Deadpool and Wolverine Soundtrack"}},"user_name":"pranavkonidena"} diff --git a/sharedTest/src/main/resources/global_listening_activity.json b/sharedTest/src/main/resources/global_listening_activity.json new file mode 100644 index 00000000..b300b5f6 --- /dev/null +++ b/sharedTest/src/main/resources/global_listening_activity.json @@ -0,0 +1 @@ +{"payload":{"from_ts":1640995200,"last_updated":1723265196,"listening_activity":[{"from_ts":1640995200,"listen_count":6691289,"time_range":"January 2022","to_ts":1643673599},{"from_ts":1643673600,"listen_count":6134594,"time_range":"February 2022","to_ts":1646092799},{"from_ts":1646092800,"listen_count":6324802,"time_range":"March 2022","to_ts":1648771199},{"from_ts":1648771200,"listen_count":6565411,"time_range":"April 2022","to_ts":1651363199},{"from_ts":1651363200,"listen_count":6940411,"time_range":"May 2022","to_ts":1654041599},{"from_ts":1654041600,"listen_count":6554844,"time_range":"June 2022","to_ts":1656633599},{"from_ts":1656633600,"listen_count":6664966,"time_range":"July 2022","to_ts":1659311999},{"from_ts":1659312000,"listen_count":6607955,"time_range":"August 2022","to_ts":1661990399},{"from_ts":1661990400,"listen_count":6429763,"time_range":"September 2022","to_ts":1664582399},{"from_ts":1664582400,"listen_count":6852810,"time_range":"October 2022","to_ts":1667260799},{"from_ts":1667260800,"listen_count":6724437,"time_range":"November 2022","to_ts":1669852799},{"from_ts":1669852800,"listen_count":7021955,"time_range":"December 2022","to_ts":1672531199},{"from_ts":1672531200,"listen_count":7013311,"time_range":"January 2023","to_ts":1675209599},{"from_ts":1675209600,"listen_count":6435625,"time_range":"February 2023","to_ts":1677628799},{"from_ts":1677628800,"listen_count":7010537,"time_range":"March 2023","to_ts":1680307199},{"from_ts":1680307200,"listen_count":6569518,"time_range":"April 2023","to_ts":1682899199},{"from_ts":1682899200,"listen_count":6734487,"time_range":"May 2023","to_ts":1685577599},{"from_ts":1685577600,"listen_count":6369453,"time_range":"June 2023","to_ts":1688169599},{"from_ts":1688169600,"listen_count":6442968,"time_range":"July 2023","to_ts":1690847999},{"from_ts":1690848000,"listen_count":6400835,"time_range":"August 2023","to_ts":1693526399},{"from_ts":1693526400,"listen_count":6330734,"time_range":"September 2023","to_ts":1696118399},{"from_ts":1696118400,"listen_count":6711283,"time_range":"October 2023","to_ts":1698796799},{"from_ts":1698796800,"listen_count":6367547,"time_range":"November 2023","to_ts":1701388799},{"from_ts":1701388800,"listen_count":6567587,"time_range":"December 2023","to_ts":1704067199}],"range":"year","to_ts":1704067200}} diff --git a/sharedTest/src/main/resources/listening_activity.json b/sharedTest/src/main/resources/listening_activity.json new file mode 100644 index 00000000..13c5acd3 --- /dev/null +++ b/sharedTest/src/main/resources/listening_activity.json @@ -0,0 +1 @@ +{"payload":{"from_ts":1640995200,"last_updated":1723265175,"listening_activity":[{"from_ts":1640995200,"listen_count":1148,"time_range":"January 2022","to_ts":1643673599},{"from_ts":1643673600,"listen_count":1177,"time_range":"February 2022","to_ts":1646092799},{"from_ts":1646092800,"listen_count":894,"time_range":"March 2022","to_ts":1648771199},{"from_ts":1648771200,"listen_count":948,"time_range":"April 2022","to_ts":1651363199},{"from_ts":1651363200,"listen_count":1154,"time_range":"May 2022","to_ts":1654041599},{"from_ts":1654041600,"listen_count":1234,"time_range":"June 2022","to_ts":1656633599},{"from_ts":1656633600,"listen_count":1066,"time_range":"July 2022","to_ts":1659311999},{"from_ts":1659312000,"listen_count":1164,"time_range":"August 2022","to_ts":1661990399},{"from_ts":1661990400,"listen_count":884,"time_range":"September 2022","to_ts":1664582399},{"from_ts":1664582400,"listen_count":536,"time_range":"October 2022","to_ts":1667260799},{"from_ts":1667260800,"listen_count":789,"time_range":"November 2022","to_ts":1669852799},{"from_ts":1669852800,"listen_count":714,"time_range":"December 2022","to_ts":1672531199},{"from_ts":1672531200,"listen_count":841,"time_range":"January 2023","to_ts":1675209599},{"from_ts":1675209600,"listen_count":718,"time_range":"February 2023","to_ts":1677628799},{"from_ts":1677628800,"listen_count":878,"time_range":"March 2023","to_ts":1680307199},{"from_ts":1680307200,"listen_count":848,"time_range":"April 2023","to_ts":1682899199},{"from_ts":1682899200,"listen_count":752,"time_range":"May 2023","to_ts":1685577599},{"from_ts":1685577600,"listen_count":654,"time_range":"June 2023","to_ts":1688169599},{"from_ts":1688169600,"listen_count":491,"time_range":"July 2023","to_ts":1690847999},{"from_ts":1690848000,"listen_count":744,"time_range":"August 2023","to_ts":1693526399},{"from_ts":1693526400,"listen_count":867,"time_range":"September 2023","to_ts":1696118399},{"from_ts":1696118400,"listen_count":873,"time_range":"October 2023","to_ts":1698796799},{"from_ts":1698796800,"listen_count":827,"time_range":"November 2023","to_ts":1701388799},{"from_ts":1701388800,"listen_count":696,"time_range":"December 2023","to_ts":1704067199}],"range":"year","to_ts":1704067200,"user_id":"akshaaatt"}} diff --git a/sharedTest/src/main/resources/loved_hated_songs.json b/sharedTest/src/main/resources/loved_hated_songs.json new file mode 100644 index 00000000..4600acdb --- /dev/null +++ b/sharedTest/src/main/resources/loved_hated_songs.json @@ -0,0 +1 @@ +{"count":25,"feedback":[{"created":1707077704,"recording_mbid":"c46464b2-ee6e-448c-8d22-871b139a0e38","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":17357251129,"caa_release_mbid":"e1656e7e-c23f-48e4-894e-fe1e0d5e6707","recording_mbid":"c46464b2-ee6e-448c-8d22-871b139a0e38","release_mbid":"e9b5c5ad-d91e-39d0-89fc-b8ac312bcc55"},"release_name":"Meteora","track_name":"Breaking the Habit"},"user_id":"akshaaatt"},{"created":1707077703,"recording_mbid":"54a3c21c-5395-44a2-b90b-b7fab8095c20","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":17357251129,"caa_release_mbid":"e1656e7e-c23f-48e4-894e-fe1e0d5e6707","recording_mbid":"54a3c21c-5395-44a2-b90b-b7fab8095c20","release_mbid":"e9b5c5ad-d91e-39d0-89fc-b8ac312bcc55"},"release_name":"Meteora","track_name":"Faint"},"user_id":"akshaaatt"},{"created":1707077702,"recording_mbid":"50347f42-2a40-4df6-b358-07f994af7a3f","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":21070222550,"caa_release_mbid":"127e18cd-99ad-3193-ad4f-441bee3c6e3b","recording_mbid":"50347f42-2a40-4df6-b358-07f994af7a3f","release_mbid":"127e18cd-99ad-3193-ad4f-441bee3c6e3b"},"release_name":"Minutes to Midnight","track_name":"What I\u2019ve Done"},"user_id":"akshaaatt"},{"created":1707077701,"recording_mbid":"352dd518-23cd-4c5a-9551-ba02097b177b","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":17357251129,"caa_release_mbid":"e1656e7e-c23f-48e4-894e-fe1e0d5e6707","recording_mbid":"352dd518-23cd-4c5a-9551-ba02097b177b","release_mbid":"e9b5c5ad-d91e-39d0-89fc-b8ac312bcc55"},"release_name":"Meteora","track_name":"Numb"},"user_id":"akshaaatt"},{"created":1707077701,"recording_mbid":"9d70086c-5d7a-4e7f-b1ed-c53c4b11310f","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":17352139218,"caa_release_mbid":"69b7b91a-43fc-483f-b8cf-22cdc8f69079","recording_mbid":"9d70086c-5d7a-4e7f-b1ed-c53c4b11310f","release_mbid":"a92e8636-a48c-4ad1-8a5f-4f8a968a50ac"},"release_name":"Hybrid Theory","track_name":"In the End"},"user_id":"akshaaatt"},{"created":1698546115,"recording_mbid":"234c556a-215b-4bb4-a0fa-2398cf331085","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Babbulicious","mbid_mapping":{"artist_mbids":["db442dbd-1579-4ff3-8472-bc4fc72c7522"],"artists":[{"artist_credit_name":"Babbulicious","artist_mbid":"db442dbd-1579-4ff3-8472-bc4fc72c7522","join_phrase":""}],"caa_id":35336159608,"caa_release_mbid":"43e2cac8-ac33-428e-9ad9-79827d4a8b34","recording_mbid":"234c556a-215b-4bb4-a0fa-2398cf331085","release_mbid":"43e2cac8-ac33-428e-9ad9-79827d4a8b34"},"release_name":"Gucci Chick","track_name":"Gucci Chick"},"user_id":"akshaaatt"},{"created":1692097010,"recording_mbid":"ea2a183f-5515-4a85-8228-72cf7e1ee90a","recording_msid":"d0f953f3-fdd4-4a7e-8004-fd000e0f804c","score":1,"track_metadata":{"additional_info":{"recording_msid":"d0f953f3-fdd4-4a7e-8004-fd000e0f804c"},"artist_name":"Billie Eilish","mbid_mapping":{"artist_mbids":["f4abc0b5-3f7a-4eff-8f78-ac078dbce533"],"artists":[{"artist_credit_name":"Billie Eilish","artist_mbid":"f4abc0b5-3f7a-4eff-8f78-ac078dbce533","join_phrase":""}],"caa_id":36301165720,"caa_release_mbid":"93ad159c-c69b-4cc5-ae5d-1136580fe04d","recording_mbid":"ea2a183f-5515-4a85-8228-72cf7e1ee90a","release_mbid":"93ad159c-c69b-4cc5-ae5d-1136580fe04d"},"release_name":"Live from the Barbie Dream House","track_name":"What Was I Made For?"},"user_id":"akshaaatt"},{"created":1685816977,"recording_mbid":"f3c609f4-be7f-42aa-9688-9f2aa38a5cb7","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":16782501966,"caa_release_mbid":"1f5f9437-fe10-448e-acf2-09a03cd81a7d","recording_mbid":"f3c609f4-be7f-42aa-9688-9f2aa38a5cb7","release_mbid":"44703741-a292-42e2-85d5-117ca1df8d9b"},"release_name":"One More Light","track_name":"Nobody Can Save Me"},"user_id":"akshaaatt"},{"created":1676722350,"recording_mbid":"9c49c2e3-dbe5-471c-8efa-d20d4341f635","recording_msid":"0b98fbc7-4792-4060-8b4c-969abab8c027","score":1,"track_metadata":{"additional_info":{"recording_msid":"0b98fbc7-4792-4060-8b4c-969abab8c027"},"artist_name":"Monali Thakur","mbid_mapping":{"artist_mbids":["adb5aa54-32b9-4587-83df-156d2c9bef1c"],"artists":[{"artist_credit_name":"Monali Thakur","artist_mbid":"adb5aa54-32b9-4587-83df-156d2c9bef1c","join_phrase":""}],"caa_id":38683132632,"caa_release_mbid":"865e074d-3209-4cdd-8589-480bb8cc1027","recording_mbid":"9c49c2e3-dbe5-471c-8efa-d20d4341f635","release_mbid":"865e074d-3209-4cdd-8589-480bb8cc1027"},"release_name":"Lootera","track_name":"Sawaar Loon"},"user_id":"akshaaatt"},{"created":1649338426,"recording_mbid":"c9319bbc-8dec-4c7c-8934-ee234345f705","recording_msid":"d07b81ac-0209-44ed-a38e-4ebcde6eddef","score":1,"track_metadata":{"additional_info":{"recording_msid":"d07b81ac-0209-44ed-a38e-4ebcde6eddef"},"artist_name":"Lauv","mbid_mapping":{"artist_mbids":["c0ef2ba5-a7b7-40ea-bd27-30acccfcac11"],"artists":[{"artist_credit_name":"Lauv","artist_mbid":"c0ef2ba5-a7b7-40ea-bd27-30acccfcac11","join_phrase":""}],"caa_id":20512551101,"caa_release_mbid":"47383b9f-d6ea-457c-9c95-6f9b6dd6fa36","recording_mbid":"c9319bbc-8dec-4c7c-8934-ee234345f705","release_mbid":"47383b9f-d6ea-457c-9c95-6f9b6dd6fa36"},"release_name":"I Like Me Better","track_name":"I Like Me Better"},"user_id":"akshaaatt"},{"created":1647712667,"recording_mbid":null,"recording_msid":"20492313-3623-491f-96aa-83aa5afa47a0","score":1,"track_metadata":{"additional_info":{"recording_msid":"20492313-3623-491f-96aa-83aa5afa47a0"},"artist_name":"Sasha Alex Sloan","release_name":"Older","track_name":"Older"},"user_id":"akshaaatt"},{"created":1647509892,"recording_mbid":"bfd84871-2523-40e5-a70f-04c9b0e28acf","recording_msid":"589e83ac-ac08-4fa1-b14e-767371f8f1b5","score":1,"track_metadata":{"additional_info":{"recording_msid":"589e83ac-ac08-4fa1-b14e-767371f8f1b5"},"artist_name":"Machine Gun Kelly feat. Bring Me the Horizon","mbid_mapping":{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33","074e3847-f67f-49f9-81f1-8c8cea147e8e"],"artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":" feat. "},{"artist_credit_name":"Bring Me the Horizon","artist_mbid":"074e3847-f67f-49f9-81f1-8c8cea147e8e","join_phrase":""}],"caa_id":32122624894,"caa_release_mbid":"a88712e3-1327-4472-af90-5e1af734c16a","recording_mbid":"bfd84871-2523-40e5-a70f-04c9b0e28acf","release_mbid":"b94f45e0-7a1a-4895-a45b-3abd2ec5eda2"},"release_name":"MAINSTREAM SELLOUT","track_name":"maybe"},"user_id":"akshaaatt"},{"created":1647509700,"recording_mbid":"24d50452-6d0a-46f2-8ba1-9ff61dd8f3d2","recording_msid":"904b3f5e-64d0-4104-8e2b-cf87237872c5","score":1,"track_metadata":{"additional_info":{"recording_msid":"904b3f5e-64d0-4104-8e2b-cf87237872c5"},"artist_name":"Linkin Park feat. Kiiara","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419","dbc26631-5e61-4ba2-84ff-e1423a1c288a"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":" feat. "},{"artist_credit_name":"Kiiara","artist_mbid":"dbc26631-5e61-4ba2-84ff-e1423a1c288a","join_phrase":""}],"caa_id":16782501966,"caa_release_mbid":"1f5f9437-fe10-448e-acf2-09a03cd81a7d","recording_mbid":"24d50452-6d0a-46f2-8ba1-9ff61dd8f3d2","release_mbid":"44703741-a292-42e2-85d5-117ca1df8d9b"},"release_name":"One More Light","track_name":"Heavy"},"user_id":"akshaaatt"},{"created":1647369312,"recording_mbid":"f9845fce-5ab7-456f-8579-6a1fcfa8cf25","recording_msid":"641886e4-5a74-49f4-990d-ed9ba82c2330","score":1,"track_metadata":{"additional_info":{"recording_msid":"641886e4-5a74-49f4-990d-ed9ba82c2330"},"artist_name":"Rihanna","mbid_mapping":{"artist_mbids":["73e5e69d-3554-40d8-8516-00cb38737a1c"],"artists":[{"artist_credit_name":"Rihanna","artist_mbid":"73e5e69d-3554-40d8-8516-00cb38737a1c","join_phrase":""}],"caa_id":14539517129,"caa_release_mbid":"3a7adfe2-6965-4b09-adb5-1b200346433d","recording_mbid":"f9845fce-5ab7-456f-8579-6a1fcfa8cf25","release_mbid":"ce71001c-f8c5-44b2-9c25-589693b64a22"},"release_name":"Music of the Sun","track_name":"Pon de Replay"},"user_id":"akshaaatt"},{"created":1646132106,"recording_mbid":"a89fa4c9-5e9f-4ed3-9478-1585783a5d5d","recording_msid":"7836aa1a-2c41-4279-a9b6-2263097b38c4","score":1,"track_metadata":{"additional_info":{"recording_msid":"7836aa1a-2c41-4279-a9b6-2263097b38c4"},"artist_name":"Carpathian Forest","mbid_mapping":{"artist_mbids":["69fa5c49-12ec-4c86-a238-4e07cc6d1a7d"],"artists":[{"artist_credit_name":"Carpathian Forest","artist_mbid":"69fa5c49-12ec-4c86-a238-4e07cc6d1a7d","join_phrase":""}],"caa_id":1033890833,"caa_release_mbid":"3f2e996b-e231-4165-bba0-64ee3b67cf60","recording_mbid":"a89fa4c9-5e9f-4ed3-9478-1585783a5d5d","release_mbid":"3f2e996b-e231-4165-bba0-64ee3b67cf60"},"release_name":"Black Shining Leather","track_name":"A Forest"},"user_id":"akshaaatt"},{"created":1646131911,"recording_mbid":"b9874074-b2d6-4135-bc2a-f4ef44b0ae69","recording_msid":"112597b6-ec52-49cf-9916-41c3aa060a77","score":1,"track_metadata":{"additional_info":{"recording_msid":"112597b6-ec52-49cf-9916-41c3aa060a77"},"artist_name":"Machine Gun Kelly","mbid_mapping":{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":27366822877,"caa_release_mbid":"ca1cafc2-3daf-4ca5-b1af-8466f7836e60","recording_mbid":"b9874074-b2d6-4135-bc2a-f4ef44b0ae69","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa"},"release_name":"Tickets to My Downfall","track_name":"title track"},"user_id":"akshaaatt"},{"created":1645670072,"recording_mbid":"198a6f88-eb15-4280-9208-82998cb39175","recording_msid":"cf41b59d-9c9d-4042-81c8-09b92797d46d","score":1,"track_metadata":{"additional_info":{"recording_msid":"cf41b59d-9c9d-4042-81c8-09b92797d46d"},"artist_name":"Machine Gun Kelly","mbid_mapping":{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":27366800098,"caa_release_mbid":"5d987f56-4c51-4c1b-9e67-b6f6e0fc46fd","recording_mbid":"198a6f88-eb15-4280-9208-82998cb39175","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa"},"release_name":"Tickets to My Downfall","track_name":"bloody valentine"},"user_id":"akshaaatt"},{"created":1645178278,"recording_mbid":"2d24a8d7-66ad-40b2-8ae7-5f6fc9981646","recording_msid":"ba6a24ac-ef0c-44d6-b233-d0bebd925c46","score":1,"track_metadata":{"additional_info":{"recording_msid":"ba6a24ac-ef0c-44d6-b233-d0bebd925c46"},"artist_name":"Ed Sheeran","mbid_mapping":{"artist_mbids":["b8a7c51f-362c-4dcb-a259-bc6e0095f0a6"],"artists":[{"artist_credit_name":"Ed Sheeran","artist_mbid":"b8a7c51f-362c-4dcb-a259-bc6e0095f0a6","join_phrase":""}],"caa_id":37184507977,"caa_release_mbid":"64ae938c-6af1-4919-8341-458eb1866474","recording_mbid":"2d24a8d7-66ad-40b2-8ae7-5f6fc9981646","release_mbid":"ccfe374a-ccfa-4cbc-a37c-1cd40b914b75"},"release_name":"=","track_name":"Bad Habits"},"user_id":"akshaaatt"},{"created":1636226683,"recording_mbid":"7ec115f0-2da7-4ddf-a183-6f4f8e10cc0e","recording_msid":"669fafd9-044b-4dc0-93af-be17ba06c4f5","score":1,"track_metadata":{"additional_info":{"recording_msid":"669fafd9-044b-4dc0-93af-be17ba06c4f5"},"artist_name":"Alex Gibson","mbid_mapping":{"artist_mbids":["aab3f709-be9e-48b9-9b9d-281fff5284f9"],"artists":[{"artist_credit_name":"Alex Gibson","artist_mbid":"aab3f709-be9e-48b9-9b9d-281fff5284f9","join_phrase":""}],"caa_id":15835295366,"caa_release_mbid":"5a097177-c962-4c56-9a9f-46e9f6868c9c","recording_mbid":"7ec115f0-2da7-4ddf-a183-6f4f8e10cc0e","release_mbid":"5a097177-c962-4c56-9a9f-46e9f6868c9c"},"release_name":"Rockabye Baby! Lullaby Renditions of AC/DC","track_name":"Dirty Deeds Done Dirt Cheap"},"user_id":"akshaaatt"},{"created":1635871036,"recording_mbid":"c0ecfe09-7521-42b2-9f99-366b854f280f","recording_msid":"defe1f54-97de-4a3f-a0df-7e1bdc9b3a8e","score":1,"track_metadata":{"additional_info":{"recording_msid":"defe1f54-97de-4a3f-a0df-7e1bdc9b3a8e"},"artist_name":"Shelly Manne & His Friends","mbid_mapping":{"artist_mbids":["7c0c1773-513e-4c47-88de-310550319921"],"artists":[{"artist_credit_name":"Shelly Manne & His Friends","artist_mbid":"7c0c1773-513e-4c47-88de-310550319921","join_phrase":""}],"caa_id":26689007265,"caa_release_mbid":"928c6630-795e-4f6b-9b25-c36b8f4212d7","recording_mbid":"c0ecfe09-7521-42b2-9f99-366b854f280f","release_mbid":"233bbe84-df40-4755-84e3-9c5937e91f8e"},"release_name":"Shelly Manne & His Friends Play Modern Jazz Performances of Songs from My Fair Lady","track_name":"Get Me to the Church on Time"},"user_id":"akshaaatt"},{"created":1632294500,"recording_mbid":"015021a9-2621-4593-b82d-f1f28bdf440a","recording_msid":"920f49da-1de3-4902-873f-8c5d2815884b","score":1,"track_metadata":{"additional_info":{"recording_msid":"920f49da-1de3-4902-873f-8c5d2815884b"},"artist_name":"Zedd & Alessia Cara","mbid_mapping":{"artist_mbids":["56c4b861-0922-4c3a-a9b9-3bfcb00f8274","97e69730-3791-423b-9770-287261588854"],"artists":[{"artist_credit_name":"Zedd","artist_mbid":"56c4b861-0922-4c3a-a9b9-3bfcb00f8274","join_phrase":" & "},{"artist_credit_name":"Alessia Cara","artist_mbid":"97e69730-3791-423b-9770-287261588854","join_phrase":""}],"caa_id":17157288690,"caa_release_mbid":"fe1176c5-aa68-4518-8548-0eb23ef8cd92","recording_mbid":"015021a9-2621-4593-b82d-f1f28bdf440a","release_mbid":"92541a9e-3507-484e-aecc-23577b8c3547"},"release_name":"Get Low","track_name":"Stay"},"user_id":"akshaaatt"},{"created":1632238295,"recording_mbid":"5780b6fb-066f-4147-ac0e-0d8c8e812e02","recording_msid":"1312abf2-b12f-483f-935b-c7a283700481","score":1,"track_metadata":{"additional_info":{"recording_msid":"1312abf2-b12f-483f-935b-c7a283700481"},"artist_name":"Ed Sheeran","mbid_mapping":{"artist_mbids":["b8a7c51f-362c-4dcb-a259-bc6e0095f0a6"],"artists":[{"artist_credit_name":"Ed Sheeran","artist_mbid":"b8a7c51f-362c-4dcb-a259-bc6e0095f0a6","join_phrase":""}],"caa_id":31935928002,"caa_release_mbid":"14c13329-88f5-426f-90d7-0170b4017e15","recording_mbid":"5780b6fb-066f-4147-ac0e-0d8c8e812e02","release_mbid":"769e64d7-b7a8-4ad1-84d8-0e0063d8fe64"},"release_name":"\u00f7","track_name":"Castle on the Hill"},"user_id":"akshaaatt"},{"created":1632224517,"recording_mbid":"07adabdd-1195-4ac5-9c75-8e2e11daa604","recording_msid":"aca55f75-a7e0-4989-8c57-88cb431bb219","score":1,"track_metadata":{"additional_info":{"recording_msid":"aca55f75-a7e0-4989-8c57-88cb431bb219"},"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":16782501966,"caa_release_mbid":"1f5f9437-fe10-448e-acf2-09a03cd81a7d","recording_mbid":"07adabdd-1195-4ac5-9c75-8e2e11daa604","release_mbid":"44703741-a292-42e2-85d5-117ca1df8d9b"},"release_name":"One More Light","track_name":"Battle Symphony"},"user_id":"akshaaatt"},{"created":1631784052,"recording_mbid":"1205dbc4-5fa4-4c19-b04b-50fdbf15ddf2","recording_msid":"eeba4144-9a77-4386-a932-d947a64c18ae","score":1,"track_metadata":{"additional_info":{"recording_msid":"eeba4144-9a77-4386-a932-d947a64c18ae"},"artist_name":"Mike Shinoda","mbid_mapping":{"artist_mbids":["c262b6bf-be56-4b26-bceb-42d7a27342f3"],"artists":[{"artist_credit_name":"Mike Shinoda","artist_mbid":"c262b6bf-be56-4b26-bceb-42d7a27342f3","join_phrase":""}],"caa_id":22130935658,"caa_release_mbid":"df9c66e7-07eb-4a9a-b9c6-4f41ae676a15","recording_mbid":"1205dbc4-5fa4-4c19-b04b-50fdbf15ddf2","release_mbid":"639eb4b8-2ea0-4455-a067-f1d9faade1c9"},"release_name":"Post Traumatic","track_name":"Watching as I Fall"},"user_id":"akshaaatt"},{"created":1631784039,"recording_mbid":"8102a8b9-0b58-49aa-8f04-e6a49528bb46","recording_msid":"b3155220-34c3-4e29-bd9f-25bcf4109aa7","score":1,"track_metadata":{"additional_info":{"recording_msid":"b3155220-34c3-4e29-bd9f-25bcf4109aa7"},"artist_name":"Mike Shinoda","mbid_mapping":{"artist_mbids":["c262b6bf-be56-4b26-bceb-42d7a27342f3"],"artists":[{"artist_credit_name":"Mike Shinoda","artist_mbid":"c262b6bf-be56-4b26-bceb-42d7a27342f3","join_phrase":""}],"caa_id":22130935658,"caa_release_mbid":"df9c66e7-07eb-4a9a-b9c6-4f41ae676a15","recording_mbid":"8102a8b9-0b58-49aa-8f04-e6a49528bb46","release_mbid":"639eb4b8-2ea0-4455-a067-f1d9faade1c9"},"release_name":"Post Traumatic","track_name":"Hold It Together"},"user_id":"akshaaatt"}],"offset":0,"total_count":31} diff --git a/sharedTest/src/main/resources/pins.json b/sharedTest/src/main/resources/pins.json new file mode 100644 index 00000000..9b690172 --- /dev/null +++ b/sharedTest/src/main/resources/pins.json @@ -0,0 +1 @@ +{"count":14,"offset":0,"pinned_recordings":[{"blurb_content":null,"created":1696606811,"pinned_until":1697211611,"recording_mbid":"9aa621e1-46f2-4c91-8111-741583985612","recording_msid":"2da9e60f-6b36-4ce1-a5fe-d84877a49006","row_id":1176,"track_metadata":{"additional_info":{"recording_msid":"2da9e60f-6b36-4ce1-a5fe-d84877a49006"},"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":17352139218,"caa_release_mbid":"69b7b91a-43fc-483f-b8cf-22cdc8f69079","recording_mbid":"9aa621e1-46f2-4c91-8111-741583985612","release_mbid":"a92e8636-a48c-4ad1-8a5f-4f8a968a50ac"},"release_name":"Hybrid Theory","track_name":"Papercut"}},{"blurb_content":"Amazing song, worth listening!","created":1648492830,"pinned_until":1649097630,"recording_mbid":"5e54d4be-a83b-41d1-9372-06fa58b4c36d","recording_msid":"8ca79982-c17d-47f2-b8f4-8ab333b7acfb","row_id":308,"track_metadata":{"additional_info":{"recording_msid":"8ca79982-c17d-47f2-b8f4-8ab333b7acfb"},"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":17454038939,"caa_release_mbid":"6c48c82c-b7df-4b0b-987d-666ec8271afb","recording_mbid":"5e54d4be-a83b-41d1-9372-06fa58b4c36d","release_mbid":"7f1a6e87-9ad2-4629-b9a5-563525607310"},"release_name":"Living Things","track_name":"Burn It Down"}},{"blurb_content":"WONDERFUL SONG!","created":1646132933,"pinned_until":1646737733,"recording_mbid":"2397ee94-274f-451e-a330-2499a93fa9fb","recording_msid":"112597b6-ec52-49cf-9916-41c3aa060a77","row_id":227,"track_metadata":{"additional_info":{"recording_msid":"112597b6-ec52-49cf-9916-41c3aa060a77"},"artist_name":"Machine Gun Kelly","release_name":"Tickets To My Downfall (SOLD OUT Deluxe)","track_name":"title track"}},{"blurb_content":"Amazing","created":1645177870,"pinned_until":1645782670,"recording_mbid":"2d24a8d7-66ad-40b2-8ae7-5f6fc9981646","recording_msid":"ba6a24ac-ef0c-44d6-b233-d0bebd925c46","row_id":219,"track_metadata":{"additional_info":{"recording_msid":"ba6a24ac-ef0c-44d6-b233-d0bebd925c46"},"artist_name":"Ed Sheeran","mbid_mapping":{"artist_mbids":["b8a7c51f-362c-4dcb-a259-bc6e0095f0a6"],"artists":[{"artist_credit_name":"Ed Sheeran","artist_mbid":"b8a7c51f-362c-4dcb-a259-bc6e0095f0a6","join_phrase":""}],"caa_id":37184507977,"caa_release_mbid":"64ae938c-6af1-4919-8341-458eb1866474","recording_mbid":"2d24a8d7-66ad-40b2-8ae7-5f6fc9981646","release_mbid":"ccfe374a-ccfa-4cbc-a37c-1cd40b914b75"},"release_name":"=","track_name":"Bad Habits"}},{"blurb_content":null,"created":1645128476,"pinned_until":1645128664,"recording_mbid":"5780b6fb-066f-4147-ac0e-0d8c8e812e02","recording_msid":"1312abf2-b12f-483f-935b-c7a283700481","row_id":217,"track_metadata":{"additional_info":{"recording_msid":"1312abf2-b12f-483f-935b-c7a283700481"},"artist_name":"Ed Sheeran","mbid_mapping":{"artist_mbids":["b8a7c51f-362c-4dcb-a259-bc6e0095f0a6"],"artists":[{"artist_credit_name":"Ed Sheeran","artist_mbid":"b8a7c51f-362c-4dcb-a259-bc6e0095f0a6","join_phrase":""}],"caa_id":31935928002,"caa_release_mbid":"14c13329-88f5-426f-90d7-0170b4017e15","recording_mbid":"5780b6fb-066f-4147-ac0e-0d8c8e812e02","release_mbid":"769e64d7-b7a8-4ad1-84d8-0e0063d8fe64"},"release_name":"\u00f7","track_name":"Castle on the Hill"}},{"blurb_content":"Wonderful song!","created":1642502920,"pinned_until":1642530147,"recording_mbid":"4fbb1e4a-c5c7-4694-a971-a3668bcffa17","recording_msid":"2e647303-cd88-4d1c-ba87-6ddda72b5edc","row_id":192,"track_metadata":{"additional_info":{"recording_msid":"2e647303-cd88-4d1c-ba87-6ddda72b5edc"},"artist_name":"Lauv","mbid_mapping":{"artist_mbids":["c0ef2ba5-a7b7-40ea-bd27-30acccfcac11"],"artists":[{"artist_credit_name":"Lauv","artist_mbid":"c0ef2ba5-a7b7-40ea-bd27-30acccfcac11","join_phrase":""}],"caa_id":25651991080,"caa_release_mbid":"63b5d3be-6538-4fc9-ac7d-a42334aebd85","recording_mbid":"4fbb1e4a-c5c7-4694-a971-a3668bcffa17","release_mbid":"63b5d3be-6538-4fc9-ac7d-a42334aebd85"},"release_name":"~how i\u2019m feeling~","track_name":"Feelings"}},{"blurb_content":"Amazing song!","created":1642333322,"pinned_until":1642502920,"recording_mbid":"cb8be04f-e8b1-425c-b68b-c4a32f2c6532","recording_msid":"b9d97e22-09c8-425a-a21d-946f631b62a9","row_id":190,"track_metadata":{"additional_info":{"recording_msid":"b9d97e22-09c8-425a-a21d-946f631b62a9"},"artist_name":"Machine Gun Kelly","mbid_mapping":{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":27366822877,"caa_release_mbid":"ca1cafc2-3daf-4ca5-b1af-8466f7836e60","recording_mbid":"cb8be04f-e8b1-425c-b68b-c4a32f2c6532","release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8"},"release_name":"Tickets to My Downfall (SOLD OUT Deluxe)","track_name":"split a pill"}},{"blurb_content":"Amazing song!","created":1638638664,"pinned_until":1639075612,"recording_mbid":"1291edb5-97d4-4893-8baa-da7c4c542377","recording_msid":"2f3f2e96-4544-4bab-b948-281fe7592b77","row_id":157,"track_metadata":{"additional_info":{"recording_msid":"2f3f2e96-4544-4bab-b948-281fe7592b77"},"artist_name":"Calum Scott","mbid_mapping":{"artist_mbids":["68fdc9cc-1094-48bb-942f-66492343e41c"],"artists":[{"artist_credit_name":"Calum Scott","artist_mbid":"68fdc9cc-1094-48bb-942f-66492343e41c","join_phrase":""}],"caa_id":18877218596,"caa_release_mbid":"86ae5c62-7989-4993-bbdb-969d77f7872a","recording_mbid":"1291edb5-97d4-4893-8baa-da7c4c542377","release_mbid":"c34d0c8b-330f-4a16-84a7-41d87a931cf1"},"release_name":"Only Human","track_name":"Dancing on My Own"}},{"blurb_content":null,"created":1637923045,"pinned_until":1638245181,"recording_mbid":"e185bbc4-7d97-44d8-93b0-b47a32fec8cd","recording_msid":"1c3a7bed-1f96-4458-b876-73191ea3f397","row_id":151,"track_metadata":{"additional_info":{"recording_msid":"1c3a7bed-1f96-4458-b876-73191ea3f397"},"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":7389110443,"caa_release_mbid":"29424f55-f1b2-43f6-822d-700aa1f8595c","recording_mbid":"e185bbc4-7d97-44d8-93b0-b47a32fec8cd","release_mbid":"5f5aaf10-36a4-3dfc-a7c1-c2d5c2286647"},"release_name":"A Thousand Suns","track_name":"Iridescent"}},{"blurb_content":null,"created":1636381481,"pinned_until":1636986281,"recording_mbid":"a24974c0-9da0-4f02-b59b-b209e81e2ede","recording_msid":"dbc56879-a052-49eb-b949-ea4d624159ff","row_id":116,"track_metadata":{"additional_info":{"recording_msid":"dbc56879-a052-49eb-b949-ea4d624159ff"},"artist_name":"Maroon 5","mbid_mapping":{"artist_mbids":["0ab49580-c84f-44d4-875f-d83760ea2cfe"],"artists":[{"artist_credit_name":"Maroon 5","artist_mbid":"0ab49580-c84f-44d4-875f-d83760ea2cfe","join_phrase":""}],"caa_id":33632889799,"caa_release_mbid":"e8fe33d4-31a6-4394-941c-45fac8834322","recording_mbid":"a24974c0-9da0-4f02-b59b-b209e81e2ede","release_mbid":"28832231-f70d-4937-8cbd-d4b679195d34"},"release_name":"V","track_name":"Maps"}},{"blurb_content":"Interesting song!","created":1636226679,"pinned_until":1636381455,"recording_mbid":"7ec115f0-2da7-4ddf-a183-6f4f8e10cc0e","recording_msid":"669fafd9-044b-4dc0-93af-be17ba06c4f5","row_id":113,"track_metadata":{"additional_info":{"recording_msid":"669fafd9-044b-4dc0-93af-be17ba06c4f5"},"artist_name":"Alex Gibson","mbid_mapping":{"artist_mbids":["aab3f709-be9e-48b9-9b9d-281fff5284f9"],"artists":[{"artist_credit_name":"Alex Gibson","artist_mbid":"aab3f709-be9e-48b9-9b9d-281fff5284f9","join_phrase":""}],"caa_id":15835295366,"caa_release_mbid":"5a097177-c962-4c56-9a9f-46e9f6868c9c","recording_mbid":"7ec115f0-2da7-4ddf-a183-6f4f8e10cc0e","release_mbid":"5a097177-c962-4c56-9a9f-46e9f6868c9c"},"release_name":"Rockabye Baby! Lullaby Renditions of AC/DC","track_name":"Dirty Deeds Done Dirt Cheap"}},{"blurb_content":null,"created":1635870142,"pinned_until":1636226679,"recording_mbid":"c0ecfe09-7521-42b2-9f99-366b854f280f","recording_msid":"defe1f54-97de-4a3f-a0df-7e1bdc9b3a8e","row_id":109,"track_metadata":{"additional_info":{"recording_msid":"defe1f54-97de-4a3f-a0df-7e1bdc9b3a8e"},"artist_name":"Shelly Manne & His Friends","mbid_mapping":{"artist_mbids":["7c0c1773-513e-4c47-88de-310550319921"],"artists":[{"artist_credit_name":"Shelly Manne & His Friends","artist_mbid":"7c0c1773-513e-4c47-88de-310550319921","join_phrase":""}],"caa_id":26689007265,"caa_release_mbid":"928c6630-795e-4f6b-9b25-c36b8f4212d7","recording_mbid":"c0ecfe09-7521-42b2-9f99-366b854f280f","release_mbid":"233bbe84-df40-4755-84e3-9c5937e91f8e"},"release_name":"Shelly Manne & His Friends Play Modern Jazz Performances of Songs from My Fair Lady","track_name":"Get Me to the Church on Time"}},{"blurb_content":"Great song!","created":1630035558,"pinned_until":1630640358,"recording_mbid":"92972c5e-867b-4fa2-98c5-7bd57a4e440e","recording_msid":"0df8fc8a-49b3-4854-a371-0bd19d4668c8","row_id":19,"track_metadata":{"additional_info":{"recording_msid":"0df8fc8a-49b3-4854-a371-0bd19d4668c8"},"artist_name":"Fort Minor","mbid_mapping":{"artist_mbids":["e1564e98-978b-4947-8698-f6fd6f8b0181"],"artists":[{"artist_credit_name":"Fort Minor","artist_mbid":"e1564e98-978b-4947-8698-f6fd6f8b0181","join_phrase":""}],"caa_id":28875936991,"caa_release_mbid":"2260aedd-155d-33b0-a4bb-6db7ce3f6598","recording_mbid":"92972c5e-867b-4fa2-98c5-7bd57a4e440e","release_mbid":"4b6ca48c-f7db-439d-ba57-6104b5fec61e"},"release_name":"The Rising Tied","track_name":"Petrified"}},{"blurb_content":null,"created":1630035458,"pinned_until":1630035558,"recording_mbid":"4c989c32-51a3-489a-965a-bb334def31a8","recording_msid":"566c2f69-5db0-4f2c-a504-a27836238fed","row_id":18,"track_metadata":{"additional_info":{"recording_msid":"566c2f69-5db0-4f2c-a504-a27836238fed"},"artist_name":"Machine Gun Kelly","mbid_mapping":{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":27366822877,"caa_release_mbid":"ca1cafc2-3daf-4ca5-b1af-8466f7836e60","recording_mbid":"4c989c32-51a3-489a-965a-bb334def31a8","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa"},"release_name":"Tickets to My Downfall","track_name":"concert for aliens"}}],"total_count":14,"user_name":"akshaaatt"} diff --git a/sharedTest/src/main/resources/top_albums.json b/sharedTest/src/main/resources/top_albums.json new file mode 100644 index 00000000..8bb1b7e2 --- /dev/null +++ b/sharedTest/src/main/resources/top_albums.json @@ -0,0 +1 @@ +{"payload":{"count":25,"from_ts":1009843200,"last_updated":1723268591,"offset":0,"range":"all_time","releases":[{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":30564814068,"caa_release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","listen_count":2463,"release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","release_name":"Tickets to My Downfall"},{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artist_name":"Linkin Park","artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":16767050943,"caa_release_mbid":"44703741-a292-42e2-85d5-117ca1df8d9b","listen_count":1115,"release_mbid":"44703741-a292-42e2-85d5-117ca1df8d9b","release_name":"One More Light"},{"artist_mbids":["c0ef2ba5-a7b7-40ea-bd27-30acccfcac11"],"artist_name":"Lauv","artists":[{"artist_credit_name":"Lauv","artist_mbid":"c0ef2ba5-a7b7-40ea-bd27-30acccfcac11","join_phrase":""}],"caa_id":25651991080,"caa_release_mbid":"63b5d3be-6538-4fc9-ac7d-a42334aebd85","listen_count":1023,"release_mbid":"63b5d3be-6538-4fc9-ac7d-a42334aebd85","release_name":"~how i\u2019m feeling~"},{"artist_mbids":["e5712ceb-c37a-4c49-a11c-ccf4e21852d4"],"artist_name":"Troye Sivan","artists":[{"artist_credit_name":"Troye Sivan","artist_mbid":"e5712ceb-c37a-4c49-a11c-ccf4e21852d4","join_phrase":""}],"caa_id":11788642695,"caa_release_mbid":"e7d13ff4-ec74-4eff-b0d3-bdc00b720944","listen_count":927,"release_mbid":"b6d0161d-1073-48e2-8503-4bd3261899ee","release_name":"Blue Neighbourhood"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":27494670999,"caa_release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8","listen_count":731,"release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8","release_name":"Tickets to My Downfall (SOLD OUT Deluxe)"},{"artist_mbids":["1a425bbd-cca4-4b2c-aeb7-71cb176c828a"],"artist_name":"One Direction","artists":[{"artist_credit_name":"One Direction","artist_mbid":"1a425bbd-cca4-4b2c-aeb7-71cb176c828a","join_phrase":""}],"caa_id":34762378442,"caa_release_mbid":"a31c02ee-5f62-4b9d-b576-2b02c5e4d750","listen_count":561,"release_mbid":"a31c02ee-5f62-4b9d-b576-2b02c5e4d750","release_name":"Four: The Ultimate Edition"},{"artist_mbids":["c262b6bf-be56-4b26-bceb-42d7a27342f3"],"artist_name":"Mike Shinoda","artists":[{"artist_credit_name":"Mike Shinoda","artist_mbid":"c262b6bf-be56-4b26-bceb-42d7a27342f3","join_phrase":""}],"caa_id":19495585058,"caa_release_mbid":"639eb4b8-2ea0-4455-a067-f1d9faade1c9","listen_count":542,"release_mbid":"639eb4b8-2ea0-4455-a067-f1d9faade1c9","release_name":"Post Traumatic"},{"artist_mbids":["e5712ceb-c37a-4c49-a11c-ccf4e21852d4"],"artist_name":"Troye Sivan","artists":[{"artist_credit_name":"Troye Sivan","artist_mbid":"e5712ceb-c37a-4c49-a11c-ccf4e21852d4","join_phrase":""}],"caa_id":20981274842,"caa_release_mbid":"2a7a4450-32da-4e21-9893-9b7282eb9c4a","listen_count":518,"release_mbid":"2a7a4450-32da-4e21-9893-9b7282eb9c4a","release_name":"Bloom"},{"artist_mbids":["074e3847-f67f-49f9-81f1-8c8cea147e8e"],"artist_name":"Bring Me the Horizon","artists":[{"artist_credit_name":"Bring Me the Horizon","artist_mbid":"074e3847-f67f-49f9-81f1-8c8cea147e8e","join_phrase":""}],"caa_id":16426388091,"caa_release_mbid":"a78e93a9-6073-4d80-a69b-d008936257f3","listen_count":513,"release_mbid":"a78e93a9-6073-4d80-a69b-d008936257f3","release_name":"That\u2019s the Spirit"},{"artist_mbids":["21a14ee3-cede-420a-9d6d-a33517d2f952"],"artist_name":"Alec Benjamin","artists":[{"artist_credit_name":"Alec Benjamin","artist_mbid":"21a14ee3-cede-420a-9d6d-a33517d2f952","join_phrase":""}],"caa_id":31885394071,"caa_release_mbid":"946b0030-b999-4f21-b9d1-9e82f02e6d96","listen_count":496,"release_mbid":"eeb50973-2e71-4d04-939e-298ba8bc0af8","release_name":"(Un)Commentary"},{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artist_name":"Linkin Park","artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":21070222550,"caa_release_mbid":"127e18cd-99ad-3193-ad4f-441bee3c6e3b","listen_count":495,"release_mbid":"127e18cd-99ad-3193-ad4f-441bee3c6e3b","release_name":"Minutes to Midnight"},{"artist_mbids":[],"artist_name":"Sanam Malik, ANA","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":419,"release_mbid":null,"release_name":"Jaane Kaise"},{"artist_mbids":["0ab49580-c84f-44d4-875f-d83760ea2cfe"],"artist_name":"Maroon 5","artists":[{"artist_credit_name":"Maroon 5","artist_mbid":"0ab49580-c84f-44d4-875f-d83760ea2cfe","join_phrase":""}],"caa_id":22165193878,"caa_release_mbid":"28832231-f70d-4937-8cbd-d4b679195d34","listen_count":384,"release_mbid":"28832231-f70d-4937-8cbd-d4b679195d34","release_name":"V"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":32210635464,"caa_release_mbid":"b94f45e0-7a1a-4895-a45b-3abd2ec5eda2","listen_count":374,"release_mbid":"b94f45e0-7a1a-4895-a45b-3abd2ec5eda2","release_name":"MAINSTREAM SELLOUT"},{"artist_mbids":["e1564e98-978b-4947-8698-f6fd6f8b0181"],"artist_name":"Fort Minor","artists":[{"artist_credit_name":"Fort Minor","artist_mbid":"e1564e98-978b-4947-8698-f6fd6f8b0181","join_phrase":""}],"caa_id":5891285623,"caa_release_mbid":"4b6ca48c-f7db-439d-ba57-6104b5fec61e","listen_count":371,"release_mbid":"4b6ca48c-f7db-439d-ba57-6104b5fec61e","release_name":"The Rising Tied"},{"artist_mbids":["89ad4ac3-39f7-470e-963a-56509c546377"],"artist_name":"\u30f4\u30a1\u30ea\u30a2\u30b9\u30fb\u30a2\u30fc\u30c6\u30a3\u30b9\u30c8","artists":[{"artist_credit_name":"\u30f4\u30a1\u30ea\u30a2\u30b9\u30fb\u30a2\u30fc\u30c6\u30a3\u30b9\u30c8","artist_mbid":"89ad4ac3-39f7-470e-963a-56509c546377","join_phrase":""}],"caa_id":38587113752,"caa_release_mbid":"9efe4f5e-3634-4aa9-94c7-43d960955eea","listen_count":351,"release_mbid":"9efe4f5e-3634-4aa9-94c7-43d960955eea","release_name":"\u604b\u3057\u305f\u304f\u306a\u308b\u30e9\u30d6\u30bd\u30f3\u30b0"},{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artist_name":"Linkin Park","artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":17668660885,"caa_release_mbid":"7f1a6e87-9ad2-4629-b9a5-563525607310","listen_count":343,"release_mbid":"7f1a6e87-9ad2-4629-b9a5-563525607310","release_name":"Living Things"},{"artist_mbids":["b1e26560-60e5-4236-bbdb-9aa5a8d5ee19"],"artist_name":"Post Malone","artists":[{"artist_credit_name":"Post Malone","artist_mbid":"b1e26560-60e5-4236-bbdb-9aa5a8d5ee19","join_phrase":""}],"caa_id":24123818307,"caa_release_mbid":"fe2c8953-e3d5-40fe-a855-cdb5eb8357a0","listen_count":342,"release_mbid":"fe2c8953-e3d5-40fe-a855-cdb5eb8357a0","release_name":"Hollywood\u2019s Bleeding"},{"artist_mbids":["ada7a83c-e3e1-40f1-93f9-3e73dbc9298a"],"artist_name":"Arctic Monkeys","artists":[{"artist_credit_name":"Arctic Monkeys","artist_mbid":"ada7a83c-e3e1-40f1-93f9-3e73dbc9298a","join_phrase":""}],"caa_id":32131848311,"caa_release_mbid":"55171afe-440e-4c63-947c-e49074f3d5b5","listen_count":339,"release_mbid":"55171afe-440e-4c63-947c-e49074f3d5b5","release_name":"AM"},{"artist_mbids":["985f7e6f-0a7e-4de7-b9ec-a5dac63cb2f7"],"artist_name":"ZAYN","artists":[{"artist_credit_name":"ZAYN","artist_mbid":"985f7e6f-0a7e-4de7-b9ec-a5dac63cb2f7","join_phrase":""}],"caa_id":22079449622,"caa_release_mbid":"871f8a05-6620-41dc-a569-624122029ce7","listen_count":321,"release_mbid":"6d4045e6-432d-4302-af8e-923e581ad6c7","release_name":"Icarus Falls"},{"artist_mbids":[],"artist_name":"TakaseToya, emi noda","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":319,"release_mbid":null,"release_name":"The 2nd of Undecimber"},{"artist_mbids":["6f500293-7396-4903-b4fd-118127d06f9e","ddd49f88-6367-4f58-9dd9-a767e976b0b7"],"artist_name":"RADWIMPS feat. \u5341\u660e","artists":[{"artist_credit_name":"RADWIMPS","artist_mbid":"6f500293-7396-4903-b4fd-118127d06f9e","join_phrase":" feat. "},{"artist_credit_name":"\u5341\u660e","artist_mbid":"ddd49f88-6367-4f58-9dd9-a767e976b0b7","join_phrase":""}],"caa_id":33711108184,"caa_release_mbid":"4fc95fab-017c-4133-affc-7f571bb55fd8","listen_count":317,"release_mbid":"4fc95fab-017c-4133-affc-7f571bb55fd8","release_name":"\u3059\u305a\u3081"},{"artist_mbids":["c8b03190-306c-4120-bb0b-6f2ebfc06ea9"],"artist_name":"The Weeknd","artists":[{"artist_credit_name":"The Weeknd","artist_mbid":"c8b03190-306c-4120-bb0b-6f2ebfc06ea9","join_phrase":""}],"caa_id":16759920843,"caa_release_mbid":"4f61a8bb-aa55-4951-8cfc-0809401db4ca","listen_count":307,"release_mbid":"4f61a8bb-aa55-4951-8cfc-0809401db4ca","release_name":"Starboy"},{"artist_mbids":["f931c961-b647-4861-be8c-f47d84a4de51"],"artist_name":"Diljit Dosanjh","artists":[{"artist_credit_name":"Diljit Dosanjh","artist_mbid":"f931c961-b647-4861-be8c-f47d84a4de51","join_phrase":""}],"caa_id":31442218448,"caa_release_mbid":"70252510-27ce-42f1-953a-f3966a5499a0","listen_count":306,"release_mbid":"70252510-27ce-42f1-953a-f3966a5499a0","release_name":"MoonChild Era"},{"artist_mbids":["98c09f94-10b5-426c-b27a-44345bb54528"],"artist_name":"Sasha Sloan","artists":[{"artist_credit_name":"Sasha Sloan","artist_mbid":"98c09f94-10b5-426c-b27a-44345bb54528","join_phrase":""}],"caa_id":32812417399,"caa_release_mbid":"87231c00-929a-47e6-a633-f464078eb4dc","listen_count":285,"release_mbid":"87231c00-929a-47e6-a633-f464078eb4dc","release_name":"Self Portrait"}],"to_ts":1723248018,"total_release_count":1360,"user_id":"akshaaatt"}} diff --git a/sharedTest/src/main/resources/top_artists.json b/sharedTest/src/main/resources/top_artists.json new file mode 100644 index 00000000..7c93c076 --- /dev/null +++ b/sharedTest/src/main/resources/top_artists.json @@ -0,0 +1 @@ +{"payload":{"artists":[{"artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","artist_name":"mgk","listen_count":3762},{"artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","artist_name":"Linkin Park","listen_count":3000},{"artist_mbid":"e5712ceb-c37a-4c49-a11c-ccf4e21852d4","artist_name":"Troye Sivan","listen_count":2515},{"artist_mbid":"c0ef2ba5-a7b7-40ea-bd27-30acccfcac11","artist_name":"Lauv","listen_count":1730},{"artist_mbid":"20244d07-534f-4eff-b4d4-930878889970","artist_name":"Taylor Swift","listen_count":1038},{"artist_mbid":"3377f3bb-60fc-4403-aea9-7e800612e060","artist_name":"Halsey","listen_count":1014},{"artist_mbid":"c262b6bf-be56-4b26-bceb-42d7a27342f3","artist_name":"Mike Shinoda","listen_count":1003},{"artist_mbid":"074e3847-f67f-49f9-81f1-8c8cea147e8e","artist_name":"Bring Me the Horizon","listen_count":983},{"artist_mbid":"f4fdbb4c-e4b7-47a0-b83b-d91bbfcfa387","artist_name":"Ariana Grande","listen_count":747},{"artist_mbid":"21a14ee3-cede-420a-9d6d-a33517d2f952","artist_name":"Alec Benjamin","listen_count":728},{"artist_mbid":"1a425bbd-cca4-4b2c-aeb7-71cb176c828a","artist_name":"One Direction","listen_count":696},{"artist_mbid":"ada7a83c-e3e1-40f1-93f9-3e73dbc9298a","artist_name":"Arctic Monkeys","listen_count":687},{"artist_mbid":"0ab49580-c84f-44d4-875f-d83760ea2cfe","artist_name":"Maroon 5","listen_count":631},{"artist_mbid":"c8b03190-306c-4120-bb0b-6f2ebfc06ea9","artist_name":"The Weeknd","listen_count":588},{"artist_mbid":"e07d9474-00ea-4460-ac27-88b46b3d976e","artist_name":"blackbear","listen_count":522},{"artist_mbid":"6f500293-7396-4903-b4fd-118127d06f9e","artist_name":"RADWIMPS","listen_count":448},{"artist_mbid":"e1564e98-978b-4947-8698-f6fd6f8b0181","artist_name":"Fort Minor","listen_count":431},{"artist_mbid":"985f7e6f-0a7e-4de7-b9ec-a5dac63cb2f7","artist_name":"ZAYN","listen_count":421},{"artist_mbid":null,"artist_name":"Sanam Malik, ANA","listen_count":419},{"artist_mbid":"f931c961-b647-4861-be8c-f47d84a4de51","artist_name":"Diljit Dosanjh","listen_count":419},{"artist_mbid":"c984f772-a70d-44b6-ad12-47ec8d3fdd35","artist_name":"Aditya A","listen_count":419},{"artist_mbid":"b1e26560-60e5-4236-bbdb-9aa5a8d5ee19","artist_name":"Post Malone","listen_count":364},{"artist_mbid":"4757df70-6c3e-46b8-99c0-a68644595c9a","artist_name":"Julia Michaels","listen_count":361},{"artist_mbid":"68fdc9cc-1094-48bb-942f-66492343e41c","artist_name":"Calum Scott","listen_count":361},{"artist_mbid":"525f1f1c-03f0-4bc8-8dfd-e7521f87631b","artist_name":"Charlie Puth","listen_count":335}],"count":25,"from_ts":1009843200,"last_updated":1723268040,"offset":0,"range":"all_time","to_ts":1723248018,"total_artist_count":1093,"user_id":"akshaaatt"}} diff --git a/sharedTest/src/main/resources/top_songs.json b/sharedTest/src/main/resources/top_songs.json new file mode 100644 index 00000000..6adb7ea1 --- /dev/null +++ b/sharedTest/src/main/resources/top_songs.json @@ -0,0 +1 @@ +{"payload":{"count":25,"from_ts":1009843200,"last_updated":1723270823,"offset":0,"range":"all_time","recordings":[{"artist_mbids":["21a14ee3-cede-420a-9d6d-a33517d2f952"],"artist_name":"Alec Benjamin","artists":[{"artist_credit_name":"Alec Benjamin","artist_mbid":"21a14ee3-cede-420a-9d6d-a33517d2f952","join_phrase":""}],"caa_id":31885394071,"caa_release_mbid":"946b0030-b999-4f21-b9d1-9e82f02e6d96","listen_count":431,"recording_mbid":"e2ef1fc7-88c5-44c0-abde-7144f55d15bd","release_mbid":"eeb50973-2e71-4d04-939e-298ba8bc0af8","release_name":"(Un)Commentary","track_name":"Older"},{"artist_mbids":[],"artist_name":"Sanam Malik, ANA","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":419,"recording_mbid":null,"release_mbid":null,"release_name":"Jaane Kaise","track_name":"Jaane Kaise"},{"artist_mbids":["c0ef2ba5-a7b7-40ea-bd27-30acccfcac11"],"artist_name":"Lauv","artists":[{"artist_credit_name":"Lauv","artist_mbid":"c0ef2ba5-a7b7-40ea-bd27-30acccfcac11","join_phrase":""}],"caa_id":25651991080,"caa_release_mbid":"63b5d3be-6538-4fc9-ac7d-a42334aebd85","listen_count":411,"recording_mbid":"4fbb1e4a-c5c7-4694-a971-a3668bcffa17","release_mbid":"63b5d3be-6538-4fc9-ac7d-a42334aebd85","release_name":"~how i\u2019m feeling~","track_name":"Feelings"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33","e07d9474-00ea-4460-ac27-88b46b3d976e"],"artist_name":"Machine Gun Kelly feat. blackbear","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":" feat. "},{"artist_credit_name":"blackbear","artist_mbid":"e07d9474-00ea-4460-ac27-88b46b3d976e","join_phrase":""}],"caa_id":30564814068,"caa_release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","listen_count":399,"recording_mbid":"6cd38dad-148f-4381-9cf4-b75f496238c6","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","release_name":"Tickets to My Downfall","track_name":"my ex\u2019s best friend"},{"artist_mbids":["e5712ceb-c37a-4c49-a11c-ccf4e21852d4"],"artist_name":"Troye Sivan","artists":[{"artist_credit_name":"Troye Sivan","artist_mbid":"e5712ceb-c37a-4c49-a11c-ccf4e21852d4","join_phrase":""}],"caa_id":11788642695,"caa_release_mbid":"e7d13ff4-ec74-4eff-b0d3-bdc00b720944","listen_count":390,"recording_mbid":"15e7a501-4f35-4977-8956-0e0c8b644af6","release_mbid":"b6d0161d-1073-48e2-8503-4bd3261899ee","release_name":"Blue Neighbourhood","track_name":"LOST BOY"},{"artist_mbids":["e5712ceb-c37a-4c49-a11c-ccf4e21852d4"],"artist_name":"Troye Sivan","artists":[{"artist_credit_name":"Troye Sivan","artist_mbid":"e5712ceb-c37a-4c49-a11c-ccf4e21852d4","join_phrase":""}],"caa_id":38587113752,"caa_release_mbid":"9efe4f5e-3634-4aa9-94c7-43d960955eea","listen_count":351,"recording_mbid":"0f8d5cc0-7e6e-486e-9544-eca4736cd8b7","release_mbid":"9efe4f5e-3634-4aa9-94c7-43d960955eea","release_name":"\u604b\u3057\u305f\u304f\u306a\u308b\u30e9\u30d6\u30bd\u30f3\u30b0","track_name":"Strawberries & Cigarettes"},{"artist_mbids":["e5712ceb-c37a-4c49-a11c-ccf4e21852d4"],"artist_name":"Troye Sivan","artists":[{"artist_credit_name":"Troye Sivan","artist_mbid":"e5712ceb-c37a-4c49-a11c-ccf4e21852d4","join_phrase":""}],"caa_id":20981274842,"caa_release_mbid":"2a7a4450-32da-4e21-9893-9b7282eb9c4a","listen_count":319,"recording_mbid":"f75b6b94-46b3-4c0a-af56-05b4dfbee957","release_mbid":"2a7a4450-32da-4e21-9893-9b7282eb9c4a","release_name":"Bloom","track_name":"Lucky Strike"},{"artist_mbids":[],"artist_name":"TakaseToya, emi noda","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":319,"recording_mbid":null,"release_mbid":null,"release_name":"The 2nd of Undecimber","track_name":"Doushite"},{"artist_mbids":["6f500293-7396-4903-b4fd-118127d06f9e","ddd49f88-6367-4f58-9dd9-a767e976b0b7"],"artist_name":"RADWIMPS feat. \u5341\u660e","artists":[{"artist_credit_name":"RADWIMPS","artist_mbid":"6f500293-7396-4903-b4fd-118127d06f9e","join_phrase":" feat. "},{"artist_credit_name":"\u5341\u660e","artist_mbid":"ddd49f88-6367-4f58-9dd9-a767e976b0b7","join_phrase":""}],"caa_id":33711108184,"caa_release_mbid":"4fc95fab-017c-4133-affc-7f571bb55fd8","listen_count":317,"recording_mbid":"4da804f6-98dc-456b-8fb3-5ffd971b18ec","release_mbid":"4fc95fab-017c-4133-affc-7f571bb55fd8","release_name":"\u3059\u305a\u3081","track_name":"\u3059\u305a\u3081"},{"artist_mbids":["f931c961-b647-4861-be8c-f47d84a4de51"],"artist_name":"Diljit Dosanjh","artists":[{"artist_credit_name":"Diljit Dosanjh","artist_mbid":"f931c961-b647-4861-be8c-f47d84a4de51","join_phrase":""}],"caa_id":31442218448,"caa_release_mbid":"70252510-27ce-42f1-953a-f3966a5499a0","listen_count":296,"recording_mbid":"170dc09e-2d38-407f-aa3f-4807e7d1f3d2","release_mbid":"70252510-27ce-42f1-953a-f3966a5499a0","release_name":"MoonChild Era","track_name":"Lover"},{"artist_mbids":["c8b03190-306c-4120-bb0b-6f2ebfc06ea9","056e4f3e-d505-4dad-8ec1-d04f521cbb56"],"artist_name":"The Weeknd featuring Daft Punk","artists":[{"artist_credit_name":"The Weeknd","artist_mbid":"c8b03190-306c-4120-bb0b-6f2ebfc06ea9","join_phrase":" featuring "},{"artist_credit_name":"Daft Punk","artist_mbid":"056e4f3e-d505-4dad-8ec1-d04f521cbb56","join_phrase":""}],"caa_id":16759920843,"caa_release_mbid":"4f61a8bb-aa55-4951-8cfc-0809401db4ca","listen_count":289,"recording_mbid":"3da01a8e-3c71-408f-a9c7-0c4d10ad4e7d","release_mbid":"4f61a8bb-aa55-4951-8cfc-0809401db4ca","release_name":"Starboy","track_name":"Starboy"},{"artist_mbids":["98c09f94-10b5-426c-b27a-44345bb54528"],"artist_name":"Sasha Sloan","artists":[{"artist_credit_name":"Sasha Sloan","artist_mbid":"98c09f94-10b5-426c-b27a-44345bb54528","join_phrase":""}],"caa_id":32812417399,"caa_release_mbid":"87231c00-929a-47e6-a633-f464078eb4dc","listen_count":284,"recording_mbid":"9ae71082-ac47-4b9c-a12b-a67fff75784a","release_mbid":"87231c00-929a-47e6-a633-f464078eb4dc","release_name":"Self Portrait","track_name":"Dancing With Your Ghost"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":30564814068,"caa_release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","listen_count":277,"recording_mbid":"4c989c32-51a3-489a-965a-bb334def31a8","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","release_name":"Tickets to My Downfall","track_name":"concert for aliens"},{"artist_mbids":[],"artist_name":"ZAYN","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":264,"recording_mbid":null,"release_mbid":null,"release_name":"Icarus Falls","track_name":"There You Are"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":30564814068,"caa_release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","listen_count":263,"recording_mbid":"b9874074-b2d6-4135-bc2a-f4ef44b0ae69","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","release_name":"Tickets to My Downfall","track_name":"title track"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":30564814068,"caa_release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","listen_count":252,"recording_mbid":"b6e4f4ea-b20c-4489-b0e6-c09f40b0ef18","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","release_name":"Tickets to My Downfall","track_name":"jawbreaker"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":30564814068,"caa_release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","listen_count":237,"recording_mbid":"198a6f88-eb15-4280-9208-82998cb39175","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","release_name":"Tickets to My Downfall","track_name":"bloody valentine"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":27494670999,"caa_release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8","listen_count":236,"recording_mbid":"5ca5de59-bdd5-41fb-beae-2b343ee6f90d","release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8","release_name":"Tickets to My Downfall (SOLD OUT Deluxe)","track_name":"hangover cure"},{"artist_mbids":["e5712ceb-c37a-4c49-a11c-ccf4e21852d4"],"artist_name":"Troye Sivan","artists":[{"artist_credit_name":"Troye Sivan","artist_mbid":"e5712ceb-c37a-4c49-a11c-ccf4e21852d4","join_phrase":""}],"caa_id":26731335835,"caa_release_mbid":"0ab6cf1d-48a5-4227-a0f4-70bc47a39b58","listen_count":230,"recording_mbid":"f87aa613-a335-44a8-a702-7b15840dc74b","release_mbid":"0ab6cf1d-48a5-4227-a0f4-70bc47a39b58","release_name":"Easy","track_name":"Easy"},{"artist_mbids":["c8b03190-306c-4120-bb0b-6f2ebfc06ea9"],"artist_name":"The Weeknd","artists":[{"artist_credit_name":"The Weeknd","artist_mbid":"c8b03190-306c-4120-bb0b-6f2ebfc06ea9","join_phrase":""}],"caa_id":25807105705,"caa_release_mbid":"fd7ebee8-8c64-4127-a9be-8e31ed6364e3","listen_count":221,"recording_mbid":"1a67e215-a19e-40c9-9b12-732de134bf5f","release_mbid":"fd7ebee8-8c64-4127-a9be-8e31ed6364e3","release_name":"After Hours","track_name":"Blinding Lights"},{"artist_mbids":[],"artist_name":"Sasha Alex Sloan","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":212,"recording_mbid":null,"release_mbid":null,"release_name":"Older","track_name":"Older"},{"artist_mbids":["aa5891dd-9306-48e5-a1ee-77d2519abb61"],"artist_name":"Dikshant","artists":[{"artist_credit_name":"Dikshant","artist_mbid":"aa5891dd-9306-48e5-a1ee-77d2519abb61","join_phrase":""}],"caa_id":32335009413,"caa_release_mbid":"3626f26a-cda7-4413-9718-7dc61c8bde7c","listen_count":206,"recording_mbid":"8491bdca-0345-4c8a-9421-b53bbffdc168","release_mbid":"3626f26a-cda7-4413-9718-7dc61c8bde7c","release_name":"Aankhon Se Batana","track_name":"Aankhon Se Batana"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":27494670999,"caa_release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8","listen_count":203,"recording_mbid":"cb8be04f-e8b1-425c-b68b-c4a32f2c6532","release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8","release_name":"Tickets to My Downfall (SOLD OUT Deluxe)","track_name":"split a pill"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33","97529a9e-4b5a-4a9c-9c08-c42401d1c268","10a048d2-9afd-42d9-912e-0eb36c724f70"],"artist_name":"Machine Gun Kelly feat. YUNGBLUD & Bert McCracken","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":" feat. "},{"artist_credit_name":"YUNGBLUD","artist_mbid":"97529a9e-4b5a-4a9c-9c08-c42401d1c268","join_phrase":" & "},{"artist_credit_name":"Bert McCracken","artist_mbid":"10a048d2-9afd-42d9-912e-0eb36c724f70","join_phrase":""}],"caa_id":27494670999,"caa_release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8","listen_count":202,"recording_mbid":"25dfdccf-5092-4805-af42-f20146c8d18f","release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8","release_name":"Tickets to My Downfall (SOLD OUT Deluxe)","track_name":"body bag"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":30564814068,"caa_release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","listen_count":196,"recording_mbid":"ddabeccd-b0e5-406e-aa1a-d4fdfa29f99e","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","release_name":"Tickets to My Downfall","track_name":"kiss kiss"}],"to_ts":1723248018,"total_recording_count":2243,"user_id":"akshaaatt"}} From 93f880671fe1a5d35eaa87d670b951bf62824d2b Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Mon, 12 Aug 2024 19:47:21 +0530 Subject: [PATCH 74/97] Added 100% Coverage Unit tests for repository --- .../android/user/UserRepositoryTest.kt | 342 ++++++++++++++++++ .../testdata/UserRepositoryTestData.kt | 4 +- .../sharedtest/utils/ResourceString.kt | 10 + .../resources/global_listening_activity.json | 2 +- .../src/main/resources/listen_count.json | 1 + .../main/resources/listening_activity.json | 2 +- .../src/main/resources/loved_hated_songs.json | 2 +- sharedTest/src/main/resources/pins.json | 2 +- .../main/resources/similar_user_error.json | 1 + .../resources/similar_users_response.json | 2 +- sharedTest/src/main/resources/top_albums.json | 2 +- .../src/main/resources/top_artists.json | 2 +- sharedTest/src/main/resources/top_songs.json | 2 +- 13 files changed, 364 insertions(+), 10 deletions(-) create mode 100644 app/src/test/java/org/listenbrainz/android/user/UserRepositoryTest.kt create mode 100644 sharedTest/src/main/resources/listen_count.json create mode 100644 sharedTest/src/main/resources/similar_user_error.json diff --git a/app/src/test/java/org/listenbrainz/android/user/UserRepositoryTest.kt b/app/src/test/java/org/listenbrainz/android/user/UserRepositoryTest.kt new file mode 100644 index 00000000..f3ca96cd --- /dev/null +++ b/app/src/test/java/org/listenbrainz/android/user/UserRepositoryTest.kt @@ -0,0 +1,342 @@ +package org.listenbrainz.android.user + +import kotlinx.coroutines.test.runTest +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.listenbrainz.android.model.ResponseError +import org.listenbrainz.android.repository.user.UserRepository +import org.listenbrainz.android.repository.user.UserRepositoryImpl +import org.listenbrainz.android.service.UserService +import org.listenbrainz.android.util.Resource +import org.listenbrainz.sharedtest.testdata.UserRepositoryTestData.listenCountTestData +import org.listenbrainz.sharedtest.utils.EntityTestUtils.testSomeOtherUser +import org.listenbrainz.sharedtest.utils.EntityTestUtils.testUserDNE +import org.listenbrainz.sharedtest.utils.EntityTestUtils.testUsername +import org.listenbrainz.sharedtest.utils.ResourceString.all_pins +import org.listenbrainz.sharedtest.utils.ResourceString.current_pins +import org.listenbrainz.sharedtest.utils.ResourceString.globalListeningActivity +import org.listenbrainz.sharedtest.utils.ResourceString.listenCount +import org.listenbrainz.sharedtest.utils.ResourceString.loved_hated_songs +import org.listenbrainz.sharedtest.utils.ResourceString.similarUserError +import org.listenbrainz.sharedtest.utils.ResourceString.similarUserErrorString +import org.listenbrainz.sharedtest.utils.ResourceString.similar_users_response +import org.listenbrainz.sharedtest.utils.ResourceString.topAlbums +import org.listenbrainz.sharedtest.utils.ResourceString.topSongs +import org.listenbrainz.sharedtest.utils.ResourceString.top_artists +import org.listenbrainz.sharedtest.utils.ResourceString.userListeningActivity +import org.listenbrainz.sharedtest.utils.ResourceString.user_does_not_exist_error +import org.listenbrainz.sharedtest.utils.RetrofitUtils + +class UserRepositoryTest { + + private lateinit var webServer: MockWebServer + private lateinit var repository: UserRepository + + @Before + fun setUp() { + webServer = MockWebServer() + webServer.dispatcher = object: Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + return when(request.path){ + // Listen Count + "/user/$testUsername/listen-count" -> { + MockResponse().setResponseCode(200).setBody( + listenCount + ) + } + + "/user/$testUserDNE/listen-count" -> { + MockResponse().setResponseCode(404).setBody( + user_does_not_exist_error + ) + } + + // Similar Users + "/user/$testUsername/similar-to/$testSomeOtherUser" -> { + MockResponse().setResponseCode(200).setBody( + similar_users_response + ) + } + + "/user/$testUsername/similar-to/$testUsername" -> { + MockResponse().setResponseCode(404).setBody( + similarUserError + ) + } + + // Current pins + "/$testUsername/pins/current" -> { + MockResponse().setResponseCode(200).setBody( + current_pins + ) + } + + "/$testUserDNE/pins/current" -> { + MockResponse().setResponseCode(404).setBody( + user_does_not_exist_error + ) + } + + // All Pins + "/$testUsername/pins" -> { + MockResponse().setResponseCode(200).setBody( + all_pins + ) + } + + "/$testUserDNE/pins" -> { + MockResponse().setResponseCode(404).setBody( + user_does_not_exist_error + ) + } + + // Artists + "/stats/user/$testUsername/artists?range=all_time&count=25" -> { + MockResponse().setResponseCode(200).setBody( + top_artists + ) + } + + "/stats/user/$testUserDNE/artists?range=all_time&count=25" -> { + MockResponse().setResponseCode(404).setBody( + user_does_not_exist_error + ) + } + + // Loved Hated Songs + "/feedback/user/$testUsername/get-feedback?metadata=true" -> { + MockResponse().setResponseCode(200).setBody( + loved_hated_songs + ) + } + + "/feedback/user/$testUserDNE/get-feedback?metadata=true" -> { + MockResponse().setResponseCode(404).setBody( + user_does_not_exist_error + ) + } + + // Listening Activity + "/stats/user/$testUsername/listening-activity?range=all_time" -> { + MockResponse().setResponseCode(200).setBody( + userListeningActivity + ) + } + + "/stats/user/$testUserDNE/listening-activity?range=all_time" -> { + MockResponse().setResponseCode(404).setBody( + user_does_not_exist_error + ) + } + + // Global Listening Activity + "/stats/sitewide/listening-activity?range=all_time" -> { + MockResponse().setResponseCode(200).setBody( + globalListeningActivity + ) + } + + // Top Albums + "/stats/user/$testUsername/releases?range=all_time" -> { + MockResponse().setResponseCode(200).setBody( + topAlbums + ) + } + + "/stats/user/$testUserDNE/releases?range=all_time" -> { + MockResponse().setResponseCode(404).setBody( + user_does_not_exist_error + ) + } + + // Top Songs + "/stats/user/$testUsername/recordings?range=all_time" -> { + MockResponse().setResponseCode(200).setBody( + topSongs + ) + } + + "/stats/user/$testUserDNE/recordings?range=all_time" -> { + MockResponse().setResponseCode(404).setBody( + user_does_not_exist_error + ) + } + + else -> { + MockResponse().setResponseCode(404) + } + } + } + + } + webServer.start() + val service = RetrofitUtils.createTestService(UserService::class.java, webServer.url("/")) + repository = UserRepositoryImpl(service) + } + + @After + fun teardown() { + webServer.close() + } + + @Test + fun `fetch listen count for existing user`() = runTest { + val result = repository.fetchUserListenCount(testUsername) + assertEquals(Resource.Status.SUCCESS, result.status) + assertEquals(listenCountTestData.payload.count, result.data?.payload?.count) + } + + @Test + fun `fetch listen count for non-existing user`() = runTest { + val result = repository.fetchUserListenCount(testUserDNE) + assertEquals(Resource.Status.FAILED, result.status) + assertEquals(ResponseError.DOES_NOT_EXIST, result.error) + } + + @Test + fun `fetch similar users for valid comparison`() = runTest { + val result = repository.fetchUserSimilarity(testUsername, testSomeOtherUser) + assertEquals(Resource.Status.SUCCESS, result.status) + assertNotNull(result.data) + assertEquals("jivteshs20", result.data?.userSimilarity?.username) + } + + @Test + fun `fetch similar users for invalid comparison`() = runTest { + val result = repository.fetchUserSimilarity(testUsername, testUsername) + assertEquals(Resource.Status.FAILED, result.status) + assertEquals(similarUserErrorString, result.error?.actualResponse) + } + + @Test + fun `fetch current pins for existing user`() = runTest { + val result = repository.fetchUserCurrentPins(testUsername) + assertEquals(Resource.Status.SUCCESS, result.status) + assertNotNull(result.data) + assertEquals(1.72335654E9f, result.data?.pinnedRecording?.created) + assertEquals("Noice", result.data?.pinnedRecording?.blurbContent) + } + + @Test + fun `fetch current pins for non-existing user`() = runTest { + val result = repository.fetchUserCurrentPins(testUserDNE) + assertEquals(Resource.Status.FAILED, result.status) + assertEquals(ResponseError.DOES_NOT_EXIST, result.error) + } + + @Test + fun `fetch all pins for existing user`() = runTest { + val result = repository.fetchUserPins(testUsername) + assertEquals(Resource.Status.SUCCESS, result.status) + assertNotNull(result.data) + assertEquals(12, result.data?.count) + assertEquals("6f4a50ca-b636-4c0b-a6a0-5b84451ab014", result.data?.pinnedRecordings?.get(0)?.recordingMsid) + assertEquals(testUsername, result.data?.userName) + } + + @Test + fun `fetch all pins for non-existing user`() = runTest { + val result = repository.fetchUserPins(testUserDNE) + assertEquals(Resource.Status.FAILED, result.status) + assertEquals(ResponseError.DOES_NOT_EXIST, result.error) + } + + @Test + fun `fetch top artists for existing user`() = runTest { + val result = repository.getTopArtists(testUsername) + assertEquals(Resource.Status.SUCCESS, result.status) + assertNotNull(result.data) + assertEquals("Karan Aujla", result.data?.payload?.artists?.get(0)?.artistName) + assertEquals(25, result.data?.payload?.count) + } + + @Test + fun `fetch top artists for non-existing user`() = runTest { + val result = repository.getTopArtists(testUserDNE) + assertEquals(Resource.Status.FAILED, result.status) + assertEquals(ResponseError.DOES_NOT_EXIST, result.error) + } + + @Test + fun `fetch loved and hated songs for existing user`() = runTest { + val result = repository.getUserFeedback(testUsername, null) + assertEquals(Resource.Status.SUCCESS, result.status) + assertNotNull(result.data) + assertEquals(25, result.data?.count) + assertEquals("Calling", result.data?.feedback?.get(0)?.trackMetadata?.trackName) + assertEquals(testUsername, result?.data?.feedback?.get(2)?.userId) + } + + @Test + fun `fetch loved and hated songs for non-existing user`() = runTest { + val result = repository.getUserFeedback(testUserDNE, null) + assertEquals(Resource.Status.FAILED, result.status) + assertEquals(ResponseError.DOES_NOT_EXIST, result.error) + } + + @Test + fun `fetch user listening activity for existing user`() = runTest { + val result = repository.getUserListeningActivity(testUsername) + assertEquals(Resource.Status.SUCCESS, result.status) + assertNotNull(result.data) + assertEquals(testUsername, result.data?.payload?.userId) + assertEquals(2826, result.data?.payload?.listeningActivity?.get(21)?.listenCount) + } + + @Test + fun `fetch user listening activity for non-existing user`() = runTest { + val result = repository.getUserListeningActivity(testUserDNE) + assertEquals(Resource.Status.FAILED, result.status) + assertEquals(ResponseError.DOES_NOT_EXIST, result.error) + } + + @Test + fun `fetch global listening activity`() = runTest { + val result = repository.getGlobalListeningActivity() + assertEquals(Resource.Status.SUCCESS, result.status) + assertNotNull(result.data) + assertEquals(4499100, result.data?.payload?.listeningActivity?.get(3)?.listenCount) + assertNull(result.data?.payload?.userId) + } + + @Test + fun `fetch top albums for existing user`() = runTest { + val result = repository.getTopAlbums(testUsername) + assertEquals(Resource.Status.SUCCESS, result.status) + assertNotNull(result.data) + assertEquals("Small Circle", result.data?.payload?.releases?.get(2)?.releaseName) + assertEquals(testUsername, result.data?.payload?.userId) + } + + @Test + fun `fetch top albums for non-existing user`() = runTest { + val result = repository.getTopAlbums(testUserDNE) + assertEquals(Resource.Status.FAILED, result.status) + assertEquals(ResponseError.DOES_NOT_EXIST, result.error) + } + + @Test + fun `fetch top songs for existing user`() = runTest { + val result = repository.getTopSongs(testUsername) + assertEquals(Resource.Status.SUCCESS, result.status) + assertNotNull(result.data) + assertEquals(testUsername, result.data?.payload?.userId) + assertEquals("Small Circle", result.data?.payload?.recordings?.get(0)?.releaseName) + } + + @Test + fun `fetch top songs for non-existing user`() = runTest { + val result = repository.getTopSongs(testUserDNE) + assertEquals(Resource.Status.FAILED, result.status) + assertEquals(ResponseError.DOES_NOT_EXIST, result.error) + } + +} \ No newline at end of file diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/testdata/UserRepositoryTestData.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/testdata/UserRepositoryTestData.kt index 20a68c33..ce7f4d1c 100644 --- a/sharedTest/src/main/java/org/listenbrainz/sharedtest/testdata/UserRepositoryTestData.kt +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/testdata/UserRepositoryTestData.kt @@ -24,10 +24,10 @@ import org.listenbrainz.sharedtest.utils.ResourceString.userListeningActivity object UserRepositoryTestData { val listenCountTestData: Listens = Listens( payload = Payload( - count = 34946, + count = 3252, latest_listen_ts = 0, listens = listOf(), - user_id = "akshaaatt" + user_id = "Jasjeet" ) ) diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/utils/ResourceString.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/utils/ResourceString.kt index 1a237663..f089956a 100644 --- a/sharedTest/src/main/java/org/listenbrainz/sharedtest/utils/ResourceString.kt +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/utils/ResourceString.kt @@ -108,6 +108,16 @@ object ResourceString { val topSongs by lazy { EntityTestUtils.loadResourceAsString("top_songs.json") } + + const val similarUserErrorString = "Similar-to user not found" + + val similarUserError by lazy { + EntityTestUtils.loadResourceAsString("similar_user_error.json") + } + + val listenCount by lazy { + EntityTestUtils.loadResourceAsString("listen_count.json") + } fun String.toClass(): T { return Gson().fromJson(this, object: TypeToken() {}.type) diff --git a/sharedTest/src/main/resources/global_listening_activity.json b/sharedTest/src/main/resources/global_listening_activity.json index b300b5f6..1e4e4a71 100644 --- a/sharedTest/src/main/resources/global_listening_activity.json +++ b/sharedTest/src/main/resources/global_listening_activity.json @@ -1 +1 @@ -{"payload":{"from_ts":1640995200,"last_updated":1723265196,"listening_activity":[{"from_ts":1640995200,"listen_count":6691289,"time_range":"January 2022","to_ts":1643673599},{"from_ts":1643673600,"listen_count":6134594,"time_range":"February 2022","to_ts":1646092799},{"from_ts":1646092800,"listen_count":6324802,"time_range":"March 2022","to_ts":1648771199},{"from_ts":1648771200,"listen_count":6565411,"time_range":"April 2022","to_ts":1651363199},{"from_ts":1651363200,"listen_count":6940411,"time_range":"May 2022","to_ts":1654041599},{"from_ts":1654041600,"listen_count":6554844,"time_range":"June 2022","to_ts":1656633599},{"from_ts":1656633600,"listen_count":6664966,"time_range":"July 2022","to_ts":1659311999},{"from_ts":1659312000,"listen_count":6607955,"time_range":"August 2022","to_ts":1661990399},{"from_ts":1661990400,"listen_count":6429763,"time_range":"September 2022","to_ts":1664582399},{"from_ts":1664582400,"listen_count":6852810,"time_range":"October 2022","to_ts":1667260799},{"from_ts":1667260800,"listen_count":6724437,"time_range":"November 2022","to_ts":1669852799},{"from_ts":1669852800,"listen_count":7021955,"time_range":"December 2022","to_ts":1672531199},{"from_ts":1672531200,"listen_count":7013311,"time_range":"January 2023","to_ts":1675209599},{"from_ts":1675209600,"listen_count":6435625,"time_range":"February 2023","to_ts":1677628799},{"from_ts":1677628800,"listen_count":7010537,"time_range":"March 2023","to_ts":1680307199},{"from_ts":1680307200,"listen_count":6569518,"time_range":"April 2023","to_ts":1682899199},{"from_ts":1682899200,"listen_count":6734487,"time_range":"May 2023","to_ts":1685577599},{"from_ts":1685577600,"listen_count":6369453,"time_range":"June 2023","to_ts":1688169599},{"from_ts":1688169600,"listen_count":6442968,"time_range":"July 2023","to_ts":1690847999},{"from_ts":1690848000,"listen_count":6400835,"time_range":"August 2023","to_ts":1693526399},{"from_ts":1693526400,"listen_count":6330734,"time_range":"September 2023","to_ts":1696118399},{"from_ts":1696118400,"listen_count":6711283,"time_range":"October 2023","to_ts":1698796799},{"from_ts":1698796800,"listen_count":6367547,"time_range":"November 2023","to_ts":1701388799},{"from_ts":1701388800,"listen_count":6567587,"time_range":"December 2023","to_ts":1704067199}],"range":"year","to_ts":1704067200}} +{"payload":{"from_ts":1009843200,"last_updated":1723276132,"listening_activity":[{"from_ts":1009843200,"listen_count":0,"time_range":"2002","to_ts":1041379199},{"from_ts":1041379200,"listen_count":0,"time_range":"2003","to_ts":1072915199},{"from_ts":1072915200,"listen_count":0,"time_range":"2004","to_ts":1104537599},{"from_ts":1104537600,"listen_count":4499100,"time_range":"2005","to_ts":1136073599},{"from_ts":1136073600,"listen_count":14968386,"time_range":"2006","to_ts":1167609599},{"from_ts":1167609600,"listen_count":23196904,"time_range":"2007","to_ts":1199145599},{"from_ts":1199145600,"listen_count":29162509,"time_range":"2008","to_ts":1230767999},{"from_ts":1230768000,"listen_count":35131923,"time_range":"2009","to_ts":1262303999},{"from_ts":1262304000,"listen_count":38607004,"time_range":"2010","to_ts":1293839999},{"from_ts":1293840000,"listen_count":40623372,"time_range":"2011","to_ts":1325375999},{"from_ts":1325376000,"listen_count":42543270,"time_range":"2012","to_ts":1356998399},{"from_ts":1356998400,"listen_count":44428432,"time_range":"2013","to_ts":1388534399},{"from_ts":1388534400,"listen_count":46991101,"time_range":"2014","to_ts":1420070399},{"from_ts":1420070400,"listen_count":47381647,"time_range":"2015","to_ts":1451606399},{"from_ts":1451606400,"listen_count":48389126,"time_range":"2016","to_ts":1483228799},{"from_ts":1483228800,"listen_count":55776319,"time_range":"2017","to_ts":1514764799},{"from_ts":1514764800,"listen_count":60656566,"time_range":"2018","to_ts":1546300799},{"from_ts":1546300800,"listen_count":61831361,"time_range":"2019","to_ts":1577836799},{"from_ts":1577836800,"listen_count":68597734,"time_range":"2020","to_ts":1609459199},{"from_ts":1609459200,"listen_count":74473231,"time_range":"2021","to_ts":1640995199},{"from_ts":1640995200,"listen_count":79513237,"time_range":"2022","to_ts":1672531199},{"from_ts":1672531200,"listen_count":78953885,"time_range":"2023","to_ts":1704067199},{"from_ts":1704067200,"listen_count":43640832,"time_range":"2024","to_ts":1735689599}],"range":"all_time","to_ts":1723248018}} diff --git a/sharedTest/src/main/resources/listen_count.json b/sharedTest/src/main/resources/listen_count.json new file mode 100644 index 00000000..786573da --- /dev/null +++ b/sharedTest/src/main/resources/listen_count.json @@ -0,0 +1 @@ +{"payload":{"count":3252}} \ No newline at end of file diff --git a/sharedTest/src/main/resources/listening_activity.json b/sharedTest/src/main/resources/listening_activity.json index 13c5acd3..15691c62 100644 --- a/sharedTest/src/main/resources/listening_activity.json +++ b/sharedTest/src/main/resources/listening_activity.json @@ -1 +1 @@ -{"payload":{"from_ts":1640995200,"last_updated":1723265175,"listening_activity":[{"from_ts":1640995200,"listen_count":1148,"time_range":"January 2022","to_ts":1643673599},{"from_ts":1643673600,"listen_count":1177,"time_range":"February 2022","to_ts":1646092799},{"from_ts":1646092800,"listen_count":894,"time_range":"March 2022","to_ts":1648771199},{"from_ts":1648771200,"listen_count":948,"time_range":"April 2022","to_ts":1651363199},{"from_ts":1651363200,"listen_count":1154,"time_range":"May 2022","to_ts":1654041599},{"from_ts":1654041600,"listen_count":1234,"time_range":"June 2022","to_ts":1656633599},{"from_ts":1656633600,"listen_count":1066,"time_range":"July 2022","to_ts":1659311999},{"from_ts":1659312000,"listen_count":1164,"time_range":"August 2022","to_ts":1661990399},{"from_ts":1661990400,"listen_count":884,"time_range":"September 2022","to_ts":1664582399},{"from_ts":1664582400,"listen_count":536,"time_range":"October 2022","to_ts":1667260799},{"from_ts":1667260800,"listen_count":789,"time_range":"November 2022","to_ts":1669852799},{"from_ts":1669852800,"listen_count":714,"time_range":"December 2022","to_ts":1672531199},{"from_ts":1672531200,"listen_count":841,"time_range":"January 2023","to_ts":1675209599},{"from_ts":1675209600,"listen_count":718,"time_range":"February 2023","to_ts":1677628799},{"from_ts":1677628800,"listen_count":878,"time_range":"March 2023","to_ts":1680307199},{"from_ts":1680307200,"listen_count":848,"time_range":"April 2023","to_ts":1682899199},{"from_ts":1682899200,"listen_count":752,"time_range":"May 2023","to_ts":1685577599},{"from_ts":1685577600,"listen_count":654,"time_range":"June 2023","to_ts":1688169599},{"from_ts":1688169600,"listen_count":491,"time_range":"July 2023","to_ts":1690847999},{"from_ts":1690848000,"listen_count":744,"time_range":"August 2023","to_ts":1693526399},{"from_ts":1693526400,"listen_count":867,"time_range":"September 2023","to_ts":1696118399},{"from_ts":1696118400,"listen_count":873,"time_range":"October 2023","to_ts":1698796799},{"from_ts":1698796800,"listen_count":827,"time_range":"November 2023","to_ts":1701388799},{"from_ts":1701388800,"listen_count":696,"time_range":"December 2023","to_ts":1704067199}],"range":"year","to_ts":1704067200,"user_id":"akshaaatt"}} +{"payload":{"from_ts":1009843200,"last_updated":1723274592,"listening_activity":[{"from_ts":1009843200,"listen_count":0,"time_range":"2002","to_ts":1041379199},{"from_ts":1041379200,"listen_count":0,"time_range":"2003","to_ts":1072915199},{"from_ts":1072915200,"listen_count":0,"time_range":"2004","to_ts":1104537599},{"from_ts":1104537600,"listen_count":0,"time_range":"2005","to_ts":1136073599},{"from_ts":1136073600,"listen_count":0,"time_range":"2006","to_ts":1167609599},{"from_ts":1167609600,"listen_count":0,"time_range":"2007","to_ts":1199145599},{"from_ts":1199145600,"listen_count":0,"time_range":"2008","to_ts":1230767999},{"from_ts":1230768000,"listen_count":0,"time_range":"2009","to_ts":1262303999},{"from_ts":1262304000,"listen_count":0,"time_range":"2010","to_ts":1293839999},{"from_ts":1293840000,"listen_count":0,"time_range":"2011","to_ts":1325375999},{"from_ts":1325376000,"listen_count":0,"time_range":"2012","to_ts":1356998399},{"from_ts":1356998400,"listen_count":0,"time_range":"2013","to_ts":1388534399},{"from_ts":1388534400,"listen_count":0,"time_range":"2014","to_ts":1420070399},{"from_ts":1420070400,"listen_count":0,"time_range":"2015","to_ts":1451606399},{"from_ts":1451606400,"listen_count":0,"time_range":"2016","to_ts":1483228799},{"from_ts":1483228800,"listen_count":0,"time_range":"2017","to_ts":1514764799},{"from_ts":1514764800,"listen_count":0,"time_range":"2018","to_ts":1546300799},{"from_ts":1546300800,"listen_count":0,"time_range":"2019","to_ts":1577836799},{"from_ts":1577836800,"listen_count":0,"time_range":"2020","to_ts":1609459199},{"from_ts":1609459200,"listen_count":0,"time_range":"2021","to_ts":1640995199},{"from_ts":1640995200,"listen_count":6,"time_range":"2022","to_ts":1672531199},{"from_ts":1672531200,"listen_count":2826,"time_range":"2023","to_ts":1704067199},{"from_ts":1704067200,"listen_count":418,"time_range":"2024","to_ts":1735689599}],"range":"all_time","to_ts":1723248018,"user_id":"Jasjeet"}} diff --git a/sharedTest/src/main/resources/loved_hated_songs.json b/sharedTest/src/main/resources/loved_hated_songs.json index 4600acdb..b7a5b23b 100644 --- a/sharedTest/src/main/resources/loved_hated_songs.json +++ b/sharedTest/src/main/resources/loved_hated_songs.json @@ -1 +1 @@ -{"count":25,"feedback":[{"created":1707077704,"recording_mbid":"c46464b2-ee6e-448c-8d22-871b139a0e38","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":17357251129,"caa_release_mbid":"e1656e7e-c23f-48e4-894e-fe1e0d5e6707","recording_mbid":"c46464b2-ee6e-448c-8d22-871b139a0e38","release_mbid":"e9b5c5ad-d91e-39d0-89fc-b8ac312bcc55"},"release_name":"Meteora","track_name":"Breaking the Habit"},"user_id":"akshaaatt"},{"created":1707077703,"recording_mbid":"54a3c21c-5395-44a2-b90b-b7fab8095c20","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":17357251129,"caa_release_mbid":"e1656e7e-c23f-48e4-894e-fe1e0d5e6707","recording_mbid":"54a3c21c-5395-44a2-b90b-b7fab8095c20","release_mbid":"e9b5c5ad-d91e-39d0-89fc-b8ac312bcc55"},"release_name":"Meteora","track_name":"Faint"},"user_id":"akshaaatt"},{"created":1707077702,"recording_mbid":"50347f42-2a40-4df6-b358-07f994af7a3f","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":21070222550,"caa_release_mbid":"127e18cd-99ad-3193-ad4f-441bee3c6e3b","recording_mbid":"50347f42-2a40-4df6-b358-07f994af7a3f","release_mbid":"127e18cd-99ad-3193-ad4f-441bee3c6e3b"},"release_name":"Minutes to Midnight","track_name":"What I\u2019ve Done"},"user_id":"akshaaatt"},{"created":1707077701,"recording_mbid":"352dd518-23cd-4c5a-9551-ba02097b177b","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":17357251129,"caa_release_mbid":"e1656e7e-c23f-48e4-894e-fe1e0d5e6707","recording_mbid":"352dd518-23cd-4c5a-9551-ba02097b177b","release_mbid":"e9b5c5ad-d91e-39d0-89fc-b8ac312bcc55"},"release_name":"Meteora","track_name":"Numb"},"user_id":"akshaaatt"},{"created":1707077701,"recording_mbid":"9d70086c-5d7a-4e7f-b1ed-c53c4b11310f","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":17352139218,"caa_release_mbid":"69b7b91a-43fc-483f-b8cf-22cdc8f69079","recording_mbid":"9d70086c-5d7a-4e7f-b1ed-c53c4b11310f","release_mbid":"a92e8636-a48c-4ad1-8a5f-4f8a968a50ac"},"release_name":"Hybrid Theory","track_name":"In the End"},"user_id":"akshaaatt"},{"created":1698546115,"recording_mbid":"234c556a-215b-4bb4-a0fa-2398cf331085","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Babbulicious","mbid_mapping":{"artist_mbids":["db442dbd-1579-4ff3-8472-bc4fc72c7522"],"artists":[{"artist_credit_name":"Babbulicious","artist_mbid":"db442dbd-1579-4ff3-8472-bc4fc72c7522","join_phrase":""}],"caa_id":35336159608,"caa_release_mbid":"43e2cac8-ac33-428e-9ad9-79827d4a8b34","recording_mbid":"234c556a-215b-4bb4-a0fa-2398cf331085","release_mbid":"43e2cac8-ac33-428e-9ad9-79827d4a8b34"},"release_name":"Gucci Chick","track_name":"Gucci Chick"},"user_id":"akshaaatt"},{"created":1692097010,"recording_mbid":"ea2a183f-5515-4a85-8228-72cf7e1ee90a","recording_msid":"d0f953f3-fdd4-4a7e-8004-fd000e0f804c","score":1,"track_metadata":{"additional_info":{"recording_msid":"d0f953f3-fdd4-4a7e-8004-fd000e0f804c"},"artist_name":"Billie Eilish","mbid_mapping":{"artist_mbids":["f4abc0b5-3f7a-4eff-8f78-ac078dbce533"],"artists":[{"artist_credit_name":"Billie Eilish","artist_mbid":"f4abc0b5-3f7a-4eff-8f78-ac078dbce533","join_phrase":""}],"caa_id":36301165720,"caa_release_mbid":"93ad159c-c69b-4cc5-ae5d-1136580fe04d","recording_mbid":"ea2a183f-5515-4a85-8228-72cf7e1ee90a","release_mbid":"93ad159c-c69b-4cc5-ae5d-1136580fe04d"},"release_name":"Live from the Barbie Dream House","track_name":"What Was I Made For?"},"user_id":"akshaaatt"},{"created":1685816977,"recording_mbid":"f3c609f4-be7f-42aa-9688-9f2aa38a5cb7","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":16782501966,"caa_release_mbid":"1f5f9437-fe10-448e-acf2-09a03cd81a7d","recording_mbid":"f3c609f4-be7f-42aa-9688-9f2aa38a5cb7","release_mbid":"44703741-a292-42e2-85d5-117ca1df8d9b"},"release_name":"One More Light","track_name":"Nobody Can Save Me"},"user_id":"akshaaatt"},{"created":1676722350,"recording_mbid":"9c49c2e3-dbe5-471c-8efa-d20d4341f635","recording_msid":"0b98fbc7-4792-4060-8b4c-969abab8c027","score":1,"track_metadata":{"additional_info":{"recording_msid":"0b98fbc7-4792-4060-8b4c-969abab8c027"},"artist_name":"Monali Thakur","mbid_mapping":{"artist_mbids":["adb5aa54-32b9-4587-83df-156d2c9bef1c"],"artists":[{"artist_credit_name":"Monali Thakur","artist_mbid":"adb5aa54-32b9-4587-83df-156d2c9bef1c","join_phrase":""}],"caa_id":38683132632,"caa_release_mbid":"865e074d-3209-4cdd-8589-480bb8cc1027","recording_mbid":"9c49c2e3-dbe5-471c-8efa-d20d4341f635","release_mbid":"865e074d-3209-4cdd-8589-480bb8cc1027"},"release_name":"Lootera","track_name":"Sawaar Loon"},"user_id":"akshaaatt"},{"created":1649338426,"recording_mbid":"c9319bbc-8dec-4c7c-8934-ee234345f705","recording_msid":"d07b81ac-0209-44ed-a38e-4ebcde6eddef","score":1,"track_metadata":{"additional_info":{"recording_msid":"d07b81ac-0209-44ed-a38e-4ebcde6eddef"},"artist_name":"Lauv","mbid_mapping":{"artist_mbids":["c0ef2ba5-a7b7-40ea-bd27-30acccfcac11"],"artists":[{"artist_credit_name":"Lauv","artist_mbid":"c0ef2ba5-a7b7-40ea-bd27-30acccfcac11","join_phrase":""}],"caa_id":20512551101,"caa_release_mbid":"47383b9f-d6ea-457c-9c95-6f9b6dd6fa36","recording_mbid":"c9319bbc-8dec-4c7c-8934-ee234345f705","release_mbid":"47383b9f-d6ea-457c-9c95-6f9b6dd6fa36"},"release_name":"I Like Me Better","track_name":"I Like Me Better"},"user_id":"akshaaatt"},{"created":1647712667,"recording_mbid":null,"recording_msid":"20492313-3623-491f-96aa-83aa5afa47a0","score":1,"track_metadata":{"additional_info":{"recording_msid":"20492313-3623-491f-96aa-83aa5afa47a0"},"artist_name":"Sasha Alex Sloan","release_name":"Older","track_name":"Older"},"user_id":"akshaaatt"},{"created":1647509892,"recording_mbid":"bfd84871-2523-40e5-a70f-04c9b0e28acf","recording_msid":"589e83ac-ac08-4fa1-b14e-767371f8f1b5","score":1,"track_metadata":{"additional_info":{"recording_msid":"589e83ac-ac08-4fa1-b14e-767371f8f1b5"},"artist_name":"Machine Gun Kelly feat. Bring Me the Horizon","mbid_mapping":{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33","074e3847-f67f-49f9-81f1-8c8cea147e8e"],"artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":" feat. "},{"artist_credit_name":"Bring Me the Horizon","artist_mbid":"074e3847-f67f-49f9-81f1-8c8cea147e8e","join_phrase":""}],"caa_id":32122624894,"caa_release_mbid":"a88712e3-1327-4472-af90-5e1af734c16a","recording_mbid":"bfd84871-2523-40e5-a70f-04c9b0e28acf","release_mbid":"b94f45e0-7a1a-4895-a45b-3abd2ec5eda2"},"release_name":"MAINSTREAM SELLOUT","track_name":"maybe"},"user_id":"akshaaatt"},{"created":1647509700,"recording_mbid":"24d50452-6d0a-46f2-8ba1-9ff61dd8f3d2","recording_msid":"904b3f5e-64d0-4104-8e2b-cf87237872c5","score":1,"track_metadata":{"additional_info":{"recording_msid":"904b3f5e-64d0-4104-8e2b-cf87237872c5"},"artist_name":"Linkin Park feat. Kiiara","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419","dbc26631-5e61-4ba2-84ff-e1423a1c288a"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":" feat. "},{"artist_credit_name":"Kiiara","artist_mbid":"dbc26631-5e61-4ba2-84ff-e1423a1c288a","join_phrase":""}],"caa_id":16782501966,"caa_release_mbid":"1f5f9437-fe10-448e-acf2-09a03cd81a7d","recording_mbid":"24d50452-6d0a-46f2-8ba1-9ff61dd8f3d2","release_mbid":"44703741-a292-42e2-85d5-117ca1df8d9b"},"release_name":"One More Light","track_name":"Heavy"},"user_id":"akshaaatt"},{"created":1647369312,"recording_mbid":"f9845fce-5ab7-456f-8579-6a1fcfa8cf25","recording_msid":"641886e4-5a74-49f4-990d-ed9ba82c2330","score":1,"track_metadata":{"additional_info":{"recording_msid":"641886e4-5a74-49f4-990d-ed9ba82c2330"},"artist_name":"Rihanna","mbid_mapping":{"artist_mbids":["73e5e69d-3554-40d8-8516-00cb38737a1c"],"artists":[{"artist_credit_name":"Rihanna","artist_mbid":"73e5e69d-3554-40d8-8516-00cb38737a1c","join_phrase":""}],"caa_id":14539517129,"caa_release_mbid":"3a7adfe2-6965-4b09-adb5-1b200346433d","recording_mbid":"f9845fce-5ab7-456f-8579-6a1fcfa8cf25","release_mbid":"ce71001c-f8c5-44b2-9c25-589693b64a22"},"release_name":"Music of the Sun","track_name":"Pon de Replay"},"user_id":"akshaaatt"},{"created":1646132106,"recording_mbid":"a89fa4c9-5e9f-4ed3-9478-1585783a5d5d","recording_msid":"7836aa1a-2c41-4279-a9b6-2263097b38c4","score":1,"track_metadata":{"additional_info":{"recording_msid":"7836aa1a-2c41-4279-a9b6-2263097b38c4"},"artist_name":"Carpathian Forest","mbid_mapping":{"artist_mbids":["69fa5c49-12ec-4c86-a238-4e07cc6d1a7d"],"artists":[{"artist_credit_name":"Carpathian Forest","artist_mbid":"69fa5c49-12ec-4c86-a238-4e07cc6d1a7d","join_phrase":""}],"caa_id":1033890833,"caa_release_mbid":"3f2e996b-e231-4165-bba0-64ee3b67cf60","recording_mbid":"a89fa4c9-5e9f-4ed3-9478-1585783a5d5d","release_mbid":"3f2e996b-e231-4165-bba0-64ee3b67cf60"},"release_name":"Black Shining Leather","track_name":"A Forest"},"user_id":"akshaaatt"},{"created":1646131911,"recording_mbid":"b9874074-b2d6-4135-bc2a-f4ef44b0ae69","recording_msid":"112597b6-ec52-49cf-9916-41c3aa060a77","score":1,"track_metadata":{"additional_info":{"recording_msid":"112597b6-ec52-49cf-9916-41c3aa060a77"},"artist_name":"Machine Gun Kelly","mbid_mapping":{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":27366822877,"caa_release_mbid":"ca1cafc2-3daf-4ca5-b1af-8466f7836e60","recording_mbid":"b9874074-b2d6-4135-bc2a-f4ef44b0ae69","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa"},"release_name":"Tickets to My Downfall","track_name":"title track"},"user_id":"akshaaatt"},{"created":1645670072,"recording_mbid":"198a6f88-eb15-4280-9208-82998cb39175","recording_msid":"cf41b59d-9c9d-4042-81c8-09b92797d46d","score":1,"track_metadata":{"additional_info":{"recording_msid":"cf41b59d-9c9d-4042-81c8-09b92797d46d"},"artist_name":"Machine Gun Kelly","mbid_mapping":{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":27366800098,"caa_release_mbid":"5d987f56-4c51-4c1b-9e67-b6f6e0fc46fd","recording_mbid":"198a6f88-eb15-4280-9208-82998cb39175","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa"},"release_name":"Tickets to My Downfall","track_name":"bloody valentine"},"user_id":"akshaaatt"},{"created":1645178278,"recording_mbid":"2d24a8d7-66ad-40b2-8ae7-5f6fc9981646","recording_msid":"ba6a24ac-ef0c-44d6-b233-d0bebd925c46","score":1,"track_metadata":{"additional_info":{"recording_msid":"ba6a24ac-ef0c-44d6-b233-d0bebd925c46"},"artist_name":"Ed Sheeran","mbid_mapping":{"artist_mbids":["b8a7c51f-362c-4dcb-a259-bc6e0095f0a6"],"artists":[{"artist_credit_name":"Ed Sheeran","artist_mbid":"b8a7c51f-362c-4dcb-a259-bc6e0095f0a6","join_phrase":""}],"caa_id":37184507977,"caa_release_mbid":"64ae938c-6af1-4919-8341-458eb1866474","recording_mbid":"2d24a8d7-66ad-40b2-8ae7-5f6fc9981646","release_mbid":"ccfe374a-ccfa-4cbc-a37c-1cd40b914b75"},"release_name":"=","track_name":"Bad Habits"},"user_id":"akshaaatt"},{"created":1636226683,"recording_mbid":"7ec115f0-2da7-4ddf-a183-6f4f8e10cc0e","recording_msid":"669fafd9-044b-4dc0-93af-be17ba06c4f5","score":1,"track_metadata":{"additional_info":{"recording_msid":"669fafd9-044b-4dc0-93af-be17ba06c4f5"},"artist_name":"Alex Gibson","mbid_mapping":{"artist_mbids":["aab3f709-be9e-48b9-9b9d-281fff5284f9"],"artists":[{"artist_credit_name":"Alex Gibson","artist_mbid":"aab3f709-be9e-48b9-9b9d-281fff5284f9","join_phrase":""}],"caa_id":15835295366,"caa_release_mbid":"5a097177-c962-4c56-9a9f-46e9f6868c9c","recording_mbid":"7ec115f0-2da7-4ddf-a183-6f4f8e10cc0e","release_mbid":"5a097177-c962-4c56-9a9f-46e9f6868c9c"},"release_name":"Rockabye Baby! Lullaby Renditions of AC/DC","track_name":"Dirty Deeds Done Dirt Cheap"},"user_id":"akshaaatt"},{"created":1635871036,"recording_mbid":"c0ecfe09-7521-42b2-9f99-366b854f280f","recording_msid":"defe1f54-97de-4a3f-a0df-7e1bdc9b3a8e","score":1,"track_metadata":{"additional_info":{"recording_msid":"defe1f54-97de-4a3f-a0df-7e1bdc9b3a8e"},"artist_name":"Shelly Manne & His Friends","mbid_mapping":{"artist_mbids":["7c0c1773-513e-4c47-88de-310550319921"],"artists":[{"artist_credit_name":"Shelly Manne & His Friends","artist_mbid":"7c0c1773-513e-4c47-88de-310550319921","join_phrase":""}],"caa_id":26689007265,"caa_release_mbid":"928c6630-795e-4f6b-9b25-c36b8f4212d7","recording_mbid":"c0ecfe09-7521-42b2-9f99-366b854f280f","release_mbid":"233bbe84-df40-4755-84e3-9c5937e91f8e"},"release_name":"Shelly Manne & His Friends Play Modern Jazz Performances of Songs from My Fair Lady","track_name":"Get Me to the Church on Time"},"user_id":"akshaaatt"},{"created":1632294500,"recording_mbid":"015021a9-2621-4593-b82d-f1f28bdf440a","recording_msid":"920f49da-1de3-4902-873f-8c5d2815884b","score":1,"track_metadata":{"additional_info":{"recording_msid":"920f49da-1de3-4902-873f-8c5d2815884b"},"artist_name":"Zedd & Alessia Cara","mbid_mapping":{"artist_mbids":["56c4b861-0922-4c3a-a9b9-3bfcb00f8274","97e69730-3791-423b-9770-287261588854"],"artists":[{"artist_credit_name":"Zedd","artist_mbid":"56c4b861-0922-4c3a-a9b9-3bfcb00f8274","join_phrase":" & "},{"artist_credit_name":"Alessia Cara","artist_mbid":"97e69730-3791-423b-9770-287261588854","join_phrase":""}],"caa_id":17157288690,"caa_release_mbid":"fe1176c5-aa68-4518-8548-0eb23ef8cd92","recording_mbid":"015021a9-2621-4593-b82d-f1f28bdf440a","release_mbid":"92541a9e-3507-484e-aecc-23577b8c3547"},"release_name":"Get Low","track_name":"Stay"},"user_id":"akshaaatt"},{"created":1632238295,"recording_mbid":"5780b6fb-066f-4147-ac0e-0d8c8e812e02","recording_msid":"1312abf2-b12f-483f-935b-c7a283700481","score":1,"track_metadata":{"additional_info":{"recording_msid":"1312abf2-b12f-483f-935b-c7a283700481"},"artist_name":"Ed Sheeran","mbid_mapping":{"artist_mbids":["b8a7c51f-362c-4dcb-a259-bc6e0095f0a6"],"artists":[{"artist_credit_name":"Ed Sheeran","artist_mbid":"b8a7c51f-362c-4dcb-a259-bc6e0095f0a6","join_phrase":""}],"caa_id":31935928002,"caa_release_mbid":"14c13329-88f5-426f-90d7-0170b4017e15","recording_mbid":"5780b6fb-066f-4147-ac0e-0d8c8e812e02","release_mbid":"769e64d7-b7a8-4ad1-84d8-0e0063d8fe64"},"release_name":"\u00f7","track_name":"Castle on the Hill"},"user_id":"akshaaatt"},{"created":1632224517,"recording_mbid":"07adabdd-1195-4ac5-9c75-8e2e11daa604","recording_msid":"aca55f75-a7e0-4989-8c57-88cb431bb219","score":1,"track_metadata":{"additional_info":{"recording_msid":"aca55f75-a7e0-4989-8c57-88cb431bb219"},"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":16782501966,"caa_release_mbid":"1f5f9437-fe10-448e-acf2-09a03cd81a7d","recording_mbid":"07adabdd-1195-4ac5-9c75-8e2e11daa604","release_mbid":"44703741-a292-42e2-85d5-117ca1df8d9b"},"release_name":"One More Light","track_name":"Battle Symphony"},"user_id":"akshaaatt"},{"created":1631784052,"recording_mbid":"1205dbc4-5fa4-4c19-b04b-50fdbf15ddf2","recording_msid":"eeba4144-9a77-4386-a932-d947a64c18ae","score":1,"track_metadata":{"additional_info":{"recording_msid":"eeba4144-9a77-4386-a932-d947a64c18ae"},"artist_name":"Mike Shinoda","mbid_mapping":{"artist_mbids":["c262b6bf-be56-4b26-bceb-42d7a27342f3"],"artists":[{"artist_credit_name":"Mike Shinoda","artist_mbid":"c262b6bf-be56-4b26-bceb-42d7a27342f3","join_phrase":""}],"caa_id":22130935658,"caa_release_mbid":"df9c66e7-07eb-4a9a-b9c6-4f41ae676a15","recording_mbid":"1205dbc4-5fa4-4c19-b04b-50fdbf15ddf2","release_mbid":"639eb4b8-2ea0-4455-a067-f1d9faade1c9"},"release_name":"Post Traumatic","track_name":"Watching as I Fall"},"user_id":"akshaaatt"},{"created":1631784039,"recording_mbid":"8102a8b9-0b58-49aa-8f04-e6a49528bb46","recording_msid":"b3155220-34c3-4e29-bd9f-25bcf4109aa7","score":1,"track_metadata":{"additional_info":{"recording_msid":"b3155220-34c3-4e29-bd9f-25bcf4109aa7"},"artist_name":"Mike Shinoda","mbid_mapping":{"artist_mbids":["c262b6bf-be56-4b26-bceb-42d7a27342f3"],"artists":[{"artist_credit_name":"Mike Shinoda","artist_mbid":"c262b6bf-be56-4b26-bceb-42d7a27342f3","join_phrase":""}],"caa_id":22130935658,"caa_release_mbid":"df9c66e7-07eb-4a9a-b9c6-4f41ae676a15","recording_mbid":"8102a8b9-0b58-49aa-8f04-e6a49528bb46","release_mbid":"639eb4b8-2ea0-4455-a067-f1d9faade1c9"},"release_name":"Post Traumatic","track_name":"Hold It Together"},"user_id":"akshaaatt"}],"offset":0,"total_count":31} +{"count":25,"feedback":[{"created":1709235151,"recording_mbid":"6b08f3d4-0d56-406c-b628-d0afe2ad5d44","recording_msid":"d7d1de71-f60c-4fed-8028-7efa1f4aa52e","score":1,"track_metadata":{"additional_info":{"recording_msid":"d7d1de71-f60c-4fed-8028-7efa1f4aa52e"},"artist_name":"Metro Boomin, Swae Lee & NAV feat. A Boogie Wit da Hoodie","mbid_mapping":{"artist_mbids":["59db3d82-86ea-451f-881f-dffc8ec387c9","a17ce9c3-8f6f-4dcc-a1b7-2a04ab9e31f0","418fe547-4bec-48fe-8df0-ed25e27f4570","c1708d03-8a66-46eb-848e-fe0d233ffb39"],"artists":[{"artist_credit_name":"Metro Boomin","artist_mbid":"59db3d82-86ea-451f-881f-dffc8ec387c9","join_phrase":", "},{"artist_credit_name":"Swae Lee","artist_mbid":"a17ce9c3-8f6f-4dcc-a1b7-2a04ab9e31f0","join_phrase":" & "},{"artist_credit_name":"NAV","artist_mbid":"418fe547-4bec-48fe-8df0-ed25e27f4570","join_phrase":" feat. "},{"artist_credit_name":"A Boogie Wit da Hoodie","artist_mbid":"c1708d03-8a66-46eb-848e-fe0d233ffb39","join_phrase":""}],"caa_id":35816827255,"caa_release_mbid":"72a73259-c593-4503-a889-9bf1b5e10874","recording_mbid":"6b08f3d4-0d56-406c-b628-d0afe2ad5d44","release_mbid":"5e38a701-16fa-4c2e-b43a-cf59cee97157"},"release_name":"METRO BOOMIN PRESENTS SPIDER\u2010MAN: ACROSS THE SPIDER\u2010VERSE: SOUNDTRACK FROM AND INSPIRED BY THE MOTION PICTURE","track_name":"Calling"},"user_id":"Jasjeet"},{"created":1709235045,"recording_mbid":"a24f67ff-a81e-4a95-817d-c0b4e5af3e6b","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Kanye West feat. Pusha T","mbid_mapping":{"artist_mbids":["164f0d73-1234-4e2c-8743-d77bf2191051","14ecd19f-7121-4192-8549-e5056241a42f"],"artists":[{"artist_credit_name":"Kanye West","artist_mbid":"164f0d73-1234-4e2c-8743-d77bf2191051","join_phrase":" feat. "},{"artist_credit_name":"Pusha T","artist_mbid":"14ecd19f-7121-4192-8549-e5056241a42f","join_phrase":""}],"caa_id":38000784873,"caa_release_mbid":"ef748325-cb18-46c6-b0fd-7276bd6f4337","recording_mbid":"a24f67ff-a81e-4a95-817d-c0b4e5af3e6b","release_mbid":"cd474271-bd7a-4d53-ba9e-e9d5bd7958cb"},"release_name":"My Beautiful Dark Twisted Fantasy","track_name":"Runaway"},"user_id":"Jasjeet"},{"created":1705662797,"recording_mbid":"03c4adcf-2f31-42d7-bcd5-056d68066f90","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Mac Miller","mbid_mapping":{"artist_mbids":["a0e8a1b1-5f8f-475a-a253-17415c17d0ff"],"artists":[{"artist_credit_name":"Mac Miller","artist_mbid":"a0e8a1b1-5f8f-475a-a253-17415c17d0ff","join_phrase":""}],"caa_id":20706929859,"caa_release_mbid":"6db76635-a3cd-4933-85e9-e29cbbc38c1c","recording_mbid":"03c4adcf-2f31-42d7-bcd5-056d68066f90","release_mbid":"62bd84b5-1c38-4d27-9176-ea0716d51064"},"release_name":"Swimming","track_name":"Self Care"},"user_id":"Jasjeet"},{"created":1705659281,"recording_mbid":null,"recording_msid":"c49769d0-6e7f-4603-84e0-ff8ba9debdd0","score":1,"track_metadata":{"additional_info":{"recording_msid":"c49769d0-6e7f-4603-84e0-ff8ba9debdd0"},"artist_name":"Seren","track_name":"Travis Scott - My Eyes (Best Part Extended)"},"user_id":"Jasjeet"},{"created":1704548910,"recording_mbid":"9fb62336-68b7-43fa-a220-e7f3637c7cc5","recording_msid":"27bdfc3c-f0ff-4f81-995b-280a1514be16","score":1,"track_metadata":{"additional_info":{"recording_msid":"27bdfc3c-f0ff-4f81-995b-280a1514be16"},"artist_name":"Liana Flores","mbid_mapping":{"artist_mbids":["9f93c34a-2dc2-400e-ada5-937c3efc3437"],"artists":[{"artist_credit_name":"Liana Flores","artist_mbid":"9f93c34a-2dc2-400e-ada5-937c3efc3437","join_phrase":""}],"caa_id":30013330623,"caa_release_mbid":"7afdc460-27eb-444e-bffd-6977f6335597","recording_mbid":"9fb62336-68b7-43fa-a220-e7f3637c7cc5","release_mbid":"7afdc460-27eb-444e-bffd-6977f6335597"},"release_name":"recently","track_name":"rises the moon"},"user_id":"Jasjeet"},{"created":1704547584,"recording_mbid":"a4fc3d78-6520-4663-82bd-10393bb0af2b","recording_msid":null,"score":1,"track_metadata":{"artist_name":"The Drums","mbid_mapping":{"artist_mbids":["c3ecef5d-84f3-4942-8c24-0a626343c3f4"],"artists":[{"artist_credit_name":"The Drums","artist_mbid":"c3ecef5d-84f3-4942-8c24-0a626343c3f4","join_phrase":""}],"caa_id":4031185360,"caa_release_mbid":"dfffc568-7e2c-44e8-80e6-458f2d9aa319","recording_mbid":"a4fc3d78-6520-4663-82bd-10393bb0af2b","release_mbid":"b6b21d16-021f-48fe-a575-c46320cf3107"},"release_name":"Portamento","track_name":"Money"},"user_id":"Jasjeet"},{"created":1704402369,"recording_mbid":"2f14ac58-d62d-4e45-a5e1-6504d1366831","recording_msid":"3ed4f41f-07ba-4cc5-ae95-b95409e028d8","score":1,"track_metadata":{"additional_info":{"recording_msid":"3ed4f41f-07ba-4cc5-ae95-b95409e028d8"},"artist_name":"Aaron May","mbid_mapping":{"artist_mbids":["9f3225ad-e686-4538-8cbe-99406c4a4caa"],"artists":[{"artist_credit_name":"Aaron May","artist_mbid":"9f3225ad-e686-4538-8cbe-99406c4a4caa","join_phrase":""}],"caa_id":22549299616,"caa_release_mbid":"ff0e48f3-83f6-4923-b5fa-a3c39f484da4","recording_mbid":"2f14ac58-d62d-4e45-a5e1-6504d1366831","release_mbid":"ff0e48f3-83f6-4923-b5fa-a3c39f484da4"},"release_name":"Chase","track_name":"I'm Good Luv, Enjoy."},"user_id":"Jasjeet"},{"created":1702622193,"recording_mbid":"c703515e-92ba-4d8f-8b8e-8fa85a480038","recording_msid":"9d3390f4-9f21-4be5-8c08-cae287e4c189","score":1,"track_metadata":{"additional_info":{"recording_msid":"9d3390f4-9f21-4be5-8c08-cae287e4c189"},"artist_name":"Rosa Walton as Hallie Coggins","mbid_mapping":{"artist_mbids":["9a6273b2-72fa-47a4-a48b-073d6cbe037d","0ebdc5f5-0a11-415a-b6f3-efe0d36c6e9d"],"artists":[{"artist_credit_name":"Rosa Walton","artist_mbid":"9a6273b2-72fa-47a4-a48b-073d6cbe037d","join_phrase":" as "},{"artist_credit_name":"Hallie Coggins","artist_mbid":"0ebdc5f5-0a11-415a-b6f3-efe0d36c6e9d","join_phrase":""}],"caa_id":28105410573,"caa_release_mbid":"53002383-0ea4-44a4-97e1-b5d7d25e91a4","recording_mbid":"c703515e-92ba-4d8f-8b8e-8fa85a480038","release_mbid":"53002383-0ea4-44a4-97e1-b5d7d25e91a4"},"release_name":"Cyberpunk 2077: Radio, Vol. 2 (Original Soundtrack)","track_name":"I Really Want to Stay at Your House"},"user_id":"Jasjeet"},{"created":1702565277,"recording_mbid":"ab100b50-31eb-4a71-98d8-52a5e4e1304e","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Dawid Podsiad\u0142o","mbid_mapping":{"artist_mbids":["68ffdf6b-001c-41ed-aa3a-20ec0128f627"],"artists":[{"artist_credit_name":"Dawid Podsiad\u0142o","artist_mbid":"68ffdf6b-001c-41ed-aa3a-20ec0128f627","join_phrase":""}],"caa_id":35924146102,"caa_release_mbid":"50274be9-730c-4642-8ede-25442d58867c","recording_mbid":"ab100b50-31eb-4a71-98d8-52a5e4e1304e","release_mbid":"cb64ddc6-dc64-4f31-b503-f977077c57d3"},"release_name":"Lata Dwudzieste z kawa\u0142kiem","track_name":"Let You Down"},"user_id":"Jasjeet"},{"created":1702402222,"recording_mbid":null,"recording_msid":"9c844e8e-b085-4954-bc0b-7651b1939e79","score":1,"track_metadata":{"additional_info":{"recording_msid":"9c844e8e-b085-4954-bc0b-7651b1939e79"},"artist_name":"Ammy Virk, Jaymeet and Rony Ajnali","release_name":"Late Checkout","track_name":"Memories - Ammy Virk (Official Song) Layers"},"user_id":"Jasjeet"},{"created":1702402221,"recording_mbid":null,"recording_msid":"17c684ff-6812-43f0-b53c-be23cf59fce1","score":1,"track_metadata":{"additional_info":{"recording_msid":"17c684ff-6812-43f0-b53c-be23cf59fce1"},"artist_name":"Mickey Singh and Jay Skilly","release_name":"The Phantom Files (From Cyberpunk 2077)","track_name":"Cruise Control"},"user_id":"Jasjeet"},{"created":1702402120,"recording_mbid":"dbad831a-7a9d-416e-9ef0-11e740fef6a0","recording_msid":null,"score":1,"track_metadata":{"artist_name":"Men I Trust","mbid_mapping":{"artist_mbids":["25ab7547-a4d2-480b-b028-c7f3497bc858"],"artists":[{"artist_credit_name":"Men I Trust","artist_mbid":"25ab7547-a4d2-480b-b028-c7f3497bc858","join_phrase":""}],"caa_id":26978507653,"caa_release_mbid":"0633a08c-660e-4992-ba7b-a62bcf82f43b","recording_mbid":"dbad831a-7a9d-416e-9ef0-11e740fef6a0","release_mbid":"c1bc7be3-7eab-4055-a260-d809487a6eb8"},"release_name":"Oncle Jazz","track_name":"Show Me How"},"user_id":"Jasjeet"},{"created":1702396242,"recording_mbid":"15c5d5a6-9ae9-4727-a413-d48c57e867d9","recording_msid":"ac081ab5-5702-4eb1-bd4d-cdc0504703c7","score":1,"track_metadata":{"additional_info":{"recording_msid":"ac081ab5-5702-4eb1-bd4d-cdc0504703c7"},"artist_name":"Namakopuri as Us Cracks","mbid_mapping":{"artist_mbids":["1b7d53f5-1746-420b-b23d-e6b7c41a0b37","07ee1730-aee1-4c90-a225-be7b3edc94be"],"artists":[{"artist_credit_name":"Namakopuri","artist_mbid":"1b7d53f5-1746-420b-b23d-e6b7c41a0b37","join_phrase":" as "},{"artist_credit_name":"Us Cracks","artist_mbid":"07ee1730-aee1-4c90-a225-be7b3edc94be","join_phrase":""}],"caa_id":28105410573,"caa_release_mbid":"53002383-0ea4-44a4-97e1-b5d7d25e91a4","recording_mbid":"15c5d5a6-9ae9-4727-a413-d48c57e867d9","release_mbid":"53002383-0ea4-44a4-97e1-b5d7d25e91a4"},"release_name":"Cyberpunk 2077: Radio, Vol. 2 (Original Soundtrack)","track_name":"PonPon Shit"},"user_id":"Jasjeet"},{"created":1702387013,"recording_mbid":null,"recording_msid":"0e10f95e-dc5a-42ea-a70d-5a274f8683ff","score":1,"track_metadata":{"additional_info":{"recording_msid":"0e10f95e-dc5a-42ea-a70d-5a274f8683ff"},"artist_name":"Semwal and Agaazz","release_name":"Hass Hass","track_name":"Dil Da Pecha"},"user_id":"Jasjeet"},{"created":1702369018,"recording_mbid":null,"recording_msid":"62b5f8da-45df-4cbf-87db-a9a7bfc7d906","score":1,"track_metadata":{"additional_info":{"recording_msid":"62b5f8da-45df-4cbf-87db-a9a7bfc7d906"},"artist_name":"Mickey Singh and Jay Skilly","release_name":"Infinity","track_name":"Cruise Control"},"user_id":"Jasjeet"},{"created":1701932614,"recording_mbid":"13c81871-58de-4c1c-bb43-f83e7b6032a7","recording_msid":"0f2644de-9cd7-4811-b638-770aeb516a89","score":1,"track_metadata":{"additional_info":{"recording_msid":"0f2644de-9cd7-4811-b638-770aeb516a89"},"artist_name":"Dawid Podsiad\u0142o & P.T. Adamczyk","mbid_mapping":{"artist_mbids":["68ffdf6b-001c-41ed-aa3a-20ec0128f627","bc1285ea-31c8-4f50-847e-b66244a142b3"],"artists":[{"artist_credit_name":"Dawid Podsiad\u0142o","artist_mbid":"68ffdf6b-001c-41ed-aa3a-20ec0128f627","join_phrase":" & "},{"artist_credit_name":"P.T. Adamczyk","artist_mbid":"bc1285ea-31c8-4f50-847e-b66244a142b3","join_phrase":""}],"caa_id":35924146102,"caa_release_mbid":"50274be9-730c-4642-8ede-25442d58867c","recording_mbid":"13c81871-58de-4c1c-bb43-f83e7b6032a7","release_mbid":"cb64ddc6-dc64-4f31-b503-f977077c57d3"},"release_name":"Lata Dwudzieste z kawa\u0142kiem","track_name":"Phantom Liberty"},"user_id":"Jasjeet"},{"created":1701932501,"recording_mbid":"7a8949b0-3abf-4f0d-b14c-dd7d4f635966","recording_msid":"f984f2d0-cfc6-4205-9838-891f4bfbff3a","score":1,"track_metadata":{"additional_info":{"recording_msid":"f984f2d0-cfc6-4205-9838-891f4bfbff3a"},"artist_name":"Idris Elba","mbid_mapping":{"artist_mbids":["b3c7c6d6-5907-45a8-b589-1ec2572be4fd"],"artists":[{"artist_credit_name":"Idris Elba","artist_mbid":"b3c7c6d6-5907-45a8-b589-1ec2572be4fd","join_phrase":""}],"caa_id":36853564662,"caa_release_mbid":"77ef1c1d-e3fe-4f54-ba43-4cdc1243ea6c","recording_mbid":"7a8949b0-3abf-4f0d-b14c-dd7d4f635966","release_mbid":"77ef1c1d-e3fe-4f54-ba43-4cdc1243ea6c"},"release_name":"The Phantom Files (From Cyberpunk 2077)","track_name":"Choke Hold"},"user_id":"Jasjeet"},{"created":1700751665,"recording_mbid":"d2ed35bd-9771-496f-9c48-ba9e7c462da2","recording_msid":"3cedd757-0142-4708-852d-cf6e2bd35750","score":1,"track_metadata":{"additional_info":{"recording_msid":"3cedd757-0142-4708-852d-cf6e2bd35750"},"artist_name":"Neeraj Shridhar, Raman Mahadevan, Pervez Quadir & Loy Mendonsa","mbid_mapping":{"artist_mbids":["db7fb490-d527-447f-9d40-ed91c479d5ee","b63974b0-43ef-4b72-b4be-7b7eb96025bf","c967a8f1-2852-4416-bf46-9e041b6594ea","5fa5c9fc-9d9c-4c0f-ad27-10333051084e"],"artists":[{"artist_credit_name":"Neeraj Shridhar","artist_mbid":"db7fb490-d527-447f-9d40-ed91c479d5ee","join_phrase":", "},{"artist_credit_name":"Raman Mahadevan","artist_mbid":"b63974b0-43ef-4b72-b4be-7b7eb96025bf","join_phrase":", "},{"artist_credit_name":"Pervez Quadir","artist_mbid":"c967a8f1-2852-4416-bf46-9e041b6594ea","join_phrase":" & "},{"artist_credit_name":"Loy Mendonsa","artist_mbid":"5fa5c9fc-9d9c-4c0f-ad27-10333051084e","join_phrase":""}],"recording_mbid":"d2ed35bd-9771-496f-9c48-ba9e7c462da2","release_mbid":"69587b26-f597-48e5-844c-3823d3ff8f26"},"release_name":"2007 It's Rocking","track_name":"Heyy Babyy (Heyy Babyy)"},"user_id":"Jasjeet"},{"created":1700588943,"recording_mbid":"da07eaed-4eda-44bc-8eea-4713e832aa2e","recording_msid":"891ed674-1c1f-4c3a-919d-1af3438fbd5a","score":1,"track_metadata":{"additional_info":{"recording_msid":"891ed674-1c1f-4c3a-919d-1af3438fbd5a"},"artist_name":"Diljit Dosanjh & Sia","mbid_mapping":{"artist_mbids":["f931c961-b647-4861-be8c-f47d84a4de51","2f548675-008d-4332-876c-108b0c7ab9c5"],"artists":[{"artist_credit_name":"Diljit Dosanjh","artist_mbid":"f931c961-b647-4861-be8c-f47d84a4de51","join_phrase":" & "},{"artist_credit_name":"Sia","artist_mbid":"2f548675-008d-4332-876c-108b0c7ab9c5","join_phrase":""}],"caa_id":37082867811,"caa_release_mbid":"308fbfe2-6327-439e-b209-f3b021e8795c","recording_mbid":"da07eaed-4eda-44bc-8eea-4713e832aa2e","release_mbid":"308fbfe2-6327-439e-b209-f3b021e8795c"},"release_name":"Hass Hass","track_name":"Hass Hass"},"user_id":"Jasjeet"},{"created":1689852464,"recording_mbid":"eb8d7744-a329-4740-94b9-ac575e979d5c","recording_msid":"2059fd62-165e-4f52-a041-125e6090785e","score":1,"track_metadata":{"additional_info":{"recording_msid":"2059fd62-165e-4f52-a041-125e6090785e"},"artist_name":"Metro Boomin & Future feat. Don Toliver","mbid_mapping":{"artist_mbids":["59db3d82-86ea-451f-881f-dffc8ec387c9","48262e82-db9f-4a92-b650-dfef979b73ec","a0723a3c-4135-438e-85c5-012712144ede"],"artists":[{"artist_credit_name":"Metro Boomin","artist_mbid":"59db3d82-86ea-451f-881f-dffc8ec387c9","join_phrase":" & "},{"artist_credit_name":"Future","artist_mbid":"48262e82-db9f-4a92-b650-dfef979b73ec","join_phrase":" feat. "},{"artist_credit_name":"Don Toliver","artist_mbid":"a0723a3c-4135-438e-85c5-012712144ede","join_phrase":""}],"caa_id":34245737801,"caa_release_mbid":"2f58792e-a91a-4038-8828-c70e072492fd","recording_mbid":"eb8d7744-a329-4740-94b9-ac575e979d5c","release_mbid":"067d55ab-5eb1-4a1b-96ae-8c67f1f50441"},"release_name":"HEROES & VILLAINS","track_name":"Too Many Nights"},"user_id":"Jasjeet"},{"created":1689852459,"recording_mbid":null,"recording_msid":"4d60c545-b353-4b63-9c91-d98bd781ddcf","score":1,"track_metadata":null,"user_id":"Jasjeet"},{"created":1685986475,"recording_mbid":null,"recording_msid":"9656809d-b8b9-4e4c-93eb-c9965fb53f20","score":1,"track_metadata":null,"user_id":"Jasjeet"},{"created":1685912721,"recording_mbid":"01552e9a-11b0-4b5d-bcd5-cd15850ebd29","recording_msid":"34e3e63b-9e4b-460a-b2c6-9628bf96273c","score":1,"track_metadata":{"additional_info":{"recording_msid":"34e3e63b-9e4b-460a-b2c6-9628bf96273c"},"artist_name":"Halsey","mbid_mapping":{"artist_mbids":["3377f3bb-60fc-4403-aea9-7e800612e060"],"artists":[{"artist_credit_name":"Halsey","artist_mbid":"3377f3bb-60fc-4403-aea9-7e800612e060","join_phrase":""}],"caa_id":33498041604,"caa_release_mbid":"2ec31e00-de8e-4181-8d6d-4ff6d8cb3a19","recording_mbid":"01552e9a-11b0-4b5d-bcd5-cd15850ebd29","release_mbid":"911dfcb8-90b8-41fe-a8b5-d1b4dfdba503"},"release_name":"\u30de\u30cb\u30c3\u30af","track_name":"Without Me"},"user_id":"Jasjeet"},{"created":1683483053,"recording_mbid":"75cd34ef-70fd-46b4-a1a6-1d3db062b363","recording_msid":"8dfdf578-35c2-436c-8f47-081cac3028f0","score":1,"track_metadata":{"additional_info":{"recording_msid":"8dfdf578-35c2-436c-8f47-081cac3028f0"},"artist_name":"Yo Yo Honey Singh","mbid_mapping":{"artist_mbids":["0dc9c4bc-8bcc-42f1-9033-bec41160377f"],"artists":[{"artist_credit_name":"Yo Yo Honey Singh","artist_mbid":"0dc9c4bc-8bcc-42f1-9033-bec41160377f","join_phrase":""}],"caa_id":13965559599,"caa_release_mbid":"c30eb11a-9c23-4983-9e8f-3bff16b239a9","recording_mbid":"75cd34ef-70fd-46b4-a1a6-1d3db062b363","release_mbid":"c30eb11a-9c23-4983-9e8f-3bff16b239a9"},"release_name":"Zorawar","track_name":"Call Aundi"},"user_id":"Jasjeet"},{"created":1683366612,"recording_mbid":"75a38235-5db3-4545-bd4c-e4de9baee068","recording_msid":"0a95a65b-94a5-4f6a-a7ef-51d0455e4ad4","score":1,"track_metadata":{"additional_info":{"recording_msid":"0a95a65b-94a5-4f6a-a7ef-51d0455e4ad4"},"artist_name":"Playboi Carti","mbid_mapping":{"artist_mbids":["2baf3276-ed6a-4349-8d2e-f4601e7b2167"],"artists":[{"artist_credit_name":"Playboi Carti","artist_mbid":"2baf3276-ed6a-4349-8d2e-f4601e7b2167","join_phrase":""}],"recording_mbid":"75a38235-5db3-4545-bd4c-e4de9baee068","release_mbid":"b2d63750-05c8-4fa0-be7c-f7c3a4737a4a"},"release_name":"Magnolia","track_name":"Magnolia"},"user_id":"Jasjeet"}],"offset":0,"total_count":36} diff --git a/sharedTest/src/main/resources/pins.json b/sharedTest/src/main/resources/pins.json index 9b690172..c74b2082 100644 --- a/sharedTest/src/main/resources/pins.json +++ b/sharedTest/src/main/resources/pins.json @@ -1 +1 @@ -{"count":14,"offset":0,"pinned_recordings":[{"blurb_content":null,"created":1696606811,"pinned_until":1697211611,"recording_mbid":"9aa621e1-46f2-4c91-8111-741583985612","recording_msid":"2da9e60f-6b36-4ce1-a5fe-d84877a49006","row_id":1176,"track_metadata":{"additional_info":{"recording_msid":"2da9e60f-6b36-4ce1-a5fe-d84877a49006"},"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":17352139218,"caa_release_mbid":"69b7b91a-43fc-483f-b8cf-22cdc8f69079","recording_mbid":"9aa621e1-46f2-4c91-8111-741583985612","release_mbid":"a92e8636-a48c-4ad1-8a5f-4f8a968a50ac"},"release_name":"Hybrid Theory","track_name":"Papercut"}},{"blurb_content":"Amazing song, worth listening!","created":1648492830,"pinned_until":1649097630,"recording_mbid":"5e54d4be-a83b-41d1-9372-06fa58b4c36d","recording_msid":"8ca79982-c17d-47f2-b8f4-8ab333b7acfb","row_id":308,"track_metadata":{"additional_info":{"recording_msid":"8ca79982-c17d-47f2-b8f4-8ab333b7acfb"},"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":17454038939,"caa_release_mbid":"6c48c82c-b7df-4b0b-987d-666ec8271afb","recording_mbid":"5e54d4be-a83b-41d1-9372-06fa58b4c36d","release_mbid":"7f1a6e87-9ad2-4629-b9a5-563525607310"},"release_name":"Living Things","track_name":"Burn It Down"}},{"blurb_content":"WONDERFUL SONG!","created":1646132933,"pinned_until":1646737733,"recording_mbid":"2397ee94-274f-451e-a330-2499a93fa9fb","recording_msid":"112597b6-ec52-49cf-9916-41c3aa060a77","row_id":227,"track_metadata":{"additional_info":{"recording_msid":"112597b6-ec52-49cf-9916-41c3aa060a77"},"artist_name":"Machine Gun Kelly","release_name":"Tickets To My Downfall (SOLD OUT Deluxe)","track_name":"title track"}},{"blurb_content":"Amazing","created":1645177870,"pinned_until":1645782670,"recording_mbid":"2d24a8d7-66ad-40b2-8ae7-5f6fc9981646","recording_msid":"ba6a24ac-ef0c-44d6-b233-d0bebd925c46","row_id":219,"track_metadata":{"additional_info":{"recording_msid":"ba6a24ac-ef0c-44d6-b233-d0bebd925c46"},"artist_name":"Ed Sheeran","mbid_mapping":{"artist_mbids":["b8a7c51f-362c-4dcb-a259-bc6e0095f0a6"],"artists":[{"artist_credit_name":"Ed Sheeran","artist_mbid":"b8a7c51f-362c-4dcb-a259-bc6e0095f0a6","join_phrase":""}],"caa_id":37184507977,"caa_release_mbid":"64ae938c-6af1-4919-8341-458eb1866474","recording_mbid":"2d24a8d7-66ad-40b2-8ae7-5f6fc9981646","release_mbid":"ccfe374a-ccfa-4cbc-a37c-1cd40b914b75"},"release_name":"=","track_name":"Bad Habits"}},{"blurb_content":null,"created":1645128476,"pinned_until":1645128664,"recording_mbid":"5780b6fb-066f-4147-ac0e-0d8c8e812e02","recording_msid":"1312abf2-b12f-483f-935b-c7a283700481","row_id":217,"track_metadata":{"additional_info":{"recording_msid":"1312abf2-b12f-483f-935b-c7a283700481"},"artist_name":"Ed Sheeran","mbid_mapping":{"artist_mbids":["b8a7c51f-362c-4dcb-a259-bc6e0095f0a6"],"artists":[{"artist_credit_name":"Ed Sheeran","artist_mbid":"b8a7c51f-362c-4dcb-a259-bc6e0095f0a6","join_phrase":""}],"caa_id":31935928002,"caa_release_mbid":"14c13329-88f5-426f-90d7-0170b4017e15","recording_mbid":"5780b6fb-066f-4147-ac0e-0d8c8e812e02","release_mbid":"769e64d7-b7a8-4ad1-84d8-0e0063d8fe64"},"release_name":"\u00f7","track_name":"Castle on the Hill"}},{"blurb_content":"Wonderful song!","created":1642502920,"pinned_until":1642530147,"recording_mbid":"4fbb1e4a-c5c7-4694-a971-a3668bcffa17","recording_msid":"2e647303-cd88-4d1c-ba87-6ddda72b5edc","row_id":192,"track_metadata":{"additional_info":{"recording_msid":"2e647303-cd88-4d1c-ba87-6ddda72b5edc"},"artist_name":"Lauv","mbid_mapping":{"artist_mbids":["c0ef2ba5-a7b7-40ea-bd27-30acccfcac11"],"artists":[{"artist_credit_name":"Lauv","artist_mbid":"c0ef2ba5-a7b7-40ea-bd27-30acccfcac11","join_phrase":""}],"caa_id":25651991080,"caa_release_mbid":"63b5d3be-6538-4fc9-ac7d-a42334aebd85","recording_mbid":"4fbb1e4a-c5c7-4694-a971-a3668bcffa17","release_mbid":"63b5d3be-6538-4fc9-ac7d-a42334aebd85"},"release_name":"~how i\u2019m feeling~","track_name":"Feelings"}},{"blurb_content":"Amazing song!","created":1642333322,"pinned_until":1642502920,"recording_mbid":"cb8be04f-e8b1-425c-b68b-c4a32f2c6532","recording_msid":"b9d97e22-09c8-425a-a21d-946f631b62a9","row_id":190,"track_metadata":{"additional_info":{"recording_msid":"b9d97e22-09c8-425a-a21d-946f631b62a9"},"artist_name":"Machine Gun Kelly","mbid_mapping":{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":27366822877,"caa_release_mbid":"ca1cafc2-3daf-4ca5-b1af-8466f7836e60","recording_mbid":"cb8be04f-e8b1-425c-b68b-c4a32f2c6532","release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8"},"release_name":"Tickets to My Downfall (SOLD OUT Deluxe)","track_name":"split a pill"}},{"blurb_content":"Amazing song!","created":1638638664,"pinned_until":1639075612,"recording_mbid":"1291edb5-97d4-4893-8baa-da7c4c542377","recording_msid":"2f3f2e96-4544-4bab-b948-281fe7592b77","row_id":157,"track_metadata":{"additional_info":{"recording_msid":"2f3f2e96-4544-4bab-b948-281fe7592b77"},"artist_name":"Calum Scott","mbid_mapping":{"artist_mbids":["68fdc9cc-1094-48bb-942f-66492343e41c"],"artists":[{"artist_credit_name":"Calum Scott","artist_mbid":"68fdc9cc-1094-48bb-942f-66492343e41c","join_phrase":""}],"caa_id":18877218596,"caa_release_mbid":"86ae5c62-7989-4993-bbdb-969d77f7872a","recording_mbid":"1291edb5-97d4-4893-8baa-da7c4c542377","release_mbid":"c34d0c8b-330f-4a16-84a7-41d87a931cf1"},"release_name":"Only Human","track_name":"Dancing on My Own"}},{"blurb_content":null,"created":1637923045,"pinned_until":1638245181,"recording_mbid":"e185bbc4-7d97-44d8-93b0-b47a32fec8cd","recording_msid":"1c3a7bed-1f96-4458-b876-73191ea3f397","row_id":151,"track_metadata":{"additional_info":{"recording_msid":"1c3a7bed-1f96-4458-b876-73191ea3f397"},"artist_name":"Linkin Park","mbid_mapping":{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":7389110443,"caa_release_mbid":"29424f55-f1b2-43f6-822d-700aa1f8595c","recording_mbid":"e185bbc4-7d97-44d8-93b0-b47a32fec8cd","release_mbid":"5f5aaf10-36a4-3dfc-a7c1-c2d5c2286647"},"release_name":"A Thousand Suns","track_name":"Iridescent"}},{"blurb_content":null,"created":1636381481,"pinned_until":1636986281,"recording_mbid":"a24974c0-9da0-4f02-b59b-b209e81e2ede","recording_msid":"dbc56879-a052-49eb-b949-ea4d624159ff","row_id":116,"track_metadata":{"additional_info":{"recording_msid":"dbc56879-a052-49eb-b949-ea4d624159ff"},"artist_name":"Maroon 5","mbid_mapping":{"artist_mbids":["0ab49580-c84f-44d4-875f-d83760ea2cfe"],"artists":[{"artist_credit_name":"Maroon 5","artist_mbid":"0ab49580-c84f-44d4-875f-d83760ea2cfe","join_phrase":""}],"caa_id":33632889799,"caa_release_mbid":"e8fe33d4-31a6-4394-941c-45fac8834322","recording_mbid":"a24974c0-9da0-4f02-b59b-b209e81e2ede","release_mbid":"28832231-f70d-4937-8cbd-d4b679195d34"},"release_name":"V","track_name":"Maps"}},{"blurb_content":"Interesting song!","created":1636226679,"pinned_until":1636381455,"recording_mbid":"7ec115f0-2da7-4ddf-a183-6f4f8e10cc0e","recording_msid":"669fafd9-044b-4dc0-93af-be17ba06c4f5","row_id":113,"track_metadata":{"additional_info":{"recording_msid":"669fafd9-044b-4dc0-93af-be17ba06c4f5"},"artist_name":"Alex Gibson","mbid_mapping":{"artist_mbids":["aab3f709-be9e-48b9-9b9d-281fff5284f9"],"artists":[{"artist_credit_name":"Alex Gibson","artist_mbid":"aab3f709-be9e-48b9-9b9d-281fff5284f9","join_phrase":""}],"caa_id":15835295366,"caa_release_mbid":"5a097177-c962-4c56-9a9f-46e9f6868c9c","recording_mbid":"7ec115f0-2da7-4ddf-a183-6f4f8e10cc0e","release_mbid":"5a097177-c962-4c56-9a9f-46e9f6868c9c"},"release_name":"Rockabye Baby! Lullaby Renditions of AC/DC","track_name":"Dirty Deeds Done Dirt Cheap"}},{"blurb_content":null,"created":1635870142,"pinned_until":1636226679,"recording_mbid":"c0ecfe09-7521-42b2-9f99-366b854f280f","recording_msid":"defe1f54-97de-4a3f-a0df-7e1bdc9b3a8e","row_id":109,"track_metadata":{"additional_info":{"recording_msid":"defe1f54-97de-4a3f-a0df-7e1bdc9b3a8e"},"artist_name":"Shelly Manne & His Friends","mbid_mapping":{"artist_mbids":["7c0c1773-513e-4c47-88de-310550319921"],"artists":[{"artist_credit_name":"Shelly Manne & His Friends","artist_mbid":"7c0c1773-513e-4c47-88de-310550319921","join_phrase":""}],"caa_id":26689007265,"caa_release_mbid":"928c6630-795e-4f6b-9b25-c36b8f4212d7","recording_mbid":"c0ecfe09-7521-42b2-9f99-366b854f280f","release_mbid":"233bbe84-df40-4755-84e3-9c5937e91f8e"},"release_name":"Shelly Manne & His Friends Play Modern Jazz Performances of Songs from My Fair Lady","track_name":"Get Me to the Church on Time"}},{"blurb_content":"Great song!","created":1630035558,"pinned_until":1630640358,"recording_mbid":"92972c5e-867b-4fa2-98c5-7bd57a4e440e","recording_msid":"0df8fc8a-49b3-4854-a371-0bd19d4668c8","row_id":19,"track_metadata":{"additional_info":{"recording_msid":"0df8fc8a-49b3-4854-a371-0bd19d4668c8"},"artist_name":"Fort Minor","mbid_mapping":{"artist_mbids":["e1564e98-978b-4947-8698-f6fd6f8b0181"],"artists":[{"artist_credit_name":"Fort Minor","artist_mbid":"e1564e98-978b-4947-8698-f6fd6f8b0181","join_phrase":""}],"caa_id":28875936991,"caa_release_mbid":"2260aedd-155d-33b0-a4bb-6db7ce3f6598","recording_mbid":"92972c5e-867b-4fa2-98c5-7bd57a4e440e","release_mbid":"4b6ca48c-f7db-439d-ba57-6104b5fec61e"},"release_name":"The Rising Tied","track_name":"Petrified"}},{"blurb_content":null,"created":1630035458,"pinned_until":1630035558,"recording_mbid":"4c989c32-51a3-489a-965a-bb334def31a8","recording_msid":"566c2f69-5db0-4f2c-a504-a27836238fed","row_id":18,"track_metadata":{"additional_info":{"recording_msid":"566c2f69-5db0-4f2c-a504-a27836238fed"},"artist_name":"Machine Gun Kelly","mbid_mapping":{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":27366822877,"caa_release_mbid":"ca1cafc2-3daf-4ca5-b1af-8466f7836e60","recording_mbid":"4c989c32-51a3-489a-965a-bb334def31a8","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa"},"release_name":"Tickets to My Downfall","track_name":"concert for aliens"}}],"total_count":14,"user_name":"akshaaatt"} +{"count":12,"offset":0,"pinned_recordings":[{"blurb_content":"","created":1720693976,"pinned_until":1721298776,"recording_mbid":null,"recording_msid":"6f4a50ca-b636-4c0b-a6a0-5b84451ab014","row_id":2136,"track_metadata":{"additional_info":{"recording_msid":"6f4a50ca-b636-4c0b-a6a0-5b84451ab014"},"artist_name":"WEEDIAN","release_name":"Trip to California (Stoner Edition)","track_name":"Mesmer - Wings of Evil"}},{"blurb_content":"","created":1720593845,"pinned_until":1720693976,"recording_mbid":"9a67108f-ded9-4653-a51a-43ccb873d32c","recording_msid":"33b761de-ad44-4ee7-ab20-b28118ed9f67","row_id":2116,"track_metadata":{"additional_info":{"recording_msid":"33b761de-ad44-4ee7-ab20-b28118ed9f67"},"artist_name":"Karkara","mbid_mapping":{"artist_mbids":["1bdc2715-507c-41cb-9e06-021a8ecd6c8e"],"artists":[{"artist_credit_name":"Karkara","artist_mbid":"1bdc2715-507c-41cb-9e06-021a8ecd6c8e","join_phrase":""}],"caa_id":38158274590,"caa_release_mbid":"d5406f53-0b82-45cb-8a78-468fc343748f","recording_mbid":"9a67108f-ded9-4653-a51a-43ccb873d32c","release_mbid":"d5406f53-0b82-45cb-8a78-468fc343748f"},"release_name":"All Is Dust","track_name":"On Edge"}},{"blurb_content":"","created":1710955582,"pinned_until":1711560384,"recording_mbid":"8fe7ac15-a253-4f29-9e81-2363200af18d","recording_msid":"c361edf3-1c86-48b9-aab2-755c4fb8dce8","row_id":1698,"track_metadata":{"additional_info":{"recording_msid":"c361edf3-1c86-48b9-aab2-755c4fb8dce8"},"artist_name":"Arjan Dhillon","mbid_mapping":{"artist_mbids":["9b34cd6a-b874-4e75-bec8-1c174fe295d0"],"artists":[{"artist_credit_name":"Arjan Dhillon","artist_mbid":"9b34cd6a-b874-4e75-bec8-1c174fe295d0","join_phrase":""}],"caa_id":37928515775,"caa_release_mbid":"d360fa73-e2be-4cd8-82d2-73eb61cadd51","recording_mbid":"8fe7ac15-a253-4f29-9e81-2363200af18d","release_mbid":"d360fa73-e2be-4cd8-82d2-73eb61cadd51"},"release_name":"Chobar","track_name":"Woah"}},{"blurb_content":null,"created":1704539945,"pinned_until":1705144745,"recording_mbid":"2f14ac58-d62d-4e45-a5e1-6504d1366831","recording_msid":"65971184-31f4-4ca3-924b-29e8ae072bc5","row_id":1438,"track_metadata":{"additional_info":{"recording_msid":"65971184-31f4-4ca3-924b-29e8ae072bc5"},"artist_name":"Aaron May","mbid_mapping":{"artist_mbids":["9f3225ad-e686-4538-8cbe-99406c4a4caa"],"artists":[{"artist_credit_name":"Aaron May","artist_mbid":"9f3225ad-e686-4538-8cbe-99406c4a4caa","join_phrase":""}],"caa_id":22549299616,"caa_release_mbid":"ff0e48f3-83f6-4923-b5fa-a3c39f484da4","recording_mbid":"2f14ac58-d62d-4e45-a5e1-6504d1366831","release_mbid":"ff0e48f3-83f6-4923-b5fa-a3c39f484da4"},"release_name":"Chase","track_name":"I'm Good Luv, Enjoy."}},{"blurb_content":"Good anime good outro good game","created":1702565315,"pinned_until":1703170115,"recording_mbid":"ab100b50-31eb-4a71-98d8-52a5e4e1304e","recording_msid":"726eab7a-5a51-4d7d-aa5e-ca5e33ced3a1","row_id":1370,"track_metadata":{"additional_info":{"recording_msid":"726eab7a-5a51-4d7d-aa5e-ca5e33ced3a1"},"artist_name":"Dawid Podsiad\u0142o","mbid_mapping":{"artist_mbids":["68ffdf6b-001c-41ed-aa3a-20ec0128f627"],"artists":[{"artist_credit_name":"Dawid Podsiad\u0142o","artist_mbid":"68ffdf6b-001c-41ed-aa3a-20ec0128f627","join_phrase":""}],"caa_id":35924146102,"caa_release_mbid":"50274be9-730c-4642-8ede-25442d58867c","recording_mbid":"ab100b50-31eb-4a71-98d8-52a5e4e1304e","release_mbid":"cb64ddc6-dc64-4f31-b503-f977077c57d3"},"release_name":"Lata Dwudzieste z kawa\u0142kiem","track_name":"Let You Down"}},{"blurb_content":"Vibe","created":1702397156,"pinned_until":1702565315,"recording_mbid":"15c5d5a6-9ae9-4727-a413-d48c57e867d9","recording_msid":"ac081ab5-5702-4eb1-bd4d-cdc0504703c7","row_id":1363,"track_metadata":{"additional_info":{"recording_msid":"ac081ab5-5702-4eb1-bd4d-cdc0504703c7"},"artist_name":"Namakopuri as Us Cracks","mbid_mapping":{"artist_mbids":["1b7d53f5-1746-420b-b23d-e6b7c41a0b37","07ee1730-aee1-4c90-a225-be7b3edc94be"],"artists":[{"artist_credit_name":"Namakopuri","artist_mbid":"1b7d53f5-1746-420b-b23d-e6b7c41a0b37","join_phrase":" as "},{"artist_credit_name":"Us Cracks","artist_mbid":"07ee1730-aee1-4c90-a225-be7b3edc94be","join_phrase":""}],"caa_id":28105410573,"caa_release_mbid":"53002383-0ea4-44a4-97e1-b5d7d25e91a4","recording_mbid":"15c5d5a6-9ae9-4727-a413-d48c57e867d9","release_mbid":"53002383-0ea4-44a4-97e1-b5d7d25e91a4"},"release_name":"Cyberpunk 2077: Radio, Vol. 2 (Original Soundtrack)","track_name":"PonPon Shit"}},{"blurb_content":"new pin","created":1692526823,"pinned_until":1692910066,"recording_mbid":"b990f72b-ec15-4c6f-801c-b990f2d711e7","recording_msid":null,"row_id":1089,"track_metadata":{"artist_name":"BigXthaPlug","mbid_mapping":{"artist_mbids":["d25f6294-686a-4569-b1b9-fe64bfef2519"],"artists":[{"artist_credit_name":"BigXthaPlug","artist_mbid":"d25f6294-686a-4569-b1b9-fe64bfef2519","join_phrase":""}],"caa_id":34865132955,"caa_release_mbid":"829adf9e-773a-4026-b674-fffa1fd141bb","recording_mbid":"b990f72b-ec15-4c6f-801c-b990f2d711e7","release_mbid":"829adf9e-773a-4026-b674-fffa1fd141bb"},"release_name":"AMAR","track_name":"Levels"}},{"blurb_content":"Wow..","created":1692526658,"pinned_until":1692526823,"recording_mbid":"43e2cac8-ac33-428e-9ad9-79827d4a8b34","recording_msid":null,"row_id":1088,"track_metadata":null},{"blurb_content":"Wow..","created":1692526234,"pinned_until":1692526658,"recording_mbid":null,"recording_msid":"40ef0ae1-5626-43eb-838f-1b34187519bf","row_id":1087,"track_metadata":null},{"blurb_content":null,"created":1683483528,"pinned_until":1684088328,"recording_mbid":"75cd34ef-70fd-46b4-a1a6-1d3db062b363","recording_msid":"8dfdf578-35c2-436c-8f47-081cac3028f0","row_id":856,"track_metadata":{"additional_info":{"recording_msid":"8dfdf578-35c2-436c-8f47-081cac3028f0"},"artist_name":"Yo Yo Honey Singh","mbid_mapping":{"artist_mbids":["0dc9c4bc-8bcc-42f1-9033-bec41160377f"],"artists":[{"artist_credit_name":"Yo Yo Honey Singh","artist_mbid":"0dc9c4bc-8bcc-42f1-9033-bec41160377f","join_phrase":""}],"caa_id":13965559599,"caa_release_mbid":"c30eb11a-9c23-4983-9e8f-3bff16b239a9","recording_mbid":"75cd34ef-70fd-46b4-a1a6-1d3db062b363","release_mbid":"c30eb11a-9c23-4983-9e8f-3bff16b239a9"},"release_name":"Zorawar","track_name":"Call Aundi"}},{"blurb_content":null,"created":1683366660,"pinned_until":1683483528,"recording_mbid":"650aa31f-cf18-4239-ac59-9cb9cca92d4d","recording_msid":"f564e069-46b8-43fc-ba95-d5f615c4a2cd","row_id":855,"track_metadata":{"additional_info":{"recording_msid":"f564e069-46b8-43fc-ba95-d5f615c4a2cd"},"artist_name":"Tegi Pannu & JJ Esko","mbid_mapping":{"artist_mbids":["092552b9-ea6d-4ead-b424-cf8b07da5877","81035abf-09f7-4cbe-a785-4510dbd7970c"],"artists":[{"artist_credit_name":"Tegi Pannu","artist_mbid":"092552b9-ea6d-4ead-b424-cf8b07da5877","join_phrase":" & "},{"artist_credit_name":"JJ Esko","artist_mbid":"81035abf-09f7-4cbe-a785-4510dbd7970c","join_phrase":""}],"caa_id":34147069927,"caa_release_mbid":"75e7ef49-6a09-4856-987b-0cbefa1ba9ea","recording_mbid":"650aa31f-cf18-4239-ac59-9cb9cca92d4d","release_mbid":"75e7ef49-6a09-4856-987b-0cbefa1ba9ea"},"release_name":"Disturbing The Peace","track_name":"Hold You Down"}},{"blurb_content":null,"created":1683190944,"pinned_until":1683366660,"recording_mbid":"75a38235-5db3-4545-bd4c-e4de9baee068","recording_msid":"0a95a65b-94a5-4f6a-a7ef-51d0455e4ad4","row_id":851,"track_metadata":{"additional_info":{"recording_msid":"0a95a65b-94a5-4f6a-a7ef-51d0455e4ad4"},"artist_name":"Playboi Carti","mbid_mapping":{"artist_mbids":["2baf3276-ed6a-4349-8d2e-f4601e7b2167"],"artists":[{"artist_credit_name":"Playboi Carti","artist_mbid":"2baf3276-ed6a-4349-8d2e-f4601e7b2167","join_phrase":""}],"recording_mbid":"75a38235-5db3-4545-bd4c-e4de9baee068","release_mbid":"b2d63750-05c8-4fa0-be7c-f7c3a4737a4a"},"release_name":"Magnolia","track_name":"Magnolia"}}],"total_count":12,"user_name":"Jasjeet"} diff --git a/sharedTest/src/main/resources/similar_user_error.json b/sharedTest/src/main/resources/similar_user_error.json new file mode 100644 index 00000000..f788e421 --- /dev/null +++ b/sharedTest/src/main/resources/similar_user_error.json @@ -0,0 +1 @@ +{"code":404,"error":"Similar-to user not found"} \ No newline at end of file diff --git a/sharedTest/src/main/resources/similar_users_response.json b/sharedTest/src/main/resources/similar_users_response.json index 9d8b0324..6d50b409 100644 --- a/sharedTest/src/main/resources/similar_users_response.json +++ b/sharedTest/src/main/resources/similar_users_response.json @@ -1 +1 @@ -{"payload":[{"similarity":0.592778543651705,"user_name":"jivteshs20"},{"similarity":0.2367332837331803,"user_name":"akshaaatt"},{"similarity":0.18639954307331036,"user_name":"lucifer"}]} +{"payload":{"similarity":0.592778543651705,"user_name":"jivteshs20"}} diff --git a/sharedTest/src/main/resources/top_albums.json b/sharedTest/src/main/resources/top_albums.json index 8bb1b7e2..b4564d19 100644 --- a/sharedTest/src/main/resources/top_albums.json +++ b/sharedTest/src/main/resources/top_albums.json @@ -1 +1 @@ -{"payload":{"count":25,"from_ts":1009843200,"last_updated":1723268591,"offset":0,"range":"all_time","releases":[{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":30564814068,"caa_release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","listen_count":2463,"release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","release_name":"Tickets to My Downfall"},{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artist_name":"Linkin Park","artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":16767050943,"caa_release_mbid":"44703741-a292-42e2-85d5-117ca1df8d9b","listen_count":1115,"release_mbid":"44703741-a292-42e2-85d5-117ca1df8d9b","release_name":"One More Light"},{"artist_mbids":["c0ef2ba5-a7b7-40ea-bd27-30acccfcac11"],"artist_name":"Lauv","artists":[{"artist_credit_name":"Lauv","artist_mbid":"c0ef2ba5-a7b7-40ea-bd27-30acccfcac11","join_phrase":""}],"caa_id":25651991080,"caa_release_mbid":"63b5d3be-6538-4fc9-ac7d-a42334aebd85","listen_count":1023,"release_mbid":"63b5d3be-6538-4fc9-ac7d-a42334aebd85","release_name":"~how i\u2019m feeling~"},{"artist_mbids":["e5712ceb-c37a-4c49-a11c-ccf4e21852d4"],"artist_name":"Troye Sivan","artists":[{"artist_credit_name":"Troye Sivan","artist_mbid":"e5712ceb-c37a-4c49-a11c-ccf4e21852d4","join_phrase":""}],"caa_id":11788642695,"caa_release_mbid":"e7d13ff4-ec74-4eff-b0d3-bdc00b720944","listen_count":927,"release_mbid":"b6d0161d-1073-48e2-8503-4bd3261899ee","release_name":"Blue Neighbourhood"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":27494670999,"caa_release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8","listen_count":731,"release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8","release_name":"Tickets to My Downfall (SOLD OUT Deluxe)"},{"artist_mbids":["1a425bbd-cca4-4b2c-aeb7-71cb176c828a"],"artist_name":"One Direction","artists":[{"artist_credit_name":"One Direction","artist_mbid":"1a425bbd-cca4-4b2c-aeb7-71cb176c828a","join_phrase":""}],"caa_id":34762378442,"caa_release_mbid":"a31c02ee-5f62-4b9d-b576-2b02c5e4d750","listen_count":561,"release_mbid":"a31c02ee-5f62-4b9d-b576-2b02c5e4d750","release_name":"Four: The Ultimate Edition"},{"artist_mbids":["c262b6bf-be56-4b26-bceb-42d7a27342f3"],"artist_name":"Mike Shinoda","artists":[{"artist_credit_name":"Mike Shinoda","artist_mbid":"c262b6bf-be56-4b26-bceb-42d7a27342f3","join_phrase":""}],"caa_id":19495585058,"caa_release_mbid":"639eb4b8-2ea0-4455-a067-f1d9faade1c9","listen_count":542,"release_mbid":"639eb4b8-2ea0-4455-a067-f1d9faade1c9","release_name":"Post Traumatic"},{"artist_mbids":["e5712ceb-c37a-4c49-a11c-ccf4e21852d4"],"artist_name":"Troye Sivan","artists":[{"artist_credit_name":"Troye Sivan","artist_mbid":"e5712ceb-c37a-4c49-a11c-ccf4e21852d4","join_phrase":""}],"caa_id":20981274842,"caa_release_mbid":"2a7a4450-32da-4e21-9893-9b7282eb9c4a","listen_count":518,"release_mbid":"2a7a4450-32da-4e21-9893-9b7282eb9c4a","release_name":"Bloom"},{"artist_mbids":["074e3847-f67f-49f9-81f1-8c8cea147e8e"],"artist_name":"Bring Me the Horizon","artists":[{"artist_credit_name":"Bring Me the Horizon","artist_mbid":"074e3847-f67f-49f9-81f1-8c8cea147e8e","join_phrase":""}],"caa_id":16426388091,"caa_release_mbid":"a78e93a9-6073-4d80-a69b-d008936257f3","listen_count":513,"release_mbid":"a78e93a9-6073-4d80-a69b-d008936257f3","release_name":"That\u2019s the Spirit"},{"artist_mbids":["21a14ee3-cede-420a-9d6d-a33517d2f952"],"artist_name":"Alec Benjamin","artists":[{"artist_credit_name":"Alec Benjamin","artist_mbid":"21a14ee3-cede-420a-9d6d-a33517d2f952","join_phrase":""}],"caa_id":31885394071,"caa_release_mbid":"946b0030-b999-4f21-b9d1-9e82f02e6d96","listen_count":496,"release_mbid":"eeb50973-2e71-4d04-939e-298ba8bc0af8","release_name":"(Un)Commentary"},{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artist_name":"Linkin Park","artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":21070222550,"caa_release_mbid":"127e18cd-99ad-3193-ad4f-441bee3c6e3b","listen_count":495,"release_mbid":"127e18cd-99ad-3193-ad4f-441bee3c6e3b","release_name":"Minutes to Midnight"},{"artist_mbids":[],"artist_name":"Sanam Malik, ANA","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":419,"release_mbid":null,"release_name":"Jaane Kaise"},{"artist_mbids":["0ab49580-c84f-44d4-875f-d83760ea2cfe"],"artist_name":"Maroon 5","artists":[{"artist_credit_name":"Maroon 5","artist_mbid":"0ab49580-c84f-44d4-875f-d83760ea2cfe","join_phrase":""}],"caa_id":22165193878,"caa_release_mbid":"28832231-f70d-4937-8cbd-d4b679195d34","listen_count":384,"release_mbid":"28832231-f70d-4937-8cbd-d4b679195d34","release_name":"V"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":32210635464,"caa_release_mbid":"b94f45e0-7a1a-4895-a45b-3abd2ec5eda2","listen_count":374,"release_mbid":"b94f45e0-7a1a-4895-a45b-3abd2ec5eda2","release_name":"MAINSTREAM SELLOUT"},{"artist_mbids":["e1564e98-978b-4947-8698-f6fd6f8b0181"],"artist_name":"Fort Minor","artists":[{"artist_credit_name":"Fort Minor","artist_mbid":"e1564e98-978b-4947-8698-f6fd6f8b0181","join_phrase":""}],"caa_id":5891285623,"caa_release_mbid":"4b6ca48c-f7db-439d-ba57-6104b5fec61e","listen_count":371,"release_mbid":"4b6ca48c-f7db-439d-ba57-6104b5fec61e","release_name":"The Rising Tied"},{"artist_mbids":["89ad4ac3-39f7-470e-963a-56509c546377"],"artist_name":"\u30f4\u30a1\u30ea\u30a2\u30b9\u30fb\u30a2\u30fc\u30c6\u30a3\u30b9\u30c8","artists":[{"artist_credit_name":"\u30f4\u30a1\u30ea\u30a2\u30b9\u30fb\u30a2\u30fc\u30c6\u30a3\u30b9\u30c8","artist_mbid":"89ad4ac3-39f7-470e-963a-56509c546377","join_phrase":""}],"caa_id":38587113752,"caa_release_mbid":"9efe4f5e-3634-4aa9-94c7-43d960955eea","listen_count":351,"release_mbid":"9efe4f5e-3634-4aa9-94c7-43d960955eea","release_name":"\u604b\u3057\u305f\u304f\u306a\u308b\u30e9\u30d6\u30bd\u30f3\u30b0"},{"artist_mbids":["f59c5520-5f46-4d2c-b2c4-822eabf53419"],"artist_name":"Linkin Park","artists":[{"artist_credit_name":"Linkin Park","artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","join_phrase":""}],"caa_id":17668660885,"caa_release_mbid":"7f1a6e87-9ad2-4629-b9a5-563525607310","listen_count":343,"release_mbid":"7f1a6e87-9ad2-4629-b9a5-563525607310","release_name":"Living Things"},{"artist_mbids":["b1e26560-60e5-4236-bbdb-9aa5a8d5ee19"],"artist_name":"Post Malone","artists":[{"artist_credit_name":"Post Malone","artist_mbid":"b1e26560-60e5-4236-bbdb-9aa5a8d5ee19","join_phrase":""}],"caa_id":24123818307,"caa_release_mbid":"fe2c8953-e3d5-40fe-a855-cdb5eb8357a0","listen_count":342,"release_mbid":"fe2c8953-e3d5-40fe-a855-cdb5eb8357a0","release_name":"Hollywood\u2019s Bleeding"},{"artist_mbids":["ada7a83c-e3e1-40f1-93f9-3e73dbc9298a"],"artist_name":"Arctic Monkeys","artists":[{"artist_credit_name":"Arctic Monkeys","artist_mbid":"ada7a83c-e3e1-40f1-93f9-3e73dbc9298a","join_phrase":""}],"caa_id":32131848311,"caa_release_mbid":"55171afe-440e-4c63-947c-e49074f3d5b5","listen_count":339,"release_mbid":"55171afe-440e-4c63-947c-e49074f3d5b5","release_name":"AM"},{"artist_mbids":["985f7e6f-0a7e-4de7-b9ec-a5dac63cb2f7"],"artist_name":"ZAYN","artists":[{"artist_credit_name":"ZAYN","artist_mbid":"985f7e6f-0a7e-4de7-b9ec-a5dac63cb2f7","join_phrase":""}],"caa_id":22079449622,"caa_release_mbid":"871f8a05-6620-41dc-a569-624122029ce7","listen_count":321,"release_mbid":"6d4045e6-432d-4302-af8e-923e581ad6c7","release_name":"Icarus Falls"},{"artist_mbids":[],"artist_name":"TakaseToya, emi noda","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":319,"release_mbid":null,"release_name":"The 2nd of Undecimber"},{"artist_mbids":["6f500293-7396-4903-b4fd-118127d06f9e","ddd49f88-6367-4f58-9dd9-a767e976b0b7"],"artist_name":"RADWIMPS feat. \u5341\u660e","artists":[{"artist_credit_name":"RADWIMPS","artist_mbid":"6f500293-7396-4903-b4fd-118127d06f9e","join_phrase":" feat. "},{"artist_credit_name":"\u5341\u660e","artist_mbid":"ddd49f88-6367-4f58-9dd9-a767e976b0b7","join_phrase":""}],"caa_id":33711108184,"caa_release_mbid":"4fc95fab-017c-4133-affc-7f571bb55fd8","listen_count":317,"release_mbid":"4fc95fab-017c-4133-affc-7f571bb55fd8","release_name":"\u3059\u305a\u3081"},{"artist_mbids":["c8b03190-306c-4120-bb0b-6f2ebfc06ea9"],"artist_name":"The Weeknd","artists":[{"artist_credit_name":"The Weeknd","artist_mbid":"c8b03190-306c-4120-bb0b-6f2ebfc06ea9","join_phrase":""}],"caa_id":16759920843,"caa_release_mbid":"4f61a8bb-aa55-4951-8cfc-0809401db4ca","listen_count":307,"release_mbid":"4f61a8bb-aa55-4951-8cfc-0809401db4ca","release_name":"Starboy"},{"artist_mbids":["f931c961-b647-4861-be8c-f47d84a4de51"],"artist_name":"Diljit Dosanjh","artists":[{"artist_credit_name":"Diljit Dosanjh","artist_mbid":"f931c961-b647-4861-be8c-f47d84a4de51","join_phrase":""}],"caa_id":31442218448,"caa_release_mbid":"70252510-27ce-42f1-953a-f3966a5499a0","listen_count":306,"release_mbid":"70252510-27ce-42f1-953a-f3966a5499a0","release_name":"MoonChild Era"},{"artist_mbids":["98c09f94-10b5-426c-b27a-44345bb54528"],"artist_name":"Sasha Sloan","artists":[{"artist_credit_name":"Sasha Sloan","artist_mbid":"98c09f94-10b5-426c-b27a-44345bb54528","join_phrase":""}],"caa_id":32812417399,"caa_release_mbid":"87231c00-929a-47e6-a633-f464078eb4dc","listen_count":285,"release_mbid":"87231c00-929a-47e6-a633-f464078eb4dc","release_name":"Self Portrait"}],"to_ts":1723248018,"total_release_count":1360,"user_id":"akshaaatt"}} +{"payload":{"count":25,"from_ts":1009843200,"last_updated":1723268864,"offset":0,"range":"all_time","releases":[{"artist_mbids":["4a779683-5404-4b90-a0d7-242495158265"],"artist_name":"Karan Aujla","artists":[{"artist_credit_name":"Karan Aujla","artist_mbid":"4a779683-5404-4b90-a0d7-242495158265","join_phrase":""}],"caa_id":32050224646,"caa_release_mbid":"bdd4aa78-9caf-40e4-a698-6639f96571eb","listen_count":156,"release_mbid":"bdd4aa78-9caf-40e4-a698-6639f96571eb","release_name":"B.T.F.U"},{"artist_mbids":["9b34cd6a-b874-4e75-bec8-1c174fe295d0"],"artist_name":"Arjan Dhillon","artists":[{"artist_credit_name":"Arjan Dhillon","artist_mbid":"9b34cd6a-b874-4e75-bec8-1c174fe295d0","join_phrase":""}],"caa_id":31811400978,"caa_release_mbid":"d8c0026f-e10c-494d-b66c-2085062b8c2e","listen_count":150,"release_mbid":"d8c0026f-e10c-494d-b66c-2085062b8c2e","release_name":"Awara"},{"artist_mbids":[],"artist_name":"Ekam Sudhar, Manni Sandhu, Rav Hanjra","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":127,"release_mbid":null,"release_name":"Small Circle"},{"artist_mbids":["d0a37e1c-a9c3-4966-b630-984c33b087df"],"artist_name":"Eifi","artists":[{"artist_credit_name":"Eifi","artist_mbid":"d0a37e1c-a9c3-4966-b630-984c33b087df","join_phrase":""}],"caa_id":35241947664,"caa_release_mbid":"1dd36e0a-16e8-4486-b062-00891253abb0","listen_count":103,"release_mbid":"1dd36e0a-16e8-4486-b062-00891253abb0","release_name":"Old School Vibe"},{"artist_mbids":["8837b875-3689-4646-b3fd-d8b53815c7a8"],"artist_name":"Lil Uzi Vert","artists":[{"artist_credit_name":"Lil Uzi Vert","artist_mbid":"8837b875-3689-4646-b3fd-d8b53815c7a8","join_phrase":""}],"caa_id":35008042732,"caa_release_mbid":"de152c73-f234-4456-a032-cac36b5c006d","listen_count":99,"release_mbid":"de152c73-f234-4456-a032-cac36b5c006d","release_name":"Watch This (ARIZONATEARS pluggnb remix)"},{"artist_mbids":["092552b9-ea6d-4ead-b424-cf8b07da5877"],"artist_name":"Tegi Pannu","artists":[{"artist_credit_name":"Tegi Pannu","artist_mbid":"092552b9-ea6d-4ead-b424-cf8b07da5877","join_phrase":""}],"caa_id":34147069927,"caa_release_mbid":"75e7ef49-6a09-4856-987b-0cbefa1ba9ea","listen_count":77,"release_mbid":"75e7ef49-6a09-4856-987b-0cbefa1ba9ea","release_name":"Disturbing The Peace"},{"artist_mbids":[],"artist_name":"Amantej Hundal","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":74,"release_mbid":null,"release_name":"Underrated"},{"artist_mbids":["63aa26c3-d59b-4da4-84ac-716b54f1ef4d"],"artist_name":"Tame Impala","artists":[{"artist_credit_name":"Tame Impala","artist_mbid":"63aa26c3-d59b-4da4-84ac-716b54f1ef4d","join_phrase":""}],"caa_id":25489083340,"caa_release_mbid":"b9410b9a-cb64-41ac-b863-fa95f8eb51ea","listen_count":64,"release_mbid":"b9410b9a-cb64-41ac-b863-fa95f8eb51ea","release_name":"Currents"},{"artist_mbids":["9537e089-79dd-43d7-9cf3-f6d51f6864fc"],"artist_name":"The Landers","artists":[{"artist_credit_name":"The Landers","artist_mbid":"9537e089-79dd-43d7-9cf3-f6d51f6864fc","join_phrase":""}],"caa_id":32015349705,"caa_release_mbid":"c7f3577a-8e75-4fc7-9498-1010054af117","listen_count":63,"release_mbid":"c7f3577a-8e75-4fc7-9498-1010054af117","release_name":"Gustakhiyan"},{"artist_mbids":["4a779683-5404-4b90-a0d7-242495158265","3ea12c3c-8596-4d70-b327-208b0a459a97"],"artist_name":"Karan Aujla, Ikky","artists":[{"artist_credit_name":"Karan Aujla","artist_mbid":"4a779683-5404-4b90-a0d7-242495158265","join_phrase":", "},{"artist_credit_name":"Ikky","artist_mbid":"3ea12c3c-8596-4d70-b327-208b0a459a97","join_phrase":""}],"caa_id":34792503592,"caa_release_mbid":"1390f1b7-7851-48ae-983d-eb8a48f78048","listen_count":63,"release_mbid":"1390f1b7-7851-48ae-983d-eb8a48f78048","release_name":"Four You"},{"artist_mbids":["381086ea-f511-4aba-bdf9-71c753dc5077"],"artist_name":"Kendrick Lamar","artists":[{"artist_credit_name":"Kendrick Lamar","artist_mbid":"381086ea-f511-4aba-bdf9-71c753dc5077","join_phrase":""}],"caa_id":2361576294,"caa_release_mbid":"e1d99364-1ad9-4f4d-9505-2242eff10a44","listen_count":51,"release_mbid":"e1d99364-1ad9-4f4d-9505-2242eff10a44","release_name":"good kid, m.A.A.d city"},{"artist_mbids":[],"artist_name":"R Nait","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":47,"release_mbid":null,"release_name":"Toy"},{"artist_mbids":[],"artist_name":"Arjan Dhillon","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":45,"release_mbid":null,"release_name":"Night Out"},{"artist_mbids":[],"artist_name":"Arjan Dhillon","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":43,"release_mbid":null,"release_name":"Kise Naal Ni Bolda"},{"artist_mbids":["610d38e7-f5b5-4ec8-b10d-17387ee11bdd","878391e2-c5e7-4b51-bbc1-236326fa810c"],"artist_name":"Amrit Maan & The PropheC","artists":[{"artist_credit_name":"Amrit Maan","artist_mbid":"610d38e7-f5b5-4ec8-b10d-17387ee11bdd","join_phrase":" & "},{"artist_credit_name":"The PropheC","artist_mbid":"878391e2-c5e7-4b51-bbc1-236326fa810c","join_phrase":""}],"caa_id":33469880728,"caa_release_mbid":"eb0701cd-5d83-4c23-ab7c-df4cc9dbb639","listen_count":40,"release_mbid":"eb0701cd-5d83-4c23-ab7c-df4cc9dbb639","release_name":"My Moon"},{"artist_mbids":["7e1cf02b-cf78-4803-b496-930c38649223"],"artist_name":"Jassa Dhillon","artists":[{"artist_credit_name":"Jassa Dhillon","artist_mbid":"7e1cf02b-cf78-4803-b496-930c38649223","join_phrase":""}],"caa_id":31988019523,"caa_release_mbid":"101876a6-8e7d-4512-97d5-a0ff08f22f53","listen_count":40,"release_mbid":"101876a6-8e7d-4512-97d5-a0ff08f22f53","release_name":"Love War"},{"artist_mbids":["9e975e9b-7538-4acd-ba91-8e5fd5df6b6e"],"artist_name":"Kid Bloom","artists":[{"artist_credit_name":"Kid Bloom","artist_mbid":"9e975e9b-7538-4acd-ba91-8e5fd5df6b6e","join_phrase":""}],"caa_id":null,"caa_release_mbid":null,"listen_count":36,"release_mbid":"75169a25-004e-40f5-aaa7-78d2de09eb28","release_name":"Electric U -Single"},{"artist_mbids":["bce6d667-cde8-485e-b078-c0a05adea36d"],"artist_name":"ScHoolboy Q","artists":[{"artist_credit_name":"ScHoolboy Q","artist_mbid":"bce6d667-cde8-485e-b078-c0a05adea36d","join_phrase":""}],"caa_id":32363103828,"caa_release_mbid":"9fc29035-a4c5-4e7c-b984-a52e9aa63ad8","listen_count":36,"release_mbid":"9fc29035-a4c5-4e7c-b984-a52e9aa63ad8","release_name":"CrasH Talk"},{"artist_mbids":[],"artist_name":"NIJJAR","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":34,"release_mbid":null,"release_name":"Chobbar"},{"artist_mbids":["ada7a83c-e3e1-40f1-93f9-3e73dbc9298a"],"artist_name":"Arctic Monkeys","artists":[{"artist_credit_name":"Arctic Monkeys","artist_mbid":"ada7a83c-e3e1-40f1-93f9-3e73dbc9298a","join_phrase":""}],"caa_id":32131848311,"caa_release_mbid":"55171afe-440e-4c63-947c-e49074f3d5b5","listen_count":33,"release_mbid":"55171afe-440e-4c63-947c-e49074f3d5b5","release_name":"AM"},{"artist_mbids":["2338cede-17d7-40ef-9ea1-a16416908eeb"],"artist_name":"Sports","artists":[{"artist_credit_name":"Sports","artist_mbid":"2338cede-17d7-40ef-9ea1-a16416908eeb","join_phrase":""}],"caa_id":16665200572,"caa_release_mbid":"afea971b-c6ed-4ff8-9552-b47b8569c751","listen_count":32,"release_mbid":"afea971b-c6ed-4ff8-9552-b47b8569c751","release_name":"Naked All The Time"},{"artist_mbids":["9f3225ad-e686-4538-8cbe-99406c4a4caa"],"artist_name":"Aaron May","artists":[{"artist_credit_name":"Aaron May","artist_mbid":"9f3225ad-e686-4538-8cbe-99406c4a4caa","join_phrase":""}],"caa_id":22549299616,"caa_release_mbid":"ff0e48f3-83f6-4923-b5fa-a3c39f484da4","listen_count":31,"release_mbid":"ff0e48f3-83f6-4923-b5fa-a3c39f484da4","release_name":"Chase"},{"artist_mbids":["3538581d-2e3f-40ac-98fe-fd434f29d04d"],"artist_name":"Nirvair Pannu","artists":[{"artist_credit_name":"Nirvair Pannu","artist_mbid":"3538581d-2e3f-40ac-98fe-fd434f29d04d","join_phrase":""}],"caa_id":33181866663,"caa_release_mbid":"9a2d9b48-146f-4980-bdc1-891bc0b598b1","listen_count":29,"release_mbid":"9a2d9b48-146f-4980-bdc1-891bc0b598b1","release_name":"Click"},{"artist_mbids":[],"artist_name":"Navaan Sandhu","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":28,"release_mbid":null,"release_name":"Relentless"},{"artist_mbids":["9b34cd6a-b874-4e75-bec8-1c174fe295d0"],"artist_name":"Arjan Dhillon","artists":[{"artist_credit_name":"Arjan Dhillon","artist_mbid":"9b34cd6a-b874-4e75-bec8-1c174fe295d0","join_phrase":""}],"caa_id":33843554085,"caa_release_mbid":"6c7dc4d8-7505-49ef-88d6-86a3b7fbb7eb","listen_count":26,"release_mbid":"6c7dc4d8-7505-49ef-88d6-86a3b7fbb7eb","release_name":"Jalwa"}],"to_ts":1723248018,"total_release_count":638,"user_id":"Jasjeet"}} diff --git a/sharedTest/src/main/resources/top_artists.json b/sharedTest/src/main/resources/top_artists.json index 7c93c076..7d698b1b 100644 --- a/sharedTest/src/main/resources/top_artists.json +++ b/sharedTest/src/main/resources/top_artists.json @@ -1 +1 @@ -{"payload":{"artists":[{"artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","artist_name":"mgk","listen_count":3762},{"artist_mbid":"f59c5520-5f46-4d2c-b2c4-822eabf53419","artist_name":"Linkin Park","listen_count":3000},{"artist_mbid":"e5712ceb-c37a-4c49-a11c-ccf4e21852d4","artist_name":"Troye Sivan","listen_count":2515},{"artist_mbid":"c0ef2ba5-a7b7-40ea-bd27-30acccfcac11","artist_name":"Lauv","listen_count":1730},{"artist_mbid":"20244d07-534f-4eff-b4d4-930878889970","artist_name":"Taylor Swift","listen_count":1038},{"artist_mbid":"3377f3bb-60fc-4403-aea9-7e800612e060","artist_name":"Halsey","listen_count":1014},{"artist_mbid":"c262b6bf-be56-4b26-bceb-42d7a27342f3","artist_name":"Mike Shinoda","listen_count":1003},{"artist_mbid":"074e3847-f67f-49f9-81f1-8c8cea147e8e","artist_name":"Bring Me the Horizon","listen_count":983},{"artist_mbid":"f4fdbb4c-e4b7-47a0-b83b-d91bbfcfa387","artist_name":"Ariana Grande","listen_count":747},{"artist_mbid":"21a14ee3-cede-420a-9d6d-a33517d2f952","artist_name":"Alec Benjamin","listen_count":728},{"artist_mbid":"1a425bbd-cca4-4b2c-aeb7-71cb176c828a","artist_name":"One Direction","listen_count":696},{"artist_mbid":"ada7a83c-e3e1-40f1-93f9-3e73dbc9298a","artist_name":"Arctic Monkeys","listen_count":687},{"artist_mbid":"0ab49580-c84f-44d4-875f-d83760ea2cfe","artist_name":"Maroon 5","listen_count":631},{"artist_mbid":"c8b03190-306c-4120-bb0b-6f2ebfc06ea9","artist_name":"The Weeknd","listen_count":588},{"artist_mbid":"e07d9474-00ea-4460-ac27-88b46b3d976e","artist_name":"blackbear","listen_count":522},{"artist_mbid":"6f500293-7396-4903-b4fd-118127d06f9e","artist_name":"RADWIMPS","listen_count":448},{"artist_mbid":"e1564e98-978b-4947-8698-f6fd6f8b0181","artist_name":"Fort Minor","listen_count":431},{"artist_mbid":"985f7e6f-0a7e-4de7-b9ec-a5dac63cb2f7","artist_name":"ZAYN","listen_count":421},{"artist_mbid":null,"artist_name":"Sanam Malik, ANA","listen_count":419},{"artist_mbid":"f931c961-b647-4861-be8c-f47d84a4de51","artist_name":"Diljit Dosanjh","listen_count":419},{"artist_mbid":"c984f772-a70d-44b6-ad12-47ec8d3fdd35","artist_name":"Aditya A","listen_count":419},{"artist_mbid":"b1e26560-60e5-4236-bbdb-9aa5a8d5ee19","artist_name":"Post Malone","listen_count":364},{"artist_mbid":"4757df70-6c3e-46b8-99c0-a68644595c9a","artist_name":"Julia Michaels","listen_count":361},{"artist_mbid":"68fdc9cc-1094-48bb-942f-66492343e41c","artist_name":"Calum Scott","listen_count":361},{"artist_mbid":"525f1f1c-03f0-4bc8-8dfd-e7521f87631b","artist_name":"Charlie Puth","listen_count":335}],"count":25,"from_ts":1009843200,"last_updated":1723268040,"offset":0,"range":"all_time","to_ts":1723248018,"total_artist_count":1093,"user_id":"akshaaatt"}} +{"payload":{"artists":[{"artist_mbid":"4a779683-5404-4b90-a0d7-242495158265","artist_name":"Karan Aujla","listen_count":263},{"artist_mbid":"9b34cd6a-b874-4e75-bec8-1c174fe295d0","artist_name":"Arjan Dhillon","listen_count":188},{"artist_mbid":null,"artist_name":"Ekam Sudhar, Manni Sandhu, Rav Hanjra","listen_count":127},{"artist_mbid":null,"artist_name":"Arjan Dhillon","listen_count":127},{"artist_mbid":"092552b9-ea6d-4ead-b424-cf8b07da5877","artist_name":"Tegi Pannu","listen_count":114},{"artist_mbid":"8837b875-3689-4646-b3fd-d8b53815c7a8","artist_name":"Lil Uzi Vert","listen_count":104},{"artist_mbid":"63aa26c3-d59b-4da4-84ac-716b54f1ef4d","artist_name":"Tame Impala","listen_count":103},{"artist_mbid":"d0a37e1c-a9c3-4966-b630-984c33b087df","artist_name":"Eifi","listen_count":103},{"artist_mbid":null,"artist_name":"Amantej Hundal","listen_count":75},{"artist_mbid":"3ea12c3c-8596-4d70-b327-208b0a459a97","artist_name":"Ikky","listen_count":65},{"artist_mbid":"381086ea-f511-4aba-bdf9-71c753dc5077","artist_name":"Kendrick Lamar","listen_count":64},{"artist_mbid":"9537e089-79dd-43d7-9cf3-f6d51f6864fc","artist_name":"The Landers","listen_count":63},{"artist_mbid":"7e1cf02b-cf78-4803-b496-930c38649223","artist_name":"Jassa Dhillon","listen_count":54},{"artist_mbid":"119a6864-622b-4e6c-8aab-a422080530c6","artist_name":"Sidhu Moose Wala","listen_count":53},{"artist_mbid":"9880800a-f6f8-4f8f-a00e-b4b664776c0c","artist_name":"Jay Rock","listen_count":52},{"artist_mbid":"878391e2-c5e7-4b51-bbc1-236326fa810c","artist_name":"The PropheC","listen_count":49},{"artist_mbid":null,"artist_name":"R Nait","listen_count":49},{"artist_mbid":"610d38e7-f5b5-4ec8-b10d-17387ee11bdd","artist_name":"Amrit Maan","listen_count":48},{"artist_mbid":null,"artist_name":"Navaan Sandhu","listen_count":40},{"artist_mbid":"6ae6bde5-3727-4202-9298-e2f3de2235cd","artist_name":"Manni Sandhu","listen_count":38},{"artist_mbid":"ada7a83c-e3e1-40f1-93f9-3e73dbc9298a","artist_name":"Arctic Monkeys","listen_count":38},{"artist_mbid":"bce6d667-cde8-485e-b078-c0a05adea36d","artist_name":"ScHoolboy Q","listen_count":36},{"artist_mbid":"9e975e9b-7538-4acd-ba91-8e5fd5df6b6e","artist_name":"Kid Bloom","listen_count":36},{"artist_mbid":"83fd7495-a02f-4254-9cfb-6753cb87c73e","artist_name":"Prem Dhillon","listen_count":34},{"artist_mbid":null,"artist_name":"NIJJAR","listen_count":34}],"count":25,"from_ts":1009843200,"last_updated":1723268083,"offset":0,"range":"all_time","to_ts":1723248018,"total_artist_count":617,"user_id":"Jasjeet"}} diff --git a/sharedTest/src/main/resources/top_songs.json b/sharedTest/src/main/resources/top_songs.json index 6adb7ea1..dc1e0bd5 100644 --- a/sharedTest/src/main/resources/top_songs.json +++ b/sharedTest/src/main/resources/top_songs.json @@ -1 +1 @@ -{"payload":{"count":25,"from_ts":1009843200,"last_updated":1723270823,"offset":0,"range":"all_time","recordings":[{"artist_mbids":["21a14ee3-cede-420a-9d6d-a33517d2f952"],"artist_name":"Alec Benjamin","artists":[{"artist_credit_name":"Alec Benjamin","artist_mbid":"21a14ee3-cede-420a-9d6d-a33517d2f952","join_phrase":""}],"caa_id":31885394071,"caa_release_mbid":"946b0030-b999-4f21-b9d1-9e82f02e6d96","listen_count":431,"recording_mbid":"e2ef1fc7-88c5-44c0-abde-7144f55d15bd","release_mbid":"eeb50973-2e71-4d04-939e-298ba8bc0af8","release_name":"(Un)Commentary","track_name":"Older"},{"artist_mbids":[],"artist_name":"Sanam Malik, ANA","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":419,"recording_mbid":null,"release_mbid":null,"release_name":"Jaane Kaise","track_name":"Jaane Kaise"},{"artist_mbids":["c0ef2ba5-a7b7-40ea-bd27-30acccfcac11"],"artist_name":"Lauv","artists":[{"artist_credit_name":"Lauv","artist_mbid":"c0ef2ba5-a7b7-40ea-bd27-30acccfcac11","join_phrase":""}],"caa_id":25651991080,"caa_release_mbid":"63b5d3be-6538-4fc9-ac7d-a42334aebd85","listen_count":411,"recording_mbid":"4fbb1e4a-c5c7-4694-a971-a3668bcffa17","release_mbid":"63b5d3be-6538-4fc9-ac7d-a42334aebd85","release_name":"~how i\u2019m feeling~","track_name":"Feelings"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33","e07d9474-00ea-4460-ac27-88b46b3d976e"],"artist_name":"Machine Gun Kelly feat. blackbear","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":" feat. "},{"artist_credit_name":"blackbear","artist_mbid":"e07d9474-00ea-4460-ac27-88b46b3d976e","join_phrase":""}],"caa_id":30564814068,"caa_release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","listen_count":399,"recording_mbid":"6cd38dad-148f-4381-9cf4-b75f496238c6","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","release_name":"Tickets to My Downfall","track_name":"my ex\u2019s best friend"},{"artist_mbids":["e5712ceb-c37a-4c49-a11c-ccf4e21852d4"],"artist_name":"Troye Sivan","artists":[{"artist_credit_name":"Troye Sivan","artist_mbid":"e5712ceb-c37a-4c49-a11c-ccf4e21852d4","join_phrase":""}],"caa_id":11788642695,"caa_release_mbid":"e7d13ff4-ec74-4eff-b0d3-bdc00b720944","listen_count":390,"recording_mbid":"15e7a501-4f35-4977-8956-0e0c8b644af6","release_mbid":"b6d0161d-1073-48e2-8503-4bd3261899ee","release_name":"Blue Neighbourhood","track_name":"LOST BOY"},{"artist_mbids":["e5712ceb-c37a-4c49-a11c-ccf4e21852d4"],"artist_name":"Troye Sivan","artists":[{"artist_credit_name":"Troye Sivan","artist_mbid":"e5712ceb-c37a-4c49-a11c-ccf4e21852d4","join_phrase":""}],"caa_id":38587113752,"caa_release_mbid":"9efe4f5e-3634-4aa9-94c7-43d960955eea","listen_count":351,"recording_mbid":"0f8d5cc0-7e6e-486e-9544-eca4736cd8b7","release_mbid":"9efe4f5e-3634-4aa9-94c7-43d960955eea","release_name":"\u604b\u3057\u305f\u304f\u306a\u308b\u30e9\u30d6\u30bd\u30f3\u30b0","track_name":"Strawberries & Cigarettes"},{"artist_mbids":["e5712ceb-c37a-4c49-a11c-ccf4e21852d4"],"artist_name":"Troye Sivan","artists":[{"artist_credit_name":"Troye Sivan","artist_mbid":"e5712ceb-c37a-4c49-a11c-ccf4e21852d4","join_phrase":""}],"caa_id":20981274842,"caa_release_mbid":"2a7a4450-32da-4e21-9893-9b7282eb9c4a","listen_count":319,"recording_mbid":"f75b6b94-46b3-4c0a-af56-05b4dfbee957","release_mbid":"2a7a4450-32da-4e21-9893-9b7282eb9c4a","release_name":"Bloom","track_name":"Lucky Strike"},{"artist_mbids":[],"artist_name":"TakaseToya, emi noda","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":319,"recording_mbid":null,"release_mbid":null,"release_name":"The 2nd of Undecimber","track_name":"Doushite"},{"artist_mbids":["6f500293-7396-4903-b4fd-118127d06f9e","ddd49f88-6367-4f58-9dd9-a767e976b0b7"],"artist_name":"RADWIMPS feat. \u5341\u660e","artists":[{"artist_credit_name":"RADWIMPS","artist_mbid":"6f500293-7396-4903-b4fd-118127d06f9e","join_phrase":" feat. "},{"artist_credit_name":"\u5341\u660e","artist_mbid":"ddd49f88-6367-4f58-9dd9-a767e976b0b7","join_phrase":""}],"caa_id":33711108184,"caa_release_mbid":"4fc95fab-017c-4133-affc-7f571bb55fd8","listen_count":317,"recording_mbid":"4da804f6-98dc-456b-8fb3-5ffd971b18ec","release_mbid":"4fc95fab-017c-4133-affc-7f571bb55fd8","release_name":"\u3059\u305a\u3081","track_name":"\u3059\u305a\u3081"},{"artist_mbids":["f931c961-b647-4861-be8c-f47d84a4de51"],"artist_name":"Diljit Dosanjh","artists":[{"artist_credit_name":"Diljit Dosanjh","artist_mbid":"f931c961-b647-4861-be8c-f47d84a4de51","join_phrase":""}],"caa_id":31442218448,"caa_release_mbid":"70252510-27ce-42f1-953a-f3966a5499a0","listen_count":296,"recording_mbid":"170dc09e-2d38-407f-aa3f-4807e7d1f3d2","release_mbid":"70252510-27ce-42f1-953a-f3966a5499a0","release_name":"MoonChild Era","track_name":"Lover"},{"artist_mbids":["c8b03190-306c-4120-bb0b-6f2ebfc06ea9","056e4f3e-d505-4dad-8ec1-d04f521cbb56"],"artist_name":"The Weeknd featuring Daft Punk","artists":[{"artist_credit_name":"The Weeknd","artist_mbid":"c8b03190-306c-4120-bb0b-6f2ebfc06ea9","join_phrase":" featuring "},{"artist_credit_name":"Daft Punk","artist_mbid":"056e4f3e-d505-4dad-8ec1-d04f521cbb56","join_phrase":""}],"caa_id":16759920843,"caa_release_mbid":"4f61a8bb-aa55-4951-8cfc-0809401db4ca","listen_count":289,"recording_mbid":"3da01a8e-3c71-408f-a9c7-0c4d10ad4e7d","release_mbid":"4f61a8bb-aa55-4951-8cfc-0809401db4ca","release_name":"Starboy","track_name":"Starboy"},{"artist_mbids":["98c09f94-10b5-426c-b27a-44345bb54528"],"artist_name":"Sasha Sloan","artists":[{"artist_credit_name":"Sasha Sloan","artist_mbid":"98c09f94-10b5-426c-b27a-44345bb54528","join_phrase":""}],"caa_id":32812417399,"caa_release_mbid":"87231c00-929a-47e6-a633-f464078eb4dc","listen_count":284,"recording_mbid":"9ae71082-ac47-4b9c-a12b-a67fff75784a","release_mbid":"87231c00-929a-47e6-a633-f464078eb4dc","release_name":"Self Portrait","track_name":"Dancing With Your Ghost"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":30564814068,"caa_release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","listen_count":277,"recording_mbid":"4c989c32-51a3-489a-965a-bb334def31a8","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","release_name":"Tickets to My Downfall","track_name":"concert for aliens"},{"artist_mbids":[],"artist_name":"ZAYN","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":264,"recording_mbid":null,"release_mbid":null,"release_name":"Icarus Falls","track_name":"There You Are"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":30564814068,"caa_release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","listen_count":263,"recording_mbid":"b9874074-b2d6-4135-bc2a-f4ef44b0ae69","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","release_name":"Tickets to My Downfall","track_name":"title track"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":30564814068,"caa_release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","listen_count":252,"recording_mbid":"b6e4f4ea-b20c-4489-b0e6-c09f40b0ef18","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","release_name":"Tickets to My Downfall","track_name":"jawbreaker"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":30564814068,"caa_release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","listen_count":237,"recording_mbid":"198a6f88-eb15-4280-9208-82998cb39175","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","release_name":"Tickets to My Downfall","track_name":"bloody valentine"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":27494670999,"caa_release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8","listen_count":236,"recording_mbid":"5ca5de59-bdd5-41fb-beae-2b343ee6f90d","release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8","release_name":"Tickets to My Downfall (SOLD OUT Deluxe)","track_name":"hangover cure"},{"artist_mbids":["e5712ceb-c37a-4c49-a11c-ccf4e21852d4"],"artist_name":"Troye Sivan","artists":[{"artist_credit_name":"Troye Sivan","artist_mbid":"e5712ceb-c37a-4c49-a11c-ccf4e21852d4","join_phrase":""}],"caa_id":26731335835,"caa_release_mbid":"0ab6cf1d-48a5-4227-a0f4-70bc47a39b58","listen_count":230,"recording_mbid":"f87aa613-a335-44a8-a702-7b15840dc74b","release_mbid":"0ab6cf1d-48a5-4227-a0f4-70bc47a39b58","release_name":"Easy","track_name":"Easy"},{"artist_mbids":["c8b03190-306c-4120-bb0b-6f2ebfc06ea9"],"artist_name":"The Weeknd","artists":[{"artist_credit_name":"The Weeknd","artist_mbid":"c8b03190-306c-4120-bb0b-6f2ebfc06ea9","join_phrase":""}],"caa_id":25807105705,"caa_release_mbid":"fd7ebee8-8c64-4127-a9be-8e31ed6364e3","listen_count":221,"recording_mbid":"1a67e215-a19e-40c9-9b12-732de134bf5f","release_mbid":"fd7ebee8-8c64-4127-a9be-8e31ed6364e3","release_name":"After Hours","track_name":"Blinding Lights"},{"artist_mbids":[],"artist_name":"Sasha Alex Sloan","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":212,"recording_mbid":null,"release_mbid":null,"release_name":"Older","track_name":"Older"},{"artist_mbids":["aa5891dd-9306-48e5-a1ee-77d2519abb61"],"artist_name":"Dikshant","artists":[{"artist_credit_name":"Dikshant","artist_mbid":"aa5891dd-9306-48e5-a1ee-77d2519abb61","join_phrase":""}],"caa_id":32335009413,"caa_release_mbid":"3626f26a-cda7-4413-9718-7dc61c8bde7c","listen_count":206,"recording_mbid":"8491bdca-0345-4c8a-9421-b53bbffdc168","release_mbid":"3626f26a-cda7-4413-9718-7dc61c8bde7c","release_name":"Aankhon Se Batana","track_name":"Aankhon Se Batana"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":27494670999,"caa_release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8","listen_count":203,"recording_mbid":"cb8be04f-e8b1-425c-b68b-c4a32f2c6532","release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8","release_name":"Tickets to My Downfall (SOLD OUT Deluxe)","track_name":"split a pill"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33","97529a9e-4b5a-4a9c-9c08-c42401d1c268","10a048d2-9afd-42d9-912e-0eb36c724f70"],"artist_name":"Machine Gun Kelly feat. YUNGBLUD & Bert McCracken","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":" feat. "},{"artist_credit_name":"YUNGBLUD","artist_mbid":"97529a9e-4b5a-4a9c-9c08-c42401d1c268","join_phrase":" & "},{"artist_credit_name":"Bert McCracken","artist_mbid":"10a048d2-9afd-42d9-912e-0eb36c724f70","join_phrase":""}],"caa_id":27494670999,"caa_release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8","listen_count":202,"recording_mbid":"25dfdccf-5092-4805-af42-f20146c8d18f","release_mbid":"9391a013-f0ab-4b66-a28f-ec5928c636b8","release_name":"Tickets to My Downfall (SOLD OUT Deluxe)","track_name":"body bag"},{"artist_mbids":["f6af669a-56ea-448a-a044-de76181ada33"],"artist_name":"Machine Gun Kelly","artists":[{"artist_credit_name":"Machine Gun Kelly","artist_mbid":"f6af669a-56ea-448a-a044-de76181ada33","join_phrase":""}],"caa_id":30564814068,"caa_release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","listen_count":196,"recording_mbid":"ddabeccd-b0e5-406e-aa1a-d4fdfa29f99e","release_mbid":"98b5a059-c8ac-490a-ba0c-b9ae9f932daa","release_name":"Tickets to My Downfall","track_name":"kiss kiss"}],"to_ts":1723248018,"total_recording_count":2243,"user_id":"akshaaatt"}} +{"payload":{"count":25,"from_ts":1009843200,"last_updated":1723271769,"offset":0,"range":"all_time","recordings":[{"artist_mbids":[],"artist_name":"Ekam Sudhar, Manni Sandhu, Rav Hanjra","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":127,"recording_mbid":null,"release_mbid":null,"release_name":"Small Circle","track_name":"Small Circle"},{"artist_mbids":["d0a37e1c-a9c3-4966-b630-984c33b087df"],"artist_name":"Eifi","artists":[{"artist_credit_name":"Eifi","artist_mbid":"d0a37e1c-a9c3-4966-b630-984c33b087df","join_phrase":""}],"caa_id":35241947664,"caa_release_mbid":"1dd36e0a-16e8-4486-b062-00891253abb0","listen_count":103,"recording_mbid":"5279bc04-6b83-4897-814d-4f604fedd79c","release_mbid":"1dd36e0a-16e8-4486-b062-00891253abb0","release_name":"Old School Vibe","track_name":"Old School Vibe"},{"artist_mbids":["8837b875-3689-4646-b3fd-d8b53815c7a8"],"artist_name":"Lil Uzi Vert","artists":[{"artist_credit_name":"Lil Uzi Vert","artist_mbid":"8837b875-3689-4646-b3fd-d8b53815c7a8","join_phrase":""}],"caa_id":35008042732,"caa_release_mbid":"de152c73-f234-4456-a032-cac36b5c006d","listen_count":99,"recording_mbid":"296c6296-570c-471b-ace2-bd1d0f6bc3f6","release_mbid":"de152c73-f234-4456-a032-cac36b5c006d","release_name":"Watch This (ARIZONATEARS pluggnb remix)","track_name":"Watch This (ARIZONATEARS pluggnb remix)"},{"artist_mbids":[],"artist_name":"Amantej Hundal","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":74,"recording_mbid":null,"release_mbid":null,"release_name":"Underrated","track_name":"Still Standing"},{"artist_mbids":["9537e089-79dd-43d7-9cf3-f6d51f6864fc"],"artist_name":"The Landers","artists":[{"artist_credit_name":"The Landers","artist_mbid":"9537e089-79dd-43d7-9cf3-f6d51f6864fc","join_phrase":""}],"caa_id":32015349705,"caa_release_mbid":"c7f3577a-8e75-4fc7-9498-1010054af117","listen_count":63,"recording_mbid":"0f0ed9f3-6960-4b8d-b8bf-ae7d52ec4385","release_mbid":"c7f3577a-8e75-4fc7-9498-1010054af117","release_name":"Gustakhiyan","track_name":"Gustakhiyan"},{"artist_mbids":["63aa26c3-d59b-4da4-84ac-716b54f1ef4d"],"artist_name":"Tame Impala","artists":[{"artist_credit_name":"Tame Impala","artist_mbid":"63aa26c3-d59b-4da4-84ac-716b54f1ef4d","join_phrase":""}],"caa_id":25489083340,"caa_release_mbid":"b9410b9a-cb64-41ac-b863-fa95f8eb51ea","listen_count":50,"recording_mbid":"f078699c-9b68-4040-9ba4-b1e4f7d86598","release_mbid":"b9410b9a-cb64-41ac-b863-fa95f8eb51ea","release_name":"Currents","track_name":"New Person, Same Old Mistakes"},{"artist_mbids":["381086ea-f511-4aba-bdf9-71c753dc5077","9880800a-f6f8-4f8f-a00e-b4b664776c0c"],"artist_name":"Kendrick Lamar featuring Jay Rock","artists":[{"artist_credit_name":"Kendrick Lamar","artist_mbid":"381086ea-f511-4aba-bdf9-71c753dc5077","join_phrase":" featuring "},{"artist_credit_name":"Jay Rock","artist_mbid":"9880800a-f6f8-4f8f-a00e-b4b664776c0c","join_phrase":""}],"caa_id":2361576294,"caa_release_mbid":"e1d99364-1ad9-4f4d-9505-2242eff10a44","listen_count":50,"recording_mbid":"c294757b-0430-4eb4-ae7a-6abcffb87405","release_mbid":"e1d99364-1ad9-4f4d-9505-2242eff10a44","release_name":"good kid, m.A.A.d city","track_name":"Money Trees"},{"artist_mbids":[],"artist_name":"R Nait","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":47,"recording_mbid":null,"release_mbid":null,"release_name":"Toy","track_name":"Toy"},{"artist_mbids":["4a779683-5404-4b90-a0d7-242495158265"],"artist_name":"Karan Aujla","artists":[{"artist_credit_name":"Karan Aujla","artist_mbid":"4a779683-5404-4b90-a0d7-242495158265","join_phrase":""}],"caa_id":32050224646,"caa_release_mbid":"bdd4aa78-9caf-40e4-a698-6639f96571eb","listen_count":47,"recording_mbid":"4fd3a889-6d06-47c5-99c2-756783f3d34e","release_mbid":"bdd4aa78-9caf-40e4-a698-6639f96571eb","release_name":"B.T.F.U","track_name":"Addi Sunni"},{"artist_mbids":[],"artist_name":"Arjan Dhillon","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":45,"recording_mbid":null,"release_mbid":null,"release_name":"Night Out","track_name":"Night Out"},{"artist_mbids":[],"artist_name":"Arjan Dhillon","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":43,"recording_mbid":null,"release_mbid":null,"release_name":"Kise Naal Ni Bolda","track_name":"Kise Naal Ni Bolda"},{"artist_mbids":["9b34cd6a-b874-4e75-bec8-1c174fe295d0"],"artist_name":"Arjan Dhillon","artists":[{"artist_credit_name":"Arjan Dhillon","artist_mbid":"9b34cd6a-b874-4e75-bec8-1c174fe295d0","join_phrase":""}],"caa_id":31811400978,"caa_release_mbid":"d8c0026f-e10c-494d-b66c-2085062b8c2e","listen_count":43,"recording_mbid":"8ff7f33a-d307-4f6f-ab7e-ddfd853a9ee8","release_mbid":"d8c0026f-e10c-494d-b66c-2085062b8c2e","release_name":"Awara","track_name":"Dope"},{"artist_mbids":["610d38e7-f5b5-4ec8-b10d-17387ee11bdd","878391e2-c5e7-4b51-bbc1-236326fa810c"],"artist_name":"Amrit Maan & The PropheC","artists":[{"artist_credit_name":"Amrit Maan","artist_mbid":"610d38e7-f5b5-4ec8-b10d-17387ee11bdd","join_phrase":" & "},{"artist_credit_name":"The PropheC","artist_mbid":"878391e2-c5e7-4b51-bbc1-236326fa810c","join_phrase":""}],"caa_id":33469880728,"caa_release_mbid":"eb0701cd-5d83-4c23-ab7c-df4cc9dbb639","listen_count":40,"recording_mbid":"440111a5-c53e-4263-9921-8c96fe92a035","release_mbid":"eb0701cd-5d83-4c23-ab7c-df4cc9dbb639","release_name":"My Moon","track_name":"My Moon"},{"artist_mbids":["092552b9-ea6d-4ead-b424-cf8b07da5877"],"artist_name":"Tegi Pannu","artists":[{"artist_credit_name":"Tegi Pannu","artist_mbid":"092552b9-ea6d-4ead-b424-cf8b07da5877","join_phrase":""}],"caa_id":34147069927,"caa_release_mbid":"75e7ef49-6a09-4856-987b-0cbefa1ba9ea","listen_count":38,"recording_mbid":"f0fd8820-2d6e-4c70-a9a0-79187d55c79a","release_mbid":"75e7ef49-6a09-4856-987b-0cbefa1ba9ea","release_name":"Disturbing The Peace","track_name":"Mood Swings"},{"artist_mbids":["9e975e9b-7538-4acd-ba91-8e5fd5df6b6e"],"artist_name":"Kid Bloom","artists":[{"artist_credit_name":"Kid Bloom","artist_mbid":"9e975e9b-7538-4acd-ba91-8e5fd5df6b6e","join_phrase":""}],"caa_id":null,"caa_release_mbid":null,"listen_count":36,"recording_mbid":"224a60b4-a861-4862-8a00-eb33c15267f2","release_mbid":"75169a25-004e-40f5-aaa7-78d2de09eb28","release_name":"Electric U -Single","track_name":"Electric U"},{"artist_mbids":["7e1cf02b-cf78-4803-b496-930c38649223"],"artist_name":"Jassa Dhillon","artists":[{"artist_credit_name":"Jassa Dhillon","artist_mbid":"7e1cf02b-cf78-4803-b496-930c38649223","join_phrase":""}],"caa_id":31988019523,"caa_release_mbid":"101876a6-8e7d-4512-97d5-a0ff08f22f53","listen_count":35,"recording_mbid":"8d3e9d59-78b8-4d25-8a2b-79290aa398fc","release_mbid":"101876a6-8e7d-4512-97d5-a0ff08f22f53","release_name":"Love War","track_name":"Shadow"},{"artist_mbids":["bce6d667-cde8-485e-b078-c0a05adea36d"],"artist_name":"ScHoolboy Q","artists":[{"artist_credit_name":"ScHoolboy Q","artist_mbid":"bce6d667-cde8-485e-b078-c0a05adea36d","join_phrase":""}],"caa_id":32363103828,"caa_release_mbid":"9fc29035-a4c5-4e7c-b984-a52e9aa63ad8","listen_count":34,"recording_mbid":"e5645698-84ef-4312-9a75-ad2fd981a1d8","release_mbid":"9fc29035-a4c5-4e7c-b984-a52e9aa63ad8","release_name":"CrasH Talk","track_name":"CrasH"},{"artist_mbids":[],"artist_name":"NIJJAR","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":34,"recording_mbid":null,"release_mbid":null,"release_name":"Chobbar","track_name":"Chobbar"},{"artist_mbids":["092552b9-ea6d-4ead-b424-cf8b07da5877","81035abf-09f7-4cbe-a785-4510dbd7970c"],"artist_name":"Tegi Pannu & JJ Esko","artists":[{"artist_credit_name":"Tegi Pannu","artist_mbid":"092552b9-ea6d-4ead-b424-cf8b07da5877","join_phrase":" & "},{"artist_credit_name":"JJ Esko","artist_mbid":"81035abf-09f7-4cbe-a785-4510dbd7970c","join_phrase":""}],"caa_id":34147069927,"caa_release_mbid":"75e7ef49-6a09-4856-987b-0cbefa1ba9ea","listen_count":33,"recording_mbid":"650aa31f-cf18-4239-ac59-9cb9cca92d4d","release_mbid":"75e7ef49-6a09-4856-987b-0cbefa1ba9ea","release_name":"Disturbing The Peace","track_name":"Hold You Down"},{"artist_mbids":["2338cede-17d7-40ef-9ea1-a16416908eeb"],"artist_name":"Sports","artists":[{"artist_credit_name":"Sports","artist_mbid":"2338cede-17d7-40ef-9ea1-a16416908eeb","join_phrase":""}],"caa_id":16665200572,"caa_release_mbid":"afea971b-c6ed-4ff8-9552-b47b8569c751","listen_count":32,"recording_mbid":"537f2df8-d1a6-4e22-9a6a-18393cdbb988","release_mbid":"afea971b-c6ed-4ff8-9552-b47b8569c751","release_name":"Naked All The Time","track_name":"You Are The Right One"},{"artist_mbids":["4a779683-5404-4b90-a0d7-242495158265","3ea12c3c-8596-4d70-b327-208b0a459a97"],"artist_name":"Karan Aujla, Ikky","artists":[{"artist_credit_name":"Karan Aujla","artist_mbid":"4a779683-5404-4b90-a0d7-242495158265","join_phrase":", "},{"artist_credit_name":"Ikky","artist_mbid":"3ea12c3c-8596-4d70-b327-208b0a459a97","join_phrase":""}],"caa_id":34792503592,"caa_release_mbid":"1390f1b7-7851-48ae-983d-eb8a48f78048","listen_count":31,"recording_mbid":"34c208ee-2de7-4d38-b47e-907074866dd3","release_mbid":"1390f1b7-7851-48ae-983d-eb8a48f78048","release_name":"Four You","track_name":"52 Bars"},{"artist_mbids":["9f3225ad-e686-4538-8cbe-99406c4a4caa"],"artist_name":"Aaron May","artists":[{"artist_credit_name":"Aaron May","artist_mbid":"9f3225ad-e686-4538-8cbe-99406c4a4caa","join_phrase":""}],"caa_id":22549299616,"caa_release_mbid":"ff0e48f3-83f6-4923-b5fa-a3c39f484da4","listen_count":29,"recording_mbid":"2f14ac58-d62d-4e45-a5e1-6504d1366831","release_mbid":"ff0e48f3-83f6-4923-b5fa-a3c39f484da4","release_name":"Chase","track_name":"I'm Good Luv, Enjoy."},{"artist_mbids":["ada7a83c-e3e1-40f1-93f9-3e73dbc9298a"],"artist_name":"Arctic Monkeys","artists":[{"artist_credit_name":"Arctic Monkeys","artist_mbid":"ada7a83c-e3e1-40f1-93f9-3e73dbc9298a","join_phrase":""}],"caa_id":32131848311,"caa_release_mbid":"55171afe-440e-4c63-947c-e49074f3d5b5","listen_count":27,"recording_mbid":"adadc047-e040-45d5-b020-0603db0d69dc","release_mbid":"55171afe-440e-4c63-947c-e49074f3d5b5","release_name":"AM","track_name":"Snap Out of It"},{"artist_mbids":[],"artist_name":"Seren","artists":null,"caa_id":null,"caa_release_mbid":null,"listen_count":26,"recording_mbid":null,"release_mbid":null,"release_name":null,"track_name":"Travis Scott - My Eyes (Best Part Extended)"},{"artist_mbids":["39d82be4-4dd4-4bda-b485-fc84c7c72938","fc63d806-ca89-4ea3-a404-ee6de695743f"],"artist_name":"Trix & Flix feat. Shaggy","artists":[{"artist_credit_name":"Trix & Flix","artist_mbid":"39d82be4-4dd4-4bda-b485-fc84c7c72938","join_phrase":" feat. "},{"artist_credit_name":"Shaggy","artist_mbid":"fc63d806-ca89-4ea3-a404-ee6de695743f","join_phrase":""}],"caa_id":38114265515,"caa_release_mbid":"0a6443f6-5f09-45db-a1cf-eae3991eeb27","listen_count":25,"recording_mbid":"e7098517-c096-4c36-968f-f014549baddd","release_mbid":"912ca54e-76a6-4c61-8746-62ef479a7a63","release_name":"Intoxication","track_name":"Like a Superstar (radio edit)"}],"to_ts":1723248018,"total_recording_count":884,"user_id":"Jasjeet"}} From 21a69405c179b0a1c7c8cba4c37d3a13a46737de Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Mon, 12 Aug 2024 22:21:02 +0530 Subject: [PATCH 75/97] Adds user view model unit tests --- .../ui/screens/profile/BaseProfileScreen.kt | 8 +- .../ui/screens/profile/ProfileScreen.kt | 4 +- .../screens/profile/listens/ListensScreen.kt | 10 +-- .../ui/screens/profile/stats/StatsScreen.kt | 4 +- .../ui/screens/profile/taste/TasteScreen.kt | 4 +- .../{ProfileViewModel.kt => UserViewModel.kt} | 44 +---------- .../android/user/UserViewModelTest.kt | 49 ++++++++++++ .../sharedtest/mocks/MockListensRepository.kt | 54 +++++++++++++ .../sharedtest/mocks/MockSocialRepository.kt | 79 +++++++++++++++++++ .../testdata/ListensRepositoryTestData.kt | 10 +++ .../sharedtest/utils/ResourceString.kt | 4 + sharedTest/src/main/resources/listens.json | 1 + 12 files changed, 213 insertions(+), 58 deletions(-) rename app/src/main/java/org/listenbrainz/android/viewmodel/{ProfileViewModel.kt => UserViewModel.kt} (93%) create mode 100644 app/src/test/java/org/listenbrainz/android/user/UserViewModelTest.kt create mode 100644 sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockListensRepository.kt create mode 100644 sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockSocialRepository.kt create mode 100644 sharedTest/src/main/java/org/listenbrainz/sharedtest/testdata/ListensRepositoryTestData.kt create mode 100644 sharedTest/src/main/resources/listens.json 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 3de228aa..887903a8 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 @@ -55,13 +55,13 @@ import org.listenbrainz.android.ui.theme.lb_orange import org.listenbrainz.android.ui.theme.lb_purple import org.listenbrainz.android.ui.theme.new_app_bg_light import org.listenbrainz.android.util.Constants -import org.listenbrainz.android.viewmodel.ProfileViewModel +import org.listenbrainz.android.viewmodel.UserViewModel @Composable fun BaseProfileScreen( username: String?, snackbarState: SnackbarHostState, - viewModel: ProfileViewModel = hiltViewModel(), + viewModel: UserViewModel = hiltViewModel(), uiState: ProfileUiState, onFollowClick: (String) -> Unit, onUnfollowClick: (String) -> Unit, @@ -201,7 +201,7 @@ fun BaseProfileScreen( when(currentTab.value) { ProfileScreenTab.LISTENS -> ListensScreen( scrollRequestState = false, - profileViewModel = viewModel, + userViewModel = viewModel, onScrollToTop = {}, snackbarState = snackbarState, username = username @@ -215,7 +215,7 @@ fun BaseProfileScreen( ) else -> ListensScreen( scrollRequestState = false, - profileViewModel = viewModel, + userViewModel = viewModel, onScrollToTop = {}, snackbarState = snackbarState, username = username, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt index d0515be7..12b885b7 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt @@ -37,12 +37,12 @@ import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.rememberLottieComposition import org.listenbrainz.android.R import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_IN -import org.listenbrainz.android.viewmodel.ProfileViewModel +import org.listenbrainz.android.viewmodel.UserViewModel @Composable fun ProfileScreen( context: Context = LocalContext.current, - viewModel: ProfileViewModel = hiltViewModel(), + viewModel: UserViewModel = hiltViewModel(), scrollRequestState: Boolean, onScrollToTop: (suspend () -> Unit) -> Unit, username: String?, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt index 1c1278f0..c730ee5e 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt @@ -83,13 +83,13 @@ import org.listenbrainz.android.ui.theme.lb_purple_night import org.listenbrainz.android.util.Utils.getCoverArtUrl import org.listenbrainz.android.viewmodel.FeedViewModel import org.listenbrainz.android.viewmodel.ListensViewModel -import org.listenbrainz.android.viewmodel.ProfileViewModel +import org.listenbrainz.android.viewmodel.UserViewModel import org.listenbrainz.android.viewmodel.SocialViewModel @Composable fun ListensScreen( viewModel: ListensViewModel = hiltViewModel(), - profileViewModel: ProfileViewModel, + userViewModel: UserViewModel, socialViewModel: SocialViewModel = hiltViewModel(), feedViewModel : FeedViewModel = hiltViewModel(), scrollRequestState: Boolean, @@ -98,7 +98,7 @@ fun ListensScreen( username: String?, ) { - val uiState by profileViewModel.uiState.collectAsState() + val uiState by userViewModel.uiState.collectAsState() val preferencesUiState by viewModel.preferencesUiState.collectAsState() val socialUiState by socialViewModel.uiState.collectAsState() val feedUiState by feedViewModel.uiState.collectAsState() @@ -158,10 +158,10 @@ fun ListensScreen( it, status -> if(!username.isNullOrEmpty()) { if(!status){ - profileViewModel.followUser(it) + userViewModel.followUser(it) } else{ - profileViewModel.unfollowUser(it) + userViewModel.unfollowUser(it) } } } 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 65e39447..fd31dae6 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 @@ -82,13 +82,13 @@ 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.FeedViewModel -import org.listenbrainz.android.viewmodel.ProfileViewModel +import org.listenbrainz.android.viewmodel.UserViewModel import org.listenbrainz.android.viewmodel.SocialViewModel @Composable fun StatsScreen( username: String?, - viewModel: ProfileViewModel = hiltViewModel(), + viewModel: UserViewModel = hiltViewModel(), socialViewModel: SocialViewModel = hiltViewModel(), feedViewModel : FeedViewModel = hiltViewModel(), snackbarState : SnackbarHostState, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt index 45617c08..75e215b6 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt @@ -61,12 +61,12 @@ import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.ui.theme.lb_purple_night import org.listenbrainz.android.util.Utils.getCoverArtUrl import org.listenbrainz.android.viewmodel.FeedViewModel -import org.listenbrainz.android.viewmodel.ProfileViewModel +import org.listenbrainz.android.viewmodel.UserViewModel import org.listenbrainz.android.viewmodel.SocialViewModel @Composable fun TasteScreen( - viewModel: ProfileViewModel = hiltViewModel(), + viewModel: UserViewModel = hiltViewModel(), socialViewModel: SocialViewModel = hiltViewModel(), feedViewModel : FeedViewModel = hiltViewModel(), snackbarState : SnackbarHostState, diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/UserViewModel.kt similarity index 93% rename from app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt rename to app/src/main/java/org/listenbrainz/android/viewmodel/UserViewModel.kt index f7509344..85f0f8df 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/UserViewModel.kt @@ -29,7 +29,7 @@ import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_OUT import javax.inject.Inject @HiltViewModel -class ProfileViewModel @Inject constructor( +class UserViewModel @Inject constructor( val appPreferences: AppPreferences, private val userRepository: UserRepository, private val listensRepository: ListensRepository, @@ -152,48 +152,6 @@ 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 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() diff --git a/app/src/test/java/org/listenbrainz/android/user/UserViewModelTest.kt b/app/src/test/java/org/listenbrainz/android/user/UserViewModelTest.kt new file mode 100644 index 00000000..9887d277 --- /dev/null +++ b/app/src/test/java/org/listenbrainz/android/user/UserViewModelTest.kt @@ -0,0 +1,49 @@ +package org.listenbrainz.android.user + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test +import org.listenbrainz.android.ui.screens.profile.stats.StatsRange +import org.listenbrainz.android.ui.screens.profile.stats.UserGlobal +import org.listenbrainz.android.viewmodel.UserViewModel +import org.listenbrainz.sharedtest.mocks.MockAppPreferences +import org.listenbrainz.sharedtest.mocks.MockListensRepository +import org.listenbrainz.sharedtest.mocks.MockSocialRepository +import org.listenbrainz.sharedtest.mocks.MockUserRepository +import org.listenbrainz.sharedtest.utils.EntityTestUtils.testUsername + +class UserViewModelTest { + private lateinit var viewModel: UserViewModel + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setup(){ + Dispatchers.setMain(StandardTestDispatcher()) + viewModel = UserViewModel(MockAppPreferences(), MockUserRepository(), MockListensRepository(), MockSocialRepository(), Dispatchers.Default) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun getDataTest() = runTest { + viewModel.getUserDataFromRemote(testUsername) + // Ensure all coroutines and tasks are completed + advanceUntilIdle() + assertNotNull(viewModel.uiState.value.statsTabUIState) + assertNotNull(viewModel.uiState.value.tasteTabUIState) + assertNotNull(viewModel.uiState.value.listensTabUiState) + assertEquals(true, viewModel.uiState.value.isSelf) + assertEquals(3, viewModel.uiState.value.listensTabUiState.similarUsers?.size) + assertEquals(3252, viewModel.uiState.value.listensTabUiState.listenCount) + assertEquals("jivteshs20", viewModel.uiState.value.listensTabUiState.followers?.get(0)?.first) + assertEquals("6b08f3d4-0d56-406c-b628-d0afe2ad5d44", viewModel.uiState.value.tasteTabUIState.lovedSongs?.feedback?.get(0)?.recordingMBID) + assertEquals(23,viewModel.uiState.value.statsTabUIState.userListeningActivity.get(Pair(UserGlobal.USER, StatsRange.ALL_TIME))?.size) + } + + +} diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockListensRepository.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockListensRepository.kt new file mode 100644 index 00000000..4cfd4a84 --- /dev/null +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockListensRepository.kt @@ -0,0 +1,54 @@ +package org.listenbrainz.sharedtest.mocks + +import android.graphics.drawable.Drawable +import org.listenbrainz.android.model.CoverArt +import org.listenbrainz.android.model.ListenBrainzExternalServices +import org.listenbrainz.android.model.ListenSubmitBody +import org.listenbrainz.android.model.Listens +import org.listenbrainz.android.model.PostResponse +import org.listenbrainz.android.model.ResponseError +import org.listenbrainz.android.model.TokenValidation +import org.listenbrainz.android.repository.listens.ListensRepository +import org.listenbrainz.android.util.Resource +import org.listenbrainz.sharedtest.testdata.ListensRepositoryTestData.listensTestData + +class MockListensRepository : ListensRepository { + override suspend fun fetchUserListens(username: String?): Resource { + return if(username.isNullOrEmpty()){ + ResponseError.DOES_NOT_EXIST.asResource() + } else{ + Resource(Resource.Status.SUCCESS, listensTestData) + } + } + + override suspend fun fetchCoverArt(mbid: String): Resource { + TODO("Not yet implemented") + } + + override suspend fun validateToken(token: String): Resource { + TODO("Not yet implemented") + } + + override fun getPackageIcon(packageName: String): Drawable? { + TODO("Not yet implemented") + } + + override fun getPackageLabel(packageName: String): String { + TODO("Not yet implemented") + } + + override suspend fun submitListen( + token: String, + body: ListenSubmitBody + ): Resource { + TODO("Not yet implemented") + } + + override suspend fun getLinkedServices( + token: String?, + username: String? + ): Resource { + TODO("Not yet implemented") + } + +} \ No newline at end of file diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockSocialRepository.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockSocialRepository.kt new file mode 100644 index 00000000..a6e4f513 --- /dev/null +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockSocialRepository.kt @@ -0,0 +1,79 @@ +package org.listenbrainz.sharedtest.mocks + +import org.listenbrainz.android.model.PinData +import org.listenbrainz.android.model.RecommendationData +import org.listenbrainz.android.model.ResponseError +import org.listenbrainz.android.model.Review +import org.listenbrainz.android.model.SearchResult +import org.listenbrainz.android.model.SimilarUserData +import org.listenbrainz.android.model.SocialData +import org.listenbrainz.android.model.SocialResponse +import org.listenbrainz.android.model.feed.FeedEvent +import org.listenbrainz.android.repository.social.SocialRepository +import org.listenbrainz.android.util.Resource +import org.listenbrainz.sharedtest.testdata.SocialRepositoryTestData.testFollowersSuccessData +import org.listenbrainz.sharedtest.testdata.SocialRepositoryTestData.testFollowingSuccessData +import org.listenbrainz.sharedtest.testdata.SocialRepositoryTestData.testSimilarUserSuccessData + +class MockSocialRepository : SocialRepository { + override suspend fun getFollowers(username: String?): Resource { + return if(username.isNullOrEmpty()){ + ResponseError.DOES_NOT_EXIST.asResource() + } + else{ + Resource(Resource.Status.SUCCESS, testFollowersSuccessData) + } + } + + override suspend fun getFollowing(username: String): Resource { + return Resource(Resource.Status.SUCCESS, testFollowingSuccessData) + } + + override suspend fun followUser(username: String): Resource { + TODO("Not yet implemented") + } + + override suspend fun unfollowUser(username: String): Resource { + TODO("Not yet implemented") + } + + override suspend fun getSimilarUsers(username: String): Resource { + return Resource(Resource.Status.SUCCESS, testSimilarUserSuccessData) + } + + override suspend fun searchUser(username: String): Resource { + TODO("Not yet implemented") + } + + override suspend fun postPersonalRecommendation( + username: String?, + data: RecommendationData + ): Resource { + TODO("Not yet implemented") + } + + override suspend fun postRecommendationToAll( + username: String?, + data: RecommendationData + ): Resource { + TODO("Not yet implemented") + } + + override suspend fun postReview(username: String?, data: Review): Resource { + TODO("Not yet implemented") + } + + override suspend fun pin( + recordingMsid: String?, + recordingMbid: String?, + blurbContent: String?, + pinnedUntil: Int + ): Resource { + TODO("Not yet implemented") + } + + override suspend fun deletePin(id: Int): Resource { + TODO("Not yet implemented") + } + +} \ No newline at end of file diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/testdata/ListensRepositoryTestData.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/testdata/ListensRepositoryTestData.kt new file mode 100644 index 00000000..62e94f04 --- /dev/null +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/testdata/ListensRepositoryTestData.kt @@ -0,0 +1,10 @@ +package org.listenbrainz.sharedtest.testdata + +import com.google.gson.Gson +import org.listenbrainz.android.model.Listens +import org.listenbrainz.sharedtest.utils.ResourceString.listens + +object ListensRepositoryTestData { + val listensTestData : Listens + get() = Gson().fromJson(listens, Listens::class.java) +} \ No newline at end of file diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/utils/ResourceString.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/utils/ResourceString.kt index f089956a..d404c300 100644 --- a/sharedTest/src/main/java/org/listenbrainz/sharedtest/utils/ResourceString.kt +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/utils/ResourceString.kt @@ -118,6 +118,10 @@ object ResourceString { val listenCount by lazy { EntityTestUtils.loadResourceAsString("listen_count.json") } + + val listens by lazy { + EntityTestUtils.loadResourceAsString("listens.json") + } fun String.toClass(): T { return Gson().fromJson(this, object: TypeToken() {}.type) diff --git a/sharedTest/src/main/resources/listens.json b/sharedTest/src/main/resources/listens.json new file mode 100644 index 00000000..1369dbe3 --- /dev/null +++ b/sharedTest/src/main/resources/listens.json @@ -0,0 +1 @@ +{"payload":{"count":25,"latest_listen_ts":1723267385,"listens":[{"inserted_at":1723267446,"listened_at":1723267385,"recording_msid":"e3e2b4ef-eac8-46c3-89f9-85ce31c82404","track_metadata":{"additional_info":{"media_player":"Slack","recording_msid":"e3e2b4ef-eac8-46c3-89f9-85ce31c82404","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Apurva","track_name":"RPReplay_Final1723230584"},"user_name":"Jasjeet"},{"inserted_at":1723144630,"listened_at":1723144571,"recording_msid":"0d3baf58-8315-4836-9ad3-2fc31821668b","track_metadata":{"additional_info":{"media_player":"Slack","recording_msid":"0d3baf58-8315-4836-9ad3-2fc31821668b","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Srikanth","track_name":"RPReplay_Final1723141917"},"user_name":"Jasjeet"},{"inserted_at":1722833349,"listened_at":1722833327,"recording_msid":"9b10be53-16cc-4aa7-9c18-2a09bad819fc","track_metadata":{"additional_info":{"duration_ms":32966,"media_player":"Slack","recording_msid":"9b10be53-16cc-4aa7-9c18-2a09bad819fc","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Srikanth","track_name":"RPReplay_Final1722682110"},"user_name":"Jasjeet"},{"inserted_at":1722693745,"listened_at":1722693685,"recording_msid":"21a2a5d7-4a29-4361-8d1b-728b93aea251","track_metadata":{"additional_info":{"media_player":"Slack","recording_msid":"21a2a5d7-4a29-4361-8d1b-728b93aea251","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Srikanth","track_name":"screen-20240803-185944.mp4"},"user_name":"Jasjeet"},{"inserted_at":1722318031,"listened_at":1722318030,"recording_msid":"d7f2d8c6-6818-4d9a-b10e-22e5787ae661","track_metadata":{"additional_info":{"media_player":"YouTube Music","recording_msid":"d7f2d8c6-6818-4d9a-b10e-22e5787ae661","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Lost Frequencies & Calum Scott","mbid_mapping":{"artist_mbids":["ea7260de-e1b1-43f1-bb11-f78274a36308","68fdc9cc-1094-48bb-942f-66492343e41c"],"artists":[{"artist_credit_name":"Lost Frequencies","artist_mbid":"ea7260de-e1b1-43f1-bb11-f78274a36308","join_phrase":" & "},{"artist_credit_name":"Calum Scott","artist_mbid":"68fdc9cc-1094-48bb-942f-66492343e41c","join_phrase":""}],"caa_id":38775931183,"caa_release_mbid":"cb090bf6-ade9-4c6a-81b8-91d04dedc866","recording_mbid":"fa2605de-3e06-4038-9337-daec9ca302e5","recording_name":"Where Are You Now","release_mbid":"3d6b4d5b-ecd2-4baa-a2a5-7b0e735da202"},"track_name":"Where Are You Now"},"user_name":"Jasjeet"},{"inserted_at":1722095358,"listened_at":1722095295,"recording_msid":"a04fa8dd-776f-4e90-a4d4-4595cad7eeeb","track_metadata":{"additional_info":{"media_player":"Slack","recording_msid":"a04fa8dd-776f-4e90-a4d4-4595cad7eeeb","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Jass","track_name":"RPReplay_Final1722095096"},"user_name":"Jasjeet"},{"inserted_at":1721920480,"listened_at":1721920420,"recording_msid":"097728c0-7a48-4073-acbe-899f4d3c5b6b","track_metadata":{"additional_info":{"media_player":"Slack","recording_msid":"097728c0-7a48-4073-acbe-899f4d3c5b6b","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Jass","track_name":"RPReplay_Final1721916666"},"user_name":"Jasjeet"},{"inserted_at":1721800380,"listened_at":1721800382,"recording_msid":"2950fbc6-0b2c-4549-a65c-dc418bb5656e","track_metadata":{"additional_info":{"media_player":"YouTube Music","recording_msid":"2950fbc6-0b2c-4549-a65c-dc418bb5656e","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Portugal. The Man","mbid_mapping":{"artist_mbids":["3599a39e-4e10-4cb5-90d4-c8a015ebc73b"],"artists":[{"artist_credit_name":"Portugal. The Man","artist_mbid":"3599a39e-4e10-4cb5-90d4-c8a015ebc73b","join_phrase":""}],"caa_id":37576308867,"caa_release_mbid":"3ba8912d-46c5-4c66-bc5e-e33c84ecd118","recording_mbid":"ded77223-a7a3-4a91-a8e6-4cbdf8c84865","recording_name":"People Say","release_mbid":"3ba8912d-46c5-4c66-bc5e-e33c84ecd118"},"release_name":"Social Cues","track_name":"People Say"},"user_name":"Jasjeet"},{"inserted_at":1721795579,"listened_at":1721795520,"recording_msid":"6c694af0-a6cd-4a8f-869e-a15743e0f556","track_metadata":{"additional_info":{"media_player":"Slack","recording_msid":"6c694af0-a6cd-4a8f-869e-a15743e0f556","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Srikanth","track_name":"Record_2024-07-24-06-41-35_ca37809c94ede43fb59780785174bf2c.mp4"},"user_name":"Jasjeet"},{"inserted_at":1721714031,"listened_at":1721714031,"recording_msid":"ab6b34bb-c28c-46ae-b596-b5f99734e485","track_metadata":{"additional_info":{"media_player":"YouTube Music","recording_msid":"ab6b34bb-c28c-46ae-b596-b5f99734e485","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Jack \u00dc","mbid_mapping":{"artist_mbids":["8a40cfa4-e190-4133-a9af-e668c7d5f6f3","42c8f9b1-60b5-4b0e-9036-f9a9596695c1"],"artists":[{"artist_credit_name":"Jack \u00dc","artist_mbid":"8a40cfa4-e190-4133-a9af-e668c7d5f6f3","join_phrase":" feat. "},{"artist_credit_name":"Kai","artist_mbid":"42c8f9b1-60b5-4b0e-9036-f9a9596695c1","join_phrase":""}],"caa_id":12067718557,"caa_release_mbid":"e0ceba78-8ecb-46c4-bfd4-c100a5ba888c","recording_mbid":"27795d1e-c9bf-4036-bb1a-11d3e696e16a","recording_name":"Mind","release_mbid":"6844eac2-4772-42ea-8946-06cf049dab2b"},"track_name":"Mind (feat. Alessia De Gasperis)"},"user_name":"Jasjeet"},{"inserted_at":1721713836,"listened_at":1721713835,"recording_msid":"0a1dc120-36d5-4302-9624-9d31ee99f37f","track_metadata":{"additional_info":{"media_player":"YouTube Music","recording_msid":"0a1dc120-36d5-4302-9624-9d31ee99f37f","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"THYPONYX","track_name":"THYPONYX - Seven Nation Army"},"user_name":"Jasjeet"},{"inserted_at":1721664791,"listened_at":1721664731,"recording_msid":"711109c7-7d4c-4d7d-93cd-7c3077366e68","track_metadata":{"additional_info":{"media_player":"Slack","recording_msid":"711109c7-7d4c-4d7d-93cd-7c3077366e68","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Ayesha Pradhan","track_name":"Screen_Recording_20240721_121100_Ultrahuman.mp4"},"user_name":"Jasjeet"},{"inserted_at":1721664236,"listened_at":1721664176,"recording_msid":"711109c7-7d4c-4d7d-93cd-7c3077366e68","track_metadata":{"additional_info":{"media_player":"Slack","recording_msid":"711109c7-7d4c-4d7d-93cd-7c3077366e68","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Ayesha Pradhan","track_name":"Screen_Recording_20240721_121100_Ultrahuman.mp4"},"user_name":"Jasjeet"},{"inserted_at":1721626987,"listened_at":1721626986,"recording_msid":"146bb92a-d34c-4187-b2e9-29bb3179d1e1","track_metadata":{"additional_info":{"media_player":"YouTube Music","recording_msid":"146bb92a-d34c-4187-b2e9-29bb3179d1e1","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Charlie Puth","mbid_mapping":{"artist_mbids":["525f1f1c-03f0-4bc8-8dfd-e7521f87631b"],"artists":[{"artist_credit_name":"Charlie Puth","artist_mbid":"525f1f1c-03f0-4bc8-8dfd-e7521f87631b","join_phrase":""}],"caa_id":19827744116,"caa_release_mbid":"3b8ce7ae-ff41-4dc6-a694-2e2b2edc24ae","recording_mbid":"19b6d048-f981-40e9-8235-a5acf969e5df","recording_name":"Attention","release_mbid":"8004db5a-0947-4745-b391-3bcc6960411d"},"track_name":"Attention"},"user_name":"Jasjeet"},{"inserted_at":1721626693,"listened_at":1721626691,"recording_msid":"0f837ca8-4610-4306-b36c-c95d17661e55","track_metadata":{"additional_info":{"media_player":"YouTube Music","recording_msid":"0f837ca8-4610-4306-b36c-c95d17661e55","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Sean Paul","mbid_mapping":{"artist_mbids":["c3da3346-2643-48a7-93cd-011f6834b3d7","6f1a58bf-9b1b-49cf-a44a-6cefad7ae04f"],"artists":[{"artist_credit_name":"Sean Paul","artist_mbid":"c3da3346-2643-48a7-93cd-011f6834b3d7","join_phrase":" ft. "},{"artist_credit_name":"Dua Lipa","artist_mbid":"6f1a58bf-9b1b-49cf-a44a-6cefad7ae04f","join_phrase":""}],"caa_id":20271298649,"caa_release_mbid":"33eed724-cd55-465a-9f50-b4afe3c7728e","recording_mbid":"7b00dbe9-53ea-41b3-877b-31503eb2fd75","recording_name":"No Lie","release_mbid":"cfdb91a1-693f-41a9-b9fc-f08d650a268e"},"track_name":"No Lie (feat. Dua Lipa)"},"user_name":"Jasjeet"},{"inserted_at":1721626556,"listened_at":1721626420,"recording_msid":"55e9adb0-3ed1-4bf1-b8ab-2a28e451f26f","track_metadata":{"additional_info":{"duration_ms":270442,"media_player":"YouTube Music","recording_msid":"55e9adb0-3ed1-4bf1-b8ab-2a28e451f26f","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Mark Ronson","mbid_mapping":{"artist_mbids":["c3c82bdc-d9e7-4836-9746-c24ead47ca19","afb680f2-b6eb-4cd7-a70b-a63b25c763d5"],"artists":[{"artist_credit_name":"Mark Ronson","artist_mbid":"c3c82bdc-d9e7-4836-9746-c24ead47ca19","join_phrase":" feat. "},{"artist_credit_name":"Bruno Mars","artist_mbid":"afb680f2-b6eb-4cd7-a70b-a63b25c763d5","join_phrase":""}],"caa_id":19394943642,"caa_release_mbid":"0577f225-9866-4fe2-9c23-e52c37186a99","recording_mbid":"38e06968-af06-44ea-9eba-a1a275832767","recording_name":"Uptown Funk","release_mbid":"04ea8e96-ef0e-441c-9594-7128addc3951"},"track_name":"Uptown Funk (Official Video) (feat. Bruno Mars)"},"user_name":"Jasjeet"},{"inserted_at":1721379271,"listened_at":1721379242,"recording_msid":"b28cf8a9-d57c-46aa-90cd-5ae74cd648df","track_metadata":{"additional_info":{"duration_ms":54187,"media_player":"Slack","recording_msid":"b28cf8a9-d57c-46aa-90cd-5ae74cd648df","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Ayush","track_name":"WhatsApp Video 2024-07-18 at 21.45.18.mp4"},"user_name":"Jasjeet"},{"inserted_at":1721369443,"listened_at":1721369442,"recording_msid":"24271df0-b683-4ec8-bf47-9f680cfba289","track_metadata":{"additional_info":{"media_player":"YouTube Music","recording_msid":"24271df0-b683-4ec8-bf47-9f680cfba289","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Gotye","release_name":"Future Nostalgia","track_name":"Somebody That I Used To Know (feat. Kimbra)"},"user_name":"Jasjeet"},{"inserted_at":1721369215,"listened_at":1721369212,"recording_msid":"0b8287c3-8b99-4635-b4da-6002f20718f8","track_metadata":{"additional_info":{"media_player":"YouTube Music","recording_msid":"0b8287c3-8b99-4635-b4da-6002f20718f8","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Arctic Monkeys","mbid_mapping":{"artist_mbids":["ada7a83c-e3e1-40f1-93f9-3e73dbc9298a"],"artists":[{"artist_credit_name":"Arctic Monkeys","artist_mbid":"ada7a83c-e3e1-40f1-93f9-3e73dbc9298a","join_phrase":""}],"caa_id":11863856066,"caa_release_mbid":"c7858e6b-9232-4c01-a703-35e60d3f7ec3","recording_mbid":"f1e57531-e0df-4b3e-938f-1ae30c5b1a11","recording_name":"Do I Wanna Know?","release_mbid":"55171afe-440e-4c63-947c-e49074f3d5b5"},"track_name":"Do I Wanna Know?"},"user_name":"Jasjeet"},{"inserted_at":1721364931,"listened_at":1721364869,"recording_msid":"8aa61f93-ebeb-46d7-a51b-e46337705757","track_metadata":{"additional_info":{"media_player":"Slack","recording_msid":"8aa61f93-ebeb-46d7-a51b-e46337705757","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Shivani","track_name":"Screen_Recording_20240719_102206_Ultrahuman.mp4"},"user_name":"Jasjeet"},{"inserted_at":1721364864,"listened_at":1721364825,"recording_msid":"6df1064e-3fb4-4888-8c10-54857a335f1c","track_metadata":{"additional_info":{"duration_ms":74441,"media_player":"Slack","recording_msid":"6df1064e-3fb4-4888-8c10-54857a335f1c","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Srikanth","track_name":"screen-20240719-102031.mp4"},"user_name":"Jasjeet"},{"inserted_at":1721236521,"listened_at":1721236461,"recording_msid":"0408a748-656f-44a4-951d-08bbc822212b","track_metadata":{"additional_info":{"media_player":"Slack","recording_msid":"0408a748-656f-44a4-951d-08bbc822212b","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Malay Pandey","track_name":"668c76196406f6459cdf1b8c-Screen_Recording_20240709_012733_One_UI_Home.mp4"},"user_name":"Jasjeet"},{"inserted_at":1720870225,"listened_at":1720870164,"recording_msid":"81a518a3-9597-40a1-9fa1-a1d4345efc8f","track_metadata":{"additional_info":{"media_player":"Slack","recording_msid":"81a518a3-9597-40a1-9fa1-a1d4345efc8f","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Trishala","track_name":"5212316802803753137_a8b5aefd-e639-4836-a25a-6dc3746edc27.mov"},"user_name":"Jasjeet"},{"inserted_at":1720861247,"listened_at":1720861187,"recording_msid":"b9b0bbe8-2beb-48de-98ea-9c19b6574ff1","track_metadata":{"additional_info":{"media_player":"Slack","recording_msid":"b9b0bbe8-2beb-48de-98ea-9c19b6574ff1","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Srikanth","track_name":"screen-20240713-142204.mp4"},"user_name":"Jasjeet"},{"inserted_at":1720861179,"listened_at":1720861114,"recording_msid":"b9b0bbe8-2beb-48de-98ea-9c19b6574ff1","track_metadata":{"additional_info":{"media_player":"Slack","recording_msid":"b9b0bbe8-2beb-48de-98ea-9c19b6574ff1","submission_client":"ListenBrainz Android","submission_client_version":"2.6.1"},"artist_name":"Srikanth","track_name":"screen-20240713-142204.mp4"},"user_name":"Jasjeet"}],"oldest_listen_ts":1670056445,"user_id":"jasjeet"}} From 869b1c8e15823a9f200250be99b15257b38e9e12 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Mon, 12 Aug 2024 22:36:24 +0530 Subject: [PATCH 76/97] Fixes Social Repository Tests --- .../java/org/listenbrainz/android/user/UserRepositoryTest.kt | 4 ++-- .../java/org/listenbrainz/sharedtest/utils/ResourceString.kt | 4 ++++ sharedTest/src/main/resources/similar_user.json | 1 + sharedTest/src/main/resources/similar_users_response.json | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 sharedTest/src/main/resources/similar_user.json diff --git a/app/src/test/java/org/listenbrainz/android/user/UserRepositoryTest.kt b/app/src/test/java/org/listenbrainz/android/user/UserRepositoryTest.kt index f3ca96cd..20c64fae 100644 --- a/app/src/test/java/org/listenbrainz/android/user/UserRepositoryTest.kt +++ b/app/src/test/java/org/listenbrainz/android/user/UserRepositoryTest.kt @@ -25,9 +25,9 @@ import org.listenbrainz.sharedtest.utils.ResourceString.current_pins import org.listenbrainz.sharedtest.utils.ResourceString.globalListeningActivity import org.listenbrainz.sharedtest.utils.ResourceString.listenCount import org.listenbrainz.sharedtest.utils.ResourceString.loved_hated_songs +import org.listenbrainz.sharedtest.utils.ResourceString.similarUser import org.listenbrainz.sharedtest.utils.ResourceString.similarUserError import org.listenbrainz.sharedtest.utils.ResourceString.similarUserErrorString -import org.listenbrainz.sharedtest.utils.ResourceString.similar_users_response import org.listenbrainz.sharedtest.utils.ResourceString.topAlbums import org.listenbrainz.sharedtest.utils.ResourceString.topSongs import org.listenbrainz.sharedtest.utils.ResourceString.top_artists @@ -62,7 +62,7 @@ class UserRepositoryTest { // Similar Users "/user/$testUsername/similar-to/$testSomeOtherUser" -> { MockResponse().setResponseCode(200).setBody( - similar_users_response + similarUser ) } diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/utils/ResourceString.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/utils/ResourceString.kt index d404c300..bdac44ee 100644 --- a/sharedTest/src/main/java/org/listenbrainz/sharedtest/utils/ResourceString.kt +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/utils/ResourceString.kt @@ -122,6 +122,10 @@ object ResourceString { val listens by lazy { EntityTestUtils.loadResourceAsString("listens.json") } + + val similarUser by lazy { + EntityTestUtils.loadResourceAsString("similar_user.json") + } fun String.toClass(): T { return Gson().fromJson(this, object: TypeToken() {}.type) diff --git a/sharedTest/src/main/resources/similar_user.json b/sharedTest/src/main/resources/similar_user.json new file mode 100644 index 00000000..2594d566 --- /dev/null +++ b/sharedTest/src/main/resources/similar_user.json @@ -0,0 +1 @@ +{"payload":{"similarity":0.592778543651705,"user_name":"jivteshs20"}} \ No newline at end of file diff --git a/sharedTest/src/main/resources/similar_users_response.json b/sharedTest/src/main/resources/similar_users_response.json index 6d50b409..31e26bc2 100644 --- a/sharedTest/src/main/resources/similar_users_response.json +++ b/sharedTest/src/main/resources/similar_users_response.json @@ -1 +1 @@ -{"payload":{"similarity":0.592778543651705,"user_name":"jivteshs20"}} +{"payload":[{"similarity":0.592778543651705,"user_name":"jivteshs20"},{"similarity":0.2367332837331803,"user_name":"akshaaatt"},{"similarity":0.18639954307331036,"user_name":"lucifer"}]} \ No newline at end of file From cb120e9e089cd79155607100be8798d4dd577555 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Wed, 14 Aug 2024 19:43:29 +0530 Subject: [PATCH 77/97] Adds Basic UI Test for Listens Tab of User Pages --- .../org/listenbrainz/android/UserPagesTest.kt | 109 ++++++++++++++++++ .../screens/profile/listens/ListensScreen.kt | 5 +- 2 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 app/src/androidTest/java/org/listenbrainz/android/UserPagesTest.kt diff --git a/app/src/androidTest/java/org/listenbrainz/android/UserPagesTest.kt b/app/src/androidTest/java/org/listenbrainz/android/UserPagesTest.kt new file mode 100644 index 00000000..ef7ede0d --- /dev/null +++ b/app/src/androidTest/java/org/listenbrainz/android/UserPagesTest.kt @@ -0,0 +1,109 @@ +package org.listenbrainz.android + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performScrollToIndex +import androidx.test.ext.junit.runners.AndroidJUnit4 +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.listenbrainz.android.model.SocialUiState +import org.listenbrainz.android.ui.screens.feed.FeedUiState +import org.listenbrainz.android.ui.screens.profile.listens.ListensScreen +import org.listenbrainz.android.ui.screens.settings.PreferencesUiState +import org.listenbrainz.android.ui.theme.ListenBrainzTheme +import org.listenbrainz.android.viewmodel.UserViewModel +import org.listenbrainz.sharedtest.mocks.MockAppPreferences +import org.listenbrainz.sharedtest.mocks.MockListensRepository +import org.listenbrainz.sharedtest.mocks.MockSocialRepository +import org.listenbrainz.sharedtest.mocks.MockUserRepository +import org.listenbrainz.sharedtest.utils.EntityTestUtils.testUsername + +@RunWith(AndroidJUnit4::class) +@HiltAndroidTest +class UserPagesTest { + + @get:Rule(order = 0) + var hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val rule = createComposeRule() + + private lateinit var viewModel: UserViewModel + + @Before + fun setup() { + val testDispatcher: TestDispatcher = StandardTestDispatcher() + viewModel = UserViewModel( + MockAppPreferences(), + MockUserRepository(), + MockListensRepository(), + MockSocialRepository(), + testDispatcher + ) + + rule.setContent { + runBlocking { + viewModel.getUserDataFromRemote(testUsername) + testDispatcher.scheduler.advanceUntilIdle() + } + val uiState by viewModel.uiState.collectAsState() + ListenBrainzTheme { + ListensScreen( + scrollRequestState = false, + onScrollToTop = {}, + username = testUsername, + uiState = uiState, + feedUiState = FeedUiState(), + preferencesUiState = PreferencesUiState(), + updateNotificationServicePermissionStatus = { /*TODO*/ }, + dropdownItemIndex = remember { + mutableStateOf(null) + }, + validateUserToken = {_, -> true}, + setToken = {}, + playListen = {}, + snackbarState = remember { + SnackbarHostState() + }, + socialUiState = SocialUiState(), + onRecommend = {}, + onErrorShown = { /*TODO*/ }, + onMessageShown = { /*TODO*/ }, + onPin = {_, _ ->}, + searchUsers = {}, + isCritiqueBrainzLinked = {true}, + onReview = {_, _, _, _, _ ->}, + onPersonallyRecommend = {_, _, _ ->}, + onFollowButtonClick = {_, _ ->} + ) + } + } + } + + @Test + fun listensTabScreenFlowTest() { + rule.onNodeWithText("You have listened to").assertExists() + rule.onNodeWithText("Recent Listens").assertExists() + rule.onNodeWithText("Followers").assertExists() + val scrollableContainer = rule.onNodeWithTag("listensScreenScrollableContainer") + scrollableContainer.performScrollToIndex(10) + rule.onNodeWithText("Similar Users").assertExists() +// rule.waitUntil ( +// timeoutMillis = 5000L, +// condition = {false} +// ) + } +} diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt index c730ee5e..5bb2a772 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.UriHandler +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign @@ -83,8 +84,8 @@ import org.listenbrainz.android.ui.theme.lb_purple_night import org.listenbrainz.android.util.Utils.getCoverArtUrl import org.listenbrainz.android.viewmodel.FeedViewModel import org.listenbrainz.android.viewmodel.ListensViewModel -import org.listenbrainz.android.viewmodel.UserViewModel import org.listenbrainz.android.viewmodel.SocialViewModel +import org.listenbrainz.android.viewmodel.UserViewModel @Composable fun ListensScreen( @@ -241,7 +242,7 @@ fun ListensScreen( } AnimatedVisibility(visible = !uiState.listensTabUiState.isLoading) { - LazyColumn(state = listState) { + LazyColumn(state = listState, modifier = Modifier.testTag("listensScreenScrollableContainer")) { item { SongsListened(username = username, listenCount = uiState.listensTabUiState.listenCount, isSelf = uiState.isSelf) } From 084c6579f73192c3ef83d31b0511efa2cc983fae Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Wed, 14 Aug 2024 23:27:12 +0530 Subject: [PATCH 78/97] Adds UI tests for all tabs of User Pages --- .../org/listenbrainz/android/UserPagesTest.kt | 122 ++++++++++++------ .../ui/screens/profile/BaseProfileScreen.kt | 35 ++++- .../screens/profile/listens/ListensScreen.kt | 7 +- .../ui/screens/profile/stats/StatsScreen.kt | 15 ++- .../ui/screens/profile/taste/TasteScreen.kt | 9 +- sharedTest/build.gradle.kts | 3 + .../sharedtest/mocks/MockFeedRepository.kt | 59 +++++++++ .../mocks/MockRemotePlaybackHandler.kt | 59 +++++++++ .../sharedtest/mocks/MockSocketRepository.kt | 13 ++ 9 files changed, 262 insertions(+), 60 deletions(-) create mode 100644 sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockFeedRepository.kt create mode 100644 sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockRemotePlaybackHandler.kt create mode 100644 sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockSocketRepository.kt diff --git a/app/src/androidTest/java/org/listenbrainz/android/UserPagesTest.kt b/app/src/androidTest/java/org/listenbrainz/android/UserPagesTest.kt index ef7ede0d..a761f378 100644 --- a/app/src/androidTest/java/org/listenbrainz/android/UserPagesTest.kt +++ b/app/src/androidTest/java/org/listenbrainz/android/UserPagesTest.kt @@ -1,13 +1,15 @@ package org.listenbrainz.android +import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToIndex import androidx.test.ext.junit.runners.AndroidJUnit4 import dagger.hilt.android.testing.HiltAndroidRule @@ -19,15 +21,18 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.listenbrainz.android.model.SocialUiState -import org.listenbrainz.android.ui.screens.feed.FeedUiState -import org.listenbrainz.android.ui.screens.profile.listens.ListensScreen -import org.listenbrainz.android.ui.screens.settings.PreferencesUiState +import org.listenbrainz.android.ui.screens.profile.BaseProfileScreen import org.listenbrainz.android.ui.theme.ListenBrainzTheme +import org.listenbrainz.android.viewmodel.FeedViewModel +import org.listenbrainz.android.viewmodel.ListensViewModel +import org.listenbrainz.android.viewmodel.SocialViewModel import org.listenbrainz.android.viewmodel.UserViewModel import org.listenbrainz.sharedtest.mocks.MockAppPreferences +import org.listenbrainz.sharedtest.mocks.MockFeedRepository import org.listenbrainz.sharedtest.mocks.MockListensRepository +import org.listenbrainz.sharedtest.mocks.MockRemotePlaybackHandler import org.listenbrainz.sharedtest.mocks.MockSocialRepository +import org.listenbrainz.sharedtest.mocks.MockSocketRepository import org.listenbrainz.sharedtest.mocks.MockUserRepository import org.listenbrainz.sharedtest.utils.EntityTestUtils.testUsername @@ -42,6 +47,9 @@ class UserPagesTest { val rule = createComposeRule() private lateinit var viewModel: UserViewModel + private lateinit var feedViewModel: FeedViewModel + private lateinit var listensViewModel: ListensViewModel + private lateinit var socialViewModel: SocialViewModel @Before fun setup() { @@ -53,6 +61,28 @@ class UserPagesTest { MockSocialRepository(), testDispatcher ) + feedViewModel = FeedViewModel( + feedRepository = MockFeedRepository(), + socialRepository = MockSocialRepository(), + listensRepository = MockListensRepository(), + appPreferences = MockAppPreferences(), + remotePlaybackHandler = MockRemotePlaybackHandler(), + testDispatcher, + testDispatcher + ) + listensViewModel = ListensViewModel( + repository = MockListensRepository(), + appPreferences = MockAppPreferences(), + socketRepository = MockSocketRepository(), + remotePlaybackHandler = MockRemotePlaybackHandler(), + testDispatcher + ) + socialViewModel = SocialViewModel( + repository = MockSocialRepository(), + appPreferences = MockAppPreferences(), + remotePlaybackHandler = MockRemotePlaybackHandler(), + testDispatcher + ) rule.setContent { runBlocking { @@ -61,49 +91,67 @@ class UserPagesTest { } val uiState by viewModel.uiState.collectAsState() ListenBrainzTheme { - ListensScreen( - scrollRequestState = false, - onScrollToTop = {}, - username = testUsername, - uiState = uiState, - feedUiState = FeedUiState(), - preferencesUiState = PreferencesUiState(), - updateNotificationServicePermissionStatus = { /*TODO*/ }, - dropdownItemIndex = remember { - mutableStateOf(null) - }, - validateUserToken = {_, -> true}, - setToken = {}, - playListen = {}, - snackbarState = remember { - SnackbarHostState() - }, - socialUiState = SocialUiState(), - onRecommend = {}, - onErrorShown = { /*TODO*/ }, - onMessageShown = { /*TODO*/ }, - onPin = {_, _ ->}, - searchUsers = {}, - isCritiqueBrainzLinked = {true}, - onReview = {_, _, _, _, _ ->}, - onPersonallyRecommend = {_, _, _ ->}, - onFollowButtonClick = {_, _ ->} - ) + Scaffold { + it -> + BaseProfileScreen( + username = testUsername, + snackbarState = remember { + SnackbarHostState() + }, + uiState = uiState, + onFollowClick = {}, + onUnfollowClick = {}, + goToUserProfile = { /*TODO*/ }, + viewModel = viewModel, + feedViewModel = feedViewModel, + socialViewModel = socialViewModel, + listensViewModel = listensViewModel, + ) + } + } } } + @Test + fun allTabsExistenceTest () { + rule.onNodeWithText("Listens").assertExists() + rule.onNodeWithText("Stats").assertExists() + rule.onNodeWithText("Taste").assertExists() + } + @Test fun listensTabScreenFlowTest() { + rule.onNodeWithText("Listens").performClick() rule.onNodeWithText("You have listened to").assertExists() rule.onNodeWithText("Recent Listens").assertExists() rule.onNodeWithText("Followers").assertExists() val scrollableContainer = rule.onNodeWithTag("listensScreenScrollableContainer") scrollableContainer.performScrollToIndex(10) rule.onNodeWithText("Similar Users").assertExists() -// rule.waitUntil ( -// timeoutMillis = 5000L, -// condition = {false} -// ) + } + + @Test + fun statsTabScreenFlowTest() { + rule.onNodeWithText("Stats").performClick() + rule.onNodeWithText("Global").assertExists() + rule.onNodeWithText("This Week").assertExists() + rule.onNodeWithText("This Month").assertExists() + rule.onNodeWithText("This Year").assertExists() + val scrollableContainer = rule.onNodeWithTag("statsScreenScrollableContainer") + scrollableContainer.performScrollToIndex(2) + rule.onNodeWithText("Artists").assertExists() + rule.onNodeWithText("Albums").assertExists() + rule.onNodeWithText("Songs").assertExists() + scrollableContainer.performScrollToIndex(3) + rule.onNodeWithText("Load More").assertExists() + } + + @Test + fun tasteTabScreenFlowTest() { + rule.onNodeWithText("Taste").performClick() + rule.onNodeWithText("Loved").assertExists() + rule.onNodeWithText("Hated").assertExists() + rule.onNodeWithText("Pins").assertExists() } } 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 887903a8..9530cde7 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 @@ -55,6 +55,9 @@ import org.listenbrainz.android.ui.theme.lb_orange import org.listenbrainz.android.ui.theme.lb_purple import org.listenbrainz.android.ui.theme.new_app_bg_light import org.listenbrainz.android.util.Constants +import org.listenbrainz.android.viewmodel.FeedViewModel +import org.listenbrainz.android.viewmodel.ListensViewModel +import org.listenbrainz.android.viewmodel.SocialViewModel import org.listenbrainz.android.viewmodel.UserViewModel @Composable @@ -66,6 +69,9 @@ fun BaseProfileScreen( onFollowClick: (String) -> Unit, onUnfollowClick: (String) -> Unit, goToUserProfile: () -> Unit, + feedViewModel: FeedViewModel = hiltViewModel(), + listensViewModel: ListensViewModel = hiltViewModel(), + socialViewModel: SocialViewModel = hiltViewModel() ){ val currentTab : MutableState = remember { mutableStateOf(ProfileScreenTab.LISTENS) } @@ -102,12 +108,15 @@ fun BaseProfileScreen( Spacer(modifier = Modifier.width(ListenBrainzTheme.paddings.chipsHorizontal / 2)) repeat(5) { position -> when(position){ - 0 -> Box(modifier = Modifier.padding(ListenBrainzTheme.paddings.chipsHorizontal,) .clip(shape = RoundedCornerShape(4.dp)).background( - when(uiState.isSelf){ - true -> lb_purple - false -> lb_orange - } - )) { + 0 -> Box(modifier = Modifier + .padding(ListenBrainzTheme.paddings.chipsHorizontal,) + .clip(shape = RoundedCornerShape(4.dp)) + .background( + when (uiState.isSelf) { + true -> lb_purple + false -> lb_orange + } + )) { Row (modifier = Modifier.padding(end = 8.dp, top = when(uiState.isSelf){ true -> 4.dp false -> 0.dp @@ -204,14 +213,23 @@ fun BaseProfileScreen( userViewModel = viewModel, onScrollToTop = {}, snackbarState = snackbarState, - username = username + username = username, + feedViewModel = feedViewModel, + socialViewModel = socialViewModel, + viewModel = listensViewModel ) ProfileScreenTab.STATS -> StatsScreen( username = username, snackbarState = snackbarState, + socialViewModel = socialViewModel, + viewModel = viewModel, + feedViewModel = feedViewModel ) ProfileScreenTab.TASTE -> TasteScreen( snackbarState = snackbarState, + socialViewModel = socialViewModel, + feedViewModel = feedViewModel, + viewModel = viewModel ) else -> ListensScreen( scrollRequestState = false, @@ -219,6 +237,9 @@ fun BaseProfileScreen( onScrollToTop = {}, snackbarState = snackbarState, username = username, + feedViewModel = feedViewModel, + socialViewModel = socialViewModel, + viewModel = listensViewModel ) } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt index 5bb2a772..69a75256 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt @@ -54,7 +54,6 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel import kotlinx.coroutines.launch import org.listenbrainz.android.R import org.listenbrainz.android.model.Listen @@ -89,10 +88,10 @@ import org.listenbrainz.android.viewmodel.UserViewModel @Composable fun ListensScreen( - viewModel: ListensViewModel = hiltViewModel(), + viewModel: ListensViewModel, userViewModel: UserViewModel, - socialViewModel: SocialViewModel = hiltViewModel(), - feedViewModel : FeedViewModel = hiltViewModel(), + socialViewModel: SocialViewModel, + feedViewModel : FeedViewModel, scrollRequestState: Boolean, onScrollToTop: (suspend () -> Unit) -> Unit, snackbarState : SnackbarHostState, 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 fd31dae6..956c0bd3 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 @@ -40,11 +40,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.UriHandler +import androidx.compose.ui.platform.testTag 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 import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottomAxis import com.patrykandpatrick.vico.compose.cartesian.axis.rememberStartAxis @@ -82,15 +82,15 @@ 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.FeedViewModel -import org.listenbrainz.android.viewmodel.UserViewModel import org.listenbrainz.android.viewmodel.SocialViewModel +import org.listenbrainz.android.viewmodel.UserViewModel @Composable fun StatsScreen( username: String?, - viewModel: UserViewModel = hiltViewModel(), - socialViewModel: SocialViewModel = hiltViewModel(), - feedViewModel : FeedViewModel = hiltViewModel(), + viewModel: UserViewModel, + socialViewModel: SocialViewModel, + feedViewModel : FeedViewModel, snackbarState : SnackbarHostState, ) { val uiState by viewModel.uiState.collectAsState() @@ -245,7 +245,7 @@ fun StatsScreen( false -> uiState.statsTabUIState.topSongs?.get(statsRangeState)?.payload?.recordings ?: listOf() } - LazyColumn { + LazyColumn (modifier = Modifier.testTag("statsScreenScrollableContainer")) { item { RangeBar( statsRangeState = statsRangeState, @@ -326,7 +326,8 @@ fun StatsScreen( .padding(start = 11.dp, end = 11.dp) .height(250.dp) .clip(RoundedCornerShape(10.dp)) - .background(Color(0xFFe0e5de)), + .background(Color(0xFFe0e5de)) + .testTag("listeningActivityChart"), chart = rememberCartesianChart( rememberColumnCartesianLayer( columnProvider = columnProvider, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt index 75e215b6..bbd71a87 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt @@ -38,7 +38,6 @@ import androidx.compose.ui.platform.UriHandler 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 kotlinx.coroutines.launch import org.listenbrainz.android.R import org.listenbrainz.android.model.Metadata @@ -61,14 +60,14 @@ import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.ui.theme.lb_purple_night import org.listenbrainz.android.util.Utils.getCoverArtUrl import org.listenbrainz.android.viewmodel.FeedViewModel -import org.listenbrainz.android.viewmodel.UserViewModel import org.listenbrainz.android.viewmodel.SocialViewModel +import org.listenbrainz.android.viewmodel.UserViewModel @Composable fun TasteScreen( - viewModel: UserViewModel = hiltViewModel(), - socialViewModel: SocialViewModel = hiltViewModel(), - feedViewModel : FeedViewModel = hiltViewModel(), + viewModel: UserViewModel, + socialViewModel: SocialViewModel, + feedViewModel : FeedViewModel, snackbarState : SnackbarHostState, ) { val uiState by viewModel.uiState.collectAsState() diff --git a/sharedTest/build.gradle.kts b/sharedTest/build.gradle.kts index 988a96db..e44d7cb1 100644 --- a/sharedTest/build.gradle.kts +++ b/sharedTest/build.gradle.kts @@ -48,6 +48,9 @@ dependencies { implementation(libs.okhttp) implementation(libs.retrofit.converter.gson) + //Spotify SDK for mocking remotePlaybackHandler + implementation(files("../app/lib/spotify-app-remote-release-0.7.2.aar")) + // Testing implementation(libs.junit) implementation(libs.mockwebserver) diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockFeedRepository.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockFeedRepository.kt new file mode 100644 index 00000000..e4a4be98 --- /dev/null +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockFeedRepository.kt @@ -0,0 +1,59 @@ +package org.listenbrainz.sharedtest.mocks + +import org.listenbrainz.android.model.SocialResponse +import org.listenbrainz.android.model.feed.FeedData +import org.listenbrainz.android.model.feed.FeedEventDeletionData +import org.listenbrainz.android.model.feed.FeedEventVisibilityData +import org.listenbrainz.android.repository.feed.FeedRepository +import org.listenbrainz.android.util.Resource + +class MockFeedRepository : FeedRepository { + override suspend fun getFeedEvents( + username: String?, + maxTs: Int?, + minTs: Int?, + count: Int + ): Resource { + TODO("Not yet implemented") + } + + override suspend fun getFeedFollowListens( + username: String?, + maxTs: Int?, + minTs: Int?, + count: Int + ): Resource { + TODO("Not yet implemented") + } + + override suspend fun getFeedSimilarListens( + username: String?, + maxTs: Int?, + minTs: Int?, + count: Int + ): Resource { + TODO("Not yet implemented") + } + + override suspend fun deleteEvent( + username: String?, + data: FeedEventDeletionData + ): Resource { + TODO("Not yet implemented") + } + + override suspend fun hideEvent( + username: String?, + data: FeedEventVisibilityData + ): Resource { + TODO("Not yet implemented") + } + + override suspend fun unhideEvent( + username: String?, + data: FeedEventVisibilityData + ): Resource { + TODO("Not yet implemented") + } + +} \ No newline at end of file diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockRemotePlaybackHandler.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockRemotePlaybackHandler.kt new file mode 100644 index 00000000..7de7f700 --- /dev/null +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockRemotePlaybackHandler.kt @@ -0,0 +1,59 @@ +package org.listenbrainz.sharedtest.mocks + +import com.spotify.protocol.types.PlayerContext +import com.spotify.protocol.types.PlayerState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import org.listenbrainz.android.model.ListenBitmap +import org.listenbrainz.android.model.ResponseError +import org.listenbrainz.android.repository.remoteplayer.RemotePlaybackHandler +import org.listenbrainz.android.util.Resource + + +class MockRemotePlaybackHandler : RemotePlaybackHandler { + override suspend fun searchYoutubeMusicVideoId( + trackName: String, + artist: String + ): Resource { + TODO("Not yet implemented") + } + + override suspend fun playOnYoutube(getYoutubeMusicVideoId: suspend () -> Resource): Resource { + TODO("Not yet implemented") + } + + override suspend fun connectToSpotify(onError: (ResponseError) -> Unit) { + TODO("Not yet implemented") + } + + override suspend fun disconnectSpotify() { + TODO("Not yet implemented") + } + + override suspend fun fetchSpotifyTrackCoverArt(playerState: PlayerState?): ListenBitmap { + TODO("Not yet implemented") + } + + override fun playUri(trackId: String, onFailure: () -> Unit) { + TODO("Not yet implemented") + } + + override fun play(onPlay: () -> Unit) { + TODO("Not yet implemented") + } + + override fun pause(onPause: () -> Unit) { + TODO("Not yet implemented") + } + + override fun getPlayerState(): Flow { + return flow { + + } + } + + override fun getPlayerContext(): Flow { + TODO("Not yet implemented") + } + +} \ No newline at end of file diff --git a/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockSocketRepository.kt b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockSocketRepository.kt new file mode 100644 index 00000000..5d2e7835 --- /dev/null +++ b/sharedTest/src/main/java/org/listenbrainz/sharedtest/mocks/MockSocketRepository.kt @@ -0,0 +1,13 @@ +package org.listenbrainz.sharedtest.mocks + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import org.listenbrainz.android.model.Listen +import org.listenbrainz.android.repository.socket.SocketRepository + +class MockSocketRepository : SocketRepository { + override fun listen(username: String): Flow { + return flow { } + } + +} \ No newline at end of file From 50aad7f89a3623d76ac8903a928e18aff3d7b45d Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Thu, 15 Aug 2024 09:27:05 +0530 Subject: [PATCH 79/97] Fixed Local AAR dependency issue --- app/build.gradle.kts | 2 +- settings.gradle | 1 + sharedTest/build.gradle.kts | 2 +- spotify-app-remote/build.gradle.kts | 2 ++ .../results.bin | 1 + .../classes.dex | Bin 0 -> 137744 bytes .../results.bin | 1 + .../classes.dex | Bin 0 -> 137792 bytes .../results.bin | 1 + .../classes.dex | Bin 0 -> 120316 bytes .../results.bin | 1 + .../classes.dex | Bin 0 -> 138012 bytes .../spotify-app-remote-release-0.7.2.aar | Bin 13 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 spotify-app-remote/build.gradle.kts create mode 100644 spotify-app-remote/build/.transforms/02890a36f7f1b7de65370910e6d441c9/results.bin create mode 100644 spotify-app-remote/build/.transforms/02890a36f7f1b7de65370910e6d441c9/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex create mode 100644 spotify-app-remote/build/.transforms/1acd802fc05d76d1989897fafaa4dcc1/results.bin create mode 100644 spotify-app-remote/build/.transforms/1acd802fc05d76d1989897fafaa4dcc1/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex create mode 100644 spotify-app-remote/build/.transforms/600d275753de81492ac32240018e723c/results.bin create mode 100644 spotify-app-remote/build/.transforms/600d275753de81492ac32240018e723c/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex create mode 100644 spotify-app-remote/build/.transforms/a236cf5d8026cbceaedc55a536fdbf38/results.bin create mode 100644 spotify-app-remote/build/.transforms/a236cf5d8026cbceaedc55a536fdbf38/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex rename {app/lib => spotify-app-remote}/spotify-app-remote-release-0.7.2.aar (100%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 15f428a8..a9d8f0ac 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -198,7 +198,7 @@ dependencies { implementation(libs.google.exoplayer.mediasession) // Spotify SDK - implementation(files("./lib/spotify-app-remote-release-0.7.2.aar")) + api(project(":spotify-app-remote")) // Networking and parsing implementation(libs.jsoup) diff --git a/settings.gradle b/settings.gradle index 479c0fef..1ed457e9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,3 +17,4 @@ dependencyResolutionManagement { include(":app") include(":sharedTest") +include(":spotify-app-remote") diff --git a/sharedTest/build.gradle.kts b/sharedTest/build.gradle.kts index e44d7cb1..2ed17476 100644 --- a/sharedTest/build.gradle.kts +++ b/sharedTest/build.gradle.kts @@ -49,7 +49,7 @@ dependencies { implementation(libs.retrofit.converter.gson) //Spotify SDK for mocking remotePlaybackHandler - implementation(files("../app/lib/spotify-app-remote-release-0.7.2.aar")) + api(project(":spotify-app-remote")) // Testing implementation(libs.junit) diff --git a/spotify-app-remote/build.gradle.kts b/spotify-app-remote/build.gradle.kts new file mode 100644 index 00000000..f479d2bf --- /dev/null +++ b/spotify-app-remote/build.gradle.kts @@ -0,0 +1,2 @@ +configurations.maybeCreate("default") +artifacts.add("default", file("spotify-app-remote-release-0.7.2.aar")) \ No newline at end of file diff --git a/spotify-app-remote/build/.transforms/02890a36f7f1b7de65370910e6d441c9/results.bin b/spotify-app-remote/build/.transforms/02890a36f7f1b7de65370910e6d441c9/results.bin new file mode 100644 index 00000000..52daf05d --- /dev/null +++ b/spotify-app-remote/build/.transforms/02890a36f7f1b7de65370910e6d441c9/results.bin @@ -0,0 +1 @@ +o/spotify-app-remote-release-0.7.2-runtime diff --git a/spotify-app-remote/build/.transforms/02890a36f7f1b7de65370910e6d441c9/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex b/spotify-app-remote/build/.transforms/02890a36f7f1b7de65370910e6d441c9/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex new file mode 100644 index 0000000000000000000000000000000000000000..bc99224bc5c3d828a98f8cb20ee6780ef815fb14 GIT binary patch literal 137744 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 CH5%Oj literal 0 HcmV?d00001 diff --git a/spotify-app-remote/build/.transforms/1acd802fc05d76d1989897fafaa4dcc1/results.bin b/spotify-app-remote/build/.transforms/1acd802fc05d76d1989897fafaa4dcc1/results.bin new file mode 100644 index 00000000..52daf05d --- /dev/null +++ b/spotify-app-remote/build/.transforms/1acd802fc05d76d1989897fafaa4dcc1/results.bin @@ -0,0 +1 @@ +o/spotify-app-remote-release-0.7.2-runtime diff --git a/spotify-app-remote/build/.transforms/1acd802fc05d76d1989897fafaa4dcc1/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex b/spotify-app-remote/build/.transforms/1acd802fc05d76d1989897fafaa4dcc1/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex new file mode 100644 index 0000000000000000000000000000000000000000..1822258368a6e70582767bbd549022cf0820e99d GIT binary patch literal 137792 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 G=>GwX{8Zrp literal 0 HcmV?d00001 diff --git a/spotify-app-remote/build/.transforms/600d275753de81492ac32240018e723c/results.bin b/spotify-app-remote/build/.transforms/600d275753de81492ac32240018e723c/results.bin new file mode 100644 index 00000000..52daf05d --- /dev/null +++ b/spotify-app-remote/build/.transforms/600d275753de81492ac32240018e723c/results.bin @@ -0,0 +1 @@ +o/spotify-app-remote-release-0.7.2-runtime diff --git a/spotify-app-remote/build/.transforms/600d275753de81492ac32240018e723c/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex b/spotify-app-remote/build/.transforms/600d275753de81492ac32240018e723c/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex new file mode 100644 index 0000000000000000000000000000000000000000..d14c20953ff9657ae8bac2ed88acd76f2629adb5 GIT binary patch literal 120316 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

F=yg&eIBn% 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 z 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 CsbLWS literal 0 HcmV?d00001 diff --git a/spotify-app-remote/build/.transforms/a236cf5d8026cbceaedc55a536fdbf38/results.bin b/spotify-app-remote/build/.transforms/a236cf5d8026cbceaedc55a536fdbf38/results.bin new file mode 100644 index 00000000..52daf05d --- /dev/null +++ b/spotify-app-remote/build/.transforms/a236cf5d8026cbceaedc55a536fdbf38/results.bin @@ -0,0 +1 @@ +o/spotify-app-remote-release-0.7.2-runtime diff --git a/spotify-app-remote/build/.transforms/a236cf5d8026cbceaedc55a536fdbf38/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex b/spotify-app-remote/build/.transforms/a236cf5d8026cbceaedc55a536fdbf38/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex new file mode 100644 index 0000000000000000000000000000000000000000..1c62d8894e783ed2b2e99a87ef6773a730a685cf GIT binary patch literal 138012 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 +xV`m<21H@w|=L&0vN~sbDI55 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 Date: Fri, 16 Aug 2024 09:56:09 +0530 Subject: [PATCH 80/97] Initialized Artist VM, Repo, Service --- .../android/di/ArtistRepositoryModule.kt | 16 ++++ .../listenbrainz/android/di/ServiceModule.kt | 20 ++++- .../di/brainzplayer/AlbumRepositoryModule.kt | 6 +- .../di/brainzplayer/ArtistRepositoryModule.kt | 8 +- .../android/di/brainzplayer/ServiceModule.kt | 8 +- .../android/model/AppNavigationItem.kt | 1 + .../android/model/artist/AlbumTagInfo.kt | 9 +++ .../listenbrainz/android/model/artist/Area.kt | 13 ++++ .../android/model/artist/Artist.kt | 7 ++ .../android/model/artist/ArtistBio.kt | 12 +++ .../model/artist/ArtistPersonalInfo.kt | 14 ++++ .../android/model/artist/ArtistTagsInfo.kt | 10 +++ .../android/model/artist/ArtistWikiExtract.kt | 5 ++ .../android/model/artist/LifeSpan.kt | 7 ++ .../android/model/artist/PopularAlbums.kt | 3 + .../android/model/artist/PopularAlbumsItem.kt | 14 ++++ .../android/model/artist/PopularTracks.kt | 3 + .../android/model/artist/PopularTracksItem.kt | 19 +++++ .../android/model/artist/Release.kt | 12 +++ .../android/model/artist/ReleaseColor.kt | 7 ++ .../listenbrainz/android/model/artist/Rels.kt | 15 ++++ .../listenbrainz/android/model/artist/Tag.kt | 8 ++ .../model/artist/TopAlbumArtistInfo.kt | 9 +++ .../android/model/artist/WikipediaExtract.kt | 9 +++ .../repository/artist/ArtistRepository.kt | 9 +++ .../repository/artist/ArtistRepositoryImpl.kt | 19 +++++ ...lbumRepository.kt => BPAlbumRepository.kt} | 2 +- ...sitoryImpl.kt => BPAlbumRepositoryImpl.kt} | 4 +- ...istRepository.kt => BPArtistRepository.kt} | 2 +- ...itoryImpl.kt => BPArtistRepositoryImpl.kt} | 4 +- .../android/service/ArtistService.kt | 15 ++++ .../android/service/BrainzPlayerService.kt | 4 +- .../listenbrainz/android/service/MBService.kt | 16 ++++ .../android/ui/navigation/AppNavigation.kt | 22 +++++- .../android/ui/navigation/TopBar.kt | 1 + .../android/ui/screens/artist/ArtistScreen.kt | 11 +++ .../ui/screens/artist/ArtistUIState.kt | 6 ++ .../ui/screens/brainzplayer/AlbumScreen.kt | 60 +++++++-------- .../ui/screens/brainzplayer/ArtistScreen.kt | 24 +++--- .../brainzplayer/BrainzPlayerScreen.kt | 14 ++-- .../android/ui/screens/main/MainActivity.kt | 4 + .../ui/screens/profile/BaseProfileScreen.kt | 9 ++- .../ui/screens/profile/ProfileScreen.kt | 6 +- .../ui/screens/profile/ProfileUiState.kt | 3 +- .../screens/profile/listens/ListensScreen.kt | 76 +++++++++++++++---- .../android/util/LocalMusicSource.kt | 6 +- .../android/viewmodel/ArtistViewModel.kt | 60 --------------- ...{AlbumViewModel.kt => BPAlbumViewModel.kt} | 22 +++--- .../android/viewmodel/BPArtistViewModel.kt | 65 ++++++++++++++++ .../android/viewmodel/UserViewModel.kt | 6 +- 50 files changed, 538 insertions(+), 167 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/di/ArtistRepositoryModule.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/AlbumTagInfo.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/Area.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/Artist.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/ArtistBio.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/ArtistPersonalInfo.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/ArtistTagsInfo.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/ArtistWikiExtract.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/LifeSpan.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/PopularAlbums.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/PopularAlbumsItem.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/PopularTracks.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/PopularTracksItem.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/Release.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/ReleaseColor.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/Rels.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/Tag.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/TopAlbumArtistInfo.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/WikipediaExtract.kt create mode 100644 app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepository.kt create mode 100644 app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepositoryImpl.kt rename app/src/main/java/org/listenbrainz/android/repository/brainzplayer/{AlbumRepository.kt => BPAlbumRepository.kt} (93%) rename app/src/main/java/org/listenbrainz/android/repository/brainzplayer/{AlbumRepositoryImpl.kt => BPAlbumRepositoryImpl.kt} (96%) rename app/src/main/java/org/listenbrainz/android/repository/brainzplayer/{ArtistRepository.kt => BPArtistRepository.kt} (94%) rename app/src/main/java/org/listenbrainz/android/repository/brainzplayer/{ArtistRepositoryImpl.kt => BPArtistRepositoryImpl.kt} (97%) create mode 100644 app/src/main/java/org/listenbrainz/android/service/ArtistService.kt create mode 100644 app/src/main/java/org/listenbrainz/android/service/MBService.kt create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistUIState.kt rename app/src/main/java/org/listenbrainz/android/viewmodel/{AlbumViewModel.kt => BPAlbumViewModel.kt} (65%) create mode 100644 app/src/main/java/org/listenbrainz/android/viewmodel/BPArtistViewModel.kt diff --git a/app/src/main/java/org/listenbrainz/android/di/ArtistRepositoryModule.kt b/app/src/main/java/org/listenbrainz/android/di/ArtistRepositoryModule.kt new file mode 100644 index 00000000..7e75195c --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/di/ArtistRepositoryModule.kt @@ -0,0 +1,16 @@ +package org.listenbrainz.android.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.listenbrainz.android.repository.artist.ArtistRepository +import org.listenbrainz.android.repository.artist.ArtistRepositoryImpl + +@Module +@InstallIn(SingletonComponent::class) +abstract class ArtistRepositoryModule { + + @Binds + abstract fun bindsArtistRepository (repository: ArtistRepositoryImpl?) : ArtistRepository? +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt b/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt index d08587f7..d627f8f8 100644 --- a/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt +++ b/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt @@ -14,9 +14,11 @@ import dagger.hilt.components.SingletonComponent import okhttp3.OkHttpClient import org.listenbrainz.android.model.yimdata.YimData import org.listenbrainz.android.repository.preferences.AppPreferences +import org.listenbrainz.android.service.ArtistService import org.listenbrainz.android.service.BlogService import org.listenbrainz.android.service.FeedService import org.listenbrainz.android.service.ListensService +import org.listenbrainz.android.service.MBService import org.listenbrainz.android.service.SocialService import org.listenbrainz.android.service.UserService import org.listenbrainz.android.service.Yim23Service @@ -24,6 +26,7 @@ import org.listenbrainz.android.service.YimService import org.listenbrainz.android.service.YouTubeApiService import org.listenbrainz.android.util.Constants.LISTENBRAINZ_API_BASE_URL import org.listenbrainz.android.util.Constants.LISTENBRAINZ_BETA_API_BASE_URL +import org.listenbrainz.android.util.Constants.MB_BASE_URL import org.listenbrainz.android.util.HeaderInterceptor import org.listenbrainz.android.util.Utils import retrofit2.Retrofit @@ -84,8 +87,21 @@ class ServiceModule { fun providesUserService(appPreferences: AppPreferences) : UserService = constructRetrofit(appPreferences) .create(UserService::class.java) - - + + @Singleton + @Provides + fun providesArtistService(appPreferences: AppPreferences): ArtistService = + constructRetrofit(appPreferences) + .create(ArtistService::class.java) + + @Singleton + @Provides + fun providesMBService(appPreferences: AppPreferences): MBService = Retrofit.Builder() + .baseUrl(MB_BASE_URL) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build().create(MBService::class.java) + @Singleton @Provides fun providesYoutubeApiService(@ApplicationContext context: Context): YouTubeApiService = diff --git a/app/src/main/java/org/listenbrainz/android/di/brainzplayer/AlbumRepositoryModule.kt b/app/src/main/java/org/listenbrainz/android/di/brainzplayer/AlbumRepositoryModule.kt index 9e68fcd8..279431fd 100644 --- a/app/src/main/java/org/listenbrainz/android/di/brainzplayer/AlbumRepositoryModule.kt +++ b/app/src/main/java/org/listenbrainz/android/di/brainzplayer/AlbumRepositoryModule.kt @@ -4,12 +4,12 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import org.listenbrainz.android.repository.brainzplayer.AlbumRepository -import org.listenbrainz.android.repository.brainzplayer.AlbumRepositoryImpl +import org.listenbrainz.android.repository.brainzplayer.BPAlbumRepository +import org.listenbrainz.android.repository.brainzplayer.BPAlbumRepositoryImpl @Module @InstallIn(SingletonComponent::class) abstract class AlbumRepositoryModule { @Binds - abstract fun bindsAlbumRepository(repository: AlbumRepositoryImpl?) : AlbumRepository? + abstract fun bindsAlbumRepository(repository: BPAlbumRepositoryImpl?) : BPAlbumRepository? } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/di/brainzplayer/ArtistRepositoryModule.kt b/app/src/main/java/org/listenbrainz/android/di/brainzplayer/ArtistRepositoryModule.kt index 41922ff2..3d9c711f 100644 --- a/app/src/main/java/org/listenbrainz/android/di/brainzplayer/ArtistRepositoryModule.kt +++ b/app/src/main/java/org/listenbrainz/android/di/brainzplayer/ArtistRepositoryModule.kt @@ -4,12 +4,12 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import org.listenbrainz.android.repository.brainzplayer.ArtistRepository -import org.listenbrainz.android.repository.brainzplayer.ArtistRepositoryImpl +import org.listenbrainz.android.repository.brainzplayer.BPArtistRepository +import org.listenbrainz.android.repository.brainzplayer.BPArtistRepositoryImpl @Module @InstallIn(SingletonComponent::class) -abstract class ArtistRepositoryModule { +abstract class BPArtistRepositoryModule { @Binds - abstract fun bindsArtistRepository(repository: ArtistRepositoryImpl?) : ArtistRepository? + abstract fun bindsBPArtistRepository(repository: BPArtistRepositoryImpl?) : BPArtistRepository? } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/di/brainzplayer/ServiceModule.kt b/app/src/main/java/org/listenbrainz/android/di/brainzplayer/ServiceModule.kt index 446d946c..c3b65359 100644 --- a/app/src/main/java/org/listenbrainz/android/di/brainzplayer/ServiceModule.kt +++ b/app/src/main/java/org/listenbrainz/android/di/brainzplayer/ServiceModule.kt @@ -11,7 +11,7 @@ import dagger.hilt.InstallIn import dagger.hilt.android.components.ServiceComponent import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.scopes.ServiceScoped -import org.listenbrainz.android.repository.brainzplayer.AlbumRepository +import org.listenbrainz.android.repository.brainzplayer.BPAlbumRepository import org.listenbrainz.android.repository.brainzplayer.PlaylistRepository import org.listenbrainz.android.repository.brainzplayer.SongRepository import org.listenbrainz.android.util.LocalMusicSource @@ -41,9 +41,9 @@ object ServiceModule { @ServiceScoped @Provides fun providesMusicSource(songRepository: SongRepository, - albumRepository: AlbumRepository, - artistRepository: AlbumRepository, + BPAlbumRepository: BPAlbumRepository, + artistRepository: BPAlbumRepository, playlistRepository: PlaylistRepository ): MusicSource = - LocalMusicSource(songRepository, albumRepository, artistRepository,playlistRepository) + LocalMusicSource(songRepository, BPAlbumRepository, artistRepository,playlistRepository) } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt b/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt index 63947019..cef7d658 100644 --- a/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt +++ b/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt @@ -10,5 +10,6 @@ sealed class AppNavigationItem(val route: String, @DrawableRes val iconUnselecte object Feed : AppNavigationItem("feed", R.drawable.feed_unselected, R.drawable.feed_selected, "Feed") object Settings: AppNavigationItem("settings", R.drawable.ic_settings, R.drawable.ic_settings, "Settings") object About: AppNavigationItem("about", R.drawable.ic_info, R.drawable.ic_info, "About") + object Artist: AppNavigationItem("artist", R.drawable.ic_artist, R.drawable.ic_artist,"Artist") } diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/AlbumTagInfo.kt b/app/src/main/java/org/listenbrainz/android/model/artist/AlbumTagInfo.kt new file mode 100644 index 00000000..67140cea --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/AlbumTagInfo.kt @@ -0,0 +1,9 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class AlbumTagInfo( + val count: Int? = null, + @SerializedName("genre_mbid") val genreMbid: String? = null, + val tag: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/Area.kt b/app/src/main/java/org/listenbrainz/android/model/artist/Area.kt new file mode 100644 index 00000000..ffcc9296 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/Area.kt @@ -0,0 +1,13 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class Area( + val disambiguation: String? = null, + val id: String? = null, + @SerializedName("iso-3166-1-codes") val isoCodes: List? = null, + val name: String? = null, + @SerializedName("sort-name") val sortName: String? = null, + val type: String? = null, + @SerializedName("type-id") val typeId: String? = null +) diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/Artist.kt b/app/src/main/java/org/listenbrainz/android/model/artist/Artist.kt new file mode 100644 index 00000000..934d1202 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/Artist.kt @@ -0,0 +1,7 @@ +package org.listenbrainz.android.model.artist + +data class Artist( + val artist_credit_name: String? = null, + val artist_mbid: String? = null, + val join_phrase: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistBio.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistBio.kt new file mode 100644 index 00000000..51a032c0 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistBio.kt @@ -0,0 +1,12 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class ArtistBio( + val area: Area? = null, + val country: String? = null, + val id: String? = null, + @SerializedName("life-span") val lifeSpan: LifeSpan? = null, + val name: String? = null, + @SerializedName("sort-name") val sortName: String? = null, +) diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistPersonalInfo.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistPersonalInfo.kt new file mode 100644 index 00000000..44c1386a --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistPersonalInfo.kt @@ -0,0 +1,14 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class ArtistPersonalInfo( + val area: String? = null, + @SerializedName("artist_mbid") val artistMbid: String? = null, + @SerializedName("begin_year") val beginYear: Int? = null, + val gender: String? = null, + @SerializedName("join_phrase") val joinPhrase: String? = null, + val name: String? = null, + val rels: Rels? = null, + val type: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistTagsInfo.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistTagsInfo.kt new file mode 100644 index 00000000..483c099a --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistTagsInfo.kt @@ -0,0 +1,10 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class ArtistTagsInfo( + @SerializedName("artist_mbid") val artistMbid: String? = null, + val count: Int? = null, + @SerializedName("genre_mbid") val genreMbid: String? = null, + val tag: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistWikiExtract.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistWikiExtract.kt new file mode 100644 index 00000000..a556647b --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistWikiExtract.kt @@ -0,0 +1,5 @@ +package org.listenbrainz.android.model.artist + +data class ArtistWikiExtract( + val wikipediaExtract: WikipediaExtract? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/LifeSpan.kt b/app/src/main/java/org/listenbrainz/android/model/artist/LifeSpan.kt new file mode 100644 index 00000000..72d9586b --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/LifeSpan.kt @@ -0,0 +1,7 @@ +package org.listenbrainz.android.model.artist + +data class LifeSpan( + val begin: String? = null, + val end: Any? = null, + val ended: Boolean? = null +) diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/PopularAlbums.kt b/app/src/main/java/org/listenbrainz/android/model/artist/PopularAlbums.kt new file mode 100644 index 00000000..b0c3938d --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/PopularAlbums.kt @@ -0,0 +1,3 @@ +package org.listenbrainz.android.model.artist + +class PopularAlbums : ArrayList() \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/PopularAlbumsItem.kt b/app/src/main/java/org/listenbrainz/android/model/artist/PopularAlbumsItem.kt new file mode 100644 index 00000000..dc0708f4 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/PopularAlbumsItem.kt @@ -0,0 +1,14 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class PopularAlbumsItem( + val artist: TopAlbumArtistInfo? = TopAlbumArtistInfo(), + val release: Release? = Release(), + @SerializedName("release_color") val releaseColor: ReleaseColor? = ReleaseColor(), + @SerializedName("release_group") val releaseGroup: Release? = Release(), + @SerializedName("release_group_mbid") val releaseGroupMbid: String? = "", + val tag: Tag? = Tag(), + @SerializedName("total_listen_count") val totalListenCount: Int? = 0, + @SerializedName("total_user_count") val totalUserCount: Int? = 0 +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/PopularTracks.kt b/app/src/main/java/org/listenbrainz/android/model/artist/PopularTracks.kt new file mode 100644 index 00000000..3c3adcc9 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/PopularTracks.kt @@ -0,0 +1,3 @@ +package org.listenbrainz.android.model.artist + +class PopularTracks : ArrayList() \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/PopularTracksItem.kt b/app/src/main/java/org/listenbrainz/android/model/artist/PopularTracksItem.kt new file mode 100644 index 00000000..25e78760 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/PopularTracksItem.kt @@ -0,0 +1,19 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class PopularTracksItem( + @SerializedName("artist_mbids") val artistMbids: List? = null, + @SerializedName("artist_name") val artistName: String? = null, + val artists: List? = null, + @SerializedName("caa_id") val caaId: Long? = null, + @SerializedName("caa_release_mbid") val caaReleaseMbid: String? = null, + val length: Int? = null, + @SerializedName("recording_mbid") val recordingMbid: String? = null, + @SerializedName("recording_name") val recordingName: String? = null, + @SerializedName("release_color") val releaseColor: ReleaseColor? = null, + @SerializedName("release_mbid") val releaseMbid: String? = null, + @SerializedName("release_name") val releaseName: String? = null, + @SerializedName("total_listen_count") val totalListenCount: Int? = null, + @SerializedName("total_user_count") val totalUserCount: Int? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/Release.kt b/app/src/main/java/org/listenbrainz/android/model/artist/Release.kt new file mode 100644 index 00000000..192e839c --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/Release.kt @@ -0,0 +1,12 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class Release( + @SerializedName("caa_id") val caaId: Long? = null, + @SerializedName("caa_release_mbid") val caaReleaseMbid: String? = null, + val date: String? = null, + val name: String? = null, + val rels: List? = null, + val type: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ReleaseColor.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ReleaseColor.kt new file mode 100644 index 00000000..6864c7bf --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/ReleaseColor.kt @@ -0,0 +1,7 @@ +package org.listenbrainz.android.model.artist + +data class ReleaseColor( + val blue: Int? = null, + val green: Int? = null, + val red: Int? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/Rels.kt b/app/src/main/java/org/listenbrainz/android/model/artist/Rels.kt new file mode 100644 index 00000000..02151c0c --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/Rels.kt @@ -0,0 +1,15 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class Rels( + @SerializedName("free streaming") val freeStreaming: String? = null, + val lyrics: String? = null, + @SerializedName("official homepage") val officialHomepage: String? = null, + @SerializedName("purchase for download") val purchaseForDownload: String? = null, + @SerializedName("purchase for mail-order") val purchaseForMailOrder : String? = null, + @SerializedName("social network") val socialNetwork: String? = null, + val streaming: String? = null, + val wikidata: String? = null, + val youtube: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/Tag.kt b/app/src/main/java/org/listenbrainz/android/model/artist/Tag.kt new file mode 100644 index 00000000..74e8f629 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/Tag.kt @@ -0,0 +1,8 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class Tag( + val artist: List? = listOf(), + @SerializedName("release_group") val releaseGroup: List? = listOf() +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/TopAlbumArtistInfo.kt b/app/src/main/java/org/listenbrainz/android/model/artist/TopAlbumArtistInfo.kt new file mode 100644 index 00000000..ed5f6e9a --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/TopAlbumArtistInfo.kt @@ -0,0 +1,9 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class TopAlbumArtistInfo( + @SerializedName("artist_credit_id") val artistCreditId: Int? = 0, + val artists: List? = listOf(), + val name: String? = "" +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/WikipediaExtract.kt b/app/src/main/java/org/listenbrainz/android/model/artist/WikipediaExtract.kt new file mode 100644 index 00000000..c1da0a7d --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/WikipediaExtract.kt @@ -0,0 +1,9 @@ +package org.listenbrainz.android.model.artist + +data class WikipediaExtract( + val canonical: String? = null, + val content: String? = null, + val language: String? = null, + val title: String? = null, + val url: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepository.kt b/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepository.kt new file mode 100644 index 00000000..dd4cb356 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepository.kt @@ -0,0 +1,9 @@ +package org.listenbrainz.android.repository.artist + + +import org.listenbrainz.android.model.artist.ArtistBio +import org.listenbrainz.android.util.Resource + +interface ArtistRepository { + suspend fun fetchArtistBio(artistMbid: String?) : Resource +} diff --git a/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepositoryImpl.kt new file mode 100644 index 00000000..22702897 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepositoryImpl.kt @@ -0,0 +1,19 @@ +package org.listenbrainz.android.repository.artist + +import org.listenbrainz.android.model.ResponseError +import org.listenbrainz.android.model.artist.ArtistBio +import org.listenbrainz.android.service.ArtistService +import org.listenbrainz.android.service.MBService +import org.listenbrainz.android.util.Resource +import org.listenbrainz.android.util.Utils.parseResponse +import javax.inject.Inject + +class ArtistRepositoryImpl @Inject constructor( + private val service: ArtistService, + private val mbService: MBService, +) : ArtistRepository { + override suspend fun fetchArtistBio(artistMbid: String?): Resource = parseResponse { + if (artistMbid.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() + mbService.getArtistBio(artistMbid) + } +} diff --git a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/AlbumRepository.kt b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/BPAlbumRepository.kt similarity index 93% rename from app/src/main/java/org/listenbrainz/android/repository/brainzplayer/AlbumRepository.kt rename to app/src/main/java/org/listenbrainz/android/repository/brainzplayer/BPAlbumRepository.kt index 5ed7028f..07fa03ce 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/AlbumRepository.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/BPAlbumRepository.kt @@ -4,7 +4,7 @@ import kotlinx.coroutines.flow.Flow import org.listenbrainz.android.model.Album import org.listenbrainz.android.model.Song -interface AlbumRepository { +interface BPAlbumRepository { fun getAlbums() : Flow> fun getAlbum(albumId: Long) : Flow fun getAllSongsOfAlbum(albumId: Long): Flow> diff --git a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/AlbumRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/BPAlbumRepositoryImpl.kt similarity index 96% rename from app/src/main/java/org/listenbrainz/android/repository/brainzplayer/AlbumRepositoryImpl.kt rename to app/src/main/java/org/listenbrainz/android/repository/brainzplayer/BPAlbumRepositoryImpl.kt index b9d48833..5c796b72 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/AlbumRepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/BPAlbumRepositoryImpl.kt @@ -19,9 +19,9 @@ import org.listenbrainz.android.util.Transformer.toAlbumEntity import javax.inject.Inject -class AlbumRepositoryImpl @Inject constructor( +class BPAlbumRepositoryImpl @Inject constructor( private val albumDao: AlbumDao -): AlbumRepository { +): BPAlbumRepository { override fun getAlbums(): Flow> = albumDao.getAlbumEntities() .map { it -> diff --git a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/ArtistRepository.kt b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/BPArtistRepository.kt similarity index 94% rename from app/src/main/java/org/listenbrainz/android/repository/brainzplayer/ArtistRepository.kt rename to app/src/main/java/org/listenbrainz/android/repository/brainzplayer/BPArtistRepository.kt index 6fc2eaa8..55a8cf77 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/ArtistRepository.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/BPArtistRepository.kt @@ -5,7 +5,7 @@ import org.listenbrainz.android.model.Album import org.listenbrainz.android.model.Artist import org.listenbrainz.android.model.Song -interface ArtistRepository { +interface BPArtistRepository { fun getArtist(artistID: String) : Flow fun getArtists(): Flow> suspend fun addArtists(userRequestedRefresh: Boolean = false): Boolean diff --git a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/ArtistRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/BPArtistRepositoryImpl.kt similarity index 97% rename from app/src/main/java/org/listenbrainz/android/repository/brainzplayer/ArtistRepositoryImpl.kt rename to app/src/main/java/org/listenbrainz/android/repository/brainzplayer/BPArtistRepositoryImpl.kt index 3941e8be..541e6ae0 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/ArtistRepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/brainzplayer/BPArtistRepositoryImpl.kt @@ -20,9 +20,9 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class ArtistRepositoryImpl @Inject constructor( +class BPArtistRepositoryImpl @Inject constructor( private val artistDao: ArtistDao -) : ArtistRepository { +) : BPArtistRepository { override fun getArtist(artistID: String): Flow { val artist = artistDao.getArtistEntity(artistID) return artist.map { diff --git a/app/src/main/java/org/listenbrainz/android/service/ArtistService.kt b/app/src/main/java/org/listenbrainz/android/service/ArtistService.kt new file mode 100644 index 00000000..5a922454 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/service/ArtistService.kt @@ -0,0 +1,15 @@ +package org.listenbrainz.android.service + +import org.listenbrainz.android.model.artist.PopularAlbums +import org.listenbrainz.android.model.artist.PopularTracks +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Path + +interface ArtistService { + @GET("popularity/top-recordings-for-artist/{artist_mbid}") + suspend fun getPopularTracksOfArtist(@Path("artist_mbid") artistMbid: String?): Response + + @GET("popularity/top-release-groups-for-artist/{artist_mbid}") + suspend fun getPopularAlbumsOfArtist(@Path("artist_mbid") artistMbid: String?): Response +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/service/BrainzPlayerService.kt b/app/src/main/java/org/listenbrainz/android/service/BrainzPlayerService.kt index 5364f971..7e615dca 100644 --- a/app/src/main/java/org/listenbrainz/android/service/BrainzPlayerService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/BrainzPlayerService.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.first import org.listenbrainz.android.model.Playable import org.listenbrainz.android.model.PlayableType -import org.listenbrainz.android.repository.brainzplayer.AlbumRepository +import org.listenbrainz.android.repository.brainzplayer.BPAlbumRepository import org.listenbrainz.android.repository.preferences.AppPreferences import org.listenbrainz.android.repository.brainzplayer.PlaylistRepository import org.listenbrainz.android.repository.brainzplayer.SongRepository @@ -38,7 +38,7 @@ class BrainzPlayerService : MediaBrowserServiceCompat() { lateinit var localMusicSource: LocalMusicSource @Inject - lateinit var albumRepository: AlbumRepository + lateinit var BPAlbumRepository: BPAlbumRepository @Inject lateinit var songRepository: SongRepository diff --git a/app/src/main/java/org/listenbrainz/android/service/MBService.kt b/app/src/main/java/org/listenbrainz/android/service/MBService.kt new file mode 100644 index 00000000..f12b9996 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/service/MBService.kt @@ -0,0 +1,16 @@ +package org.listenbrainz.android.service + +import org.listenbrainz.android.model.artist.ArtistBio +import org.listenbrainz.android.model.artist.ArtistWikiExtract +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Path + +interface MBService { + @GET("ws/2/artist/{artist_mbid}?fmt=json") + suspend fun getArtistBio(@Path("artist_mbid") artistMbid: String?): Response + + @GET("artist/{artist_mbid}/wikipedia-extract") + suspend fun getArtistWikiExtract(@Path("artist_mbid") artistMbid: String?): Response + +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt index 434bf758..72888baf 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt @@ -3,6 +3,7 @@ package org.listenbrainz.android.ui.navigation import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.navigation.NavController import androidx.navigation.NavHostController @@ -12,6 +13,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import org.listenbrainz.android.model.AppNavigationItem +import org.listenbrainz.android.ui.screens.artist.ArtistScreen import org.listenbrainz.android.ui.screens.brainzplayer.BrainzPlayerScreen import org.listenbrainz.android.ui.screens.explore.ExploreScreen import org.listenbrainz.android.ui.screens.feed.FeedScreen @@ -25,6 +27,7 @@ fun AppNavigation( onScrollToTop: (suspend () -> Unit) -> Unit, snackbarState : SnackbarHostState, goToUserProfile: () -> Unit, + goToArtistPage: (String) -> Unit, ) { NavHost( navController = navController as NavHostController, @@ -64,11 +67,28 @@ fun AppNavigation( scrollRequestState = scrollRequestState, username = username, snackbarState = snackbarState, - goToUserProfile = goToUserProfile + goToUserProfile = goToUserProfile, + goToArtistPage = goToArtistPage ) } composable(route = AppNavigationItem.Settings.route){ SettingsScreen() } + composable(route = "${AppNavigationItem.Artist.route}/{mbid}", arguments = listOf( + navArgument("mbid"){ + type = NavType.StringType + } + )){ + val artistMbid = it.arguments?.getString("mbid") + if(artistMbid == null){ + LaunchedEffect(Unit) { + snackbarState.showSnackbar("The artist page can't be loaded") + } + } + else{ + ArtistScreen(artistMbid = artistMbid) + } + + } } } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/navigation/TopBar.kt b/app/src/main/java/org/listenbrainz/android/ui/navigation/TopBar.kt index 634d8db5..1aac418e 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/navigation/TopBar.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/navigation/TopBar.kt @@ -41,6 +41,7 @@ fun TopBar( "${AppNavigationItem.Profile.route}/{username}" -> AppNavigationItem.Profile.title AppNavigationItem.Settings.route -> AppNavigationItem.Settings.title AppNavigationItem.About.route -> AppNavigationItem.About.title + "${AppNavigationItem.Artist.route}/{mbid}" -> AppNavigationItem.Artist.title else -> "" } } ?: "ListenBrainz" diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt new file mode 100644 index 00000000..2b6458a8 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt @@ -0,0 +1,11 @@ +package org.listenbrainz.android.ui.screens.artist + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable + +@Composable +fun ArtistScreen( + artistMbid: String +) { + Text(text = artistMbid) +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistUIState.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistUIState.kt new file mode 100644 index 00000000..c4d26d52 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistUIState.kt @@ -0,0 +1,6 @@ +package org.listenbrainz.android.ui.screens.artist + +data class ArtistUIState( + val artistName: String? = null, + +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumScreen.kt index 0cd08efa..6b37396c 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumScreen.kt @@ -45,19 +45,19 @@ import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.forwardingPainter import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.util.BrainzPlayerExtensions.toSong -import org.listenbrainz.android.viewmodel.AlbumViewModel +import org.listenbrainz.android.viewmodel.BPAlbumViewModel import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel @OptIn(ExperimentalMaterialApi::class) @Composable fun AlbumScreen(navigateToAlbum: (id: Long) -> Unit) { - val albumViewModel = hiltViewModel() - val albums = albumViewModel.albums.collectAsState(listOf()) - val refreshing by albumViewModel.isRefreshing.collectAsState() + val BPAlbumViewModel = hiltViewModel() + val albums = BPAlbumViewModel.albums.collectAsState(listOf()) + val refreshing by BPAlbumViewModel.isRefreshing.collectAsState() val pullRefreshState = rememberPullRefreshState( refreshing = refreshing, - onRefresh = { albumViewModel.fetchAlbumsFromDevice(userRequestedRefresh = true) } + onRefresh = { BPAlbumViewModel.fetchAlbumsFromDevice(userRequestedRefresh = true) } ) // Content @@ -88,14 +88,14 @@ private fun AlbumsList( var albumCardMoreOptionsDropMenuExpanded by rememberSaveable { mutableStateOf(-1) } val currentlyPlayingSong = brainzPlayerViewModel.currentlyPlayingSong.collectAsState().value.toSong - val albumViewModel = hiltViewModel() + val BPAlbumViewModel = hiltViewModel() val currentSongIndex = - albumViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID == currentlyPlayingSong.mediaID } + BPAlbumViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID == currentlyPlayingSong.mediaID } ?.plus(1) LazyVerticalGrid(columns = GridCells.Fixed(2)) { items(albums.value) { - val albumSongs = albumViewModel.getAllSongsOfAlbum(it.albumId).collectAsState(listOf()).value + val albumSongs = BPAlbumViewModel.getAllSongsOfAlbum(it.albumId).collectAsState(listOf()).value Box(modifier = Modifier .padding(2.dp) .height(240.dp) @@ -114,13 +114,13 @@ private fun AlbumsList( text = { Text(text = "Play Next") }, onClick = { if (currentSongIndex != null) { - albumViewModel.appPreferences.currentPlayable?.songs?.toMutableList()?.addAll(currentSongIndex, albumSongs) + BPAlbumViewModel.appPreferences.currentPlayable?.songs?.toMutableList()?.addAll(currentSongIndex, albumSongs) } brainzPlayerViewModel.changePlayable( - albumViewModel.appPreferences.currentPlayable?.songs?.toMutableList() ?: mutableListOf(), + BPAlbumViewModel.appPreferences.currentPlayable?.songs?.toMutableList() ?: mutableListOf(), PlayableType.ALL_SONGS, - albumViewModel.appPreferences.currentPlayable?.id ?: 0, - albumViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID == currentlyPlayingSong.mediaID } + BPAlbumViewModel.appPreferences.currentPlayable?.id ?: 0, + BPAlbumViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID == currentlyPlayingSong.mediaID } ?: 0, brainzPlayerViewModel.songCurrentPosition.value ) brainzPlayerViewModel.queueChanged( @@ -132,18 +132,18 @@ private fun AlbumsList( DropdownMenuItem( text = { Text(text = "Add to queue") }, onClick = { - albumViewModel.appPreferences.currentPlayable?.songs?.size?.let { it1 -> - albumViewModel.appPreferences.currentPlayable?.songs?.toMutableList()?.addAll( + BPAlbumViewModel.appPreferences.currentPlayable?.songs?.size?.let { it1 -> + BPAlbumViewModel.appPreferences.currentPlayable?.songs?.toMutableList()?.addAll( it1, albumSongs ) } - albumViewModel.appPreferences.currentPlayable?.songs?.let { it1 -> + BPAlbumViewModel.appPreferences.currentPlayable?.songs?.let { it1 -> brainzPlayerViewModel.changePlayable( it1, PlayableType.ALL_SONGS, - albumViewModel.appPreferences.currentPlayable?.id ?: 0, - albumViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID == currentlyPlayingSong.mediaID } + BPAlbumViewModel.appPreferences.currentPlayable?.id ?: 0, + BPAlbumViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID == currentlyPlayingSong.mediaID } ?: 0, brainzPlayerViewModel.songCurrentPosition.value ) } @@ -219,16 +219,16 @@ private fun AlbumsList( @Composable fun OnAlbumClickScreen(albumID: Long) { - val albumViewModel = hiltViewModel() + val BPAlbumViewModel = hiltViewModel() val brainzPlayerViewModel = hiltViewModel() val selectedAlbum = - albumViewModel.getAlbumFromID(albumID).collectAsState(initial = Album()).value - val albumSongs = albumViewModel.getAllSongsOfAlbum(albumID).collectAsState(listOf()).value + BPAlbumViewModel.getAlbumFromID(albumID).collectAsState(initial = Album()).value + val albumSongs = BPAlbumViewModel.getAllSongsOfAlbum(albumID).collectAsState(listOf()).value var albumCardMoreOptionsDropMenuExpanded by rememberSaveable { mutableStateOf(-1) } val currentlyPlayingSong = brainzPlayerViewModel.currentlyPlayingSong.collectAsState().value.toSong val currentSongIndex = - albumViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID == currentlyPlayingSong.mediaID } + BPAlbumViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID == currentlyPlayingSong.mediaID } ?.plus(1) LazyColumn { item { @@ -318,15 +318,15 @@ fun OnAlbumClickScreen(albumID: Long) { text = { Text(text = "Play Next") }, onClick = { if (currentSongIndex != null) { - albumViewModel.appPreferences.currentPlayable?.songs?.toMutableList() + BPAlbumViewModel.appPreferences.currentPlayable?.songs?.toMutableList() ?.add(currentSongIndex, it) } - albumViewModel.appPreferences.currentPlayable?.songs?.let { it1 -> + BPAlbumViewModel.appPreferences.currentPlayable?.songs?.let { it1 -> brainzPlayerViewModel.changePlayable( it1, PlayableType.ALL_SONGS, - albumViewModel.appPreferences.currentPlayable?.id ?: 0, - albumViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID == currentlyPlayingSong.mediaID } + BPAlbumViewModel.appPreferences.currentPlayable?.id ?: 0, + BPAlbumViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID == currentlyPlayingSong.mediaID } ?: 0, brainzPlayerViewModel.songCurrentPosition.value ) } @@ -339,20 +339,20 @@ fun OnAlbumClickScreen(albumID: Long) { DropdownMenuItem( text = { Text(text = "Add to queue") }, onClick = { - albumViewModel.appPreferences.currentPlayable?.songs?.size?.let { it1 -> - albumViewModel.appPreferences.currentPlayable?.songs?.toMutableList() + BPAlbumViewModel.appPreferences.currentPlayable?.songs?.size?.let { it1 -> + BPAlbumViewModel.appPreferences.currentPlayable?.songs?.toMutableList() ?.add( it1, it ) } - albumViewModel.appPreferences.currentPlayable?.songs?.toMutableList() + BPAlbumViewModel.appPreferences.currentPlayable?.songs?.toMutableList() ?.let { it1 -> brainzPlayerViewModel.changePlayable( it1, PlayableType.ALL_SONGS, - albumViewModel.appPreferences.currentPlayable?.id ?: 0, - albumViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID == currentlyPlayingSong.mediaID } + BPAlbumViewModel.appPreferences.currentPlayable?.id ?: 0, + BPAlbumViewModel.appPreferences.currentPlayable?.songs?.indexOfFirst { song -> song.mediaID == currentlyPlayingSong.mediaID } ?: 0, brainzPlayerViewModel.songCurrentPosition.value ) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistScreen.kt index eac62aef..09585077 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistScreen.kt @@ -49,21 +49,21 @@ import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.forwardingPainter import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.util.BrainzPlayerExtensions.toSong -import org.listenbrainz.android.viewmodel.AlbumViewModel -import org.listenbrainz.android.viewmodel.ArtistViewModel +import org.listenbrainz.android.viewmodel.BPAlbumViewModel +import org.listenbrainz.android.viewmodel.BPArtistViewModel import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel @OptIn(ExperimentalMaterialApi::class) @Composable fun ArtistScreen(navigateToArtistScreen: (id: Long) -> Unit) { - val artistViewModel = hiltViewModel() - val artists = artistViewModel.artists.collectAsState(initial = listOf()) + val BPArtistViewModel = hiltViewModel() + val artists = BPArtistViewModel.artists.collectAsState(initial = listOf()) - val refreshing by artistViewModel.isRefreshing.collectAsState() + val refreshing by BPArtistViewModel.isRefreshing.collectAsState() val pullRefreshState = rememberPullRefreshState( refreshing = refreshing, - onRefresh = { artistViewModel.fetchArtistsFromDevice(userRequestedRefresh = true) } + onRefresh = { BPArtistViewModel.fetchArtistsFromDevice(userRequestedRefresh = true) } ) // Content @@ -207,13 +207,13 @@ private fun ArtistsScreen( @Composable fun OnArtistClickScreen(artistID: String, navigateToAlbum: (id: Long) -> Unit) { val brainzPlayerViewModel = hiltViewModel() - val artistViewModel = hiltViewModel() - val albumViewModel = hiltViewModel() - val artist = artistViewModel.getArtistByID(artistID).collectAsState(initial = Artist()).value + val BPArtistViewModel = hiltViewModel() + val BPAlbumViewModel = hiltViewModel() + val artist = BPArtistViewModel.getArtistByID(artistID).collectAsState(initial = Artist()).value val artistAlbums = - artistViewModel.getAllAlbumsOfArtist(artist).collectAsState(initial = listOf()).value.distinctBy { it.albumId } + BPArtistViewModel.getAllAlbumsOfArtist(artist).collectAsState(initial = listOf()).value.distinctBy { it.albumId } val artistSongs = - artistViewModel.getAllSongsOfArtist(artist).collectAsState(initial = listOf()).value.distinctBy { it.mediaID } + BPArtistViewModel.getAllSongsOfArtist(artist).collectAsState(initial = listOf()).value.distinctBy { it.mediaID } val currentlyPlayingSong = brainzPlayerViewModel.currentlyPlayingSong.collectAsState().value.toSong var artistCardMoreOptionsDropMenuExpanded by rememberSaveable { mutableStateOf(-1) } @@ -234,7 +234,7 @@ fun OnArtistClickScreen(artistID: String, navigateToAlbum: (id: Long) -> Unit) { item { LazyRow { items(items = artistAlbums) { - val albumSongs = albumViewModel.getAllSongsOfAlbum(it.albumId).collectAsState(listOf()).value + val albumSongs = BPAlbumViewModel.getAllSongsOfAlbum(it.albumId).collectAsState(listOf()).value Box( modifier = Modifier .height(240.dp) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt index af3b9a09..d0bef744 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerScreen.kt @@ -39,8 +39,8 @@ import org.listenbrainz.android.ui.screens.brainzplayer.overview.RecentPlaysScre import org.listenbrainz.android.ui.screens.brainzplayer.overview.SongsOverviewScreen import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.util.BrainzPlayerExtensions.toSong -import org.listenbrainz.android.viewmodel.AlbumViewModel -import org.listenbrainz.android.viewmodel.ArtistViewModel +import org.listenbrainz.android.viewmodel.BPAlbumViewModel +import org.listenbrainz.android.viewmodel.BPArtistViewModel import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel import org.listenbrainz.android.viewmodel.PlaylistViewModel import org.listenbrainz.android.viewmodel.SongViewModel @@ -49,16 +49,16 @@ import org.listenbrainz.android.viewmodel.SongViewModel @Composable fun BrainzPlayerScreen() { // View models - val albumViewModel = hiltViewModel() + val BPAlbumViewModel = hiltViewModel() val songsViewModel = hiltViewModel() - val artistViewModel = hiltViewModel() + val BPArtistViewModel = hiltViewModel() val playlistViewModel = hiltViewModel() val brainzPlayerViewModel = hiltViewModel() // Data streams - val albums = albumViewModel.albums.collectAsState(initial = listOf()).value // TODO: Introduce initial values to avoid flicker. + val albums = BPAlbumViewModel.albums.collectAsState(initial = listOf()).value // TODO: Introduce initial values to avoid flicker. val songs = songsViewModel.songs.collectAsState(initial = listOf()).value - val artists = artistViewModel.artists.collectAsState(initial = listOf()).value + val artists = BPArtistViewModel.artists.collectAsState(initial = listOf()).value val playlists by playlistViewModel.playlists.collectAsState(initial = listOf()) val songsPlayedToday = brainzPlayerViewModel.songsPlayedToday.collectAsState(initial = listOf()).value val recentlyPlayed = brainzPlayerViewModel.recentlyPlayed.collectAsState(initial = mutableListOf()).value @@ -68,7 +68,7 @@ fun BrainzPlayerScreen() { val albumSongsMap : MutableMap> = mutableMapOf() for(i in 1..albums.size){ - val albumSongs : List = albumViewModel.getAllSongsOfAlbum(albums[i-1].albumId).collectAsState( + val albumSongs : List = BPAlbumViewModel.getAllSongsOfAlbum(albums[i-1].albumId).collectAsState( initial = listOf() ).value albumSongsMap[albums[i-1]] = albumSongs diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt index 939c0e6c..91b609f5 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt @@ -202,6 +202,10 @@ class MainActivity : ComponentActivity() { snackbarState = snackbarState, goToUserProfile = { navController.navigate("${AppNavigationItem.Profile.route}/${username}") + }, + goToArtistPage = { + mbid -> + navController.navigate("artist/${mbid}") } ) } 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 9530cde7..4bfd0764 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 @@ -71,7 +71,8 @@ fun BaseProfileScreen( goToUserProfile: () -> Unit, feedViewModel: FeedViewModel = hiltViewModel(), listensViewModel: ListensViewModel = hiltViewModel(), - socialViewModel: SocialViewModel = hiltViewModel() + socialViewModel: SocialViewModel = hiltViewModel(), + goToArtistPage: (String) -> Unit, ){ val currentTab : MutableState = remember { mutableStateOf(ProfileScreenTab.LISTENS) } @@ -216,7 +217,8 @@ fun BaseProfileScreen( username = username, feedViewModel = feedViewModel, socialViewModel = socialViewModel, - viewModel = listensViewModel + viewModel = listensViewModel, + goToArtistPage = goToArtistPage ) ProfileScreenTab.STATS -> StatsScreen( username = username, @@ -239,7 +241,8 @@ fun BaseProfileScreen( username = username, feedViewModel = feedViewModel, socialViewModel = socialViewModel, - viewModel = listensViewModel + viewModel = listensViewModel, + goToArtistPage = goToArtistPage ) } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt index 12b885b7..6816082c 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt @@ -47,7 +47,8 @@ fun ProfileScreen( onScrollToTop: (suspend () -> Unit) -> Unit, username: String?, snackbarState: SnackbarHostState, - goToUserProfile: () -> Unit + goToUserProfile: () -> Unit, + goToArtistPage: (String) -> Unit, ) { val scrollState = rememberScrollState() val uiState = viewModel.uiState.collectAsState() @@ -76,7 +77,8 @@ fun ProfileScreen( onUnfollowClick = { viewModel.unfollowUser(it) }, - goToUserProfile = goToUserProfile + goToUserProfile = goToUserProfile, + goToArtistPage = goToArtistPage ) } 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 aff967b5..f366ed71 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,6 +6,7 @@ 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.Artist import org.listenbrainz.android.model.user.ListeningActivity import org.listenbrainz.android.model.user.TopAlbums import org.listenbrainz.android.model.user.TopArtists @@ -34,7 +35,7 @@ data class ListensTabUiState ( val followers: List>? = emptyList(), val following: List>? = emptyList(), val similarUsers: List? = emptyList(), - val similarArtists: List = emptyList(), + val similarArtists: List = emptyList(), val isFollowing: Boolean = false ) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt index 69a75256..3411cb49 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.ClickableText import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -62,6 +63,7 @@ import org.listenbrainz.android.model.SimilarUser import org.listenbrainz.android.model.SocialUiState import org.listenbrainz.android.model.TrackMetadata import org.listenbrainz.android.model.feed.ReviewEntityType +import org.listenbrainz.android.model.user.Artist import org.listenbrainz.android.ui.components.ErrorBar import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.SimilarUserCard @@ -96,6 +98,7 @@ fun ListensScreen( onScrollToTop: (suspend () -> Unit) -> Unit, snackbarState : SnackbarHostState, username: String?, + goToArtistPage: (String) -> Unit, ) { val uiState by userViewModel.uiState.collectAsState() @@ -164,7 +167,8 @@ fun ListensScreen( userViewModel.unfollowUser(it) } } - } + }, + goToArtistPage = goToArtistPage ) } @@ -208,6 +212,7 @@ fun ListensScreen( onReview: (type: ReviewEntityType, blurbContent: String, rating: Int?, locale: String, metadata: Metadata) -> Unit, onPersonallyRecommend: (metadata: Metadata, users: List, blurbContent: String) -> Unit, onFollowButtonClick: (String?, Boolean) -> Unit, + goToArtistPage: (String) -> Unit, ) { val listState = rememberLazyListState() @@ -335,7 +340,7 @@ fun ListensScreen( Spacer(modifier = Modifier.height(30.dp)) Text("Your Compatibility", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), modifier = Modifier.padding(start = 16.dp)) Spacer(modifier = Modifier.height(10.dp)) - CompatibilityCard(compatibility = uiState.listensTabUiState.compatibility ?: 0f, uiState.listensTabUiState.similarArtists) + CompatibilityCard(compatibility = uiState.listensTabUiState.compatibility ?: 0f, uiState.listensTabUiState.similarArtists, goToArtistPage = goToArtistPage) } } @@ -440,44 +445,81 @@ fun ListensScreen( } @Composable -private fun BuildSimilarArtists(similarArtists: List) { +private fun BuildSimilarArtists(similarArtists: List, onArtistClick: (String) -> Unit) { val white = Color.White when { similarArtists.size > 5 -> { val topSimilarArtists = similarArtists.take(5) - val artists = topSimilarArtists.joinToString(", ") val text = buildAnnotatedString { withStyle(style = SpanStyle(color = white)) { append("You both listen to ") } - withStyle(style = SpanStyle(color = lb_purple_night)) { - append(artists) + topSimilarArtists.forEachIndexed { index, artist -> + pushStringAnnotation(tag = "ARTIST", annotation = artist.artistMbid) + withStyle(style = SpanStyle(color = lb_purple_night)) { + append(artist.artistName) + } + pop() + if (index < topSimilarArtists.size - 1) { + append(", ") + } } withStyle(style = SpanStyle(color = white)) { append(" and more.") } } - Text(text = text, modifier = Modifier.padding(horizontal = 16.dp)) + ClickableText( + text = text, + modifier = Modifier.padding(horizontal = 16.dp), + onClick = { offset -> + text.getStringAnnotations(tag = "ARTIST", start = offset, end = offset) + .firstOrNull()?.let { annotation -> + onArtistClick(annotation.item) + } + } + ) } similarArtists.isEmpty() -> { - Text("You have no common artists", color = white, modifier = Modifier.padding(horizontal = 16.dp), style = MaterialTheme.typography.bodyMedium) + Text( + "You have no common artists", + color = white, + modifier = Modifier.padding(horizontal = 16.dp), + style = MaterialTheme.typography.bodyMedium + ) } else -> { - val artists = similarArtists.joinToString(", ") val text = buildAnnotatedString { withStyle(style = SpanStyle(color = white)) { append("You both listen to ") } - withStyle(style = SpanStyle(color = lb_purple_night)) { - append(artists) + similarArtists.forEachIndexed { index, artist -> + pushStringAnnotation(tag = "ARTIST", annotation = artist.artistMbid) + withStyle(style = SpanStyle(color = lb_purple_night)) { + append(artist.artistName) + } + pop() + if (index < similarArtists.size - 1) { + append(", ") + } } } - Text(text = text, modifier = Modifier.padding(horizontal = 16.dp), style = MaterialTheme.typography.bodyMedium) + ClickableText( + text = text, + modifier = Modifier.padding(horizontal = 16.dp), + style = MaterialTheme.typography.bodyMedium, + onClick = { offset -> + text.getStringAnnotations(tag = "ARTIST", start = offset, end = offset) + .firstOrNull()?.let { annotation -> + onArtistClick(annotation.item) + } + } + ) } } } + @Composable fun Dialogs( deactivateDialog: () -> Unit, @@ -609,7 +651,7 @@ private fun FollowersInformation(followersCount: Int?, followingCount: Int?){ } @Composable -fun CompatibilityCard(compatibility: Float, similarArtists: List){ +fun CompatibilityCard(compatibility: Float, similarArtists: List, goToArtistPage: (String) -> Unit){ Row (modifier = Modifier.padding(start = 16.dp)) { LinearProgressIndicator(progress = { compatibility @@ -620,7 +662,10 @@ fun CompatibilityCard(compatibility: Float, similarArtists: List){ Text("${(compatibility*100).toInt()} %", color = app_bg_mid, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) } Spacer(modifier = Modifier.height(10.dp)) - BuildSimilarArtists(similarArtists = similarArtists) + BuildSimilarArtists(similarArtists = similarArtists, onArtistClick = { + mbid -> + goToArtistPage(mbid) + }) } @Composable @@ -791,6 +836,7 @@ fun ListensScreenPreview() { dropdownItemIndex = remember { mutableStateOf(null) }, snackbarState = remember { SnackbarHostState() }, username = "pranavkonidena", - onFollowButtonClick = {_,_ -> } + onFollowButtonClick = {_,_ -> }, + goToArtistPage = {} ) } diff --git a/app/src/main/java/org/listenbrainz/android/util/LocalMusicSource.kt b/app/src/main/java/org/listenbrainz/android/util/LocalMusicSource.kt index bd9c28da..b07e4831 100644 --- a/app/src/main/java/org/listenbrainz/android/util/LocalMusicSource.kt +++ b/app/src/main/java/org/listenbrainz/android/util/LocalMusicSource.kt @@ -8,15 +8,15 @@ import com.google.android.exoplayer2.MediaItem import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.listenbrainz.android.model.State -import org.listenbrainz.android.repository.brainzplayer.AlbumRepository +import org.listenbrainz.android.repository.brainzplayer.BPAlbumRepository import org.listenbrainz.android.repository.brainzplayer.PlaylistRepository import org.listenbrainz.android.repository.brainzplayer.SongRepository import javax.inject.Inject class LocalMusicSource @Inject constructor( private val songRepository: SongRepository, - private val albumRepository: AlbumRepository, - private val artistRepository: AlbumRepository, + private val BPAlbumRepository: BPAlbumRepository, + private val artistRepository: BPAlbumRepository, private val playlistRepository: PlaylistRepository ) : MusicSource { diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/ArtistViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/ArtistViewModel.kt index 3fc6310f..e69de29b 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ArtistViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ArtistViewModel.kt @@ -1,60 +0,0 @@ -package org.listenbrainz.android.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import org.listenbrainz.android.model.Album -import org.listenbrainz.android.model.Artist -import org.listenbrainz.android.model.Song -import org.listenbrainz.android.repository.preferences.AppPreferences -import org.listenbrainz.android.repository.brainzplayer.ArtistRepository -import javax.inject.Inject - -@HiltViewModel -class ArtistViewModel @Inject constructor( - private val artistRepository: ArtistRepository, - private val appPreferences: AppPreferences -) : ViewModel() { - val artists = artistRepository.getArtists() - - // Refreshing variables. - private val _isRefreshing = MutableStateFlow(false) - val isRefreshing = _isRefreshing.asStateFlow() - - init { - fetchArtistsFromDevice() - } - - fun fetchArtistsFromDevice(userRequestedRefresh: Boolean = false){ - viewModelScope.launch(Dispatchers.IO) { - if (userRequestedRefresh){ - _isRefreshing.update { true } - appPreferences.albumsOnDevice = artistRepository.addArtists(userRequestedRefresh = true) - _isRefreshing.update { false } - } else { - artists.collectLatest { - if (it.isEmpty()) { - _isRefreshing.update { true } - appPreferences.albumsOnDevice = artistRepository.addArtists() - _isRefreshing.update { false } - } - } - } - } - } - - fun getArtistByID(artistID: String): Flow { - return artistRepository.getArtist(artistID) - } - - fun getAllSongsOfArtist(artist: Artist): Flow> { - return flowOf(artist.songs) - } - - fun getAllAlbumsOfArtist(artist: Artist): Flow> { - return flowOf(artist.albums) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/AlbumViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/BPAlbumViewModel.kt similarity index 65% rename from app/src/main/java/org/listenbrainz/android/viewmodel/AlbumViewModel.kt rename to app/src/main/java/org/listenbrainz/android/viewmodel/BPAlbumViewModel.kt index f9f7bd24..c55b65d0 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/AlbumViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/BPAlbumViewModel.kt @@ -4,20 +4,24 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.listenbrainz.android.model.Album import org.listenbrainz.android.model.Song -import org.listenbrainz.android.repository.brainzplayer.AlbumRepository +import org.listenbrainz.android.repository.brainzplayer.BPAlbumRepository import org.listenbrainz.android.repository.preferences.AppPreferences import javax.inject.Inject @HiltViewModel -class AlbumViewModel @Inject constructor( - val albumRepository: AlbumRepository, +class BPAlbumViewModel @Inject constructor( + val BPAlbumRepository: BPAlbumRepository, val appPreferences: AppPreferences ) : ViewModel() { - val albums = albumRepository.getAlbums() + val albums = BPAlbumRepository.getAlbums() // Refreshing variables. private val _isRefreshing = MutableStateFlow(false) @@ -31,13 +35,13 @@ class AlbumViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { if (userRequestedRefresh){ _isRefreshing.update { true } - appPreferences.albumsOnDevice = albumRepository.addAlbums(userRequestedRefresh = true) + appPreferences.albumsOnDevice = BPAlbumRepository.addAlbums(userRequestedRefresh = true) _isRefreshing.update { false } } else { albums.collectLatest { if (it.isEmpty()) { _isRefreshing.update { true } - appPreferences.albumsOnDevice = albumRepository.addAlbums() + appPreferences.albumsOnDevice = BPAlbumRepository.addAlbums() _isRefreshing.update { false } } } @@ -46,9 +50,9 @@ class AlbumViewModel @Inject constructor( } fun getAlbumFromID(albumID: Long): Flow { - return albumRepository.getAlbum(albumID) + return BPAlbumRepository.getAlbum(albumID) } fun getAllSongsOfAlbum(albumID: Long): Flow>{ - return albumRepository.getAllSongsOfAlbum(albumID) + return BPAlbumRepository.getAllSongsOfAlbum(albumID) } } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/BPArtistViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/BPArtistViewModel.kt new file mode 100644 index 00000000..eb0353f0 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/BPArtistViewModel.kt @@ -0,0 +1,65 @@ +package org.listenbrainz.android.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.listenbrainz.android.model.Album +import org.listenbrainz.android.model.Artist +import org.listenbrainz.android.model.Song +import org.listenbrainz.android.repository.brainzplayer.BPArtistRepositoryImpl +import org.listenbrainz.android.repository.preferences.AppPreferences +import javax.inject.Inject + +@HiltViewModel +class BPArtistViewModel @Inject constructor( + private val bpArtistRepository: BPArtistRepositoryImpl, + private val appPreferences: AppPreferences +) : ViewModel() { + val artists = bpArtistRepository.getArtists() + + // Refreshing variables. + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing = _isRefreshing.asStateFlow() + + init { + fetchArtistsFromDevice() + } + + fun fetchArtistsFromDevice(userRequestedRefresh: Boolean = false){ + viewModelScope.launch(Dispatchers.IO) { + if (userRequestedRefresh){ + _isRefreshing.update { true } + appPreferences.albumsOnDevice = bpArtistRepository.addArtists(userRequestedRefresh = true) + _isRefreshing.update { false } + } else { + artists.collectLatest { + if (it.isEmpty()) { + _isRefreshing.update { true } + appPreferences.albumsOnDevice = bpArtistRepository.addArtists() + _isRefreshing.update { false } + } + } + } + } + } + + fun getArtistByID(artistID: String): Flow { + return bpArtistRepository.getArtist(artistID) + } + + fun getAllSongsOfArtist(artist: Artist): Flow> { + return flowOf(artist.songs) + } + + fun getAllAlbumsOfArtist(artist: Artist): Flow> { + return flowOf(artist.albums) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/UserViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/UserViewModel.kt index 85f0f8df..8f1a58ea 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/UserViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/UserViewModel.kt @@ -55,17 +55,17 @@ class UserViewModel @Inject constructor( } } - private suspend fun getSimilarArtists(username: String?) : List { + private suspend fun getSimilarArtists(username: String?) : List { val currentUsername = appPreferences.username.get() val currentUserTopArtists = userRepository.getTopArtists(currentUsername, count = 100) val userTopArtists = userRepository.getTopArtists(username, count = 100) - val similarArtists = mutableListOf() + val similarArtists = mutableListOf() currentUserTopArtists.data?.payload?.artists?.map { currentUserTopArtist -> userTopArtists.data?.payload?.artists?.map{ userTopArtist -> if(currentUserTopArtist.artistName == userTopArtist.artistName){ - similarArtists.add(currentUserTopArtist.artistName) + similarArtists.add(currentUserTopArtist) } } } From 622ebd139de2a98c31e5ac3f0189796342336ac3 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sat, 17 Aug 2024 12:27:04 +0530 Subject: [PATCH 81/97] Set up artist VM, Repository and DI for interaction with remote --- .../listenbrainz/android/di/ServiceModule.kt | 21 +++++-- .../android/model/artist/AlbumTagInfo.kt | 9 --- .../listenbrainz/android/model/artist/Area.kt | 13 ---- .../android/model/artist/Artist.kt | 19 +++++- .../android/model/artist/ArtistBio.kt | 12 ---- .../android/model/artist/ArtistReview.kt | 11 ++++ .../artist/{ArtistTagsInfo.kt => ArtistX.kt} | 2 +- .../android/model/artist/ArtistXX.kt | 9 +++ .../android/model/artist/AverageRating.kt | 6 ++ .../android/model/artist/LastRevision.kt | 11 ++++ .../android/model/artist/LifeSpan.kt | 7 --- .../android/model/artist/Listeners.kt | 8 +++ .../android/model/artist/ListeningStats.kt | 15 +++++ .../android/model/artist/PopularAlbums.kt | 3 - .../android/model/artist/PopularAlbumsItem.kt | 14 ----- ...pularTracksItem.kt => PopularRecording.kt} | 28 ++++----- .../android/model/artist/PopularTracks.kt | 3 - .../android/model/artist/Release.kt | 12 ---- .../android/model/artist/ReleaseGroup.kt | 16 +++++ .../listenbrainz/android/model/artist/Rels.kt | 4 -- .../android/model/artist/Review.kt | 28 +++++++++ ...ArtistPersonalInfo.kt => SimilarArtist.kt} | 9 ++- .../listenbrainz/android/model/artist/Tag.kt | 5 +- .../model/artist/TopAlbumArtistInfo.kt | 9 --- .../listenbrainz/android/model/artist/User.kt | 13 ++++ .../repository/artist/ArtistRepository.kt | 8 ++- .../repository/artist/ArtistRepositoryImpl.kt | 22 +++++-- .../android/service/ArtistService.kt | 12 ++-- .../listenbrainz/android/service/CBService.kt | 12 ++++ .../listenbrainz/android/service/MBService.kt | 5 -- .../ui/screens/artist/ArtistUIState.kt | 27 ++++++++- .../listenbrainz/android/util/Constants.kt | 1 + .../android/viewmodel/ArtistViewModel.kt | 60 +++++++++++++++++++ 33 files changed, 296 insertions(+), 138 deletions(-) delete mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/AlbumTagInfo.kt delete mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/Area.kt delete mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/ArtistBio.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/ArtistReview.kt rename app/src/main/java/org/listenbrainz/android/model/artist/{ArtistTagsInfo.kt => ArtistX.kt} (91%) create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/ArtistXX.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/AverageRating.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/LastRevision.kt delete mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/LifeSpan.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/Listeners.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/ListeningStats.kt delete mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/PopularAlbums.kt delete mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/PopularAlbumsItem.kt rename app/src/main/java/org/listenbrainz/android/model/artist/{PopularTracksItem.kt => PopularRecording.kt} (55%) delete mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/PopularTracks.kt delete mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/Release.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/ReleaseGroup.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/Review.kt rename app/src/main/java/org/listenbrainz/android/model/artist/{ArtistPersonalInfo.kt => SimilarArtist.kt} (54%) delete mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/TopAlbumArtistInfo.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/User.kt create mode 100644 app/src/main/java/org/listenbrainz/android/service/CBService.kt diff --git a/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt b/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt index d627f8f8..2c19c8a9 100644 --- a/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt +++ b/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt @@ -16,6 +16,7 @@ import org.listenbrainz.android.model.yimdata.YimData import org.listenbrainz.android.repository.preferences.AppPreferences import org.listenbrainz.android.service.ArtistService import org.listenbrainz.android.service.BlogService +import org.listenbrainz.android.service.CBService import org.listenbrainz.android.service.FeedService import org.listenbrainz.android.service.ListensService import org.listenbrainz.android.service.MBService @@ -24,6 +25,8 @@ import org.listenbrainz.android.service.UserService import org.listenbrainz.android.service.Yim23Service import org.listenbrainz.android.service.YimService import org.listenbrainz.android.service.YouTubeApiService +import org.listenbrainz.android.util.Constants.CB_BASE_URL +import org.listenbrainz.android.util.Constants.LB_BASE_URL import org.listenbrainz.android.util.Constants.LISTENBRAINZ_API_BASE_URL import org.listenbrainz.android.util.Constants.LISTENBRAINZ_BETA_API_BASE_URL import org.listenbrainz.android.util.Constants.MB_BASE_URL @@ -90,18 +93,28 @@ class ServiceModule { @Singleton @Provides - fun providesArtistService(appPreferences: AppPreferences): ArtistService = - constructRetrofit(appPreferences) - .create(ArtistService::class.java) + fun providesArtistService(): ArtistService = Retrofit.Builder() + .baseUrl(LB_BASE_URL) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build().create(ArtistService::class.java) @Singleton @Provides - fun providesMBService(appPreferences: AppPreferences): MBService = Retrofit.Builder() + fun providesMBService(): MBService = Retrofit.Builder() .baseUrl(MB_BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build().create(MBService::class.java) + @Singleton + @Provides + fun providesCBService(): CBService = Retrofit.Builder() + .baseUrl(CB_BASE_URL) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build().create(CBService::class.java) + @Singleton @Provides fun providesYoutubeApiService(@ApplicationContext context: Context): YouTubeApiService = diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/AlbumTagInfo.kt b/app/src/main/java/org/listenbrainz/android/model/artist/AlbumTagInfo.kt deleted file mode 100644 index 67140cea..00000000 --- a/app/src/main/java/org/listenbrainz/android/model/artist/AlbumTagInfo.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.listenbrainz.android.model.artist - -import com.google.gson.annotations.SerializedName - -data class AlbumTagInfo( - val count: Int? = null, - @SerializedName("genre_mbid") val genreMbid: String? = null, - val tag: String? = null -) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/Area.kt b/app/src/main/java/org/listenbrainz/android/model/artist/Area.kt deleted file mode 100644 index ffcc9296..00000000 --- a/app/src/main/java/org/listenbrainz/android/model/artist/Area.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.listenbrainz.android.model.artist - -import com.google.gson.annotations.SerializedName - -data class Area( - val disambiguation: String? = null, - val id: String? = null, - @SerializedName("iso-3166-1-codes") val isoCodes: List? = null, - val name: String? = null, - @SerializedName("sort-name") val sortName: String? = null, - val type: String? = null, - @SerializedName("type-id") val typeId: String? = null -) diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/Artist.kt b/app/src/main/java/org/listenbrainz/android/model/artist/Artist.kt index 934d1202..dc1bda5a 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/Artist.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/Artist.kt @@ -1,7 +1,20 @@ package org.listenbrainz.android.model.artist +import com.google.gson.annotations.SerializedName + data class Artist( - val artist_credit_name: String? = null, - val artist_mbid: String? = null, - val join_phrase: String? = null + val area: String? = "", + @SerializedName("artist_mbid") val artistMbid: String? = "", + @SerializedName("begin_year") val beginYear: Int? = 0, + val gender: String? = "", + val mbid: String? = "", + val name: String? = "", + val rels: Rels? = Rels(), + val tag: Tag? = Tag(), + val type: String? = "", + val coverArt: String? = "", + val listeningStats: ListeningStats? = null, + val popularRecordings: List = listOf(), + val releaseGroups: List = listOf(), + val similarArtists: List = listOf() ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistBio.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistBio.kt deleted file mode 100644 index 51a032c0..00000000 --- a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistBio.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.listenbrainz.android.model.artist - -import com.google.gson.annotations.SerializedName - -data class ArtistBio( - val area: Area? = null, - val country: String? = null, - val id: String? = null, - @SerializedName("life-span") val lifeSpan: LifeSpan? = null, - val name: String? = null, - @SerializedName("sort-name") val sortName: String? = null, -) diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistReview.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistReview.kt new file mode 100644 index 00000000..06048d13 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistReview.kt @@ -0,0 +1,11 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class ArtistReview( + @SerializedName("average_rating") val averageRating: AverageRating? = null, + val count: Int? = null, + val limit: Int? = null, + val offset: Int? = null, + val reviews: List? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistTagsInfo.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistX.kt similarity index 91% rename from app/src/main/java/org/listenbrainz/android/model/artist/ArtistTagsInfo.kt rename to app/src/main/java/org/listenbrainz/android/model/artist/ArtistX.kt index 483c099a..0097d035 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistTagsInfo.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistX.kt @@ -2,7 +2,7 @@ package org.listenbrainz.android.model.artist import com.google.gson.annotations.SerializedName -data class ArtistTagsInfo( +data class ArtistX( @SerializedName("artist_mbid") val artistMbid: String? = null, val count: Int? = null, @SerializedName("genre_mbid") val genreMbid: String? = null, diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistXX.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistXX.kt new file mode 100644 index 00000000..285b3191 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistXX.kt @@ -0,0 +1,9 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class ArtistXX( + @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/artist/AverageRating.kt b/app/src/main/java/org/listenbrainz/android/model/artist/AverageRating.kt new file mode 100644 index 00000000..e277d272 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/AverageRating.kt @@ -0,0 +1,6 @@ +package org.listenbrainz.android.model.artist + +data class AverageRating( + val count: Int? = null, + val rating: Double? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/LastRevision.kt b/app/src/main/java/org/listenbrainz/android/model/artist/LastRevision.kt new file mode 100644 index 00000000..b0f3dede --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/LastRevision.kt @@ -0,0 +1,11 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class LastRevision( + val id: Int? = null, + val rating: Int? = null, + @SerializedName("review_id") val reviewId: String? = null, + val text: String? = null, + val timestamp: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/LifeSpan.kt b/app/src/main/java/org/listenbrainz/android/model/artist/LifeSpan.kt deleted file mode 100644 index 72d9586b..00000000 --- a/app/src/main/java/org/listenbrainz/android/model/artist/LifeSpan.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.listenbrainz.android.model.artist - -data class LifeSpan( - val begin: String? = null, - val end: Any? = null, - val ended: Boolean? = null -) diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/Listeners.kt b/app/src/main/java/org/listenbrainz/android/model/artist/Listeners.kt new file mode 100644 index 00000000..0cab9d15 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/Listeners.kt @@ -0,0 +1,8 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class Listeners( + @SerializedName("listen_count") val listenCount: Int? = null, + @SerializedName("user_name") val userName: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ListeningStats.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ListeningStats.kt new file mode 100644 index 00000000..90233821 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/ListeningStats.kt @@ -0,0 +1,15 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class ListeningStats( + @SerializedName("artist_mbid") val artistMbid: String? = null, + @SerializedName("artist_name") val artistName: String? = null, + @SerializedName("from_ts") val fromTs: Int? = null, + @SerializedName("last_updated") val lastUpdated: Int? = null, + val listeners: List? = null, + @SerializedName("stats_range") val statsRange: String? = null, + @SerializedName("to_ts") val toTs: Int? = null, + @SerializedName("total_listen_count") val totalListenCount: Int? = null, + @SerializedName("total_user_count") val totalUserCount: Int? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/PopularAlbums.kt b/app/src/main/java/org/listenbrainz/android/model/artist/PopularAlbums.kt deleted file mode 100644 index b0c3938d..00000000 --- a/app/src/main/java/org/listenbrainz/android/model/artist/PopularAlbums.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.listenbrainz.android.model.artist - -class PopularAlbums : ArrayList() \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/PopularAlbumsItem.kt b/app/src/main/java/org/listenbrainz/android/model/artist/PopularAlbumsItem.kt deleted file mode 100644 index dc0708f4..00000000 --- a/app/src/main/java/org/listenbrainz/android/model/artist/PopularAlbumsItem.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.listenbrainz.android.model.artist - -import com.google.gson.annotations.SerializedName - -data class PopularAlbumsItem( - val artist: TopAlbumArtistInfo? = TopAlbumArtistInfo(), - val release: Release? = Release(), - @SerializedName("release_color") val releaseColor: ReleaseColor? = ReleaseColor(), - @SerializedName("release_group") val releaseGroup: Release? = Release(), - @SerializedName("release_group_mbid") val releaseGroupMbid: String? = "", - val tag: Tag? = Tag(), - @SerializedName("total_listen_count") val totalListenCount: Int? = 0, - @SerializedName("total_user_count") val totalUserCount: Int? = 0 -) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/PopularTracksItem.kt b/app/src/main/java/org/listenbrainz/android/model/artist/PopularRecording.kt similarity index 55% rename from app/src/main/java/org/listenbrainz/android/model/artist/PopularTracksItem.kt rename to app/src/main/java/org/listenbrainz/android/model/artist/PopularRecording.kt index 25e78760..adfdf0fa 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/PopularTracksItem.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/PopularRecording.kt @@ -2,18 +2,18 @@ package org.listenbrainz.android.model.artist import com.google.gson.annotations.SerializedName -data class PopularTracksItem( - @SerializedName("artist_mbids") val artistMbids: List? = null, - @SerializedName("artist_name") val artistName: String? = null, - val artists: List? = null, - @SerializedName("caa_id") val caaId: Long? = null, - @SerializedName("caa_release_mbid") val caaReleaseMbid: String? = null, - val length: Int? = null, - @SerializedName("recording_mbid") val recordingMbid: String? = null, - @SerializedName("recording_name") val recordingName: String? = null, - @SerializedName("release_color") val releaseColor: ReleaseColor? = null, - @SerializedName("release_mbid") val releaseMbid: String? = null, - @SerializedName("release_name") val releaseName: String? = null, - @SerializedName("total_listen_count") val totalListenCount: Int? = null, - @SerializedName("total_user_count") val totalUserCount: Int? = null +data class PopularRecording( + @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? = "", + val length: Int? = 0, + @SerializedName("recording_mbid") val recordingMbid: String? = "", + @SerializedName("recording_name") val recordingName: String? = "", + @SerializedName("release_color") val releaseColor: ReleaseColor? = ReleaseColor(), + @SerializedName("release_mbid") val releaseMbid: String? = "", + @SerializedName("release_name") val releaseName: String? = "", + @SerializedName("total_listen_count") val totalListenCount: Int? = 0, + @SerializedName("total_user_count") val totalUserCount: Int? = 0 ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/PopularTracks.kt b/app/src/main/java/org/listenbrainz/android/model/artist/PopularTracks.kt deleted file mode 100644 index 3c3adcc9..00000000 --- a/app/src/main/java/org/listenbrainz/android/model/artist/PopularTracks.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.listenbrainz.android.model.artist - -class PopularTracks : ArrayList() \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/Release.kt b/app/src/main/java/org/listenbrainz/android/model/artist/Release.kt deleted file mode 100644 index 192e839c..00000000 --- a/app/src/main/java/org/listenbrainz/android/model/artist/Release.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.listenbrainz.android.model.artist - -import com.google.gson.annotations.SerializedName - -data class Release( - @SerializedName("caa_id") val caaId: Long? = null, - @SerializedName("caa_release_mbid") val caaReleaseMbid: String? = null, - val date: String? = null, - val name: String? = null, - val rels: List? = null, - val type: String? = null -) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ReleaseGroup.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ReleaseGroup.kt new file mode 100644 index 00000000..c8f98ea6 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/ReleaseGroup.kt @@ -0,0 +1,16 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class ReleaseGroup( + @SerializedName("artist_credit_name") val artistCreditName: String? = "", + val artists: List? = listOf(), + @SerializedName("caa_id") val caaId: Long? = 0, + @SerializedName("caa_release_mbid") val caaReleaseMbid: String? = "", + val date: String? = "", + val mbid: String? = "", + val name: String? = "", + @SerializedName("total_listen_count") val totalListenCount: Int? = 0, + @SerializedName("total_user_count") val totalUserCount: Int? = 0, + val type: String? = "" +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/Rels.kt b/app/src/main/java/org/listenbrainz/android/model/artist/Rels.kt index 02151c0c..bff2642f 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/Rels.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/Rels.kt @@ -4,12 +4,8 @@ import com.google.gson.annotations.SerializedName data class Rels( @SerializedName("free streaming") val freeStreaming: String? = null, - val lyrics: String? = null, - @SerializedName("official homepage") val officialHomepage: String? = null, @SerializedName("purchase for download") val purchaseForDownload: String? = null, - @SerializedName("purchase for mail-order") val purchaseForMailOrder : String? = null, @SerializedName("social network") val socialNetwork: String? = null, - val streaming: String? = null, val wikidata: String? = null, val youtube: String? = null ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/Review.kt b/app/src/main/java/org/listenbrainz/android/model/artist/Review.kt new file mode 100644 index 00000000..0c54825e --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/Review.kt @@ -0,0 +1,28 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class Review( + val created: String? = null, + val edits: Int? = null, + @SerializedName("entity_id") val entityId: String? = null, + @SerializedName("entity_type") val entityType: String? = null, + @SerializedName("full_name") val fullName: String? = null, + val id: String? = null, + @SerializedName("info_url") val infoUrl: String? = null, + @SerializedName("is_draft") val isDraft: Boolean? = null, + @SerializedName("is_hidden") val isHidden: Boolean? = null, + val language: String? = null, + @SerializedName("last_revision") val lastRevision: LastRevision? = null, + @SerializedName("last_updated") val lastUpdated: String? = null, + @SerializedName("license_id") val licenseId: String? = null, + val popularity: Int? = null, + @SerializedName("published_on") val publishedOn: String? = null, + val rating: Int? = null, + val source: Any? = null, + @SerializedName("source_url") val sourceUrl: Any? = null, + val text: String? = null, + val user: User? = null, + @SerializedName("votes_negative_count") val votesNegativeCount: Int? = null, + @SerializedName("votes_positive_count") val votesPositiveCount: Int? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistPersonalInfo.kt b/app/src/main/java/org/listenbrainz/android/model/artist/SimilarArtist.kt similarity index 54% rename from app/src/main/java/org/listenbrainz/android/model/artist/ArtistPersonalInfo.kt rename to app/src/main/java/org/listenbrainz/android/model/artist/SimilarArtist.kt index 44c1386a..b70b00a3 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistPersonalInfo.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/SimilarArtist.kt @@ -2,13 +2,12 @@ package org.listenbrainz.android.model.artist import com.google.gson.annotations.SerializedName -data class ArtistPersonalInfo( - val area: String? = null, +data class SimilarArtist( @SerializedName("artist_mbid") val artistMbid: String? = null, - @SerializedName("begin_year") val beginYear: Int? = null, + val comment: String? = null, val gender: String? = null, - @SerializedName("join_phrase") val joinPhrase: String? = null, val name: String? = null, - val rels: Rels? = null, + @SerializedName("reference_mbid") val referenceMbid: String? = null, + val score: Int? = null, val type: String? = null ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/Tag.kt b/app/src/main/java/org/listenbrainz/android/model/artist/Tag.kt index 74e8f629..670118fa 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/Tag.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/Tag.kt @@ -1,8 +1,5 @@ package org.listenbrainz.android.model.artist -import com.google.gson.annotations.SerializedName - data class Tag( - val artist: List? = listOf(), - @SerializedName("release_group") val releaseGroup: List? = listOf() + val artist: List? = listOf() ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/TopAlbumArtistInfo.kt b/app/src/main/java/org/listenbrainz/android/model/artist/TopAlbumArtistInfo.kt deleted file mode 100644 index ed5f6e9a..00000000 --- a/app/src/main/java/org/listenbrainz/android/model/artist/TopAlbumArtistInfo.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.listenbrainz.android.model.artist - -import com.google.gson.annotations.SerializedName - -data class TopAlbumArtistInfo( - @SerializedName("artist_credit_id") val artistCreditId: Int? = 0, - val artists: List? = listOf(), - val name: String? = "" -) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/User.kt b/app/src/main/java/org/listenbrainz/android/model/artist/User.kt new file mode 100644 index 00000000..68bf74ca --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/User.kt @@ -0,0 +1,13 @@ +package org.listenbrainz.android.model.artist + +import com.google.gson.annotations.SerializedName + +data class User( + val created: String? = null, + @SerializedName("display_name") val displayName: String? = null, + val id: String? = null, + val karma: Int? = null, + @SerializedName("musicbrainz_username") val musicbrainzUsername: String? = null, + @SerializedName("user_ref") val userRef: String? = null, + @SerializedName("user_type") val userType: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepository.kt b/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepository.kt index dd4cb356..56e00bad 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepository.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepository.kt @@ -1,9 +1,13 @@ package org.listenbrainz.android.repository.artist -import org.listenbrainz.android.model.artist.ArtistBio +import org.listenbrainz.android.model.artist.Artist +import org.listenbrainz.android.model.artist.ArtistReview +import org.listenbrainz.android.model.artist.ArtistWikiExtract import org.listenbrainz.android.util.Resource interface ArtistRepository { - suspend fun fetchArtistBio(artistMbid: String?) : Resource + suspend fun fetchArtistData(artistMbid: String?): Resource + suspend fun fetchArtistWikiExtract(artistMbid: String?): Resource + suspend fun fetchArtistReviews(artistMbid: String?): Resource } diff --git a/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepositoryImpl.kt index 22702897..1e3ed637 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepositoryImpl.kt @@ -1,8 +1,11 @@ package org.listenbrainz.android.repository.artist import org.listenbrainz.android.model.ResponseError -import org.listenbrainz.android.model.artist.ArtistBio +import org.listenbrainz.android.model.artist.Artist +import org.listenbrainz.android.model.artist.ArtistReview +import org.listenbrainz.android.model.artist.ArtistWikiExtract import org.listenbrainz.android.service.ArtistService +import org.listenbrainz.android.service.CBService import org.listenbrainz.android.service.MBService import org.listenbrainz.android.util.Resource import org.listenbrainz.android.util.Utils.parseResponse @@ -11,9 +14,20 @@ import javax.inject.Inject class ArtistRepositoryImpl @Inject constructor( private val service: ArtistService, private val mbService: MBService, + private val cbService: CBService, ) : ArtistRepository { - override suspend fun fetchArtistBio(artistMbid: String?): Resource = parseResponse { - if (artistMbid.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() - mbService.getArtistBio(artistMbid) + override suspend fun fetchArtistData(artistMbid: String?): Resource = parseResponse { + if(artistMbid.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() + service.getArtistData(artistMbid) + } + + override suspend fun fetchArtistWikiExtract(artistMbid: String?): Resource = parseResponse { + if(artistMbid.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() + mbService.getArtistWikiExtract(artistMbid) + } + + override suspend fun fetchArtistReviews(artistMbid: String?): Resource = parseResponse { + if(artistMbid.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() + cbService.getArtistReviews(artistMbid) } } diff --git a/app/src/main/java/org/listenbrainz/android/service/ArtistService.kt b/app/src/main/java/org/listenbrainz/android/service/ArtistService.kt index 5a922454..eccf22a1 100644 --- a/app/src/main/java/org/listenbrainz/android/service/ArtistService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/ArtistService.kt @@ -1,15 +1,11 @@ package org.listenbrainz.android.service -import org.listenbrainz.android.model.artist.PopularAlbums -import org.listenbrainz.android.model.artist.PopularTracks import retrofit2.Response -import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path +import org.listenbrainz.android.model.artist.Artist interface ArtistService { - @GET("popularity/top-recordings-for-artist/{artist_mbid}") - suspend fun getPopularTracksOfArtist(@Path("artist_mbid") artistMbid: String?): Response - - @GET("popularity/top-release-groups-for-artist/{artist_mbid}") - suspend fun getPopularAlbumsOfArtist(@Path("artist_mbid") artistMbid: String?): Response + @POST("artist/{artist_mbid}") + suspend fun getArtistData(@Path("artist_mbid") artistMbid: String?): Response } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/service/CBService.kt b/app/src/main/java/org/listenbrainz/android/service/CBService.kt new file mode 100644 index 00000000..da202e2f --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/service/CBService.kt @@ -0,0 +1,12 @@ +package org.listenbrainz.android.service + +import org.listenbrainz.android.model.artist.ArtistReview +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface CBService { + @GET("ws/1/review/?limit=5&entity_id={artist_mbid}") + suspend fun getArtistReviews(@Path("artist_mbid") artistMbid: String?, @Query("entity_type") entityType: String? = "artist"): Response +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/service/MBService.kt b/app/src/main/java/org/listenbrainz/android/service/MBService.kt index f12b9996..01423909 100644 --- a/app/src/main/java/org/listenbrainz/android/service/MBService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/MBService.kt @@ -1,16 +1,11 @@ package org.listenbrainz.android.service -import org.listenbrainz.android.model.artist.ArtistBio import org.listenbrainz.android.model.artist.ArtistWikiExtract import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path interface MBService { - @GET("ws/2/artist/{artist_mbid}?fmt=json") - suspend fun getArtistBio(@Path("artist_mbid") artistMbid: String?): Response - @GET("artist/{artist_mbid}/wikipedia-extract") suspend fun getArtistWikiExtract(@Path("artist_mbid") artistMbid: String?): Response - } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistUIState.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistUIState.kt index c4d26d52..695852c2 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistUIState.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistUIState.kt @@ -1,6 +1,29 @@ package org.listenbrainz.android.ui.screens.artist -data class ArtistUIState( - val artistName: String? = null, +import org.listenbrainz.android.model.artist.ArtistReview +import org.listenbrainz.android.model.artist.ArtistWikiExtract +import org.listenbrainz.android.model.artist.Listeners +import org.listenbrainz.android.model.artist.PopularRecording +import org.listenbrainz.android.model.artist.ReleaseGroup +import org.listenbrainz.android.model.artist.Rels +import org.listenbrainz.android.model.artist.SimilarArtist +import org.listenbrainz.android.model.artist.Tag +data class ArtistUIState( + val isLoading: Boolean? = true, + val name: String? = null, + val coverArt: String? = null, + val beginYear: Int? = null, + val area: String? = null, + val totalPlays: Int? = null, + val totalListeners: Int? = null, + val wikiExtract: ArtistWikiExtract? = null, + val tags: Tag? = null, + val links: Rels? = null, + val popularTracks: List? = listOf(), + val albums: List? = listOf(), + val appearsOn: List? = listOf(), + val similarArtists: List? = listOf(), + val topListeners: List? = listOf(), + val reviews: ArtistReview? = null, ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/util/Constants.kt b/app/src/main/java/org/listenbrainz/android/util/Constants.kt index 7ed02df2..2226fd9a 100644 --- a/app/src/main/java/org/listenbrainz/android/util/Constants.kt +++ b/app/src/main/java/org/listenbrainz/android/util/Constants.kt @@ -17,6 +17,7 @@ object Constants { const val ABOUT_URL = "https://listenbrainz.org/about" const val LB_BASE_URL = "https://listenbrainz.org/" const val MB_BASE_URL = "https://musicbrainz.org/" + const val CB_BASE_URL = "https://critiquebrainz.org/" object Strings { const val TIMESTAMP = "timestamp" diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/ArtistViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/ArtistViewModel.kt index e69de29b..d5bed67e 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ArtistViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ArtistViewModel.kt @@ -0,0 +1,60 @@ +package org.listenbrainz.android.viewmodel + +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import org.listenbrainz.android.repository.artist.ArtistRepository +import org.listenbrainz.android.ui.screens.artist.ArtistUIState +import javax.inject.Inject + +class ArtistViewModel @Inject constructor( + private val repository: ArtistRepository +) : BaseViewModel() { + private val artistUIStateFlow: MutableStateFlow = MutableStateFlow(ArtistUIState()) + + suspend fun fetchArtistData(artistMbid: String?) { + val artistData = repository.fetchArtistData(artistMbid).data + val artistReviews = repository.fetchArtistReviews(artistMbid).data + val artistWikiExtract = repository.fetchArtistWikiExtract(artistMbid).data + val appearsOn = artistData?.releaseGroups?.filter { releaseGroup -> + releaseGroup?.artists?.firstOrNull()?.artistMbid != artistMbid + } + val artistUiState = ArtistUIState( + isLoading = false, + name = artistData?.name, + coverArt = artistData?.coverArt, + beginYear = artistData?.beginYear, + area = artistData?.area, + totalPlays = artistData?.listeningStats?.totalListenCount, + totalListeners = artistData?.listeningStats?.totalUserCount, + wikiExtract = artistWikiExtract, + tags = artistData?.tag, + links = artistData?.rels, + popularTracks = artistData?.popularRecordings, + albums = artistData?.releaseGroups, + appearsOn = appearsOn, + similarArtists = artistData?.similarArtists, + topListeners = artistData?.listeningStats?.listeners, + reviews = artistReviews + ) + artistUIStateFlow.emit(artistUiState) + } + + + override val uiState: StateFlow = createUiStateFlow() + + override fun createUiStateFlow(): StateFlow { + return combine( + artistUIStateFlow + ) { + it[0] + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + ArtistUIState() + ) + } +} \ No newline at end of file From 4b31181729407fc27acc73d6c4ca7659799f0fb1 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sun, 18 Aug 2024 09:41:41 +0530 Subject: [PATCH 82/97] Implemented Artist Bio card, links in artist pages --- .../android/model/artist/Artist.kt | 5 - .../android/model/artist/ArtistPayload.kt | 10 + .../artist/{ArtistX.kt => ArtistWithTags.kt} | 2 +- .../artist/{ArtistXX.kt => ArtistsInAlbum.kt} | 2 +- .../android/model/artist/PopularRecording.kt | 22 +- .../android/model/artist/ReleaseGroup.kt | 20 +- .../listenbrainz/android/model/artist/Rels.kt | 6 +- .../listenbrainz/android/model/artist/Tag.kt | 2 +- .../listenbrainz/android/model/user/Artist.kt | 6 +- .../repository/artist/ArtistRepository.kt | 4 +- .../repository/artist/ArtistRepositoryImpl.kt | 4 +- .../android/service/ArtistService.kt | 6 +- .../ui/screens/artist/ArtistLinksEnum.kt | 7 + .../android/ui/screens/artist/ArtistScreen.kt | 442 +++++++++++++++++- .../ui/screens/artist/ArtistUIState.kt | 2 +- .../screens/profile/listens/ListensScreen.kt | 2 + .../ui/screens/profile/stats/StatsScreen.kt | 2 +- .../android/viewmodel/ArtistViewModel.kt | 12 +- app/src/main/res/drawable/deezer.xml | 9 + app/src/main/res/drawable/facebook.xml | 9 + app/src/main/res/drawable/instagram.xml | 9 + app/src/main/res/drawable/itunes.xml | 9 + .../res/drawable/lb_radio_play_button.xml | 9 + app/src/main/res/drawable/listeners_icon.xml | 9 + app/src/main/res/drawable/listens_icon.xml | 9 + app/src/main/res/drawable/mail_order.xml | 9 + app/src/main/res/drawable/wiki_data.xml | 9 + 27 files changed, 588 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/ArtistPayload.kt rename app/src/main/java/org/listenbrainz/android/model/artist/{ArtistX.kt => ArtistWithTags.kt} (91%) rename app/src/main/java/org/listenbrainz/android/model/artist/{ArtistXX.kt => ArtistsInAlbum.kt} (92%) create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistLinksEnum.kt create mode 100644 app/src/main/res/drawable/deezer.xml create mode 100644 app/src/main/res/drawable/facebook.xml create mode 100644 app/src/main/res/drawable/instagram.xml create mode 100644 app/src/main/res/drawable/itunes.xml create mode 100644 app/src/main/res/drawable/lb_radio_play_button.xml create mode 100644 app/src/main/res/drawable/listeners_icon.xml create mode 100644 app/src/main/res/drawable/listens_icon.xml create mode 100644 app/src/main/res/drawable/mail_order.xml create mode 100644 app/src/main/res/drawable/wiki_data.xml diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/Artist.kt b/app/src/main/java/org/listenbrainz/android/model/artist/Artist.kt index dc1bda5a..72820c28 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/Artist.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/Artist.kt @@ -12,9 +12,4 @@ data class Artist( val rels: Rels? = Rels(), val tag: Tag? = Tag(), val type: String? = "", - val coverArt: String? = "", - val listeningStats: ListeningStats? = null, - val popularRecordings: List = listOf(), - val releaseGroups: List = listOf(), - val similarArtists: List = listOf() ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistPayload.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistPayload.kt new file mode 100644 index 00000000..894d6960 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistPayload.kt @@ -0,0 +1,10 @@ +package org.listenbrainz.android.model.artist + +data class ArtistPayload( + val artist: Artist? = null, + val coverArt: String? = null, + val listeningStats: ListeningStats? = null, + val popularRecordings: List = listOf(), + val releaseGroups: List = listOf(), + val similarArtists: List = listOf() +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistX.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistWithTags.kt similarity index 91% rename from app/src/main/java/org/listenbrainz/android/model/artist/ArtistX.kt rename to app/src/main/java/org/listenbrainz/android/model/artist/ArtistWithTags.kt index 0097d035..c2369f23 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistX.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistWithTags.kt @@ -2,7 +2,7 @@ package org.listenbrainz.android.model.artist import com.google.gson.annotations.SerializedName -data class ArtistX( +data class ArtistWithTags( @SerializedName("artist_mbid") val artistMbid: String? = null, val count: Int? = null, @SerializedName("genre_mbid") val genreMbid: String? = null, diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistXX.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistsInAlbum.kt similarity index 92% rename from app/src/main/java/org/listenbrainz/android/model/artist/ArtistXX.kt rename to app/src/main/java/org/listenbrainz/android/model/artist/ArtistsInAlbum.kt index 285b3191..514191da 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistXX.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistsInAlbum.kt @@ -2,7 +2,7 @@ package org.listenbrainz.android.model.artist import com.google.gson.annotations.SerializedName -data class ArtistXX( +data class ArtistsInAlbum( @SerializedName("artist_credit_name") val artistCreditName: String? = null, @SerializedName("artist_mbid") val artistMbid: String? = null, @SerializedName("join_phrase") val joinPhrase: String? = null diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/PopularRecording.kt b/app/src/main/java/org/listenbrainz/android/model/artist/PopularRecording.kt index adfdf0fa..9805a303 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/PopularRecording.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/PopularRecording.kt @@ -4,16 +4,16 @@ import com.google.gson.annotations.SerializedName data class PopularRecording( @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? = "", - val length: Int? = 0, - @SerializedName("recording_mbid") val recordingMbid: String? = "", - @SerializedName("recording_name") val recordingName: String? = "", + @SerializedName("artist_name") val artistName: String? = null, + val artists: List? = listOf(), + @SerializedName("caa_id") val caaId: Long? = null, + @SerializedName("caa_release_mbid") val caaReleaseMbid: String? = null, + val length: Int? = null, + @SerializedName("recording_mbid") val recordingMbid: String? = null, + @SerializedName("recording_name") val recordingName: String? = null, @SerializedName("release_color") val releaseColor: ReleaseColor? = ReleaseColor(), - @SerializedName("release_mbid") val releaseMbid: String? = "", - @SerializedName("release_name") val releaseName: String? = "", - @SerializedName("total_listen_count") val totalListenCount: Int? = 0, - @SerializedName("total_user_count") val totalUserCount: Int? = 0 + @SerializedName("release_mbid") val releaseMbid: String? = null, + @SerializedName("release_name") val releaseName: String? = null, + @SerializedName("total_listen_count") val totalListenCount: Int? = null, + @SerializedName("total_user_count") val totalUserCount: Int? = null ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ReleaseGroup.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ReleaseGroup.kt index c8f98ea6..ee070f36 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/ReleaseGroup.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/ReleaseGroup.kt @@ -3,14 +3,14 @@ package org.listenbrainz.android.model.artist import com.google.gson.annotations.SerializedName data class ReleaseGroup( - @SerializedName("artist_credit_name") val artistCreditName: String? = "", - val artists: List? = listOf(), - @SerializedName("caa_id") val caaId: Long? = 0, - @SerializedName("caa_release_mbid") val caaReleaseMbid: String? = "", - val date: String? = "", - val mbid: String? = "", - val name: String? = "", - @SerializedName("total_listen_count") val totalListenCount: Int? = 0, - @SerializedName("total_user_count") val totalUserCount: Int? = 0, - val type: String? = "" + @SerializedName("artist_credit_name") val artistCreditName: String? = null, + val artists: List? = listOf(), + @SerializedName("caa_id") val caaId: Long? = null, + @SerializedName("caa_release_mbid") val caaReleaseMbid: String? = null, + val date: String? = null, + val mbid: String? = null, + val name: String? = null, + @SerializedName("total_listen_count") val totalListenCount: Int? = null, + @SerializedName("total_user_count") val totalUserCount: Int? = null, + val type: String? = null ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/Rels.kt b/app/src/main/java/org/listenbrainz/android/model/artist/Rels.kt index bff2642f..489dc5e7 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/Rels.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/Rels.kt @@ -5,7 +5,11 @@ import com.google.gson.annotations.SerializedName data class Rels( @SerializedName("free streaming") val freeStreaming: String? = null, @SerializedName("purchase for download") val purchaseForDownload: String? = null, + @SerializedName("purchase for mail order") val purchaseForMailOrder: String? = null, + @SerializedName("official homepage") val officialHomePage: String? = null, @SerializedName("social network") val socialNetwork: String? = null, val wikidata: String? = null, - val youtube: String? = null + val youtube: String? = null, + val lyrics: String? = null, + val streaming: String? = null, ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/Tag.kt b/app/src/main/java/org/listenbrainz/android/model/artist/Tag.kt index 670118fa..b885fad5 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/Tag.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/Tag.kt @@ -1,5 +1,5 @@ package org.listenbrainz.android.model.artist data class Tag( - val artist: List? = listOf() + val artist: List? = listOf() ) \ No newline at end of file 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 ee723710..3ced24b7 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 @@ -3,7 +3,7 @@ package org.listenbrainz.android.model.user import com.google.gson.annotations.SerializedName data class Artist( - @SerializedName("artist_mbid") val artistMbid: String, - @SerializedName("artist_name") val artistName: String, - @SerializedName("listen_count") val listenCount: Int + @SerializedName("artist_mbid") val artistMbid: String? = null, + @SerializedName("artist_name") val artistName: String? = null, + @SerializedName("listen_count") val listenCount: Int? = null, ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepository.kt b/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepository.kt index 56e00bad..6aa45e77 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepository.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepository.kt @@ -1,13 +1,13 @@ package org.listenbrainz.android.repository.artist -import org.listenbrainz.android.model.artist.Artist +import org.listenbrainz.android.model.artist.ArtistPayload import org.listenbrainz.android.model.artist.ArtistReview import org.listenbrainz.android.model.artist.ArtistWikiExtract import org.listenbrainz.android.util.Resource interface ArtistRepository { - suspend fun fetchArtistData(artistMbid: String?): Resource + suspend fun fetchArtistData(artistMbid: String?): Resource suspend fun fetchArtistWikiExtract(artistMbid: String?): Resource suspend fun fetchArtistReviews(artistMbid: String?): Resource } diff --git a/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepositoryImpl.kt index 1e3ed637..5dba55cd 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepositoryImpl.kt @@ -1,7 +1,7 @@ package org.listenbrainz.android.repository.artist import org.listenbrainz.android.model.ResponseError -import org.listenbrainz.android.model.artist.Artist +import org.listenbrainz.android.model.artist.ArtistPayload import org.listenbrainz.android.model.artist.ArtistReview import org.listenbrainz.android.model.artist.ArtistWikiExtract import org.listenbrainz.android.service.ArtistService @@ -16,7 +16,7 @@ class ArtistRepositoryImpl @Inject constructor( private val mbService: MBService, private val cbService: CBService, ) : ArtistRepository { - override suspend fun fetchArtistData(artistMbid: String?): Resource = parseResponse { + override suspend fun fetchArtistData(artistMbid: String?): Resource = parseResponse { if(artistMbid.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() service.getArtistData(artistMbid) } diff --git a/app/src/main/java/org/listenbrainz/android/service/ArtistService.kt b/app/src/main/java/org/listenbrainz/android/service/ArtistService.kt index eccf22a1..c76afb70 100644 --- a/app/src/main/java/org/listenbrainz/android/service/ArtistService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/ArtistService.kt @@ -1,11 +1,13 @@ package org.listenbrainz.android.service +import org.listenbrainz.android.model.artist.ArtistPayload import retrofit2.Response +import retrofit2.http.Headers import retrofit2.http.POST import retrofit2.http.Path -import org.listenbrainz.android.model.artist.Artist interface ArtistService { + @Headers("Accept: application/json") @POST("artist/{artist_mbid}") - suspend fun getArtistData(@Path("artist_mbid") artistMbid: String?): Response + suspend fun getArtistData(@Path("artist_mbid") artistMbid: String?): Response } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistLinksEnum.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistLinksEnum.kt new file mode 100644 index 00000000..893e8ca5 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistLinksEnum.kt @@ -0,0 +1,7 @@ +enum class ArtistLinksEnum (val label: String) { + ALL("All"), + MAIN("Main"), + STREAMING("Streaming"), + SOCIAL_MEDIA("Social Media"), + LYRICS("Lyrics") +} diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt index 2b6458a8..eeb3c78b 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt @@ -1,11 +1,447 @@ package org.listenbrainz.android.ui.screens.artist -import androidx.compose.material.Text +import ArtistLinksEnum +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Toast +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +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.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +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.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.SettingsVoice +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ElevatedSuggestionChip +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +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.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.hilt.navigation.compose.hiltViewModel +import org.listenbrainz.android.R +import org.listenbrainz.android.ui.components.LoadingAnimation +import org.listenbrainz.android.ui.theme.ListenBrainzTheme +import org.listenbrainz.android.ui.theme.app_bg_dark +import org.listenbrainz.android.ui.theme.app_bg_mid +import org.listenbrainz.android.ui.theme.app_bg_secondary_dark +import org.listenbrainz.android.ui.theme.lb_purple_night +import org.listenbrainz.android.util.Constants.MB_BASE_URL +import org.listenbrainz.android.viewmodel.ArtistViewModel + @Composable fun ArtistScreen( - artistMbid: String + artistMbid: String, + viewModel: ArtistViewModel = hiltViewModel() +) { + LaunchedEffect(Unit) { + viewModel.fetchArtistData(artistMbid) + } + val uiState by viewModel.uiState.collectAsState() + ArtistScreen(artistMbid = artistMbid,uiState = uiState) +} + +@Composable +private fun ArtistScreen( + artistMbid: String, + uiState: ArtistUIState +) { + Box(modifier = Modifier.fillMaxSize()){ + AnimatedVisibility( + visible = uiState.isLoading, + modifier = Modifier.align(Alignment.Center), + enter = fadeIn(initialAlpha = 0.4f), + exit = fadeOut(animationSpec = tween(durationMillis = 250)) + ) { + LoadingAnimation() + } + AnimatedVisibility(visible = !uiState.isLoading) { + ListenBrainzTheme { + LazyColumn { + item { + ArtistBioCard(uiState = uiState) + } + item { + Links(uiState = uiState, artistMbid = artistMbid) + } + item { + + } + } + } + + } + } +} + +@Composable +private fun ArtistBioCard( + uiState: ArtistUIState ) { - Text(text = artistMbid) + Box(modifier = Modifier + .fillMaxWidth() + .clip(shape = RoundedCornerShape(bottomStart = 18.dp, bottomEnd = 18.dp)) + .background(Color(0xFF2B2E35)) + .padding(23.dp)){ + Column { + Row (horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + Text(uiState.name ?: "", color = Color.White, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 25.sp)) + LbRadioButton { + + } + } + Row { + if (uiState.coverArt != null) { + SvgWithWebView( + svgContent = uiState.coverArt, + width = 200.dp, + height = 200.dp + ) + } + Spacer(modifier = Modifier.width(20.dp)) + Column { + Text(uiState.beginYear.toString(), color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + Text(uiState.area.toString(), color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + Spacer(modifier = Modifier.height(10.dp)) + HorizontalDivider(color = app_bg_dark, thickness = 3.dp, modifier = Modifier.padding(end = 50.dp)) + Spacer(modifier = Modifier.height(10.dp)) + Row { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.listens_icon), + contentDescription = null, + tint = app_bg_mid + ) + Spacer(modifier = Modifier.width(5.dp)) + Text((uiState.totalPlays ?: 0).toString() + " plays", color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + } + Row { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.listeners_icon), + contentDescription = null, + tint = app_bg_mid + ) + Spacer(modifier = Modifier.width(5.dp)) + Text((uiState.totalListeners ?: 0).toString() + " listeners", color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + } + } + } + if(uiState.wikiExtract?.wikipediaExtract?.content != null){ + Spacer(modifier = Modifier.height(20.dp)) + Text(removeHtmlTags(uiState.wikiExtract.wikipediaExtract.content).trim() , maxLines = 4, color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp), overflow = TextOverflow.Ellipsis) + if(uiState.wikiExtract.wikipediaExtract.url != null){ + val uriHandlder = LocalUriHandler.current + Text("read more", color = lb_purple_night, modifier = Modifier.clickable { + uriHandlder.openUri(uiState.wikiExtract.wikipediaExtract.url) + }) + } + } + Row (modifier = Modifier + .horizontalScroll(rememberScrollState()) + .padding(top = 10.dp)) { + uiState.tags?.artist?.map { + if(it.tag != null){ + Box (modifier = Modifier + .clip( + RoundedCornerShape((16.dp)) + ) + .background(app_bg_secondary_dark) + .padding(10.dp)) { + Row { + Text(it.tag, color= lb_purple_night, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + Spacer(modifier = Modifier.width(8.dp)) + Text((it.count ?: 0).toString(), color = Color.White ,style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + } + } + Spacer(modifier = Modifier.width(10.dp)) + } + + } + } + + } + } +} + +class LinkCardData (val iconResId: ImageVector, val label: String, val url: String) {} + +@Composable +private fun Links( + artistMbid: String, + uiState: ArtistUIState +) { + //TODO: Move this logic to vm and get map to ui state + val allLinkCards: MutableList = mutableListOf() + val mainLinkCards: MutableList = mutableListOf() + val streamingLinkCards: MutableList = mutableListOf() + val socialMediaLinkCards: MutableList = mutableListOf() + val lyricsLinkCards: MutableList = mutableListOf() + val links = uiState.links + if(links?.wikidata != null){ + val wikidata = LinkCardData(ImageVector.vectorResource(id = R.drawable.wiki_data), "Wikidata", links.wikidata) + allLinkCards.add(wikidata) + mainLinkCards.add(wikidata) + } + if(links?.lyrics != null){ + val lyrics = LinkCardData(Icons.Default.SettingsVoice, "Lyrics", links.lyrics) + allLinkCards.add(lyrics) + lyricsLinkCards.add(lyrics) + } + if(links?.officialHomePage != null){ + val homePage = LinkCardData(ImageVector.vectorResource(id = R.drawable.home_icon), "Homepage", links.officialHomePage) + allLinkCards.add(homePage) + mainLinkCards.add(homePage) + } + if(links?.purchaseForDownload != null){ + val purchase = LinkCardData(ImageVector.vectorResource(id = R.drawable.mail_order), "Purchase for Download", links.purchaseForDownload) + allLinkCards.add(purchase) + streamingLinkCards.add(purchase) + } + if(links?.purchaseForMailOrder != null){ + val mailOrder = LinkCardData(ImageVector.vectorResource(id = R.drawable.mail_order), "Purchase for mail order", links.purchaseForMailOrder) + allLinkCards.add(mailOrder) + streamingLinkCards.add(mailOrder) + } + mainLinkCards.add(LinkCardData(ImageVector.vectorResource(id = R.drawable.musicbrainz_logo), "Edit", MB_BASE_URL + "artist/${artistMbid}")) + val linksMap: Map> = mapOf( + ArtistLinksEnum.ALL to allLinkCards, + ArtistLinksEnum.MAIN to mainLinkCards, + ArtistLinksEnum.LYRICS to lyricsLinkCards, + ArtistLinksEnum.STREAMING to streamingLinkCards, + ArtistLinksEnum.SOCIAL_MEDIA to socialMediaLinkCards + ) + val linkOptionSelectionState: MutableState = remember { + mutableStateOf(ArtistLinksEnum.MAIN) + } + Box(modifier = Modifier + //.background(brush = ListenBrainzTheme.colorScheme.gradientBrush) + .fillMaxWidth() + .padding(23.dp)){ + Column { + Text("Links", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Row (modifier = Modifier.horizontalScroll(rememberScrollState()).padding(top = 10.dp)) { + repeat(5) { + position -> + val reqdState = when(position){ + 0 -> linkOptionSelectionState.value == ArtistLinksEnum.ALL + 1 -> linkOptionSelectionState.value == ArtistLinksEnum.MAIN + 2 -> linkOptionSelectionState.value == ArtistLinksEnum.STREAMING + 3 -> linkOptionSelectionState.value == ArtistLinksEnum.SOCIAL_MEDIA + 4 -> linkOptionSelectionState.value == ArtistLinksEnum.LYRICS + else -> false + } + ElevatedSuggestionChip( + onClick = { + when(position){ + 0 -> linkOptionSelectionState.value = ArtistLinksEnum.ALL + 1 -> linkOptionSelectionState.value = ArtistLinksEnum.MAIN + 2 -> linkOptionSelectionState.value = ArtistLinksEnum.STREAMING + 3 -> linkOptionSelectionState.value = ArtistLinksEnum.SOCIAL_MEDIA + 4 -> linkOptionSelectionState.value = ArtistLinksEnum.LYRICS + } + }, + label = { + val label = when(position){ + 0 -> ArtistLinksEnum.ALL.label + 1 -> ArtistLinksEnum.MAIN.label + 2 -> ArtistLinksEnum.STREAMING.label + 3 -> ArtistLinksEnum.SOCIAL_MEDIA.label + 4 -> ArtistLinksEnum.LYRICS.label + else -> "" + } + 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(12.dp)) + } + + } + Column (modifier = Modifier.padding(top = 20.dp)) { + val items = linksMap[linkOptionSelectionState.value] + items?.chunked(3)?.forEach { rowItems -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + rowItems.forEach { item -> + LinkCard( + icon = item.iconResId, + label = item.label, + url = item.url, + ) + } + // Fill remaining empty space with spacers + repeat(3 - rowItems.size) { + Spacer(modifier = Modifier.weight(1f)) + } + } + Spacer(modifier = Modifier.height(10.dp)) + } + } + } + } +} + +@Composable +private fun PopularTracks( + uiState: ArtistUIState +) { + +} + +@Composable +private fun LinkCard( + icon: ImageVector, + label: String, + url: String, +) { + val uriHandler = LocalUriHandler.current + val context = LocalContext.current + Box( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(color = app_bg_secondary_dark) + .padding(top = 10.dp, bottom = 10.dp, start = 16.dp, end = 16.dp) + .clickable { + try { + uriHandler.openUri(url) + } catch (err: Error) { + Toast + .makeText(context, "Some unknown error occurred", Toast.LENGTH_SHORT) + .show() + } + } + ) { + Row (verticalAlignment = Alignment.CenterVertically) { + Icon(imageVector = icon, contentDescription = null, tint = when(icon){ + ImageVector.vectorResource(id = R.drawable.musicbrainz_logo) -> Color.Unspecified + else -> lb_purple_night + }) + Spacer(modifier = Modifier.width(10.dp)) + Text(label, color = lb_purple_night, style = MaterialTheme.typography.bodyMedium) + } + } +} + + +@Composable +private fun LbRadioButton( + onClick: () -> Unit +) { + OutlinedButton(onClick = onClick, colors = ButtonColors(containerColor = Color(0xFF353070), contentColor = Color.White, disabledContentColor = Color(0xFF353070), disabledContainerColor = Color(0xFF353070))) { + Row (horizontalArrangement = Arrangement.SpaceBetween) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.lb_radio_play_button), + contentDescription = "" + ) + Text("Radio") + } + } +} + +@Composable +fun SvgWithWebView(svgContent: String, width: Dp, height: Dp) { + val context = LocalContext.current + val webView = remember { WebView(context) } + + LaunchedEffect(svgContent) { + webView.webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + // Optionally, handle size adjustments or interactions if needed + } + } + + val htmlContent = """ + + + + + + +

+ $svgContent +
+ + + """ + + webView.loadDataWithBaseURL(null, htmlContent, "text/html", "UTF-8", null) + } + + AndroidView( + factory = { webView }, + modifier = Modifier + .width(width) + .height(height) + ) +} + +fun removeHtmlTags(input: String): String { + // Regular expression pattern to match HTML tags + val regex = "<[^>]*>".toRegex() + // Replace all matches of the pattern with an empty string + return input.replace(regex, "") } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistUIState.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistUIState.kt index 695852c2..2b9c97c7 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistUIState.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistUIState.kt @@ -10,7 +10,7 @@ import org.listenbrainz.android.model.artist.SimilarArtist import org.listenbrainz.android.model.artist.Tag data class ArtistUIState( - val isLoading: Boolean? = true, + val isLoading: Boolean = true, val name: String? = null, val coverArt: String? = null, val beginYear: Int? = null, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt index 3411cb49..10f062e3 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt @@ -456,6 +456,7 @@ private fun BuildSimilarArtists(similarArtists: List, onArtistClick: (St append("You both listen to ") } topSimilarArtists.forEachIndexed { index, artist -> + if(artist.artistMbid != null) pushStringAnnotation(tag = "ARTIST", annotation = artist.artistMbid) withStyle(style = SpanStyle(color = lb_purple_night)) { append(artist.artistName) @@ -494,6 +495,7 @@ private fun BuildSimilarArtists(similarArtists: List, onArtistClick: (St append("You both listen to ") } similarArtists.forEachIndexed { index, artist -> + if(artist.artistMbid != null) pushStringAnnotation(tag = "ARTIST", annotation = artist.artistMbid) withStyle(style = SpanStyle(color = lb_purple_night)) { append(artist.artistName) 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 956c0bd3..fbb35fcf 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 @@ -418,7 +418,7 @@ fun StatsScreen( Column (horizontalAlignment = Alignment.CenterHorizontally) { topArtists.map { topArtist -> - ArtistCard(artistName = topArtist.artistName, listenCount = topArtist.listenCount){} + ArtistCard(artistName = topArtist.artistName ?: "", listenCount = topArtist.listenCount){} } Spacer(modifier = Modifier.height(10.dp)) if((uiState.statsTabUIState.topArtists?.size ?: 0) > 5){ diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/ArtistViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/ArtistViewModel.kt index d5bed67e..9da11f66 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ArtistViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ArtistViewModel.kt @@ -1,6 +1,7 @@ package org.listenbrainz.android.viewmodel import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -10,6 +11,7 @@ import org.listenbrainz.android.repository.artist.ArtistRepository import org.listenbrainz.android.ui.screens.artist.ArtistUIState import javax.inject.Inject +@HiltViewModel class ArtistViewModel @Inject constructor( private val repository: ArtistRepository ) : BaseViewModel() { @@ -24,15 +26,15 @@ class ArtistViewModel @Inject constructor( } val artistUiState = ArtistUIState( isLoading = false, - name = artistData?.name, + name = artistData?.artist?.name, coverArt = artistData?.coverArt, - beginYear = artistData?.beginYear, - area = artistData?.area, + beginYear = artistData?.artist?.beginYear, + area = artistData?.artist?.area, totalPlays = artistData?.listeningStats?.totalListenCount, totalListeners = artistData?.listeningStats?.totalUserCount, wikiExtract = artistWikiExtract, - tags = artistData?.tag, - links = artistData?.rels, + tags = artistData?.artist?.tag, + links = artistData?.artist?.rels, popularTracks = artistData?.popularRecordings, albums = artistData?.releaseGroups, appearsOn = appearsOn, diff --git a/app/src/main/res/drawable/deezer.xml b/app/src/main/res/drawable/deezer.xml new file mode 100644 index 00000000..75447838 --- /dev/null +++ b/app/src/main/res/drawable/deezer.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/facebook.xml b/app/src/main/res/drawable/facebook.xml new file mode 100644 index 00000000..839cf9d3 --- /dev/null +++ b/app/src/main/res/drawable/facebook.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/instagram.xml b/app/src/main/res/drawable/instagram.xml new file mode 100644 index 00000000..3e620aeb --- /dev/null +++ b/app/src/main/res/drawable/instagram.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/itunes.xml b/app/src/main/res/drawable/itunes.xml new file mode 100644 index 00000000..5fac5d57 --- /dev/null +++ b/app/src/main/res/drawable/itunes.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/lb_radio_play_button.xml b/app/src/main/res/drawable/lb_radio_play_button.xml new file mode 100644 index 00000000..ff6826ec --- /dev/null +++ b/app/src/main/res/drawable/lb_radio_play_button.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/listeners_icon.xml b/app/src/main/res/drawable/listeners_icon.xml new file mode 100644 index 00000000..39b26daa --- /dev/null +++ b/app/src/main/res/drawable/listeners_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/listens_icon.xml b/app/src/main/res/drawable/listens_icon.xml new file mode 100644 index 00000000..900e5129 --- /dev/null +++ b/app/src/main/res/drawable/listens_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/mail_order.xml b/app/src/main/res/drawable/mail_order.xml new file mode 100644 index 00000000..c716c1ff --- /dev/null +++ b/app/src/main/res/drawable/mail_order.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/wiki_data.xml b/app/src/main/res/drawable/wiki_data.xml new file mode 100644 index 00000000..b3f3223a --- /dev/null +++ b/app/src/main/res/drawable/wiki_data.xml @@ -0,0 +1,9 @@ + + + From d0b057a41edcf207f42045d64c42ced7603c9bcc Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sun, 18 Aug 2024 10:52:57 +0530 Subject: [PATCH 83/97] Implemented album , appears on card, added similar artists --- .../android/ui/screens/artist/ArtistScreen.kt | 143 +++++++++++++++++- .../android/viewmodel/ArtistViewModel.kt | 2 +- 2 files changed, 139 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt index eeb3c78b..d9df87ae 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.fillMaxSize 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.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState @@ -46,9 +47,12 @@ 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.FilterQuality import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp @@ -56,14 +60,21 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.navigation.compose.hiltViewModel +import coil.compose.AsyncImage +import coil.request.ImageRequest import org.listenbrainz.android.R +import org.listenbrainz.android.model.artist.ReleaseGroup +import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.LoadingAnimation +import org.listenbrainz.android.ui.screens.profile.listens.LoadMoreButton +import org.listenbrainz.android.ui.screens.profile.stats.ArtistCard import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.ui.theme.app_bg_dark import org.listenbrainz.android.ui.theme.app_bg_mid import org.listenbrainz.android.ui.theme.app_bg_secondary_dark import org.listenbrainz.android.ui.theme.lb_purple_night import org.listenbrainz.android.util.Constants.MB_BASE_URL +import org.listenbrainz.android.util.Utils import org.listenbrainz.android.viewmodel.ArtistViewModel @@ -103,7 +114,16 @@ private fun ArtistScreen( Links(uiState = uiState, artistMbid = artistMbid) } item { - + PopularTracks(uiState = uiState) + } + item { + AlbumsCard(header = "Albums", albumsList = uiState.albums) + } + item { + AlbumsCard(header = "Appears On", albumsList = uiState.appearsOn) + } + item { + SimilarArtists(uiState = uiState) } } } @@ -251,12 +271,14 @@ private fun Links( mutableStateOf(ArtistLinksEnum.MAIN) } Box(modifier = Modifier - //.background(brush = ListenBrainzTheme.colorScheme.gradientBrush) + .background(brush = ListenBrainzTheme.colorScheme.gradientBrush) .fillMaxWidth() .padding(23.dp)){ Column { Text("Links", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) - Row (modifier = Modifier.horizontalScroll(rememberScrollState()).padding(top = 10.dp)) { + Row (modifier = Modifier + .horizontalScroll(rememberScrollState()) + .padding(top = 10.dp)) { repeat(5) { position -> val reqdState = when(position){ @@ -322,12 +344,11 @@ private fun Links( url = item.url, ) } - // Fill remaining empty space with spacers repeat(3 - rowItems.size) { Spacer(modifier = Modifier.weight(1f)) } } - Spacer(modifier = Modifier.height(10.dp)) + Spacer(modifier = Modifier.height(5.dp)) } } } @@ -338,9 +359,121 @@ private fun Links( private fun PopularTracks( uiState: ArtistUIState ) { + val popularTracksCollapsibleState: MutableState = remember { + mutableStateOf(true) + } + val popularTracks = when (popularTracksCollapsibleState.value) { + true -> uiState.popularTracks?.take(5) ?: listOf() + false -> uiState.popularTracks ?: listOf() + } + + Box(modifier = Modifier + .fillMaxWidth() + .background(brush = ListenBrainzTheme.colorScheme.gradientBrush) + .padding(start = 23.dp, end = 23.dp, top = 23.dp)){ + Column { + Text("Popular Tracks", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Spacer(modifier = Modifier.height(20.dp)) + popularTracks.map { + ListenCardSmall(trackName = it?.recordingName ?: "", artistName = it?.artistName ?: "", coverArtUrl = Utils.getCoverArtUrl(it?.caaReleaseMbid, it?.caaId)) { + } + Spacer(modifier = Modifier.height(12.dp)) + + } + if((uiState.popularTracks?.size ?: 0) > 5){ + Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + LoadMoreButton( + state = popularTracksCollapsibleState.value, + onClick = { + popularTracksCollapsibleState.value = !popularTracksCollapsibleState.value + } + ) + Spacer(modifier = Modifier.height(60.dp)) + } + + } + } + } } +@Composable +private fun AlbumsCard( + header: String, + albumsList: List? +) { + Box(modifier = Modifier + .fillMaxWidth() + .background(brush = ListenBrainzTheme.colorScheme.gradientBrush) + .padding(23.dp)){ + Column { + Text(header, color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Row (modifier = Modifier + .horizontalScroll(rememberScrollState()) + .padding(top = 20.dp)) { + albumsList?.map { + Box (modifier = Modifier) { + Column { + val coverArt = Utils.getCoverArtUrl(it?.caaReleaseMbid, it?.caaId, 500) + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(coverArt) + .build(), + fallback = painterResource(id = R.drawable.ic_coverartarchive_logo_no_text), + modifier = Modifier.size(ListenBrainzTheme.sizes.listenCardHeight * 3f), + contentScale = ContentScale.Fit, + placeholder = painterResource(id = R.drawable.ic_coverartarchive_logo_no_text), + filterQuality = FilterQuality.Low, + contentDescription = "Album Cover Art" + ) + Spacer(modifier = Modifier.height(10.dp)) + Text(it?.name ?: "", color = lb_purple_night, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 18.sp)) + } + } + Spacer(modifier = Modifier.width(40.dp)) + } + } + } + } +} + +@Composable +private fun SimilarArtists( + uiState: ArtistUIState +) { + val similarArtistsCollapisbleState: MutableState = remember { + mutableStateOf(true) + } + val similarArtists = when(similarArtistsCollapisbleState.value){ + true -> uiState.similarArtists?.take(5) ?: listOf() + false -> uiState.similarArtists ?: listOf() + } + + Box(modifier = Modifier + .fillMaxWidth() + .background(brush = ListenBrainzTheme.colorScheme.gradientBrush) + .padding(23.dp)){ + Column { + Text("Similar Artists", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + similarArtists.map { + ArtistCard(artistName = it?.name ?: "") { + + } + Spacer(modifier = Modifier.height(12.dp)) + } + if((uiState.similarArtists?.size ?: 0) > 5){ + Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + LoadMoreButton(state = similarArtistsCollapisbleState.value) { + similarArtistsCollapisbleState.value = !similarArtistsCollapisbleState.value + } + } + Spacer(modifier = Modifier.height(20.dp)) + } + } + } +} + + @Composable private fun LinkCard( icon: ImageVector, diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/ArtistViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/ArtistViewModel.kt index 9da11f66..18994e16 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ArtistViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ArtistViewModel.kt @@ -22,7 +22,7 @@ class ArtistViewModel @Inject constructor( val artistReviews = repository.fetchArtistReviews(artistMbid).data val artistWikiExtract = repository.fetchArtistWikiExtract(artistMbid).data val appearsOn = artistData?.releaseGroups?.filter { releaseGroup -> - releaseGroup?.artists?.firstOrNull()?.artistMbid != artistMbid + releaseGroup?.artists?.get(0)?.artistMbid != artistMbid } val artistUiState = ArtistUIState( isLoading = false, From 312cc1d9c4da385ef4bc07e5da07ff2d02ed836d Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sun, 18 Aug 2024 10:58:55 +0530 Subject: [PATCH 84/97] Added comments in tests --- .../org/listenbrainz/android/UserPagesTest.kt | 27 +++- .../android/user/UserRepositoryTest.kt | 117 +++++++++++++++++- .../android/user/UserViewModelTest.kt | 26 +++- 3 files changed, 164 insertions(+), 6 deletions(-) diff --git a/app/src/androidTest/java/org/listenbrainz/android/UserPagesTest.kt b/app/src/androidTest/java/org/listenbrainz/android/UserPagesTest.kt index a761f378..b03b7583 100644 --- a/app/src/androidTest/java/org/listenbrainz/android/UserPagesTest.kt +++ b/app/src/androidTest/java/org/listenbrainz/android/UserPagesTest.kt @@ -114,7 +114,8 @@ class UserPagesTest { } @Test - fun allTabsExistenceTest () { + fun allTabsExistenceTest() { + // Verify that all tabs ("Listens", "Stats", "Taste") exist on the main screen rule.onNodeWithText("Listens").assertExists() rule.onNodeWithText("Stats").assertExists() rule.onNodeWithText("Taste").assertExists() @@ -122,36 +123,60 @@ class UserPagesTest { @Test fun listensTabScreenFlowTest() { + // Navigate to the "Listens" tab rule.onNodeWithText("Listens").performClick() + + // Verify that key elements on the "Listens" screen are present rule.onNodeWithText("You have listened to").assertExists() rule.onNodeWithText("Recent Listens").assertExists() rule.onNodeWithText("Followers").assertExists() + + // Locate the scrollable container for the "Listens" screen val scrollableContainer = rule.onNodeWithTag("listensScreenScrollableContainer") + + // Scroll to a specific index to ensure additional content is loaded scrollableContainer.performScrollToIndex(10) + + // Verify that more elements are present after scrolling rule.onNodeWithText("Similar Users").assertExists() } @Test fun statsTabScreenFlowTest() { + // Navigate to the "Stats" tab rule.onNodeWithText("Stats").performClick() + + // Verify that key elements on the "Stats" screen are present rule.onNodeWithText("Global").assertExists() rule.onNodeWithText("This Week").assertExists() rule.onNodeWithText("This Month").assertExists() rule.onNodeWithText("This Year").assertExists() + + // Locate the scrollable container for the "Stats" screen val scrollableContainer = rule.onNodeWithTag("statsScreenScrollableContainer") + + // Scroll to a specific index to ensure additional content is loaded scrollableContainer.performScrollToIndex(2) + + // Verify that more elements are present after scrolling rule.onNodeWithText("Artists").assertExists() rule.onNodeWithText("Albums").assertExists() rule.onNodeWithText("Songs").assertExists() + + // Scroll further to check the presence of additional content scrollableContainer.performScrollToIndex(3) rule.onNodeWithText("Load More").assertExists() } @Test fun tasteTabScreenFlowTest() { + // Navigate to the "Taste" tab rule.onNodeWithText("Taste").performClick() + + // Verify that key elements on the "Taste" screen are present rule.onNodeWithText("Loved").assertExists() rule.onNodeWithText("Hated").assertExists() rule.onNodeWithText("Pins").assertExists() } + } diff --git a/app/src/test/java/org/listenbrainz/android/user/UserRepositoryTest.kt b/app/src/test/java/org/listenbrainz/android/user/UserRepositoryTest.kt index 20c64fae..826c662b 100644 --- a/app/src/test/java/org/listenbrainz/android/user/UserRepositoryTest.kt +++ b/app/src/test/java/org/listenbrainz/android/user/UserRepositoryTest.kt @@ -186,57 +186,97 @@ class UserRepositoryTest { fun teardown() { webServer.close() } - @Test fun `fetch listen count for existing user`() = runTest { + // Fetch listen count for a user known to exist val result = repository.fetchUserListenCount(testUsername) + + // Assert that the operation was successful assertEquals(Resource.Status.SUCCESS, result.status) + + // Verify that the listen count matches the expected data assertEquals(listenCountTestData.payload.count, result.data?.payload?.count) } @Test fun `fetch listen count for non-existing user`() = runTest { + // Attempt to fetch listen count for a user that does not exist val result = repository.fetchUserListenCount(testUserDNE) + + // Assert that the operation failed assertEquals(Resource.Status.FAILED, result.status) + + // Verify that the correct error is returned for a non-existent user assertEquals(ResponseError.DOES_NOT_EXIST, result.error) } @Test fun `fetch similar users for valid comparison`() = runTest { + // Fetch similar users for a valid comparison between two different users val result = repository.fetchUserSimilarity(testUsername, testSomeOtherUser) + + // Assert that the operation was successful assertEquals(Resource.Status.SUCCESS, result.status) + + // Ensure that the result data is not null assertNotNull(result.data) + + // Verify that the similar user's username matches the expected value assertEquals("jivteshs20", result.data?.userSimilarity?.username) } @Test fun `fetch similar users for invalid comparison`() = runTest { + // Attempt to fetch similar users for a comparison with the same user val result = repository.fetchUserSimilarity(testUsername, testUsername) + + // Assert that the operation failed assertEquals(Resource.Status.FAILED, result.status) + + // Verify that the error message matches the expected value for invalid comparison assertEquals(similarUserErrorString, result.error?.actualResponse) } @Test fun `fetch current pins for existing user`() = runTest { + // Fetch current pins for a user known to exist val result = repository.fetchUserCurrentPins(testUsername) + + // Assert that the operation was successful assertEquals(Resource.Status.SUCCESS, result.status) + + // Ensure that the result data is not null assertNotNull(result.data) + + // Verify that the pinned recording's creation time and blurb content match the expected values assertEquals(1.72335654E9f, result.data?.pinnedRecording?.created) assertEquals("Noice", result.data?.pinnedRecording?.blurbContent) } @Test fun `fetch current pins for non-existing user`() = runTest { + // Attempt to fetch current pins for a user that does not exist val result = repository.fetchUserCurrentPins(testUserDNE) + + // Assert that the operation failed assertEquals(Resource.Status.FAILED, result.status) + + // Verify that the correct error is returned for a non-existent user assertEquals(ResponseError.DOES_NOT_EXIST, result.error) } @Test fun `fetch all pins for existing user`() = runTest { + // Fetch all pins for a user known to exist val result = repository.fetchUserPins(testUsername) + + // Assert that the operation was successful assertEquals(Resource.Status.SUCCESS, result.status) + + // Ensure that the result data is not null assertNotNull(result.data) + + // Verify the total count and check the first pinned recording's MSID and username assertEquals(12, result.data?.count) assertEquals("6f4a50ca-b636-4c0b-a6a0-5b84451ab014", result.data?.pinnedRecordings?.get(0)?.recordingMsid) assertEquals(testUsername, result.data?.userName) @@ -244,99 +284,174 @@ class UserRepositoryTest { @Test fun `fetch all pins for non-existing user`() = runTest { + // Attempt to fetch all pins for a user that does not exist val result = repository.fetchUserPins(testUserDNE) + + // Assert that the operation failed assertEquals(Resource.Status.FAILED, result.status) + + // Verify that the correct error is returned for a non-existent user assertEquals(ResponseError.DOES_NOT_EXIST, result.error) } @Test fun `fetch top artists for existing user`() = runTest { + // Fetch top artists for a user known to exist val result = repository.getTopArtists(testUsername) + + // Assert that the operation was successful assertEquals(Resource.Status.SUCCESS, result.status) + + // Ensure that the result data is not null assertNotNull(result.data) + + // Verify that the first artist's name and the total count of top artists match the expected values assertEquals("Karan Aujla", result.data?.payload?.artists?.get(0)?.artistName) assertEquals(25, result.data?.payload?.count) } @Test fun `fetch top artists for non-existing user`() = runTest { + // Attempt to fetch top artists for a user that does not exist val result = repository.getTopArtists(testUserDNE) + + // Assert that the operation failed assertEquals(Resource.Status.FAILED, result.status) + + // Verify that the correct error is returned for a non-existent user assertEquals(ResponseError.DOES_NOT_EXIST, result.error) } @Test fun `fetch loved and hated songs for existing user`() = runTest { + // Fetch loved and hated songs for a user known to exist val result = repository.getUserFeedback(testUsername, null) + + // Assert that the operation was successful assertEquals(Resource.Status.SUCCESS, result.status) + + // Ensure that the result data is not null assertNotNull(result.data) + + // Verify the count of feedback and the name of the first loved song assertEquals(25, result.data?.count) assertEquals("Calling", result.data?.feedback?.get(0)?.trackMetadata?.trackName) + + // Verify that the feedback user ID matches the expected username assertEquals(testUsername, result?.data?.feedback?.get(2)?.userId) } @Test fun `fetch loved and hated songs for non-existing user`() = runTest { + // Attempt to fetch loved and hated songs for a user that does not exist val result = repository.getUserFeedback(testUserDNE, null) + + // Assert that the operation failed assertEquals(Resource.Status.FAILED, result.status) + + // Verify that the correct error is returned for a non-existent user assertEquals(ResponseError.DOES_NOT_EXIST, result.error) } @Test fun `fetch user listening activity for existing user`() = runTest { + // Fetch user listening activity for a user known to exist val result = repository.getUserListeningActivity(testUsername) + + // Assert that the operation was successful assertEquals(Resource.Status.SUCCESS, result.status) + + // Ensure that the result data is not null assertNotNull(result.data) + + // Verify the user ID and listen count for a specific activity assertEquals(testUsername, result.data?.payload?.userId) assertEquals(2826, result.data?.payload?.listeningActivity?.get(21)?.listenCount) } @Test fun `fetch user listening activity for non-existing user`() = runTest { + // Attempt to fetch user listening activity for a user that does not exist val result = repository.getUserListeningActivity(testUserDNE) + + // Assert that the operation failed assertEquals(Resource.Status.FAILED, result.status) + + // Verify that the correct error is returned for a non-existent user assertEquals(ResponseError.DOES_NOT_EXIST, result.error) } @Test fun `fetch global listening activity`() = runTest { + // Fetch global listening activity, which is not user-specific val result = repository.getGlobalListeningActivity() + + // Assert that the operation was successful assertEquals(Resource.Status.SUCCESS, result.status) + + // Ensure that the result data is not null assertNotNull(result.data) + + // Verify the listen count for a specific activity and ensure userId is null for global data assertEquals(4499100, result.data?.payload?.listeningActivity?.get(3)?.listenCount) assertNull(result.data?.payload?.userId) } @Test fun `fetch top albums for existing user`() = runTest { + // Fetch top albums for a user known to exist val result = repository.getTopAlbums(testUsername) + + // Assert that the operation was successful assertEquals(Resource.Status.SUCCESS, result.status) + + // Ensure that the result data is not null assertNotNull(result.data) + + // Verify the release name of the third album and the username associated with the data assertEquals("Small Circle", result.data?.payload?.releases?.get(2)?.releaseName) assertEquals(testUsername, result.data?.payload?.userId) } @Test fun `fetch top albums for non-existing user`() = runTest { + // Attempt to fetch top albums for a user that does not exist val result = repository.getTopAlbums(testUserDNE) + + // Assert that the operation failed assertEquals(Resource.Status.FAILED, result.status) + + // Verify that the correct error is returned for a non-existent user assertEquals(ResponseError.DOES_NOT_EXIST, result.error) } @Test fun `fetch top songs for existing user`() = runTest { + // Fetch top songs for a user known to exist val result = repository.getTopSongs(testUsername) + + // Assert that the operation was successful assertEquals(Resource.Status.SUCCESS, result.status) + + // Ensure that the result data is not null assertNotNull(result.data) + + // Verify the user ID and the name of the top song assertEquals(testUsername, result.data?.payload?.userId) assertEquals("Small Circle", result.data?.payload?.recordings?.get(0)?.releaseName) } @Test fun `fetch top songs for non-existing user`() = runTest { + // Attempt to fetch top songs for a user that does not exist val result = repository.getTopSongs(testUserDNE) + + // Assert that the operation failed assertEquals(Resource.Status.FAILED, result.status) + + // Verify that the correct error is returned for a non-existent user assertEquals(ResponseError.DOES_NOT_EXIST, result.error) } + } \ No newline at end of file diff --git a/app/src/test/java/org/listenbrainz/android/user/UserViewModelTest.kt b/app/src/test/java/org/listenbrainz/android/user/UserViewModelTest.kt index 9887d277..b0559c66 100644 --- a/app/src/test/java/org/listenbrainz/android/user/UserViewModelTest.kt +++ b/app/src/test/java/org/listenbrainz/android/user/UserViewModelTest.kt @@ -31,19 +31,37 @@ class UserViewModelTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun getDataTest() = runTest { + // Start the data retrieval process using the ViewModel viewModel.getUserDataFromRemote(testUsername) - // Ensure all coroutines and tasks are completed + + // Ensure all coroutines and background tasks are completed before assertions advanceUntilIdle() + + // Check that the statsTabUIState is not null, meaning data has been loaded assertNotNull(viewModel.uiState.value.statsTabUIState) + + // Check that the tasteTabUIState is not null, indicating successful data load assertNotNull(viewModel.uiState.value.tasteTabUIState) + + // Ensure the listensTabUiState is not null after data retrieval assertNotNull(viewModel.uiState.value.listensTabUiState) + + // Assert that the data loaded belongs to the user themselves assertEquals(true, viewModel.uiState.value.isSelf) + + // Verify the number of similar users loaded is as expected (3 in this case) assertEquals(3, viewModel.uiState.value.listensTabUiState.similarUsers?.size) + + // Confirm the listen count is accurate after data loading assertEquals(3252, viewModel.uiState.value.listensTabUiState.listenCount) + + // Validate that the first follower's username is correctly loaded assertEquals("jivteshs20", viewModel.uiState.value.listensTabUiState.followers?.get(0)?.first) - assertEquals("6b08f3d4-0d56-406c-b628-d0afe2ad5d44", viewModel.uiState.value.tasteTabUIState.lovedSongs?.feedback?.get(0)?.recordingMBID) - assertEquals(23,viewModel.uiState.value.statsTabUIState.userListeningActivity.get(Pair(UserGlobal.USER, StatsRange.ALL_TIME))?.size) - } + // Check that the correct MusicBrainz Identifier (MBID) for a loved song is loaded + assertEquals("6b08f3d4-0d56-406c-b628-d0afe2ad5d44", viewModel.uiState.value.tasteTabUIState.lovedSongs?.feedback?.get(0)?.recordingMBID) + // Ensure that the user listening activity data for all time is loaded with the correct size + assertEquals(23, viewModel.uiState.value.statsTabUIState.userListeningActivity.get(Pair(UserGlobal.USER, StatsRange.ALL_TIME))?.size) + } } From 97b98b9908640be9bc6d78f0f364e9a27feff5d2 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sun, 18 Aug 2024 11:01:44 +0530 Subject: [PATCH 85/97] Remove build files from commit --- .../results.bin | 1 - .../classes.dex | Bin 137744 -> 0 bytes .../results.bin | 1 - .../classes.dex | Bin 137792 -> 0 bytes .../results.bin | 1 - .../classes.dex | Bin 120316 -> 0 bytes .../results.bin | 1 - .../classes.dex | Bin 138012 -> 0 bytes 8 files changed, 4 deletions(-) delete mode 100644 spotify-app-remote/build/.transforms/02890a36f7f1b7de65370910e6d441c9/results.bin delete mode 100644 spotify-app-remote/build/.transforms/02890a36f7f1b7de65370910e6d441c9/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex delete mode 100644 spotify-app-remote/build/.transforms/1acd802fc05d76d1989897fafaa4dcc1/results.bin delete mode 100644 spotify-app-remote/build/.transforms/1acd802fc05d76d1989897fafaa4dcc1/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex delete mode 100644 spotify-app-remote/build/.transforms/600d275753de81492ac32240018e723c/results.bin delete mode 100644 spotify-app-remote/build/.transforms/600d275753de81492ac32240018e723c/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex delete mode 100644 spotify-app-remote/build/.transforms/a236cf5d8026cbceaedc55a536fdbf38/results.bin delete mode 100644 spotify-app-remote/build/.transforms/a236cf5d8026cbceaedc55a536fdbf38/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex diff --git a/spotify-app-remote/build/.transforms/02890a36f7f1b7de65370910e6d441c9/results.bin b/spotify-app-remote/build/.transforms/02890a36f7f1b7de65370910e6d441c9/results.bin deleted file mode 100644 index 52daf05d..00000000 --- a/spotify-app-remote/build/.transforms/02890a36f7f1b7de65370910e6d441c9/results.bin +++ /dev/null @@ -1 +0,0 @@ -o/spotify-app-remote-release-0.7.2-runtime diff --git a/spotify-app-remote/build/.transforms/02890a36f7f1b7de65370910e6d441c9/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex b/spotify-app-remote/build/.transforms/02890a36f7f1b7de65370910e6d441c9/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex deleted file mode 100644 index bc99224bc5c3d828a98f8cb20ee6780ef815fb14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 137744 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 CH5%Oj diff --git a/spotify-app-remote/build/.transforms/1acd802fc05d76d1989897fafaa4dcc1/results.bin b/spotify-app-remote/build/.transforms/1acd802fc05d76d1989897fafaa4dcc1/results.bin deleted file mode 100644 index 52daf05d..00000000 --- a/spotify-app-remote/build/.transforms/1acd802fc05d76d1989897fafaa4dcc1/results.bin +++ /dev/null @@ -1 +0,0 @@ -o/spotify-app-remote-release-0.7.2-runtime diff --git a/spotify-app-remote/build/.transforms/1acd802fc05d76d1989897fafaa4dcc1/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex b/spotify-app-remote/build/.transforms/1acd802fc05d76d1989897fafaa4dcc1/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex deleted file mode 100644 index 1822258368a6e70582767bbd549022cf0820e99d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 137792 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 G=>GwX{8Zrp diff --git a/spotify-app-remote/build/.transforms/600d275753de81492ac32240018e723c/results.bin b/spotify-app-remote/build/.transforms/600d275753de81492ac32240018e723c/results.bin deleted file mode 100644 index 52daf05d..00000000 --- a/spotify-app-remote/build/.transforms/600d275753de81492ac32240018e723c/results.bin +++ /dev/null @@ -1 +0,0 @@ -o/spotify-app-remote-release-0.7.2-runtime diff --git a/spotify-app-remote/build/.transforms/600d275753de81492ac32240018e723c/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex b/spotify-app-remote/build/.transforms/600d275753de81492ac32240018e723c/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex deleted file mode 100644 index d14c20953ff9657ae8bac2ed88acd76f2629adb5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120316 zcmbrn2Y6IP)VM!)H;oWrv!Mo*CA45&Ix0#Kkd6oofJyy1^iL0G@_9uoPZ{O|T7iz-~AIN8mV|fUn_O_z`}Ei{P9yCKeJP36h~K zq`@_CJ+y*O&=c;0kuVJw!y4ENUqkF~)DdbzV`u{rxDy7$Fc=LJU?!}9O>hjpg+Cza zyfNh=9cn>CXa=`JXLt*KhdVA9^EOoY-I(Dp62`()FdgQ?0$2hs!CUY)Y=U=T4}1t8 z!Dny^euZLxP=BZbVYms}Ll%sNsjvdpzz#S7pMvjCV~RrvYCv;n1?`~=^oMMC7@mNs zumZNhQMdrbFB+2yS3(`=43EQW@Gg7{f5T;e8B-r_hrTczCc<<5$VnD(#$&O)`IWA1=wU@QC%)lwXDFRX%} zpk_(OjDlSdEajM<@B(}Z6-rY_coBYwnq?ew7p#Ww;Ht8Y=?j~oXgSBUhOzJ-R7iD9 z4_F4jz*XhxH&_9mL&*w`$$)!c7Hol&5WI|W4tK$HSOdo)<#PHL9)Okb4Wv|b%#F|o zX24!BA=(x)VIpjRFCkFLG40@CSPI7=F^x8aN8xq&8cJ1W48vV81$IJA71D%3uoymv zVpSb;9XtqY!Kvn$mhdRN1gD^4b;4i>EQ5m(mrlRK6xavFYETCl0UP00xZw)NJO*pw z6YyQ>n5&@&On{Ye7Rq16+yl8V1Gd4pQ2c883cA5#umbkOpK!%B)D33BR`?M@*V49d zFHD2A@Cg(RGcQ1I7z1y@G4R!-KcO=`1~0-M_zp_cVm^TgWW!@H6PCkKI0fgSNNwg0 zxE5MNPq+^rhvl#yK7g;`0#vBO`~v-8Jj{bl@Fm37bxb*^53Qj;42MbZ3cL?Lz-9HY zfm`7&coJ5_Zukr?K(YGF6;KzNLnhn}!(l2chSjhOeu2~m^cQr4@$fusfg|u0d!2C5hdZG^ z+zSuEc$fwA;RRR+yWvMDnL%4XQ|JbLVFXNq=imj{4&TBr@DCJgOgqB$&;{;=N$>`o z0RPR5WoQVuK^M3ihQSnA0;^#s9ETs^4+u2j8ip(2dT0bqp*3`b?l1tx!wc{}{0^q6 zW0IgeREIF!2(95(=nRj*6j%q_;8XY=Vw#aJl!qE{19XFXU<8bY$KffM1kb>1SO71; z%kTzlgPpJk_QMhQ1ipaN@GJZY{#)pGr~=nO185AbAp&G5NNmCQg(qMqoQH}n8JBQB zJPGsRHP`^}!D09Uet!}6Rw9Aa4U3%p3n~-gi$aKCc-RO3@c#`Y=$H7 zDV&5K;17sx%{YW|Pz|nu8=yI~gG}fHgCPeVfDteeo`WUu60C;pa0E`lS@;VQ+c3vM z7@9(7=m~@2K^O;fU4#6;8lK2zKK7hUU;8hQQM>7hZyQ;UIhtKf&KntTX#&xEyLgU1$q; z!T=Zo55Q=63TDG{cn#LVPB;Lc!}o9jir&tB15|`6Py^~iTNn)Y!DBE1o`L6J2`qzG zU@g1{pTlYR8GKzRCsc-O;6`W*na~f0!ecN47Q<_>1@^;N@I9Odrz`CQmq8V{25y43 zkO>1J7e>Jxcp28hR@eid!ng1z6u*PH7_NtA&=LB;UGN}Ghv#7x?1p3T1H@-KCJnBG zTc8I#0FS}bFdtUHYFG!m;4pj!-@|$M2jaT1&OH2eY=A-)IW11duUXaje`U>E|A z!6cXoFTz`}752b~@CEz~=OMNyYYA)=mEJf5~jcl@HT9P1MnsM z1HnFAd(a4Oh0f3u20|`84AWpXEP>ZxEo^{oup5rR3HSm2gv7qo2P#1oxC-h(Ludva zpg%kckHaLG1&iSocn5aFAvg^eA*CPV7pg!#XbRmS3r4{Nm<|izMOY4R!A95x2jOG* z3ciP5Af`XpCsc%*&>T9$KzJCY!XkJXw!-^x3{JwY;2S`Es0cOT251Gh!ky3;2El#s zFgy;EVJ@tIO|T1&zz<*sG9I8LREPS|8U{lyJPJ?3G?)jkz$W+*K8929Bm4$`L6JeM zIZz3%g@(`;GNBjT0}sGRcmgKDN>~H?;4qwk@8EZc8B9Mw6}Sd6pfyCG2iyb0;0c%s zOW-y55RStcxB!WF(U(vi>Oxbv1Ny;(@EA;hSK)1VA5Ow=;M~nM3^kxGG>4AR3+{nY zFbU?vQg|IU!gkmTpTHUT3I2ejd#Df8ffmpWdclM61T29S@D^-^58w;<6G~;V7DH3$ z1_NOzJPwm#8q9_lU_I=IkKrr$7A}Cfm$@0rKvk#%H$XFJ10A6&^o4ui5tss{#&9zVQ(RBMM=|o zMlX58rDeD*48z+nBn?B$=+H29hoR|BF5Cx0;C>hi55O>Z5FUbu;Sm@PkHQET38P>% zJO*Q6EIbZRz&IEWPr_3$0iK45FbO8Z6qpLrU^>i%S@0}82eV-g%!PR{9~QtuSOiPp z1$Ysb!ZKJ6E8rz~1zv@funJy>)$k^)fpzdUtcQ1C18js%uo)KfsT07Jh=C;TQN7&cSbR9xlM|@CW<}7vV2(e8$82;5xV-Zh$jz8PD~qKsBfi=}-f%fGgoDxEij3Yat9W?-12~PVld|w|!3^sCy6Y zFVuYy_bGKB$9+=Wr=#3waTljOc?4-L;7(LG4_1s1cO2pJk#KP*5H9X`+yUH0aTmc| zR_%-8mXD;FgbWd040mzd>9{348FwAEPr)r8$*Z*MG;z4A*L0-JH%7S| zsawh;ZYi_4GjNZKa=)bYlJIDo$=D>T{VdI^F*fp%yqdXA>%K+Zl71W9l8)H4jk38l z%H1Q%Uff>3eQ-;kN?VD00d7gNzuHLnAlyp{mo^!Ud!^2^Y7FLup%aOWP)> zd%dP9;bpb%;@(EMq%ZD0Q8ov0Cu#WcsPNNK?z2(u3sG*VmyAhCQ^us1zWLEDzl0~? z_QFd>g;&5W?JqXc{!*7QxV>>GZm-?MJz4V=cRF#U-NYTnEqyT-w|vAs&UIS%cy){Y z1l$eO{^_W2ai93kO>-JlP+yk|2;%*-m-bLfi*0@pzaUa#Z z#N87cX&-S*`-sgP+ykTB(&l0_m+)*27q@34?qM1}54Y4&#?J!WBXLVyaZga2MYyGH zrL7j@mXEam^GIob314ADUH2+=OWEGWEpa8y4Y;4tyu>|6)8C;slFokI@{#n#?e(v? zrJNFeK*MD`e2rVigXDD*x0lyx+)LFSkylP$yRaWAkGQ2go_i&3X$J}S$|i1UPf1_g z(s!Qyny9#&qTF7;h`r3=Qm=Ek&_wK_T~_AOZjCE5w|yo zh}$chxTS28uec>&X$M~Gupg<5xZ7*Kyp~~-$VJCS_ z8{qy)!!vOItnN0re^GarD4T(}f7S3IxVMCOaNJ&7$^7iaeF=Lne07w2 zJ?@LzR@-oU?RFfuSFh8!y>x!X{kPg*#2u^UPx?9EW+3@`aVrq+<(rP%3lHP=@@g0r z-Xh9Ag4@e?7-{~l`F4v6m$}!ok-5c7e_)i2%oXP~Zdu$H)I9_nFU^s-y>d>#?bmue zgF8muOQLMnM7d>N^{LG!!o7MO#Ql%vbpp3h8(Ap4yh6C+G(0^jyiSx`=4#KrA>m$q zWp4E9+m!HlO-JU^1a-G3yokEH;7(L`&!}|dp2BM%ndiOs$;QUZ_mL=jnLoWUgt3=< zp%k4bFIRU9!YitKC+?8C<({IFy2p^FXVZGEwfbQSNe4?($LY%hg?klcOhNxliJ60BMy# zc8+0P`FQ3Q?5IB3-T$_&qPYT-MCYc_PJAKcMb2w_~?ke$cUA0yxyEXF2pZ# zk;=tZ#yh)6ub;VKrXl-tdMV?hKYx#^9HH`MmD`9nfb@^y7nx!G=giG2pSLp3rZ<38 zrjzaf#_tj2vX$=m8pK`bO8kRqmwx0g za=6O%D&J8l_W+`Qfbt9`#VIzPTdu*3%-5{HkJ(`5G_z6D-GjW_G&DPr_iDS|%Xr?7 zKU@8|r27cziyWnLw93a+j#2r($`4fTRk=^)ew7DQeyFlL<(Bk&sO+h-m&&Osr>T5K z<#d%ZRL)fSoXXiM=cwGFawqaW@>@;*BHvK?rpmWeu2H#G1piRd zy&oyT(0t{ z%3~@^QND*MkJL}3)K6rxhD&|KFZFoDEHW?B|HDbQH}QuPzQoG4W{wSa$LDY}!n|qy z51MVrQQV!cvvR-LWaUV+1v!TJW39a0c|!e9S{d&=r~Y|X`piQ0zohbIE8Y2ZjMQJl zk0Qrvx?{P{C0+4Lx?_!-uJ|S0vBX=Ta*^6e`l6Tg$Ljnob_dk%L)A+@nHAj?Lh%ZwB@=*(AM=oNb;pv#fm1tgzB8 z$84@+x!z}!uk_0tdTN}N`)&Ko;covB{<+9!t#rfZ65|8wKVbGE=h3d?tt{cpSN}eh z`;qgxGhB;YK)K(t(p?u8>ioV$f2I0YaXR`6`q#)`);p1#>7UotFH-!g>8C!7^Ec@45!i|R&dPHp5&fI^ zKS%#2c6|w7L%el_uSG@(U(0ywWM#bbla(7y7wp#Zw*D>Ht;4Q0{~AG~k)NuRc9ZyNHoaJh=gf;LUsWmbw(`_42fMB0C;cE&`a$Fx^xH`9IR5SU@ggC~>H4?FnlgkEH4mA9+xqOz;XJ5**`>GsP`(l14NyC|2` zN2Jt8q>K-dQm^;;lYQTNw98=1{~qxg626;13Exe4U)oV*Kb8Ge4p2Ez;{m| z9#RUD&K~NOV&!gA(n_9BTe;7yMZQmdeaJ`Tt(0G6dzCk!|A5)QE9r{trE)2Tz@PXJ zsQ;ka9pbu*A>TvlKa771{v+7Eh`q?ADj%X8N3a`BdLkdg?x^9N4gAL_$1_&C{*Uy! zjC10gkBDE~Deins`-q%^KaTr^E~FR76CQ_naom^GuyVAy(#ixgll0?Ef|+IITjm{P zy!q91A)R*s6(AKDP>-^{?aNRHS4XvB;TbGyK&}YDQ#h8?dWMNyCKKGUaXlbt#vh^3|1+TNyI3 zR*p7t$oksu^(nXb8<@*XFXG>z={DpEQa93VXeyZi`iABT6SVS5bD5P_o2#w7#@uLS z(CKMqcQcoCZZ!4GO5%%LWo3Qyn)+Y2GL!EP$$jM#b30{i#Q2kbXhh5Ivf-7S_mCM{ z&J6BX?;u`=sbc!u@Tw-)%4+5jE32DFtxPwMBO9CFZF-G)^Fq>XOwT1yzQ)wIgp~tL zNd2{}oM-A;8Fa=H|7K>+AMoGI3Hh(cCaiF;kbe`xKSee(H<{vuH^ZW)l{cCI@ou37 zQ}N$Ii-~N`j4Jszr=Dl=w_wH>e+%=o`2~MVv&s5flF!fhTQPHsz7_Evw*D>V5i1Yd z^0dP4NBpgs$HcBR`<0rMM`UX&-Ey|pa<505X zWs(iQneTr|cvF=(n{>5rX61FJv&!48Y-Ap`vVj?HWeYRX%6evk%BQVtVkTOdX%?tl zsQT3^-$1t0a<^k^`I3?R1>B({b9)xZ|`PSC)*^cE%m2?Ku6E zaVoNpl_j0KtW0nQ*?be6d#rTF@vZdljii4o?URI^_|sI@$L?0{u;;5>glumdN6LE} zrSLxcXx25%XKGyx$f?uymH-Ts(q%8<4hgLnOw0lj=QlEN%^}mPA^d3JJo-uj??ZM zzq^k2?mFJPvqC+DeGl$fWZv(o_2@~w56QnL-^4s<vzXvFUl+9 zQKXE=UX)M9V=o<#;+OF#dKr(sj5{7h%6RO>O!1JF?zrs53@78V7gwH)ONl4rQlyN_ zUR>ESE_=~3GA>2RxD>sNOOY}zds82ouS81w^yY1aD=24gt#5Dcgz8&=DQ5@veHZ~U zZ}h=l=8ZnA%)j2JO#C`z# zFS+gqX!-**{Q;WJ03GKddy(z{cI%cW z{4&3Yl=&r_aVYbPNSR-9wEQ{T2j5S;T=fr8dB4h`NExRhr5zsNZJ_T-_W|?0k#*() zbJngu5`ND5zq8l5=>N9X9M zdVP%`o?O2p^tuu$*VPEUjz(ZF*UdsJL>&*z{Xzjnzlu-J6jP8fVu!6~cXbcurh$phDm90!a;*DYD9$;lF zGtkO5CfmxECP%}Es(ir8)^`6ih8@W$4IhmhYyPqI7|WwWsn1yQi6@`2yn$N8%F%oW zUHl9A9=fY9XC?2+sjQ&kQcj64~H@8h^a%MT&krE40j;<2Bv!ny&aIJ@JctQsaq~cu#72Pinj; zHJU=U)=aZ>ApGdgOCsU0(pNL-Olc~HUDD%lwt_zt@ zBwXf`soc%Wd@_yuOG$ScGn(|zG*(FI$7!sLvTjUcMorZ+*=bt8XE;%+~QBdKnM1bv(>wM|UsnJ)0ffeaJbS#!9*8m`}_h;?Lo{ z?X;ELvk-5t`PS;^qCY_RT-D1vTcV#w`VxO0JHB7h&oh0@IV*dc->l3w)vR4l^QD!& z%qc5*KuZ4e7!UQV{Lx&e{_BzRHQo8T&WMzCWK}zRW1LkTK<*hgw?Oq@~_nL zuj1rZ;;rIjM&^N4?C7LitF&CJv|O)gKCfwizNX_+o(0Lce2w&8r#!E#-Ro-iy4uOv zr`Wx&cB|EHHE)qg{;Snao^6TUYPEYq?cU%mGO>F@?c~{)*u9~4Z*q<=`ZrZC&x}O> zrt06)bk^ia_Z)1k`q%OtLE^8~`10&b;;+^C>(p+Yj@xx=C(qWzZk^h_jh)PQZ)C%?P|AO?Y67kyK46? zBU0>k;Ftc`q5UE6Vn~1N;0{6hV+Y^vk^b1J@pfwZJ2icIrX}sUQ`6t2cDr~pQPSU~ zcJj~^W$dz!CEv3pO)=X+`|&&I_5J+L zh}|AdSKiqWyFF_6zS_O7^?G0JJFIp`)b0o~wAdX{yCZ6Mg#AN*@;{>XM_JLt?x@-wRl8&6 z1si@$!{r?hDfdU3{zv9(tN%##^32`UyYs;pCdSDA`77N&ea#yHavhxH$^Y+!pJYdr zOnN7I&!ju@w4RHc=FV2^zR`TX(f0dB*Y9t1JwC(pj+aR149_(#Aiq<)?^XWDovozv zv-*G0^nTIu{-WhQ$J@(N|8p9Cj-CAGD*?rhkqdzU1?p z=JOjn>L$ehjUBKnr>T5K<#d%ZRL)fCw%d74=REx*`;80QeizIzd;b4BcaUEbevy?^ z%KaBRK55Utd7g6I$~oq8^ndeweG}y}jysPz&H~dGz2l_XeW2r%R~c*j&2i#XPPFT* z<4m^F=gc!-5zpr=HYZ8f=gc=7NY}6F#yCNz8R0R`FmoCi>y&e@$6loDH|0HP85gn6 zT$788*YJ2}xUG-8izfPbXOS6Z`+b zonrhnVV*tgXYhr>DXPC&^ik!P`+$My-&cK;OX$a;Kc@Poh4l0BFGc?~ z`YE(evrFjLpg*VaZz-hz8UG3NvD4f(XoEkx{&)EZ;LDK7s&88;zUb4@*Hitih4f#e z--rHj)%Pf*?~6Xb7cCd7zAyUZsQy`sKaBn*)%U|MpOUUHBZ!}Y{w>w_Cp?gm-+!VX zh<>-~2THuC^jDxChu-@Pa@BSWN|@+pq5o6k-$ht-|G$KOANtzQxM|!|NWTXCY4puh zFXJ`3|2Loy^Ci=}RG))Bdi)pVn#n*vO!c{i(wFjdK|db-68itXLg|ZsAo|_te?vdy z67h$j{|bGnrTjL`CG_La|AM|8dbu8>>o4igLZ2vK)aCO4MAxqj&v45MfFdL9=!Qk;L(S_Ka74K`drmdE~I}1 zeZppbqe1mkE}>6HKS%YlUPSlLl68)d5pMzdLUizgOznjn}%yQdb))Vo1_cQuXuDMVC+F zhqv$>E~;OsdU?lCKC3C;0`#-c_n`iZ2#YTN-}rZ;U$5~O7t;TO|1A3VRlh{^;H58~ zz*c@Q7X2X7f4)$B2YovFZ#4c3=%cTXqj+1O_dYMGL+ZcjJK1kDKcfFl8~lI zm-~-|ZT$X->fgqnoRPnNQACrDev#@o;g4?r3;3fyaz7j$e-QoGg7_yi{(kh){LKT7($Ro|L$mUpW^t9tp7hI6WKht8{?e8=r|;x|BF zZnj&8_Nrfql#i^Jo6t{0Ka~Ez4Zm1=dKu61nS*{L`VP3G^=+km#9yxQI~LO4hW;%2 zHL8yk(!YsbKJTJ`lJq;_j!yqA)gMIv4EoN6^lQ;4yvuLMpr40c?#-j)e~LbY{;cNT zrI7v<`UdF#R()61OZ)Q(f>RgfZuCiW_-z={zavVYfL`Xyap)VMmro{$OX3I6FF@`@ z-&6J7@O$G&zQ6AE|D&qEGfE#lKBlR@J9;nu5b-6S3#7kP^}V9>>FDp4`tM-=M*kA! z?;WKNqkjl}!cKkzd9GXjKBCw0-vIri=zF5SPxXVN;%A_j{o_dVkDy;g`gcd^6Kr;Zz%i23+R3G-2Ahn^j*-OMBjcFzu}|$;ZgdY=q0~d=m(L=oNVqEnwUf0-!xh<=Ic+oOL; z^(zSX@_$M7!_eQ@)iOMj0Z_~nzd$gSU6++O{p{KxSJKH#_cRKKo}{tW&u=xjT1 z*b=w=y9(*=K`%dQR9p4$71G~}J_CIZ)$cB(&q1G!ev0b%6w=>^eiQoLs(-(beiZt^ z0rtn}k5Io43hAFh-vE93^KSX~7Sg|pei-_Q>h~4WA4Is!X}3G`v~hgE;Lkp2twJ<z6*N! z94n+Rf&K#eT-AS6NS}&6e31Q(>IX#W*HeBuKk0&gHTuuVf0XKFUXafduo8V~&KBe& z=VjvZ^m2c56n%Zw_f);4BcB226Atkk;;QG@;B5TB343g4vI8=M7dJjaF*PL#FM_PV zo6b3*;{E_9j}@Ip_ID{HU!k1Z$h(DE0ePz|iKoJ)on&;eK!CXno6C`Dq?79O<#(&H z!!7s~t3kXUpGJDA{#L$VNeQbNGsp>;VCBlSOFM0i-`Cdh#|-ipCvCs|<6U;rVo5aF z;bQ)PuQ+e)aX*H?2H_={Y;Cm7 zGo7HeJ3*!mIw@19Km}Sz;z$hEjuUQpBv3}va>FBe;pGa1cgYJcTOhn!UU+JO@Sb_$ zfJFpR`rokWfQ4X zg>gBUdyxN*UzPaP(8th<(x&5ypNNcvsuI5%-=#lda>93+0HaL$UdkXg(S1|hrtgjn zPZtSPH@Trq6S8T?*mx5)?Mx?~T(6)_YD8UAZv5%kB#}o}U=shP@kVKOK<>jOEq;}e zd}XAwA3EXQ7iR_j;MJoj|jlP`FT&@_kifs@ZM@o0_ zesFpqEv5tC>N?MNDh~LvL)Y-)Qg(Q;iHVh*nvj!xZ^dWZdMSDJz@L_IugMNw<}5X7 ziS786xL){n@XRL7-yPQmo(`qOWaFylj3MPrTyOGDTw2^6xK{EOTx8}ICVoNusG$i@ zkyaApPc)s1Ls)m=!AFgs8(aSSOj>zye|? zsZ9%P`k7#2R_Giyk>_gA67Bt@>a--)0cIho{z?l-eY`$idqTb|Bfl?DnKxEaotgCT zJST-dzk*zn=;EBfy|i+YKN6~8TRv>#d-I`O<1%;U1hV-z$zBW6!_qIC(e*}#!V7py zJJH04D;jDRD9wb)w}5;>+}HX^+X>a`Y_F4Kf2wbkKPz+@+lxeB1789y(wxgcN=+aVsV+-lJpj#%olBvGYo{lm~ejU&)7F}E#EtVZF z=L`6g?MRdHeuz3nUvpP6datx&F@`qfw?om_#4ZxLiq=kb!qodZMo*gmHb$?!btGe| zE-@mZE4iw~s;pC!Hpt3IcETl-{5gTU9ck~1{=tl)Rwg@Rkg3u!E0pLkQe%txz4*+` zbM-;=*wj}3SkPp%uePP{eh7j*6#)h23fA)1^)^9&cE%7BN*Tlnlw5aZSd$$8Am?iGmzI(7;C!xg zg`DBOSvL}`X)DHwG(QR zYHd%yAS?%@7-o~4c z^7m(DX!ZQPP+@y7ogx3nbpE&WA1aiNm;Qg{TR7ZHf25a=?k{EBP!ThR@BlK0H&$g; zZor&kcNXz!zOR_w6a2OKT~fK$+&+8?dzqJ+V>4bh4m~J-_d1+%iL_Grb&;IV<^Cqj zJUQVCCLER4vr%ciS}3jg=wy#jFn+w>jqk1lFJMy?n@G4B^Tmt)3cj~YZuoU4;7hXC zj<;ScFJQOyUv{yyg!K6<*hyPRdTGv4YJbt!*i?vli(heF=C@Z}Z zU+H>wZxygxj@>cJ`Bq*zy*c)6?4tKP%@}8KluXLD8C@x4PT(45@kryK^5g9#UL_mv zZjQh@W!&U6V19PvuQQRR#XX7S;B!61WC}yh> zv`KGTD~TDWVx+y1^U-2F*T;G`{GR%pAC9iJCB0ln-2f{f5p zj?_miBn6pI-Rrd&HtzK*>-$q&2X9{TdJPt^dm6hv1?>D<-ZE;J#@mfS+I@vDKpV?9 z+}-Qn8~^38lYK=bEZ<^1NC|gRrlgqc&}pA{eb0y*ym#U*XFW14&&Rw>Y z?;7@~c_o??RU)r$*ZmLnvkTa}bK^}~4r#+wXRbTfu@4DUkr`3ePj{R&#ZLNM`Xnpx zBFBA+%mOQXA=@*IYj!POzqItqVEg5LEZ)uQk9kpT;n}w#QJc10lP*2vh&(36T1pBjtY&0oIqt}xulp&jfTZhZRwS(UxECd|5tvV-Jk+? zFZ`$7Jq7GWVz;Zn`r@URTflA>c5+?iuS4G6=mG3x9%w+XrgNqscLd&C^@!@cvw=4B zwtLUV|80X#_Lx}#u}<=(m<+DVtT5l`#Mc0Sq%x`cOITT{b}&$jyR`K9NE-8o{}!Lm zpX^I!?vVLd%Ir9amnV>t%+pViw%q^Zgg)b)oTRv%z~_`PDXus7ak-(^{%n&JUorl2 zN3PX)byvy`a+hA0(~(>J@xIiUh)HGZk`+$#H;GNMcV~V0y}<_bU9fbCxT8LwlYU0_ z`ZC^pbG+~*wIws>_B>Q5GNmL{zNA`5TXAD`P(z= zP4(x7+p)__HPfui&&kL$uFH}6z}ttuM&7cY$v@|G+gi??q`X<77{eK--M@Kr%A3TI zbt5|vB%L(gUCj=K{UMVXxXsQrp51!vB(DhJSpoSrh@68CU>wIXZhm0x%wMmzVC&wq z_DAQPtM)H&&$>fxdUNld73i+%m35?TC9bsPZfxoy)A(Lls`IivS3P6TRXed-5a#Eb zpPZ|nvFEB0+KsT-M@_JrgmsJ?M7yTN5*EMB7i=J5_a_XB?QDY0D?d<{d);`>U*lui z`s3r;#>OWM;-oJB-s3~^wr#2BuyP&xD9bUz%OZQCO>t69oj@_>tW4^_;HFJ-LMOQc zlRcsh4aKTY#-n^bC%$_=*P9(iM^l?0^auvzT&08^vto0yfK8-;O&saS_4_S01F%iy z+l@h|GXG6%s(&4K19P3qjNhL0=T^#_*`X)Ccv@v)foJQJ9s#oI5 z3S7;3Y6WgE!L*>iRw!sXbc$n-Q--t3q}a-V%N)K9=TD9OggobS z_LH8G6-c4RNwKM>F|%C2y3=@XJ=OOy^Hm+c%>3Em3jX#!X;C??Ni|Jui329WU4Nz9 za>I8x?Va@Uk@m4%o18LsiZz+ddYDMFp7hPTq?lez+;UEq>WruD%F@%)2eGzJZhP~L zn>tjLcJWsZ%NgS;Z5L_z>`)n#VRG!gFDWKc=}wc`n3JneSwhnjQezgI%C+78u48(0 z4Q5uBrzh?`mbAO$l;uH;+b=hfk3GxuRZb%x^O|jo5Z8KUum``^N1F#3bHVQ1ImfaZ zEOC4`EjKpL^NH^#?M$azf$(;D;obAr8@LRnBOk)Yn@r&N^!<4wo>alCcSJie^#IzB}wuJOIB`5->~wf`k!Y$ znD1}~n2_pk*!G19(r-3=jc*)vpR1=O8~SSv#haDtS?mC%@$y^mQ@ z+-)iEL)M)Y=wegL3Jp_##QJ+PXV#8wKn{K<*giYdgzGBRTpODexLx#)teUY-R)F7Y zp+#~6RVZ*0z4=6}Xi8xliA{BGwkv5YW3Q=Sc9~+670RWAT59g?a_GaX&=7LZ3S`-` zXIAd3<4R6w_|7x!VAqR`KkV6?H@;^Ro`zofh5b7H!YOjF59_G(WxUNTH?$UAdSWEd z2fL%VlYF_MbxsJ&{*1<)P!Yo1dwH) z$-@_EBKf)P&K(uM!<-X}bpj@h-!h0a?rS19_cN|djICdGMmr}^E7F)$eA5`M^XTD! z&}4^qIzHkiIF)Suy>sGM$XE6lIpLyCCBHjH7{&Z??{q@jM$gCGP~KM}j$g`Zvh1Ej z=41J+^TK(GA^U-U+h?o2v^)^nrm+IcycT_5^AmAJ=hg4Ny!<(LLFb-r$TO8nj8r)x4KnXadrDtY59hW7aSIo< z9I56Jp1PDH$lcF+dcM47%b9A17tnj_eGGbcz0V0G+V!-8BeQstzmmP<;DkI>)P`q; zYFSy)*Mt9!${mOD+jW#D_LNihC#USUPT3!vvVSUSma-Elzbp*J13UhXv~8$ z!rwZzSVg{%Dxdc>#r8AtuOq(P=SIT!GtOF)q}&&J_k$_8z5Br|+!bhpMBjD3v;X7< z|(?|YVDD< zAN-H-;r|gX^IySq-1%q}_Af&LV2kXb}Iz5EMb$KG``mU!-U^af+9g3r5- z+8BOYUzV4?*S3Y{31p`4ZFb5Z5aQOKdoxaIr6rif zW3xN%_C>0u#ct&Petg_1X_69#IHN=s8RCo;nK;B5BeLiaXN1V4AhB(7T@|10@iLX2~ZZKuZ^sS+t zdoV}H^(J#E|EDqatVTT}Ro2pf++{iLU6!29$X!;LcCW|}hsg7XN4R2II(0Z*dz*AJ zWKW-1%pZ)Ay*@ifUru-d?{dkC$^D=7zO%%UbwKvXGF~^5m&|J^F=1w%GWJYUq`hOv zkoP!(^4rKxuq0<;F}?kZOt6Z$g0rPro_NtQK7v$%@H@A(p-^bPa z9HTtgOWH;HAXrQ82|0aZ#~Dm+%lDXh?iCxXBrYktyf-E1A(GZ9(vo#C7?9Yk8cw9c z8WXH7ccQ@A z|2$o#ZLAX$tkX8m;YmjqC&kuF-ffby{H*m#p@+0Id1;AX?(l;(FP)~Bns@Gdfwa;{ zGZI=$8rRwJk{i0*CudBl#vQLx)~ry->LNTBt5h>L+|voO4=m=l@56*rv9C@lvKG2y zQ=UzEa|J&Oplg_n?VB8TeRJpFSah-`WjY)9|6|wNcL+qv{S0d_XSDLp%0RkS&atJ3 zDGhO?*CT;<>>A?DIRVXww;wRfbl#!Wy!oy)I$5W)0s|Q7^8O7c*}Rh_&&(s0B)=oF zzH>$P;;!X?UI*I!AJzdm?UX%cdGf7{OrWG`fwBC*8(aBJ^mk1pW;=I>n@uEcm+6qO z&2&iIX(B~8nMl$`6DhXEM2hb)!4e6Sg^6j|&=QYOI{B`NcP~*<+aS}~nAZkmW2)qZ zZz>R8BQJb&f$(ec!nYI%udU%-ooxle8|2wL&E#!h3_j6J}58z zV1e+@zi`K4hrpo%;aO@Q3A|UJ4)^7S?|leC*YUb92n zv^T#-;@uNT-g~fj_d(;xe=W;>^EjB72j`Z7 zXKWrvqw%#ELrhK&E-*yo0yoO*Af{`EZY_u-?bb)xk zMe8yq+IUh%`TT`W?!m94M3<_E*!WIH+f3KGeq`(FjpKN1r2L%%$L;mztuI9huR?fc z@MGrpm9lPd&vqH7TOV_(TMqdNl6TmV-uYJpo*ali5^%$$ZF575SsRkUZQT^owa?`P zpOUWpVn}*IF;>Zwwse`ql-M$Yyff9a@^w4*AMVLZIpT-#Me`i)iTsx2n*g>Y|EtAhjC*Tv9nzE* z$$t}OIx8_P<+$lOq`Qjti;g4bUvb3AbUw?we!cg&Zy>#^(M7_aktSchqsEb%;@)M~ z>#UGrPbcRT6*T-k8y>0U+DJ=gh2niY0pk15HfDIY(>PY%t?BDz*m8Jh%`LP&B0Q(| z@{;t~3z0tGgQ(<&%RWKUtjS7Fnqp0w8Jed1mX3@7>9`1K+A-vFcpjJJ?rlOB zotm~!+`ZkM*vs?eROeu|s8!!d4IxbvVD;(aCA-Cb~M6=;j>OSXm2wnc#cLtdHzH-G8NtZpVF8#`3d zei#&*kBoBnVLP-y|EV0Sd+GL z`&h=PoH(@@_!G|D5_y*)E2F&=xH&z(SWKk-PuR)m zi$s3nZL&DZm1M8)h3xd@83lG``SoP(w!2WZZIbPzWA|2;yO?d zQjd?!a#J71_X$XyLjXy?gr+a^hl~=L8dcH z*Zj&vYJEr8%rU)lU*-Ie^ekzXt_JG_@Q%RSHa?$LK^ zS#krFZU3hSkbC=o|FkUl{;p6Lqo<$npr?vrFfUno%`+CBKPBx$G^++xh;!a z19EL1CO^5~kvUu5hnQ-zL+{XPnLKaH44yEVDbm+n?I`!wp;N?(BTj?|9a(`0eIi#t zew$0a-hIJ$*h-x>#`a;gJl|5Bx+~lbBz8cC7L|GPVYz^*m4QMlrK} z!EBl6T<28e$;}C#+(-;RXEDsA^p3n07U_7Nu&-#PM1NNJcAm&YI0%h&A~clXyUp+& z3Wr}g4$A#?+gP58@RYKWNiR~2=V24LAL2PB=NY9aq3m;NN~<{wokqB&?1FPj&N*Zc zlz~_FJ*=7L34f|Hj_(#c?)Tn#k$YCr=LBw6yRU7%r5|W06HhwPdp2)PL}mL!-i!Em zn0v1&@#UP1ONh2VLyfPsc}TB&aq{m)&)9zU*1@9WD}CnemCsNrFAl+O-aLiy!ruB^ zua&^JnvUe|eq`Lpsk3J*q4{BMXmX+azSaE1u8iu^c;6yB^f9B&O-pogza`~NM`|GU0&#ZlNKc8ss4ZMsy68Mpw+!|`)t`qJ! zxE_0%oAT{hL-|n$d43`?H}DPFx_u=1H~L@g&sckJFV+-$nOh}&_k1Clb>utJm-lyQ zr%KX3-WkJbTRLz5+LE|3rb3}Pu6^KV-jklgs>eJZ`q<~5y$0?1tMq`BPueXW8NoIy z@F|V#J}+~3o_t?J%cJAZ{|i@$_ii%xYn*56`pR?nQTTtN?rwSb{S@Msv1$K8OQzW? zU)~Is86d)co-J=Bhh(i$UtfIgjO48|-%vm4Z@2e*lAc??xmv$-)K6A*=KKGspPZAs z^^-Lrzkaf}miqT6P1z^8^Js4~%E=6zH<=}W_3FjGfqDkdSwFKj{@M6{i>hZ%UOj)k zL_KBA`GK0b^>kOD|GA#SNKfu_B7qB}*V-Slxz4dWc<;H#qu99P#4l?M&q_vd-O0P) zY4mt@#(BrrI8x^VW9Db>&D!~6c&g{MSNYU^6{pZ{ce#L zXF^_?{)~z(7k=+Os!a4TWtwfWd6wqz=Ez*i^rz!*9I5grdxCRrnRxrm8;A1D z*?qqIJN=Z&B`$A59A`Yb>#TQuJVzdt$V>W5-mfZQpOf9;m*-@^<1fk^iJgd@$y;O5 zzCR22{;=cB+fP1Enw7owpsm8}^ld9J%6Kcuf8L~*3Z~c-S|ew&slKP|8ZNub%6|7< z8ScMAa~!u{y*67#x-uua{dg<==v^DeyoFB2y4we$laaL_zqD^bTUm?Ubn%93b+4oRmd^<^ zrZ}51{dg=lG z2ilMcHVxJdR;yLBHoi1dt7ds*7|9#WCnU~5;>aj=@ATU6Yq0Xpaa*SaJLKGqx=yf3 z`kw#YuA$hi`QPoX6T2nytiY|8bK3AfTT!p}m#SCAf3;O>ry6^*oWPfMrj|Bq?bKsu z7K}(c-eSvK&#BZT{Zo0K;hql_Bai=o{ZT)!Kjb}c*@KoO&1rUiyPVKHix;2osLOep>@&Q*_$cy7z^{8r$M0nNVr&`QGp_OIr9UIdSMvO< z0na!K_UB~PSL7LARz}^tKJ~`UO!RVwz;`4lVUm;P+d#QTabA_o({XpqNE+^#sGS$z z8x!&!QMd2QoQnFt;7$9!S!cYk37}Rzs#S=CcFW;Hp4v(|pTWr?W$~Saq|)VZK`+yQ>$e zT~%LAQ^CjhCp5%&H@LSXj%Zh)t2Gd|h1)tMmGFuj*t>aE{3g;C|;{+Uv0D-0{$e{>-1}K0z1Vm$Gk`GJa0D%tLAX zUlNg;MYxU=NVk$+$G6z)*lVwo*vg(J5^!y#z3d)2P?6N~uUpUlyZ^KgVV{4kdTshM z_A;krIz`yE%9zUuw9>soH9IF}ItfwN=CPlRmc4EU+ILZ>ov=}di=(@T=VW*N|KX|)hflL>~c-SeB)lrnU3tZrR;d&=?2!rHv$JQQ}Ei()g9V?C{NulVnrO=glx7efsa6NquRbgGj##$;6d+C(=wh z*G3wy-t28T5e-+iS43}Sq^xiiy*gG={+3bWpWjm>zVv;B-yX@LuI~8%ckXg#N?qf< z>*8|aMPC<5)Km5jGIpz=`~O&b6Zklb^L~8ZJ#BoG2LB!rL@a`cy^ zq#x3RhPLUi|L^n6%zLbMEzy4sdS~XDXJ(#x=9y=nIhT5AMfsMn8`4s9$rnLUGy2>T z*V*1LV&$t8>+@J67%V0EbvN>gSOH$!2wy@mgclB!8R4NaglPV?)_nIADbli~%ZZbB|NX+&^HsPO=wPt`hl3)0ts)^%8S zuQqV97dp<_X@ORKR8Xi)$-AT zxB`T=t}lhI$L~I$fp1^3@$F0C+Y-gMCG+@3F_LfP;9L1S;2X^rv-1tPERt_mntUV5 zGWk|+!WPH3@B(~W0=|`lZ*oTlzatXt2VdxzrL@D6kA6%vd=T4lGBz zFVETTR4=M=6lD;v!ZpCn(py5c73d+IOnmk-#goaiaO_vZyb^B93Cy)>I)uEb|hUCTy=7yFJI?qsp zc*zU4I#Ao8+Lfpy$wxJ2TaqiG+7)N4H|2=mX;kIOy5U7O)NKXoMXg5hA40r+?AA5} zt)Y`V@p>Izu8=l_lcRs-jH2M)zZoK;46aFuaoO^ed=5?ZBFRw1Qx1fUp+Qw@o(Mu<+~@0m9O5I-bu zK8?EgQI~MT8dH~jA05#?27n$~$typF`EU`hK+C}3P3FC4g_ z1{yI{-eZm=S{@z*4Q-%7%EQmhzCqkOpXH%N$wRo7xFum1m@uN^BJ5hxC@l3S-UDM_ zxR!V%VRQzaeBs(^l}EO|L@(n}yFUz?sJ~QrD!g>#yTYCS9y(D=BYqzKBe{A6I5bA- zKD7qAb+xhIy~^0{sWSFsPf6-o(re!XF6}K^diLv<%y{tfN#k0)wy~C5j9N87E#Rfw z+j7h%siZQ=ww3Fez|GJ>x(`x$bRWdh!T)UXbgjvc?BfZgeF3zpaTurNC zqXzRfz&(eI>b#G;D$XJJ$ga4n)>!8OJnv0WU9iSo(M<4@@wjKbu`Unrf?Gv((cil& zx(R;oP4T>SM#W|8Pp!i3(*m5#E%5fb3i5kB1%g99I@#=hYyplTA^*hjrJhW_sn(`Uy|HeM!e4GDVn~WDD4V@*nWc*vG z+Yaofp6$UY+QthY;TNC|u&Skmdzx{^;9U0(NI8ulv}U2@yaIViIY){N+U;__5_p!J z|BET-G`o}iKDV5&L0T>6Co<$b^WCHiZ1t=`y7j2%mUgUmFJI!Rx%d%`xXoz$CC1sF zM?|RM93#|rj?q*@kWj-$gf^;BxIwrXjc=+KEhO8-%A+P-KuVH*`Jn@%M9sQ+z6 zStQrezL;~^7dsGFgg7lP@4^^-4(yAKHra0j&KD@XIk)+?P4>SH8*LlpdYdWNbqLj& zp?ZYs&Cq6qHk+Xagc?*R9B5QMs1bE?_8^KOSqlfYDx9tJa41INY*9E{=HXC`#Mz{9 zHqFDK7>VOkIKFu}6eDq(6b{`JbJ9#P5@)-_8QPB3tX(vwV@}62KS1b>R_a4~T#tg+ zSAy4}hHY$vg&P|&it&sPb2&QsgKUZV4>PNz$ATJ zO<27N!#W`65~|&7!nT{R1_jI3Z^;6twUHy>3+a`L;))V!%dl0GSCL;)P*GS>l>a>4 zCz7^IDX%u;Hye=oIc_Ziz@*EGVe*$TU6i?Kg%ld(VFXY4O%g3Ytt*jH3%>?^D{ z_LXcl_7&3&485K)e_n<=e&)RFG0Z8NF{fx&eRn9Z1EC#ePxix#^-GySugF}-(0bFF zmjO3pte|mjsJ2dnV(JbWBHybxTw(BxqYLM<`f&&!8mB+f->(~A&-9>~(BT7P{O zb>59Sha0e4nYC5JO`t8uTmuww%r*07ETAJt8qnlev0=$+yiiL{V+%_TwJF)iUj}_4 zxKh`6U0QC4%TjKvwJ+kTgi)Qz7p^5PN$R9Z2* zFFt`X$Q}#Tw5z#)hvHzUHmH2zTBO9hk;WR*;F_o3L)slCPd$pKq1txTEpt7j!-VZo zJfQqF-i0Rrs3iN`FdKiUe6j;yK>4P9bIFGxJ2Zcz6QsB!h&XHCYoE~$`=$f-O%OV< z4Ski~!=W{g*A-p`X;+>%lCGuq>2GBXtKYi)HSp~Abu08}06KOj^1B#1_9Ejz-i5}2 z{1)V`+i(6H@<~e%c%FoX-HBG|H2YRy!8s&wGxVi>Jax_?J53qy1fQjx=C-YEYq_}H z=$u3TS@Buhik+&CV4wIe#5va^pR(2QDG_SyH9{S|S=S>y|E%YYy^i%rTBo+=2)j+W z>Q(ZRO(yi5wg7qZ9O2EWn!m}Yhx6UKeMT?pL9#@1z!Ib*S)y|3-9*|O)oa<#IrOm6 zhd5mxj#;WX^ml|chsGT>2qDQK{ZTy&;f{!Y%*5V<;^aCB9ij3|nHRJ# zL^k{iq&)<89e(ATPV7{>hOxFWb@9DChaSQl8gSPDUSHeKbLe|{4!sw1Xuv&VqB?Jg z=g{}^9C`qAXu$JkF^AskqB-=vJcqsnb7;T|?!+AWQl3NK%X8?Ev918{qNhc5$z`sJ z%L)JK&&LY}jdewUmuxbs%djq)B>3-s5HAWF74KdD>}t%pOEBjy!JNAUbM6w%xl4+B z3rb3Ri%OQnOZve-J@;Ox_%44(VxRj`#Obwe>OW4|!W*C37V$g{*}4p}by-%~qIDO^ z&&wRL3-3QO7Xa(6R@-&tF+QWj95E=}F>>2=SO8^iWqGGPs)TBva4m60>aZ?ztRPNE7`^FBzHlwo zT*A6d*nS1er8h`l1W~S9TNrpiwXGgYx)5iLC3wLLx_=nDe;;)Je$~#%LJR>Fcl0ZDkkR|)i*m&{`crpf_jG4A@4564A8bN5p3`G%& znxTWn(9l5}_vE?^iV|}ullDo6a5qNICt}Ky8LAyIeNoeQkhv?T~3dD?!-pE{EN2ry}_TmN>MYq!!<}ESITSklno~Uuadl2@^DDtH_THG}E6F?emeK> z_a>^lT&KGib?HEz!ta?d_1KS5No&t!Xk^@wJ0hWxE3iggheOpn-1#_oz4RW5l+=aIQ6)fCHe3bdo z>u{_m8;NfO%O-o;HvSUmBfV$Y#w%fxkZt@JOP98dzY07$m!a*$3CPwsZ0#$IVf>C% zbDl?xPNCkp^r5!t?6&aVXN>doPCM@v@R^r=#+1@^)Ms7N!n@ey#k4eT=_Z%Y7|zMat2~R%gOe3YJZtsav)%L$x!g6ZQFAbt0Lw+oaED$g0yO zJz&h(^!vY~zPCwPRr>vTs~!c1DV(c-<5&edK{13g zbmcLOWXH5!apilBp;PZQww#zhjx2vei1&bJ;rARh`^sK4f~~KNpqA(nv@V{p_dbHU zabLM4V}uwSnaet&pq`9BZ)bi4waXEMI&rV#bu?|?M30%i_i|6v<4^cC+)SG&!Yby!5B7&vFm*pv)+&O^Q(*lg~yBoMenug1V7R`$FCQxzT|lU zop?b^)x6@;cParAorsfT+i}(xq{ryr9?XmFa9Z)AFrDKf zI!45;MksI`D{1O|D!xA}->0fJPT~!)vtc2~v)M!g{p~?nv_?U6RQy3w62~u@5Rdd( z0_PsGzDJbx5%>%lK;{C~;ff%heh z|JUMt(6x|WtO%c!OD)a|R4j4T)cgqdg5k$4jk8@p!ujFrjPSbaxW#Jt9JqB3g8C-S%Ls?shT2NE zCGmm!Xf<5fHor9KBJ9Pq1La{K^Z)gXWBz`;W@xQd+SC1Jfppi{kB=C`VC zLbW%5M!BESY4#hco9s)tbHToFE!9QBvdNY_lW{l7ApKEMPG>&2P5GVW4 zKD;ow-*pqlpc`%d=rh2P?+5Dhzb{*D;dxnvI&L;XjWyO zrh}|NWordKh*g^pW;y%xHRMNcmV|0Pl!Gq0lJh}xE+JRTto!|v#t&ic`9T}M=IfnT zEIK`}K=0fFy>kn5fzEy|yaxkZ);rdzOMCxz>YdfNvm^JXZ-H!(ZIjJr(eqAw|8!%w z%|7lpZTbDiE!-CP-DN^6!e6l{JN+P(5j8LUXxt`>xR^fc=+i=8$iGZ{oMLXF48;`wF2^6ebpLnIFGSWC45k(jUS8!| z^7wG8Xf!LbvyT4Ni#-w(@1Sgoi)%n)-7pLur2T8owUjd`r^Hdhb?t8Xj%(|lI#I_d z??bcA-8C#CNaR}nVb|>c>ppBOU*bv~?e0D-eo8{HcJ@GbZ_k`~1BI-;zOK7l{D?X6 zek$jiEGCC1hKKieUpV)#uBqz|iaRLUs8|xe{`&o#F@D~N<*u&&aNX?d@$OTn4&Qb1 zz}%g)J4FPO3abWBeni}WwB66|JT)r{r8wZ}_cE~;f2){ax)$z8*)XMcLu~hxbDhp@Asp!?V5o#EtY_ zfOyfZ;YdvNp-0`znr{#dh}%mU^sqo-GmtW&6eKt}nF}AiSzAaAPd~TPjK4noU4lOn z1Xwh9g_%6d=)+>YXMbJYoKarBxtsp=;O|~Irz8>ahlWpyIjZa?W7QIv)XcU`#+q_* z6F+xE5oec^Cb}Ndw^mFtu1KQ>n*_HAKK4l0a#P2Cf?2L~$PZ54PO_p}%V+761| zLZ5x~*-0PjFakXs>WV(^rVm&M0i@3W{^-ZcHXzUn)dBBk>2p7QzDl3Jq0jB~`51jZ zLZ463=Nt5Sf?zy(qNq^b@!US2lN8G!x>f*&7dI3Rl*SdF)!2+xY&|%)9G{ z@nAnN>d-uIc6W&+=U8{|o$%9ZgowFUmVY`f#-v0eR?w^Vhk8UJUp+U;`v+6FZ>+YGG-7m_CJKwc1@lz_uN?b4d9SDLVqs(15e4xek zwm1rAeTI_U$$j)C`qq8l4e9~E4Q9#C&VXM*$`8(Cyq>8 z>!{zvTdDt@rVe+SME$gY*@J#Y&o2|s^*D+S=$sg2Fxl+rCcS`@vIP7FrsX{p3!Qe9 zfFgyjh*Kfm!~1atuWYC5M&r8KIq_bC4Il0qjt`3ua>Co_dp&m*7Y-bk_4VQ5-#wjp zPXVchMJweLpw9?C@i>CEzQ^b*PEt>{S( z78WJkD4p}!=@uI)0kiBQV!J5&hdbllqK{w~;sZ5CiMuhirKIcdcdkr+mUO^E)_ly5 zQ#?|f659z`YWH8@7)04G#HA9l9$C)O@Nx=;kV-g(d(u?OS%%nixE|V$q~Jxd8M!V) zcc7nzC(1GMB3i5mxEx?oUl^7dI6EuqMcEU>L_3(V2~TTE`KaSeC+e>v#n~;EtE@ag ztKjrob(uy*`9oB68;XAM&KF6y7;Be{T9AGoEj%*~&k*6t#xs($8fzk59vVk?7f_aU)KZnANqW+DEkOvH>u`oMY$Lz z_n;#54{v;C{2!;MVavK-f06z@Gz?MyB+=H26`}4Dre1x(Nb4WzS=42A7G(1O<=_I1 z7BUwxj4Ysq8D)-mOMM6?i*e3MXtGVXYGCpO7vHZF4W^x@N@2NeB`bE%;X2|2j4H4V zNzW(pU~UoHz__^=#5Tn^bZ{V>6wv?%a;RNkOs*B{ZDqp3B-$wBZV1#`QKi~fa}#m_ zYXwvf!>l_h>P-Wgc|d~_rGMy7loo%gdsbYEvWEZrp%<|XM~!ox@Yz$qDp|`ZP+4>V z_G&tp5_^cw)ga?68;!$^MkP4^FP{|?oZ=yBFmUxtqMH`+Jl#mFq9oI^I7y|ND9P5L|qE=eAOsrN6VWHhjy^Si&b!{bi!QLv&0%maM->Kk1Z(y>dUO+@s3BHmr?TI|e}7gum=bUtgoSbQVCfMq^$v0mEB7t9l-FDM zlA8w~NzQVNsxI^rf|VL;X^e%$Eo1jQxRl3(_#^m#ia&DQzXczi=cKql;*V(hqk@rt zxnafA8;gYJ#fMyIQqJozOfkyyFjh_}O`ZsVB8fv`!u=Nh2<3{aaAF8w1Hy_#|H>yX^8#64pPUPHILiz$)x;Z-J3Vf`obr!VEdocTz@8-=~lp~MSn_cpjhC#~I) zyG*(Ch6ROJD3|tKD15GR8{kr24d4pFDIW^AVOXKO$^9{0@^?ehbUA$p6J8%6lx|#w z$6*l@?qSeGdWGsP{f8KT5|duSe=q(h{rkknIsAV334TJsPl!i2pKHaZpiN|7yafuh z6I19QG?LaWA5-b>Rp}nY%#HGW5IiLH{3Y=7i^z#M`Yp5_^&O(=tLi_Z6>q-qS|pYK z9R7&E&xvnCrM|_oP4e?R_hUCsZ9T(uy#PvepLjvsqF^6m80G&0WP$#p{K-#pKy!iD6o#|`U zTT2$*ebIhdCdrtrG3gnaMCFdZMwRntreEhT%ch<~Y0u(5^=J5WpQ4^aam&D0stZx2 z|D*a!Ju2(4-1slfJ0B8FY3Oe`-ku_NEp#lk)qmkHf5YBD&rons6m5tlW+pczqLVRE zD*egSSUeG(5Z;Xq+qYgOO3&F;Ga8+ajVDucJMoGcg0b1@R5ZO)l)xXKjE={;#t>7c zL*cop7{bLAP6x)uVq-fwmv}lj6;CI}Vmn14;wIvg@#&qSoV>}zcyBD7MpmkbNuW)@ zPl!aE$}c%J9Zx1y(Nh!Ax#-9QvRe+URP4%`cq%s58H-NOq+-A!%F{C=)A4EGNXoUp z08ulkIG|LABe7H(*&^gR=Y7IBSGdkSPk7HgFC16`lw2AM2?u>%MuNQ^r2*G+7jbZmcI08Dn3F@ z2kK2>Br2sSvM)Iuo!CkBoTi>bprex0-iCT#eUpf6>GW?8_;)n=8@KoycKEk<`kS`- zeLjD4o8Py^-?-i19`Fa-{X5$H4bA@c9scHk-xu_E2K?K$`?qfOZx8z08~trN{5yPp zUx$B7yT7sB-`?!s(dqBp=5O2~F5cR_b!5zUaC<}ZmS|IB(@4Yi9Ze0*+qP_LX!h-h zjcwnurC~b|c5E5*HEs3zwncaNw(r>5bkNt>+|(FtIw&IBxB9ns_#2!2olX9xPJeKl zKhWXd(c}*_`P(}E4Q)i<_U-;)v%fj$-_GB*27h2HQI9G$H~O2m`5S!xcAtO8R(~hx z-r;X(pc*ze`I{S10HU|~+Zu@t&0G9iwouJFoBdmmc#FSdn|}v#-{Nm>_O~_rw{E9= z8+Z7@xrVL&EnEG~?fy3K3(h-*gqZT|Lc{-y>Z3QTU> z?(b~y2iyGHz{aiqZ9(#b%%-jW9qs<0&%Xt1Z1!(&_jmaGji6>bwZc{uhEjt>4a#n6 z1~X8=cB*&>5^V8r>m+ssw^Dh{AiT-n*x=v3g9>X#6lm=5`w+_rXf0Gau!YF#M5a5a z>hL!;`hD&GZSBO~j-W97!rdD73lBbiQP4U%5l_UYFBEyL(lss=t_#lQa$nd2)rJp#ZxpFrili`bwcsQ#)7k>G1e(Er=qr@uAYua zc;7%!l()@5uR%E+0zWcVW;jXb*&gyZS@>L}7b>Uth329PAK9%G=eq%LMm_M5O{p zI)d$?U|lfxtdY+$VV8D=+jmF8{gIx2P>5XjcC`n^avd)- zj|_%EAxOfo6^f0*q7X}@KaC+qv4Q|Ysko?Vk0#EUt{F`x60yoJ%oU6d z*l>Gb<-`&oY=wn85KToV6>6CU4oB12)l*^Jy{iz$7(9b9I7>~DwX>!QVAT{{gIScA zrk*8g+mkaBV>KieU~D>86HBEa17nhQZc!UbItIy^loqLtOJX(W(wCg>glH)-g@Tdn zK9?g&mUASp0VNQVuRB#t!466PT(<^%Ly`XAvqNtJ2q?>oJ&CaK>4&! zMAK>E?FhCF?GpJN!Op-?PgoRm1jAAj6iOc{f|8D|LDe;(A__aA)6uTP)XcOfVr?i3 zF6oG&r^TZa@vG2Sg&pz9Sc2?f;SKfzm(HQ_I3p?6-B|xsp&b%q>3QPixz(gD`prBMJP2VRk>A-B?h6irRl6`OwC~0s7Yg=8`a2_C;b5;Q zWoW2>aIiA~Q>2iBUA=)_7zD{X&=c5)=8?lfH9^o2{k;%@4zzEmAI(PD49bLqfxT$F zWrP;qJ=ELQN0pPg6z|#<62giZrt0clGzoC1*3w<+WMU8gc(4^^ zIw&=#SVCc`KV_R1?hf>I4DJqe2ZeWcu&1XVPXAD7P%PaYo0v#CbloyL=vXQ_L9Pu`b@gB^pl44HP_Xvf13V8tUC01mhM=v{)a)Gw z*08AU0hdzA_}FH)RoI~1JUBBog{6yhPdqW*BF;8ZM&&@Z87A;lG6Bix!;(mgSd|5V z7Xn&D)x4-K9t&H<`gt*f(#%2^BI^N)zeUu{Lzn)k-jao8`N$y6AYF1lQc*wzCT0URm$)5+vSdUI-rEm3;2G;`9h zMaJ0B+1u12>Nwd!EcL`vvy&5>4@(7MOShSNcGq|!nF7ar?*ttaP8iQ*#J7m;i%ggp zotPPmZGgffw?)*yqjW7|^WyLoX>%>&{2U1|mx*!Jcafn@Gh=ED;_-ZDnYggQVehqw ztqUTilB19oyRyv9+%XISBM`&QmNm&rjYsBLsIy$Hk}IKF$ZZkl<)G6{NNwMTRk+-O zVxrn7Oj;c0v&uw=YGk`u&iCcYa*Noq5J5UwEQKaK66dbjvQQ%1I6xJ$*ob;Hzr(~P z0WHG6P|`87{x_$m$2Jd6kJ(Cvg#6Fbw&v_hCU0$}n&hI)Tz&A&=~_ht*K<6X#Jcuo zp4`FQNW^S9BE1=^Xc7yyyqZI5kvfV>F%Fh*CWEL&tj!8nD>pc2Ixm`+AW2LheC|gA9|0DxbvgLsA z&d6>N7caidWQwdaWo7E+A+5C+RW1#1QY$TpuGWL7M7;{M=$2Gz7e|^oJ8BVqi{M31 zax^-j*Z_t!ZrBisjLun?iMeJHcU3diBe8vrj z#dSTIQTBDq7IERCNx?)8w*Gi*tSey~G!~z}A3M$QM2vPzZRr;ykg^7f=a&8m$5L2r z!1AemZrQo$`l$Rm;^|STk{tOgzKK*ma;U(dL^~F?3id3Tpdsu)Oif`6**wFb)=6x+ zbu2zN2pgJ=1SpxJ$&@V@X^N(E4wYDaV0~l_f#%+sHscqOShW}vxG|j%7g<8 zRN5rhRK%3-OltLiIY%f~X|*nbD-IeLR%>S6)Z&XZpIAsjt&~_G#S~77rqzGsy7fCC z-BgmYSr+R0CIO+?L=0P;s-w_w!1L5av&_-8TEzavii0CvS2Bx0KsHGqP4v%9lWxL* zyZe8ddB)=B!kX2_9@?!|%>y!dOXwX)Mn*MTMEhdvFgSLFOx!*ZryUEa2gxRB5sU63 zIWw^&O)iK9{{NEo4q7J{QRb@MqT5+XtvS?prB%IRW1$&Zo|DTOi|F~8^1qPXYzhdB z8Jt71x6}q%^rH3n{2UgxbIjtE*&;iA<6;|eFSZjgWjCjdnn9S9^Z5=MTcFI?zk&A3 zuszWtoMu#3+#v14F|u42I%CFiQ-KzIX{vul)NpbtKB}kNXTsDI;dwZhMkl9aPuhT; zsTk(Wn5;Nwnr4k0xO@^o8ml=7wWB9zFgoX~k_~f^cE*5No4xWDX3km@in)p`*KX(4 z8mEk)JM7KbZab)>*Vq=3463g~?@#BHg!!d)VKc(n2Tdj`WRfm9*r8o$uU2o({*M#3 z<`SrRCFX3Wd6>8*n7y4eCLaN55zZbsude3wTJ2PlE?^OBeg0&<-?SKC<%UX_(o z+Eo?UiriqHt;*Sv9OyjhfE1s(-ZJaG*iW-%FPG(Wk#8l@MXWz#12FzVg2~#+k}v0a zOlD*CCoC3Pvl(k$B!@Os>=}@Doi^j%0dlL_fXKxeN6xhGqjxM8ouE}M7UoJ|AIw^W zbJCW{B|EO-mt!5t;1J}qk9_{@ncRY~3H%wi?UwNfmM?>x7zmuquVL@I1? zSh#wAMv<1XGlcEE^ahq(ElkcOw)rhWB*$#Kw~} zW#yh)E^N8zWY;~m1r+Dh)#SEJGnlbBXiJyFy2GgjY(b}aBUkMs<*;q-nDStY-i@4B zk4#kOF76`oms_Pd6{ODJ+&Gi;kb>;au5@yxIMY$+%q7EY^wk$TJ&CN>s?!QaLn-=GKDQ=q7V+V&&w4gPfcbplR$N;8YJ?SfF!DStpSS zhi?<)9Q`Dc{d{=Nj-^o5l_=X;@tn4&O5r$Dyg-%sWNS`=kr^&{!g7Jy+D}L7V=430 zlq||RyO>jpD?~bPxWPQ4yg)6vt1Ko*d-)sB)Xci`<(yS3GWm>Lj)}KI!Fk687}ygZ zNkvn0YC-UfbkU@N7CTzRyUs+uM#EXB9J5VHoH^;wF=8n!T+_e-06M~+bLuBCY3Z1* z4d|P8nOogvH+7=(Rl5xX@|K^@gMm4%t1KBT0ymbzjDW6CE(+H?9hhT&Y|#))!J;;= z5$0SBuqROLJfpIaw>}pT{t%rb<(j4=3SyhQ` zHy|mAJnE7|MQ{>*Y$OK`FAK_pK)Eq4#a$uUg4lP*J>~_vGV%n9JXDt}Q|z@-v&{hU zj2&iga$;sO))PAt!+zKzFqJIinA%HLUK*RguoN?wVAl1FMJJBLudp3_+8o3|Mci>s zjPr>~8p+OpqmM_Ol?AB_hc{9xliDoty|GDLc4Y)9d-KbY+4Gr~7#1?Yvb9+wo!GG=l(Tog}k#zENW)SP)(h)+mj)f6x+31OQ` z-N$PYrCM>yGRkyN-DD~^!^hOEb~AxIt-qp&t~+irI64d|B7Mpe zaXJgkNjR{0_!&v2YX# z&KTW_`v;EH6Sy2er_V5+;YuDI#H!1J(3Lns8@!O?a5Oa@vo~#37If0;bP|(G!r9D2 zIK@A1Hkj%k3G|!MLv(5x1W4B;J{=oHAD^DvtW1`U_&7#f(ypAP87*5gFo9<*p#LXr z&84`>hunGgO`Vy}xpM{<80x0d25V13&gJs}W@lyO-O=6*2Y4wXXB;AACtBt_t|s+;u|E0a$Hm_~xm_UgdU2m{s8{6o z;#wTtbSqJwNLzo$K2dHDMY@CgM3oZJnuDq2q`u_L_d!?cn0VT{p*mKN9I>>D36G>= zsR|vNlu*2lQQm8r;A;4@L5@K;PUAs|qQ1c1u3dazQ+WFN`+}kX-$>sOE+nn!OV(gf zY#dr_oNv7H-4L-V+i@>h!Ya~wc!`54#iC^LXlx9RcBEqU!j1cx_@IDQ*^Yb3^y|;O z?IDqt(2c=1+_Az%E8Oht5akvCF?6@J!h{SC1aXV7v7uo;tSJj@+k9A4mb6;{TbfY@ zl4gQ-;ORKR!u3Ynv<>4TsVGrldEK#)Lh_bm5qmEg3J#$O*iTncS8HDcm+N;&`Um)8 zE#F8TM7=}(;r{mi9{sig(B(ZWbyW(pbkkT$i{%;Tgp^P?c620cr2|I0JgRvVTpdV3 zk5}h)<=%iqTA?RndP1kmQS&^Wqe<8rjmofNQ+5oz|3a~REYg+(hiQ@40Rts8LzA%O zGM$1Qldt)p`8hW=Ka-f%4j67#%Q+9keW$3DarQZyOmz1A2)fTtujurF2E1A%5!f_w zP|1*Em{-u-JIk|8UBwdIeZ{rp9$X?Mx#P`_N8i7b2~`2qrO{@tmxEHJ8#~bgQ2R4j8g(2Vapvb{n*9|3FYN@&PgH{%lWAc zl*zy``B)BK_%Y{*qSW*r+c@?NuXVo`o=DNk+}2oD{%5$-{}s**q0(T)4) zVR&$X88?N+s=;8en;ygn1$&@k_XZ;a{e!sA+utWv&J5el zLg2xmSg&yg0wLTE4-IBRI@?4F^><`LDAfpfpQNW(tkgkV=%>d;_?|zwnMdjrRa+%p zXj{Ai(C2t1pv)fC-wpl@wGFn1y4vs-iRtYSYjYhBlTxrDSF}1kCb9D5@fu`oE})AN zwVom|%2nuCT0gV3YEbSC>6cCPqbIR3u_iNW%yuvtl9aKjBuh9e6P(eLH)O_VoL-Z; zoTsoXRc{?e$jGo(p=LcbEpuC@P}Dh330$Xu4lR>?$$%KK# zPA8`_qKYspQiuFSCC!jV9cScZ!AtFHOcGRP2jRwfg)_{m$N`gko1#2(t6LO>gQ4E8 zK0I{;9Uk5d@$Eq8hXM~?7Hr3ZM`t=>O~4sJk)3QOHpDAbO;x^x_2ntS+_%M z7~+ResQ6K7j@aI0(S1oQ8%kV}K=t&yEwHS|#5oI}s+S}cV4Y9;s^H69Ba|_#vc#wr zrb0yN0foE-rdRynR~RMg9Hq>v+;*x`Le3@#tnMr)_9em+LZI$2qgS0Qz_C#rW5(MP zc$RQ4EbzUZcw}HN-ksP>j}YKPj}Yvo2Pg2sy9V^dYhLX1h*IeSL@!ey;U4@^0(!>) zANgefY`=(`7hcyiiHe#(s;yeBSQ%8J8FoMF6jApt;DKID;4uwe~?2d`r6?HTSw z5mX{DTziK>(J<9%m?H7mHU5Eib>RyM{aPmWn81Bz~1ftg`%>FVIxmqB66{zcko` zM^J*II_r4`)#{nXRXG71^TK5Wu}(I~bjzIU!V~Dj1I(xsq^1m5flCEoP#v=~qL8O z$;g)KLYrh~+V!f~FIPc$Sy-(AFSEn+f};aSqa&>;F12IICBmX8uhVX5z-(9hX(p}x zG&8pRO{Sk_#~QxX;kR(MHQ*5;nSbX*bUZEG(XlZ=#>UJkO+E$4x-NkyoK$P~!W*54 zrY6b3&bugptnj7@GMJhY75LOiGog-z4-fB%JUT*?7(-Rvz zr7IW~MdYfd1B)#`R*)zFDj7H_UwmckBupPnXHeeJXna}}(|ZaqFZmI?Joy}~@Q%=< zf^hJFU|M)au>vFV=!+6d$u%ccqDTg50wfB^&o8$zGM?vwOE1ZaB^eLZiE;qsqz(O( zrre^;gz%Uy@(~`4#jX&=>XBV?gOl8ML|zlkSda%LD|J%Ylh z_=tk>Jep`vMAA_(M*65^R24839Z3*{RuK2SkH~zd`HkC%vc9>#qp=CfL%wD#yrcA( ztSG^my3s2z^wPqXC>*8d)#y$+Qt+e}DcA*rz1T-f#K3h7w_+{!-RjfQFt1l{n$$!7 zq-YBN8Kdj93aaOG#ZIGHv@SAj)&&7kWlue7d!|sF?}(AMR3wep6Gsn4rjwBgx|)o( zVS!uiz?BQYW+T2?+&CWrulLfe ztO&gY9l^sq)HAZ6(aVkM-ed$z0sL}ZHZ)jI5h{}ilvg4}rM=LD>fz}6BaOKtnsP;K z5o_#OVc#R-;5+PAOH?>k9q)-G4@Tm!;KizW82UJmSdkeOr$aFFOQgPqI;MHX^sdZA zDV!8lkEKOYN$`P+P0!Dq`q`MXy@=px{nT?rmVU84|H%IqDQ#OgiGD_2}j}0wb#DA=ghqXJa=yqYA8Gn4^vsVX*3} z5!mL=o{$+EN%CW%5oi`15O9*^;IcynVDV}u7O@xN=t5`!^oxnPI?iFtDavW+H0pt= z*aSAuj>IBU>P>1Fx2428;IUPf@ z^uTf@wlc6ok-V2{V1jY8lxw2EIBt4rEkDK-;Of40VWD-FYHG68)&jTsu*N27QJ zjyD7`tfD?x)LANBKJOrxWg)2?HE*TGGdhhy-hg38&j5)+`pSn)^W&W9`!HDjQ37 zCEB3r##8iypeWEk8-P;P4+umsf*BC57(LX=wt0R`KAJ1aaK-_KLpUiXKcW;RgA3RN zW-~7j+Y8YNJfq9T3u5ihyNUu1V(|tMj>lY!WRPFrEo46}Yl)>X-p7vh<82JNp)HEA ztd_z?E&7beQv-?n;D!c)PopTIx2dTcw#~t;TuveTG9*J+&xcyJeg45XMnAj*Ty~I- z;Do`dX*__K7WsJET3WC0;6#P+&~~#Zpf4S_7VhyFf2Ktt2KE4t(_#sE6L{ML9gt4s zh!q5wnwn5{T)UbHcnOS|6+1L1Dr28{j$2fi;Q{?}lqhDHY6mV9CKydu2oA5BBXL${ zFfB?DZS`X+l0s>w)qawi^KaKi{ZioR=d}9|((0&;*Csp_irSD;B;`wmBA8Z(T9_uT zEWa)-I5%yH@k)`%M+&;A%1mJ6oqe)15+B*8I{{TUD9s4s9coP#;S~ffn|<>4Hn?mK zONB!9=ODer%9ZHi(@uC1o$~;NnD_vRxQ|mC^al7U1bd=0iP1x79JC`|JX1Tu%r^Vk zI8g*#O(tc?Pa$yiSboX@qRmcA=BORz`T`0j#;L67MNsCHb7xvqTPSL?+FT&AC+E_L zPikzTdJnKIs~bp$B>VFa8;Txf=?GzjPbR3FQc<){pXE7VE-Dlo!;iy^P7^NGTTPrq z75lSng{u73aaOU6@tix7q7p#t$ze-wcWky3D=e|p6c$>hxfRrmt_`P=6kE8S)UQ;O z0w-vyNvxF}1gxcevkpHi=S4_Kp&HR*F!768QVzM)fb0vTRo^4Ly=sb-kB8jGM&>Y? zr5ik=^blsS6EQ6h#kOB%5%~y^B#07tBBN^RA_@>X)D9gh^3-7GJw$IL3nMOEaddCI z){6Hj)v~-OjO(|8MKODo)Jr(4(E^5Z8?jH-j{OlH`6WgKxfe<3G^t;i1$UFB@x%a* zxR(Pes>4}q=B0%q-SI4<4(ZRu(YKH;!RHMj46OsxnAUAacKZTU9e#c0Z z!vmF$S7_;BQ_`zN@pO9>r|IxcJE~B$Pf_NPm?{WaMyB}p zbQnA(&)EwPPoKPUepmp9Pp%2iVUm+V`pUPoi^;1V85BhnP)ewP{PZBF$h#so2SZhO zu0R`zr4#(F*#<~q7vJM(5X%(^HmNpLF+^sAHj2CnoK2cOgly)W=JoQiTTw8fE!I3< zSxAXu+fPf0LJCCKR5d0={v?e(P{_sXQG-y4Er7+sNgAV-!BohBG%6{`s}_;+CzZJZ zm^sEww%ca{$i*6Pmg#`q4zI8QwIRO52IJpH;B2(fT|lv1l~VR$5mcJpMUyl*DdTuj zj!I?HHO+p6omPzub05sd*+`!CdMD``n<$;M-{TQwGDx~MH3sIn>Cx^Ie9RYUDMYQS zx_6Nb4dM+du#;m{x0V6!wH1^4{Txz2@(3D`t*(VUZGEeVEg30=CG6K?Sj-_MhlLzq zL9CdZe*`>lQjU1uN!HAzlX7fRiO-%q^Eow+8sYgVL&KM7u0OUArLv#>;&w!%Csq-{>leFZY7B!O#oU}l_ z1`j2VQa{}rj~x|DCzW=CWh#k}1sfjk1|O-lO$McVG2Q;xPE zWW72A5KdYK66mubv!+-vnRO&zo&lC!qRfJFfAR7YhN56n5AJ!Ba%eA@G>7p?wF?kP@Q9>T6%OEku2$?2nDFRUSG$@jWTtYz`Gmtr15JpnuCHdq5xtJbeawLDH zP2^)-8Nu^+qS!VVhzc_ReJOu&NvyQPtg|Y}DZz$0a*}$FJf!GEp-ekNt1-#ME@kadw3Xv!9NkYRP#tsN*Sg zIb6w@k(eYC1)GpDQDugB5wc5fmSI8R1GvdbEy2f{0eg9YS-4Y} z{S;uGY%Gk^`XUcL8m9}TOEe%-jS zxV9lq4 zF$}XdFRU)8N;srHirA4lO!{S?RDM=OC5)pLP%(!f`m{s4gabB3AaQkf_M~W=yolx> z>Zj=n(p_p&kuN6z=onIC9hcxv4ny)wIy>SnGxlv36>63?-=UBfmR72TQMJA z-iHK?SJBWZO0XJl)gKYOe1r&;=~zi}1krmDHVt;l#M0VlTOkZE)?m`3XA+=8F85G6 z%e3YY{h}aG_c241xIz8b{g44t}?ADFtl86Y1m_#8lvct5r*r^FCDK^X9Yf**>yMalxg=BYJ#@@_c#Xu5r zlRZ?joVLceWYb7uj!>-DnI-n0RfYW1kF3f$Tm}62O*e;yNZ6LWSVke<@QoZH`!Qdi z1t{b>JpE2Ea@0R6gVPjglDq=isFLd)8BD4$O@|+&hcq?|O#?$znBI`7Ln}>KIO$k~t+GXs9GO?@L9@0|!bur3$P}!w z!L6f=4s>bTO7tQVJq1M|vt$bg@@NY|8x$bSKd&nc9M-{6n>5xG3a9DLFxw@|&21O^ zM0J@HDlPH?x>G>+&qO}?py!2GW?ej;w1(!q=_K`F0U)_@W?C?<_Z~{*hNbEwr81<& z--~?}?0o|p4h`P`;0J->&{^W<2S>oj-FW?0NyhhF?BA{Rc%Jdr;aDwenc3JiD=ch*-JX={Xv`0>8X@6yt(ENkU^W zOu72>9L$tSI>$53$Cry3DMlMMVzuSRi5ey*q7Qh9m02RG_N#30j1wW%wy4Zj zFy{oTn7~RO3(wI*FiN~f>GzvO;ZfW*d#x>6yv&I{!iJXT>(H!=vbR(W&@4 z{5+rf>GmNK; z*A!Ro^}TBNZgJsTIqa);`M&G&6}f!Rx_oyUzGqyY@y~Zp*CBZ{4aL z+7%-U^FZN8RWiGDeckOV^!mQ)cFEk0sy3A}XixdK83le_vn!9QP`QBw;vcQ1v6nFA z@s)XfZ|F)>ik};mzsyLXrEDq#k8@^e)C6{9&@y08@rog7S!2;+^@0J9?`K-Z@^0PQ2b%xG&wMHdQ|8hQ_Bi~DP)l@_NquH~anf_Yy%~QE92TH3h z*HgJR2g+Itr6vc;&on_w5&ec3=g>0qA-{IwKonmjh!&V0TYAJj@5F}Qnh*J-g=3fS zpAp!}n6JCV8uz1lp6Y)u@SOi_ab*B&Y8e!Kj4H|M zC8J*7Pc?rn=uh3gO}ZKu^dhhCUJbQ36W~bj&pI33RWHhhd#~5G(d&Cc<9hUPfhqY@qr6*`akQ8hYh9?eKs zIj^oOH?x;&t5f>SfuW$%nfP+bVdK1JV57>&U{DMvEe2RL;Jk_z!W2&h4XB33i zmpad=tW}wxux0MMLF)*Ql>dF;fx^2sBdcyrv#!-mx|K9wP>Y%dT#^a+gC>WRVFNcD z8%TehX&{+~ozCi+%))-2NlselZMVFTvC{cgaa!}1T0)ZPLl-Iab$c*i`+n~6rQN<~ zyuM33zCMrdb6($Py}m)W?-IA~4{lgtD*tjhoI(=y|CPU1UbCK;E5j_J(s*I*T9IZO zo>YvtWaCY@?^P`uH`PDn|1*m{;PHK4V|gpj_U%xkh>5D3_;>aO`~=w>5chdl z=KLjJO6;(G&N8oXC<`+|flh!Hvdwc;8aoSF#aIaoZ3&@m=BKrI{1wOih=&xOTG{7E zPE~N);*COwA@Q7Mqvp*~-JBY5fme0Y+wAgiS4Q(-JlDp{t)?ParF

F=yg&eIBn% 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 z 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 CsbLWS diff --git a/spotify-app-remote/build/.transforms/a236cf5d8026cbceaedc55a536fdbf38/results.bin b/spotify-app-remote/build/.transforms/a236cf5d8026cbceaedc55a536fdbf38/results.bin deleted file mode 100644 index 52daf05d..00000000 --- a/spotify-app-remote/build/.transforms/a236cf5d8026cbceaedc55a536fdbf38/results.bin +++ /dev/null @@ -1 +0,0 @@ -o/spotify-app-remote-release-0.7.2-runtime diff --git a/spotify-app-remote/build/.transforms/a236cf5d8026cbceaedc55a536fdbf38/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex b/spotify-app-remote/build/.transforms/a236cf5d8026cbceaedc55a536fdbf38/transformed/spotify-app-remote-release-0.7.2-runtime/spotify-app-remote-release-0.7.2-runtime_dex/classes.dex deleted file mode 100644 index 1c62d8894e783ed2b2e99a87ef6773a730a685cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138012 zcmbrn37n1P`}lu9=gfw|nPVU1%pjwvV{FqWWe~}}9KxV6mZ*#wNh%dlX(6HV5g~mF zrBsASw#phx(ym3R6j@69|9#!h^~^jo`Fy|M-|K&-ch`O2*LCmfvz%kt@Cjw3O`Dsn zc8BlzsC}cQ!586PIPoLSiKMHYU2tF~>$2^J9`@PQb4aU&b+&p(dOM>Cg}^ zf~L?CT0vXr0G**X^n-zL4Ge{mFa~acyWwG249~$D*aGjvckmljEbExs&K?Y<(JLm@eU?_}%i7*rX0~=uA=xoiARVIc zFK7?lp&tx^e3$@t!eK}+@0cm@6|}G5n5SVGJP)g2BfJSaU=O?phv6&u7LLOi_yd9! z9g_qhs0}Tk4RnS6a4p;dkHNFB9uC0gZ~}gZ#E@eygqCm>^nwDo5pIFW@DThPo`RLI z9uB|{;G{Sv4O&7wh`}JZ6JCIi;5bwcJEj42fLxda)8T1Y1H0jS_!}x!qD;^l`oahp z4>MsgyaHR{YbaCMG3P@M7!H$Q4m=J|!Sk>V-hy}FQ#b~{z+Vtwg+76{FaSowU9bSw z!t3xZd=9@ru&QIKK|Qz#E`w}X46ndD@G*P~KSQvZW0D~qnm{Y)2G_w&a6in3^{^8@ zgzw>J_!Hu)lOHsKi=ipBgjSFZ?V&5gpdSo|YoQSCg4wVTR>SM?Hhc`n;cuu^!!h-t z1+;;FFchwbe3%5Y;Tc#1+u=(%4Zpx&5J+V%Kv{@DJ!lAxArrEoHCzFmpgZ)2@$eYD z1iRrUs8o~ofg52qtcHE?8`P@BT!$_&5^jgtunb;@_u&jA*QOkB6-?_0V81O45=-N3*+EFa1fHS7LC#=}y04}OJ9+EZqj4(s7FDBpo` z3s=Lf@E_O>ry=!9#yH#vtKcvsccjf=6wHRT@G%6gB43EXz3?pTfxjWW6Xk)MU?J>= z6HukIW12%hxC0izI@k?|;S{8GpF_-4g+HKrZ`uet!PRgLTnhzo2h4=!um$$Pmv9Ek z#Hc6Kg!<45u7UYG?>z*2Yv-iOmrwlDJp zT0jnrg?r#BSOweRV>k&3S2HHyQWyyNa2L#lW$-$@1^eL#F#TB9Aq%=d9^3-=z@xAj zR>Es=0DghE{+uV^e8_}0a20fi7z~0DFd62;bFdb+!an!_zJMR#4=6XlF=40yjp1_0 zgL`2qtbx7oKAeO=4s#1?Ko$&!5ik}e!7Nw-yWk554CIUnyK<+^o46R!KFi4?_m}kfTQpygoiOFp$S|DS3w^b0;Ay; zxEp4{WAGHLfc3BicEG!E2#!JgaOOJHhI()TG=tXA1!6D=ZiHLmF1Q~ag~hN8R>Kz9 z3Gc&~@E!aDe?!U$`U@h^5Sl<1Tn-(e7Yu>Xa5LNq_rhFQ4zI#i*ax4$aR^?^ScZns z6fT1IpR>DsB77}h?PCySRfN8K0*1}%+4&q0X4m5)v zFb?Lz8aM=B!*Tcxg87^+;e5CZy2JG_9`1()@H}jR18^9=gw~KA=j5s1Ja-oWI+$e zh3nvExE1b(8Sn(GfYq=8cEJbmC7gu6pv+kM2O2LXJ@kfAa1T5L3t=g&gjZk_ zya7AmJ@^{_gt!}72jD!&gx1gr`ok!=879LlSO_n{>+m*w3O|4uN7azyeqcJK#O|435HYka!dG04{(I&=-cl7`PMeg-75;*bIB%8#n`H$2+Dj zTn>F84{n3mun?BPdUy-=!9h3zC*gM}b2IgTnvf3lp&>MfE1)yN1s1_N*bQI6SqR{v!mq15|!9chHCc-p$80Nw=um(23PB;W#!B6lfq)cR-LLyC1gWC$c14r8g7PrU=BPEi{NQk z0qbB3?1N9>82kyrNvyq40ct@#XavonE%b$a7!P;CG?)!fzzSFoZ@?iq2EoZ(+dySF zADTfo7z~9l5hlTXFb5uoC*WCF58L4b_#D26pW#nPxQq21s=)aWg*MO|M!^KQ9~QtW z*amyy2possA!!QN2~ZEB&<1+LAh-bv;ZC>*9)QPSF)V{suo3pbAvgl3AZaS&3+g}% z=mhs)gePGIyaJnHFMJ5!!mp5UFZ~M5;d1B# z1K8}zAy+zzyz2Hi(nOOhdpovPC@8?_6ev97ehPf26=EBJOV3V8+;5$;b%yA zfVPBs&;S}kQ@9j5z(6R3+h8g@0CQmxtbjG}1{{Eo;0PRpQ}8RqJ;*u(wcsMS6xu;Y z=ms&!fnhKaroiLy3haP=@Bw@d-@z~NHzZCcUC4yXAqQ@R8L%8S!`pBGK7y~|6eP~z zdJQTGsoa=4l27+c_DI13@R{1ga7C8!Klpej^@>QDnx zp(fOV+E51~kOt{c7wSQMI3F571~h~V;6i8wjo~7=7^2VwGNCCngXYizT0$0F0{?c=eVM-dNl3`jHhOS|l8HS9Z z+s#8T6K2C)_&3ah$6-D!fG1!fEP}=GBrJjdz*1NS%i&pg4pzYP@G`81HLw=e!v@$4 zZ@>=N3AEvOB3AOdNS4t1d()Q9t-0c1c!xBxDMM$i~8h9-~+ zO`#byhZfKhvfvW<7hDRh;BsgOSHP9f5jsI<=mK4#8+3;r&=U;LM4Q6DpcPyOmqTmF zhBnX^+QAjj9y)-mITBqypMtn62f1EQcP-q9)m;zw5p_4h{gt}2eC}-AN%TQHx{kOF z|M582ek5GnJ=7)SMAGLYx;%oWZWYOFNeDV?gHEro`QR#+E-G0$*Zcm z<8U{?EoGK`#VvVNz@346s?YtP+DP~u+>%Zdw|pe8Cdm03o@qlFQ-n9iE$Nq6_fwjV zgs=34ul2dN;gcZYi_4rOe{a#=Xzy{v%Kv?zcIFO+~dota-K3G$pV0 zK6eLoOZr`KOFClH)o0V)=f1{gFK#d2p}3__rLDw$6t|=~Tx}$LB<>T0OIwY?eOB|D zgj+u1orILJCv7WkY1;&K#}O!LO87{vySS4Hm-NM5*=JJ=cUdnTUwG8#&hojleQv3j zj7dpT#-x{iM{K1065a#17e3GzJ`A_Czt~9o<26%oOI@V=;`Z82+=qPO1sHoa6C@)4 zO~uB3aNp~5PgA$pKY)9Z+CS(E7k5yC;)>0m>Xx{Yg#93; z9pxi7;y$GI;+FD@y_C;>kYXnvkwSqO2Hooc33t>jbquI`pwwO6Qr|dr4OSFf zpQX&6{U3yT?l>C8b0_=Um3{77K6gEzyAf_L-)6q>jtm$t%~n2}D{y;d>*90w^Ti$J zv%kR?KF$|D5x1AlRNR%czB7F`^KpA+nCmOg0iXLRpZ#+_`&B;o2A|C~++M!B{WiWd zxB1)=pL-GRYFf@izPMRFn@PSt{n!_N%x8bh7k(PI*Z#6Td-Xkyy%+w6&mG4G6xK2% zzi@SolH(=wXxgEDx&I7n>sLjW?Q`LRUXCsS* zXD{n&b+t(%+^bhD+*LF@19w$*cl5;_iMy7D7x=;_`rNWsdwESI+-nnA8@>8INO)~c zN7mBw)IFE*I_h48JEHETzI3vD?IY{F*FI~p@$%i~vzPVLE5iiJ(3CX8x_&lO_e{c@ zt2;!REz~X76fM=gn>0NenRlLB`qXnvpL%f*Veh#Q4jPg4=$ORekPiK6iDWJJsi|t?n}1CECVCS9$*A++7iQYn)ssB|9H+Q5WXQ z?rCIIQ_b9jT~+)wxEQO$-w?vj=kE>THRNv_@=|u&VJdI5GVDB~ewvYMlPZp6-I~8V zmARyojsHdR%_jaDE1%?i=K8<3GR`?>W!O21Y@_yV)V>WTH?bGL*tapReH(gK?5`kv z1o??vs&c2wYsg>1UsicO{d=Y9#&vhEMan{Q~BPWTgOmrh)z zBqBST>&#Esbv8GcGsrIVkkq>if4NqkHWi51&9pOj;O~arE9BpetGadAcjJodH7h%q zx2+62`>pJ14qG|Hd}HNIbJWW2#$+pKJm4h{WhzXcc*o(Z*N;h5}cN*8=7r9pDIx7>L)1)`d z{9-mDhjXV=+G#j{6II@>@>!K15^n_QzmH#JU+X_@`l(!RWr9s_gt?t-0kNxQ_FDOX z`B42ITbXMOhRI}bncYKYce?P)Migp=F{vvNvDc1txU$1h5 z%ITD66e(`7@!WEaVuXHT{X@;SR?aftX}Z54uQ#pCDP+F3Yd+)o1pYDVFC?E^Nnhlh zDkrI&tnx0EyHxI0xku&OD)*|CYY<6Ku0KSMque5IQaN7b%_^6wT&8ll%4bzRr*ehL zl`3CU`I5>{RDOyaOMWkrzsQ$Wu2Q*LkE4f!8a)QcRRKBQkoyvDq9#T1jbR^#EDmSV8QRQ)!7gD}k zDUZ}oq|{Gj0}Yq@h+pb)n^|n$q5mgJd5Axe@K>!|Yu>Wq?)aR@YVx`DPcX-klejuR zV&y*by_I*GACOasztYOu&P(cFZDoSD z8sYbud(8dF>E?d31^el&|NE?T+hIC)1b@W;u$g7o?OC+sTKu!P8sBcEyFSb^!d~d1Tu5yQ!aZWD%@d#Ian~C=*SJR&&=b5=?tCjyY2d#9=F^_pH z^L-xqO20gACYhJ5+-KY8ak<(j-}%UGgwMxL+HF2DezkJH`3<>%c3ov<1!t%Fe^>bj z@(E52Um+J#?k}u#_k~5ezAsVv40kTyME{J*wBu~4`j>LI@?FB0tN&TGe@^A|+#!7r z{qxKk*#}=x{|oA0$z9VA(7#CjvfqjP8~Kv@MT-Ar`e_p5d=>q@4119^315Z&GURIf z4&ke@yC1oR@Uw)kMczQXwT!pXRwg)=(62Qc%|t8rm^-jrhuvuWuksX3_Lo=r6Nkt? z^qa_4DnC}4NqVo6Z-Dsgk=GG_Jtwe2D+4B#eSU+6Z_xC`FX6A@A4t57$eEg(%_PCSyBBu~<6Y-=yH{&0He=~lm z_hw2o&dRWJh1H)nU#k2;rNrC96T-RJZ6QDD2a(bbB7a71CA}o%Hqq0LBIVwgNLd#} zey#Ea+EM&Js60TtHyAhFk#F)8Qu^Ud{;o$a@&=WoRpzT4qq0C{p_OjGyhZvQNpCyl zlKP00`iPYAAyVqKgFiXea!@o#sj_d9G~bUh@lbH~9@AACbc;zsM0P3(@ak-?@==Mc$$Eh|15< zzfFFdk$Y+7K7{XO1`V+?&dIYfXy?aX?dQGNKS23J&al#*e|xn(_OYJilK(!+A>(^L zf8yV-{&&^x0Q0ID`UC2J55JSZGd=8H!(QY@m2)Y_LF}F_?cG#EWuM;wvC*MC3UePSEb|Ib)eM3Ge>aSEJYdejw zuVezw0PHJkcvTZJEeNklj8wv_vJyRotj3BX`Br14apkM#VZy7M$)-K_HOzU=*W^=! zbw2@rs);jS;7_N#wP??}*bO1P0ci{)yaE0ZR)$T4@C?mAgOcnbJi}B`dAs?@`YV~i zh;cKEsP53On8 zvb0BQ-f$>~%+_*d(^BJzmrctrBwn_uXP&jPzFBSM`DTrk4b1DvHoRdW>9sMxs!TH% zP`)wvc|;Di(k*9aEoT>P-!7bU#J-EU(6(KdJH*vYVE>8!MBPyPJ;3ZrTssw0>guH}&qO$6u*o|(aU%o!U`tiQKXE=A*>)TTj`F=A*^^Z zE{8A^Wn4-;8J8kuTn=Hz%eWju`^dNyDdSS~GA>2RxXhzIvR;Xl_Q~V^TNlcir}fP< zQ%!&Cuk0i-zlJgrWZf8wy{sEU*_mG?onht;8-Exl*5TL>*K~*LIyjt_T-L$iyx}GD zbOiTXB)<`yyJWtP&~!&=x+6555jws_-b=b8IH@-zzY&~2F0k@#^B*hSa*SZ-eb@Sv zotoq`lB*@jXCx;vSN7+DmiQ;BtY}iK{{vH>cq17fu3Tcy$3Kb_zr-8Gn;so0=O}ip ztE_BdI$3#->1<_h)5Xe|>1t(D)6L5FZF`L}&FsE5O6xa@^}ieWj$-}JvGRafLVnkw zm;A27K8*i*PTu0b-aKZqu)CgicnEoe+TCC-GA*tDBGbppDQ19`7n?h+bk~E?8h^CL z&)0F9&xx}a<;*wj?f#h0N-z6lKIso7em*&k>)BZ@8bOe zxsS5n+D+tscZ7HoHQ$NG&3B@4^PR|5j^sO$c948UO1@$*`A+1@S@NC86XCg5?lR9K zZ`XA1(D`)-@nqiKq4P?l%&R*%;mbU_1ACb_cj`JhiMt!p?vwcc7~)N0-TTQ(cio#r zk6umuNu+bPl}mU|F8*HT87nU_Ymt++|0YvH>F+6e4q{cc=b$NAbRfRSE3LfBJVCrE z?C^`Myvi)Lva?xcWk<7I!(ULj(#lTu+%$!g$*UT^9y!(gW$Q83{H=1bX-Ga(dCT-d zD<_+atz2aOrT$B;EUgH#SyeID{|-S6%(?tXWV?swvs zeNOx$@1=cYoQst9zLy ztNnDfmvFJ4uH$LC?xWLn9Eo1W(R58`hOSRDbbXqEekk>uq3hEOU6*F)y7Un1Qf=Zt zMEY{RoXM4->{~O5C*%KNt^}n$9@h2bVO>ui*7ZcfWj%S=xa*1NWj%S=xa-No%nMmh zBwW^$hq)Ue>&YyxD<$1otZ33dv)Cb}A7`0icHfxAiAwg1Sz4c2TAx|$jD4xkEUnip zt=BBwUuS9kW@-IqV<+Q9q~te;JHB#VI7ip{IgHOstUtw>g?xl|5W7dz?h&E%+v8Oj}zZg>mO*ILq2Yf+Vmgi&CoN% zf1LYlNz_N)-5Q0QZ@#no`RGrgpRam(S4;E@NMGu`zzjB3tv}CHvvP>3Zsi!$(aLK~ z3i&NCxu%MhKbVWH{Knj1<&S2J`U{XxXu416J|j~0nJ0K2C;QA3+=-c_aA8E6_WNSpe-?AXk^N_}ard7mSz(To-jkfD zP9T@4{Swk`kKI%3T=lG+Z0(-be4gg5TWPPSc|!0j?fEos^SiRPz3zV6oM+`T+z%4F zXEgt3H2-Hb|D~#5s`)G>Un$Qr&1aeBv&@XP`7G1?mYJG1{bk0@Z#j2DC7D>>0gxmIeqR%*Fk)O=pl{(Mo#rQFMuarq+Y{X}_Q zQoEPb?j^O8XH{bNlG?qjb}ySmJKtYcJ9(BRb}y^lDz#h1+hbDBRca^Cg2Zl>+O6jP zz35k~ezodXtNs;DXHAiG?|-dT|5|gzroUF>%d;{`f33z}r*`Xf+^$nQdA22X>(uU5 z>}0)rRm&^SI&;y#%86L6D_-SWBXV7_UejC8ojfV`dQHzh6UBbLrnkZTX3Mof_4ZjQ z`qwo6Yg(SyR4?ySN_k$>_#4%3BRwzqY*f3AYPV7CHmThv?)Hn_Cbiq7cAM00v)XMo zf7tXktKDX`+pKn5)NTuRjU~M;YPUu0wy51!wcD!o*s6A0)o!cWZBx5#%wS1xo7!zt zyKQRshT6Tsh!ndw@k@Wasr~ULZt0IVxiXOcc$07NNPoPg@!rz(-_rEu8J4u?TbllM zwcE}ciIVFy8m4u;t8 zRQp|Ow~MC;Vz*1vm1o#uw@dAItKDv`*KW0wXWU}9TkZC!-5&FWO>d9d$un@V+oN`G ztKHi=Z{Josc}6aFZ>!y2wcBexx9ROwJ9&mKc6-&1yB4;8_G$U{sog%VylP`7&)R#c zlz96z-hPd@U*ql9c>8&>E%D^pyu=eJ@%C%HcePya>UeusZuq=P&jA^c7G3WgZ;i ziGNMPk8mPtYGpNND)Jk>FY*mnwqkcw^Es;RcU1T9qq-k|%X5yM>>uCq{34b3->cm* zl|P#CHl3f;e^S#sspUPX<>e`iZO_vhewvegEz +xV`m<21H@w|=L&0vN~sbDI55 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 Date: Sun, 18 Aug 2024 12:56:38 +0530 Subject: [PATCH 86/97] Adds top listeners --- .gitignore | 1 + .../android/ui/screens/artist/ArtistScreen.kt | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/.gitignore b/.gitignore index 4b8f8df0..7966cda0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ .externalNativeBuild play_config.json .kotlin +/spotify-app-remote/build diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt index d9df87ae..6b48d4a4 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt @@ -125,6 +125,9 @@ private fun ArtistScreen( item { SimilarArtists(uiState = uiState) } + item { + TopListenersCard(uiState = uiState) + } } } @@ -473,6 +476,43 @@ private fun SimilarArtists( } } +@Composable +private fun TopListenersCard( + uiState: ArtistUIState +) { + val topListenersCollapsibleState: MutableState = remember { + mutableStateOf(true) + } + val topListeners = when(topListenersCollapsibleState.value){ + true -> uiState.topListeners?.take(5) ?: listOf() + false -> uiState.topListeners ?: listOf() + } + Box(modifier = Modifier + .fillMaxWidth() + .background(brush = ListenBrainzTheme.colorScheme.gradientBrush) + .padding(23.dp)) { + Column { + Text("Top Listeners", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Spacer(modifier = Modifier.height(20.dp)) + topListeners.map { + ArtistCard(artistName = it?.userName ?: "", listenCount = it?.listenCount ?: 0) { + + } + Spacer(modifier = Modifier.height(12.dp)) + } + if((uiState.topListeners?.size ?: 0) > 5){ + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + LoadMoreButton(state = topListenersCollapsibleState.value) { + topListenersCollapsibleState.value = !topListenersCollapsibleState.value + } + Spacer(modifier = Modifier.height(20.dp)) + } + } + } + } +} + + @Composable private fun LinkCard( @@ -566,6 +606,7 @@ fun SvgWithWebView(svgContent: String, width: Dp, height: Dp) { AndroidView( factory = { webView }, + update = {view ->}, modifier = Modifier .width(width) .height(height) From 39436ac676b785c53aa8f7ecd41bbf56db4f6370 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sun, 18 Aug 2024 22:47:25 +0530 Subject: [PATCH 87/97] Makes artists clickable in ListenCardSmall component --- .../android/model/artist/ArtistsInAlbum.kt | 9 ---- .../android/model/artist/PopularRecording.kt | 3 +- .../android/model/artist/ReleaseGroup.kt | 3 +- .../android/model/feed/FeedEventType.kt | 21 +++++--- .../android/model/feed/FeedListenArtist.kt | 2 +- .../android/model/user/Recording.kt | 3 +- .../android/model/user/Release.kt | 3 +- .../android/model/user/TopArtistInfo.kt | 9 ---- .../ui/components/BrainzPlayerListenCard.kt | 4 +- .../android/ui/components/ListenCardSmall.kt | 47 +++++++++++------- .../android/ui/navigation/AppNavigation.kt | 16 +++++- .../android/ui/screens/artist/ArtistScreen.kt | 15 +++--- .../ui/screens/brainzplayer/AlbumScreen.kt | 4 +- .../ui/screens/brainzplayer/ArtistScreen.kt | 6 ++- .../BrainzPlayerBackDropScreen.kt | 6 ++- .../ui/screens/brainzplayer/PlaylistScreen.kt | 4 +- .../android/ui/screens/feed/FeedScreen.kt | 49 +++++++++++++------ .../screens/feed/events/ListenFeedLayout.kt | 9 ++-- .../feed/events/ListenLikeFeedLayout.kt | 9 ++-- .../PersonalRecommendationFeedLayout.kt | 7 ++- .../ui/screens/feed/events/PinFeedLayout.kt | 5 +- .../RecordingRecommendationFeedLayout.kt | 7 ++- .../screens/feed/events/ReviewFeedLayout.kt | 7 ++- .../ui/screens/profile/BaseProfileScreen.kt | 6 ++- .../screens/profile/listens/ListensScreen.kt | 8 +-- .../ui/screens/profile/stats/StatsScreen.kt | 15 ++++-- .../ui/screens/profile/taste/TasteScreen.kt | 17 ++++--- 27 files changed, 185 insertions(+), 109 deletions(-) delete mode 100644 app/src/main/java/org/listenbrainz/android/model/artist/ArtistsInAlbum.kt delete mode 100644 app/src/main/java/org/listenbrainz/android/model/user/TopArtistInfo.kt diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistsInAlbum.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ArtistsInAlbum.kt deleted file mode 100644 index 514191da..00000000 --- a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistsInAlbum.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.listenbrainz.android.model.artist - -import com.google.gson.annotations.SerializedName - -data class ArtistsInAlbum( - @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/artist/PopularRecording.kt b/app/src/main/java/org/listenbrainz/android/model/artist/PopularRecording.kt index 9805a303..2dbf5ab3 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/PopularRecording.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/PopularRecording.kt @@ -1,11 +1,12 @@ package org.listenbrainz.android.model.artist import com.google.gson.annotations.SerializedName +import org.listenbrainz.android.model.feed.FeedListenArtist data class PopularRecording( @SerializedName("artist_mbids") val artistMbids: List? = listOf(), @SerializedName("artist_name") val artistName: String? = null, - val artists: List? = listOf(), + val artists: List? = listOf(), @SerializedName("caa_id") val caaId: Long? = null, @SerializedName("caa_release_mbid") val caaReleaseMbid: String? = null, val length: Int? = null, diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ReleaseGroup.kt b/app/src/main/java/org/listenbrainz/android/model/artist/ReleaseGroup.kt index ee070f36..356432c2 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/ReleaseGroup.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/ReleaseGroup.kt @@ -1,10 +1,11 @@ package org.listenbrainz.android.model.artist import com.google.gson.annotations.SerializedName +import org.listenbrainz.android.model.feed.FeedListenArtist data class ReleaseGroup( @SerializedName("artist_credit_name") val artistCreditName: String? = null, - val artists: List? = listOf(), + val artists: List? = listOf(), @SerializedName("caa_id") val caaId: Long? = null, @SerializedName("caa_release_mbid") val caaReleaseMbid: String? = null, val date: String? = null, diff --git a/app/src/main/java/org/listenbrainz/android/model/feed/FeedEventType.kt b/app/src/main/java/org/listenbrainz/android/model/feed/FeedEventType.kt index a581c1e5..4110b403 100644 --- a/app/src/main/java/org/listenbrainz/android/model/feed/FeedEventType.kt +++ b/app/src/main/java/org/listenbrainz/android/model/feed/FeedEventType.kt @@ -114,7 +114,8 @@ enum class FeedEventType ( onReview: () -> Unit, onPin: () -> Unit, onClick: () -> Unit, - goToUserPage: (String?) -> Unit + goToUserPage: (String?) -> Unit, + goToArtistPage: (String) -> Unit, ){ when (this){ RECORDING_RECOMMENDATION -> RecordingRecommendationFeedLayout( @@ -131,7 +132,8 @@ enum class FeedEventType ( onReview = onReview, onPersonallyRecommend = onPersonallyRecommend, onRecommend = onRecommend, - goToUserPage = goToUserPage + goToUserPage = goToUserPage, + goToArtistPage = goToArtistPage ) PERSONAL_RECORDING_RECOMMENDATION -> PersonalRecommendationFeedLayout( event = event, @@ -146,7 +148,8 @@ enum class FeedEventType ( onReview = onReview, onPersonallyRecommend = onPersonallyRecommend, onRecommend = onRecommend, - goToUserPage = goToUserPage + goToUserPage = goToUserPage, + goToArtistPage = goToArtistPage ) RECORDING_PIN -> PinFeedLayout( event = event, @@ -162,7 +165,8 @@ enum class FeedEventType ( onReview = onReview, onPersonallyRecommend = onPersonallyRecommend, onRecommend = onRecommend, - goToUserPage = goToUserPage + goToUserPage = goToUserPage, + goToArtistPage = goToArtistPage ) LIKE -> ListenLikeFeedLayout( event = event, @@ -177,7 +181,8 @@ enum class FeedEventType ( onReview = onReview, onPersonallyRecommend = onPersonallyRecommend, onRecommend = onRecommend, - goToUserPage = goToUserPage + goToUserPage = goToUserPage, + goToArtistPage = goToArtistPage ) LISTEN -> ListenFeedLayout( event = event, @@ -192,7 +197,8 @@ enum class FeedEventType ( onReview = onReview, onPersonallyRecommend = onPersonallyRecommend, onRecommend = onRecommend, - goToUserPage = goToUserPage + goToUserPage = goToUserPage, + goToArtistPage = goToArtistPage ) FOLLOW -> FollowFeedLayout(event = event, parentUser = parentUser, goToUserPage = goToUserPage) NOTIFICATION -> NotificationFeedLayout(event = event, onDeleteOrHide = onDeleteOrHide, goToUserPage = goToUserPage) @@ -209,7 +215,8 @@ enum class FeedEventType ( onReview = onReview, onPersonallyRecommend = onPersonallyRecommend, onRecommend = onRecommend, - goToUserPage = goToUserPage + goToUserPage = goToUserPage, + goToArtistPage = goToArtistPage ) UNKNOWN -> UnknownFeedLayout(event = event) } diff --git a/app/src/main/java/org/listenbrainz/android/model/feed/FeedListenArtist.kt b/app/src/main/java/org/listenbrainz/android/model/feed/FeedListenArtist.kt index afb0a570..b093f94b 100644 --- a/app/src/main/java/org/listenbrainz/android/model/feed/FeedListenArtist.kt +++ b/app/src/main/java/org/listenbrainz/android/model/feed/FeedListenArtist.kt @@ -4,6 +4,6 @@ import com.google.gson.annotations.SerializedName data class FeedListenArtist( @SerializedName("artist_credit_name") val artistCreditName: String, - @SerializedName("artist_mbid") val artistMbid: String, + @SerializedName("artist_mbid") val artistMbid: String?, @SerializedName("join_phrase")val joinPhrase: String ) \ No newline at end of file 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 index 0500d7f7..56730c23 100644 --- a/app/src/main/java/org/listenbrainz/android/model/user/Recording.kt +++ b/app/src/main/java/org/listenbrainz/android/model/user/Recording.kt @@ -1,11 +1,12 @@ package org.listenbrainz.android.model.user import com.google.gson.annotations.SerializedName +import org.listenbrainz.android.model.feed.FeedListenArtist data class Recording( @SerializedName("artist_mbids") val artistMbids: List? = listOf(), @SerializedName("artist_name") val artistName: String? = "", - val artists: List? = listOf(), + 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, 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 index b9b73821..54a0f2cc 100644 --- a/app/src/main/java/org/listenbrainz/android/model/user/Release.kt +++ b/app/src/main/java/org/listenbrainz/android/model/user/Release.kt @@ -1,11 +1,12 @@ package org.listenbrainz.android.model.user import com.google.gson.annotations.SerializedName +import org.listenbrainz.android.model.feed.FeedListenArtist data class Release( @SerializedName("artist_mbids") val artistMbids: List? = listOf(), @SerializedName("artist_name") val artistName: String? = "", - val artists: List? = listOf(), + 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, 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 deleted file mode 100644 index 6a967d4e..00000000 --- a/app/src/main/java/org/listenbrainz/android/model/user/TopArtistInfo.kt +++ /dev/null @@ -1,9 +0,0 @@ -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/ui/components/BrainzPlayerListenCard.kt b/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerListenCard.kt index 2bb0abbb..c2d5c963 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerListenCard.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/components/BrainzPlayerListenCard.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest import org.listenbrainz.android.R +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.ui.theme.ListenBrainzTheme @Composable @@ -66,7 +67,8 @@ fun BrainzPlayerListenCard( TitleAndSubtitle( modifier = Modifier.padding(end = 6.dp), title = title, - subtitle = subTitle + goToArtistPage = {}, + artists = listOf(FeedListenArtist(subTitle, null, "")), ) } Box(modifier = Modifier 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 328e473d..e6d1c685 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 @@ -36,6 +36,7 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest import org.listenbrainz.android.R +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.ui.theme.ListenBrainzTheme /**Small configuration of listen card. @@ -52,7 +53,7 @@ import org.listenbrainz.android.ui.theme.ListenBrainzTheme fun ListenCardSmall( modifier: Modifier = Modifier, trackName: String, - artistName: String, + artists: List, coverArtUrl: String?, listenCount: Int? = null, @DrawableRes errorAlbumArt: Int = R.drawable.ic_coverartarchive_logo_no_text, @@ -66,6 +67,7 @@ fun ListenCardSmall( color: Color = ListenBrainzTheme.colorScheme.level1, titleColor: Color = ListenBrainzTheme.colorScheme.listenText, subtitleColor: Color = titleColor.copy(alpha = 0.7f), + goToArtistPage: (String) -> Unit, onClick: () -> Unit, ) { Surface( @@ -103,7 +105,7 @@ fun ListenCardSmall( Spacer(modifier = Modifier.width(ListenBrainzTheme.paddings.coverArtAndTextGap)) - TitleAndSubtitle(modifier = Modifier.padding(end = 6.dp), title = trackName, subtitle = artistName, titleColor = titleColor, subtitleColor = subtitleColor) + TitleAndSubtitle(modifier = Modifier.padding(end = 6.dp), title = trackName, artists = artists, titleColor = titleColor, subtitleColor = subtitleColor, goToArtistPage = goToArtistPage) } Box( @@ -196,15 +198,18 @@ private fun AlbumArt( ) } -/** [title] corresponds to release name and [subtitle] corresponds to artist name.*/ +/** [title] corresponds to release name and [artists] corresponds to all the artists as per + * MB's credit system. + * The [artists] list consists of artist names and join phrases used to join multiple artists together*/ @Composable fun TitleAndSubtitle( modifier: Modifier = Modifier, title: String, - subtitle: String = "", + artists: List, alignment: Alignment.Horizontal = Alignment.Start, titleColor: Color = ListenBrainzTheme.colorScheme.listenText, - subtitleColor: Color = titleColor.copy(alpha = 0.7f) + subtitleColor: Color = titleColor.copy(alpha = 0.7f), + goToArtistPage: (String) -> Unit, ) { Column(modifier = modifier, horizontalAlignment = alignment) { Text( @@ -214,14 +219,17 @@ fun TitleAndSubtitle( maxLines = 1, overflow = TextOverflow.Ellipsis ) - if (subtitle.isNotEmpty()){ - Text( - text = subtitle, - style = ListenBrainzTheme.textStyles.listenSubtitle, - color = subtitleColor, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + Row { + artists.map { + Text(it.artistCreditName + it.joinPhrase, style = ListenBrainzTheme.textStyles.listenSubtitle, + color = subtitleColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis, modifier = Modifier.clickable { + if(it.artistMbid != null){ + goToArtistPage(it.artistMbid) + } + }) + } } } } @@ -233,16 +241,17 @@ private fun ListenCardSmallPreview() { ListenBrainzTheme { ListenCardSmall( trackName = "Title", - artistName = "Artist", + artists = listOf(FeedListenArtist("Artist", "", "")), coverArtUrl = "", enableDropdownIcon = true, enableTrailingContent = true, trailingContent = { modifier -> Column(modifier = modifier) { - TitleAndSubtitle(title = "Userrrrrrrrrrrrrr", subtitle = "60%") + TitleAndSubtitle(title = "Userrrrrrrrrrrrrr", goToArtistPage = {}, artists = listOf(FeedListenArtist("Artist", "", "")),) } }, enableBlurbContent = true, + goToArtistPage = {}, blurbContent = { Column(modifier = it) { Text(text = "Blurb Content", color = ListenBrainzTheme.colorScheme.text) @@ -259,15 +268,16 @@ private fun ListenCardSmallNoBlurbContentPreview() { ListenBrainzTheme { ListenCardSmall( trackName = "Title", - artistName = "Artist", + artists = listOf(FeedListenArtist("Artist", "", "")), coverArtUrl = "", enableDropdownIcon = true, enableTrailingContent = true, trailingContent = { modifier -> Column(modifier = modifier) { - TitleAndSubtitle(title = "Userrrrrrrrrrrrrr", subtitle = "60%") + TitleAndSubtitle(title = "Userrrrrrrrrrrrrr", goToArtistPage = {}, artists = listOf(FeedListenArtist("Artist", "", "")),) } }, + goToArtistPage = {}, enableBlurbContent = false ) {} } @@ -280,10 +290,11 @@ private fun ListenCardSmallSimplePreview() { ListenBrainzTheme { ListenCardSmall( trackName = "Title", - artistName = "Artist", + artists = listOf(FeedListenArtist("Artist", "", "")), coverArtUrl = "", enableDropdownIcon = true, enableTrailingContent = false, + goToArtistPage = {}, enableBlurbContent = false ) {} } diff --git a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt index 72888baf..aa5c3b25 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt @@ -47,7 +47,19 @@ fun AppNavigation( // Restore previous state restoreState = true } - } }) + } }, goToArtistPage = { + mbid -> + navController.navigate("${AppNavigationItem.Artist.route}/$mbid"){ + // Avoid building large backstack + popUpTo(AppNavigationItem.Feed.route){ + saveState = true + } + // Avoid copies + launchSingleTop = true + // Restore previous state + restoreState = true + } + }) } composable(route = AppNavigationItem.BrainzPlayer.route){ BrainzPlayerScreen() @@ -86,7 +98,7 @@ fun AppNavigation( } } else{ - ArtistScreen(artistMbid = artistMbid) + ArtistScreen(artistMbid = artistMbid, goToArtistPage = goToArtistPage) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt index 6b48d4a4..e8b6d490 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt @@ -81,19 +81,21 @@ import org.listenbrainz.android.viewmodel.ArtistViewModel @Composable fun ArtistScreen( artistMbid: String, - viewModel: ArtistViewModel = hiltViewModel() + viewModel: ArtistViewModel = hiltViewModel(), + goToArtistPage: (String) -> Unit ) { LaunchedEffect(Unit) { viewModel.fetchArtistData(artistMbid) } val uiState by viewModel.uiState.collectAsState() - ArtistScreen(artistMbid = artistMbid,uiState = uiState) + ArtistScreen(artistMbid = artistMbid,uiState = uiState, goToArtistPage = goToArtistPage) } @Composable private fun ArtistScreen( artistMbid: String, - uiState: ArtistUIState + uiState: ArtistUIState, + goToArtistPage: (String) -> Unit ) { Box(modifier = Modifier.fillMaxSize()){ AnimatedVisibility( @@ -114,7 +116,7 @@ private fun ArtistScreen( Links(uiState = uiState, artistMbid = artistMbid) } item { - PopularTracks(uiState = uiState) + PopularTracks(uiState = uiState, goToArtistPage = goToArtistPage) } item { AlbumsCard(header = "Albums", albumsList = uiState.albums) @@ -360,7 +362,8 @@ private fun Links( @Composable private fun PopularTracks( - uiState: ArtistUIState + uiState: ArtistUIState, + goToArtistPage: (String) -> Unit, ) { val popularTracksCollapsibleState: MutableState = remember { mutableStateOf(true) @@ -378,7 +381,7 @@ private fun PopularTracks( Text("Popular Tracks", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) Spacer(modifier = Modifier.height(20.dp)) popularTracks.map { - ListenCardSmall(trackName = it?.recordingName ?: "", artistName = it?.artistName ?: "", coverArtUrl = Utils.getCoverArtUrl(it?.caaReleaseMbid, it?.caaId)) { + ListenCardSmall(trackName = it?.recordingName ?: "", artists = it?.artists ?: listOf(), coverArtUrl = Utils.getCoverArtUrl(it?.caaReleaseMbid, it?.caaId), goToArtistPage = goToArtistPage) { } Spacer(modifier = Modifier.height(12.dp)) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumScreen.kt index 6b37396c..cb1fa5e0 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/AlbumScreen.kt @@ -40,6 +40,7 @@ import coil.compose.AsyncImage import org.listenbrainz.android.R import org.listenbrainz.android.model.Album import org.listenbrainz.android.model.PlayableType +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.ui.components.BPLibraryEmptyMessage import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.forwardingPainter @@ -286,10 +287,11 @@ fun OnAlbumClickScreen(albumID: Long) { vertical = ListenBrainzTheme.paddings.lazyListAdjacent ), trackName = it.title, - artistName = it.artist, + artists = listOf(FeedListenArtist(it.artist, null, "")), coverArtUrl = it.albumArt, errorAlbumArt = R.drawable.ic_erroralbumart, enableDropdownIcon = true, + goToArtistPage = {}, onDropdownIconClick = { albumCardMoreOptionsDropMenuExpanded = albumSongs.indexOf(it) } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistScreen.kt index 09585077..56408e76 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/ArtistScreen.kt @@ -44,6 +44,7 @@ import coil.compose.AsyncImage import org.listenbrainz.android.R import org.listenbrainz.android.model.Artist import org.listenbrainz.android.model.PlayableType +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.ui.components.BPLibraryEmptyMessage import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.forwardingPainter @@ -372,13 +373,14 @@ fun OnArtistClickScreen(artistID: String, navigateToAlbum: (id: Long) -> Unit) { vertical = ListenBrainzTheme.paddings.lazyListAdjacent ), trackName = it.title, - artistName = it.artist, + artists = listOf(FeedListenArtist(it.artist, null, "")), coverArtUrl = it.albumArt, errorAlbumArt = R.drawable.ic_erroralbumart, enableDropdownIcon = true, onDropdownIconClick = { artistCardMoreOptionsDropMenuExpanded = artistSongs.indexOf(it) - } + }, + goToArtistPage = {} ) { brainzPlayerViewModel.changePlayable( artistSongs, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerBackDropScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerBackDropScreen.kt index aa42bf31..87b87d1a 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerBackDropScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/BrainzPlayerBackDropScreen.kt @@ -83,6 +83,7 @@ import org.listenbrainz.android.model.PlayableType import org.listenbrainz.android.model.Playlist.Companion.recentlyPlayed import org.listenbrainz.android.model.RepeatMode import org.listenbrainz.android.model.Song +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.PlayPauseIcon import org.listenbrainz.android.ui.components.SeekBar @@ -432,9 +433,10 @@ fun PlayerScreen( ListenCardSmall( modifier = modifier, trackName = song.title, - artistName = song.artist, + artists = listOf(FeedListenArtist(song.artist, null, "")), coverArtUrl = song.albumArt, - errorAlbumArt = R.drawable.ic_erroralbumart + errorAlbumArt = R.drawable.ic_erroralbumart, + goToArtistPage = {} ) { brainzPlayerViewModel.skipToPlayable(index) brainzPlayerViewModel.appPreferences.currentPlayable?.songs?.let { diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/PlaylistScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/PlaylistScreen.kt index c877ecbd..7073f5c2 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/PlaylistScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/brainzplayer/PlaylistScreen.kt @@ -37,6 +37,7 @@ import kotlinx.coroutines.launch import org.listenbrainz.android.R import org.listenbrainz.android.model.PlayableType import org.listenbrainz.android.model.Playlist +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.forwardingPainter import org.listenbrainz.android.ui.theme.ListenBrainzTheme @@ -411,10 +412,11 @@ fun OnPlaylistClickScreen(playlistID: Long) { vertical = ListenBrainzTheme.paddings.lazyListAdjacent ), trackName = it.title, - artistName = it.artist, + artists = listOf(FeedListenArtist(it.artist, null, "")), coverArtUrl = it.albumArt, errorAlbumArt = R.drawable.ic_erroralbumart, enableDropdownIcon = true, + goToArtistPage = {}, onDropdownIconClick = { selectedPlaylistItemIndex = selectedPlaylist.items.indexOf(it) } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/FeedScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/FeedScreen.kt index 002f9361..0f94a618 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/FeedScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/FeedScreen.kt @@ -91,7 +91,8 @@ fun FeedScreen( socialViewModel: SocialViewModel = hiltViewModel(), scrollToTopState: Boolean, onScrollToTop: (suspend () -> Unit) -> Unit, - goToUserPage: (String?) -> Unit + goToUserPage: (String?) -> Unit, + goToArtistPage: (String) -> Unit ) { val uiState by viewModel.uiState.collectAsState() @@ -123,7 +124,8 @@ fun FeedScreen( onPlay = { event -> viewModel.play(event) }, - goToUserPage = goToUserPage + goToUserPage = goToUserPage, + goToArtistPage = goToArtistPage ) } @@ -143,7 +145,8 @@ fun FeedScreen( searchFollower: (String) -> Unit, isCritiqueBrainzLinked: suspend () -> Boolean?, onPlay: (event: FeedEvent) -> Unit, - goToUserPage: (String?) -> Unit + goToUserPage: (String?) -> Unit, + goToArtistPage: (String) -> Unit ) { val myFeedPagingData = uiState.myFeedState.eventList.collectAsLazyPagingItems() val myFeedListState = rememberLazyListState() @@ -245,7 +248,8 @@ fun FeedScreen( ) }, onPlay = onPlay, - goToUserPage = goToUserPage + goToUserPage = goToUserPage, + goToArtistPage = goToArtistPage ) 1 -> FollowListens( @@ -270,7 +274,8 @@ fun FeedScreen( FeedDialogBundleKeys.feedDialogBundle(1, index) ) }, - onPlay = onPlay + onPlay = onPlay, + goToArtistPage = goToArtistPage ) 2 -> SimilarListens( @@ -295,7 +300,8 @@ fun FeedScreen( FeedDialogBundleKeys.feedDialogBundle(2, index) ) }, - onPlay = onPlay + onPlay = onPlay, + goToArtistPage = goToArtistPage ) } } @@ -410,6 +416,7 @@ private fun MyFeed( pin: (index: Int) -> Unit, onPlay: (FeedEvent) -> Unit, goToUserPage: (String?) -> Unit, + goToArtistPage: (String) -> Unit, uriHandler: UriHandler = LocalUriHandler.current ) { // Since, at most one drop down will be active at a time, then we only need to maintain one state variable. @@ -478,7 +485,8 @@ private fun MyFeed( onPlay(event) dropdownItemIndex.value = null }, - goToUserPage = goToUserPage + goToUserPage = goToUserPage, + goToArtistPage = goToArtistPage ) } @@ -503,7 +511,8 @@ fun FollowListens( review: (index: Int) -> Unit, pin: (index: Int) -> Unit, onPlay: (FeedEvent) -> Unit, - uriHandler: UriHandler = LocalUriHandler.current + uriHandler: UriHandler = LocalUriHandler.current, + goToArtistPage: (String) -> Unit, ) { // Since, at most one drop down will be active at a time, then we only need to maintain one state variable. val dropdownItemIndex: MutableState = rememberSaveable { @@ -527,7 +536,7 @@ fun FollowListens( vertical = ListenBrainzTheme.paddings.lazyListAdjacent ), trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", - artistName = event.metadata.trackMetadata?.artistName ?: "Unknown", + artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), coverArtUrl = Utils.getCoverArtUrl( caaReleaseMbid = event.metadata.trackMetadata?.mbidMapping?.caaReleaseMbid, @@ -570,7 +579,9 @@ fun FollowListens( Column(modifier, horizontalAlignment = Alignment.End) { TitleAndSubtitle( title = event.username ?: "Unknown", - titleColor = ListenBrainzTheme.colorScheme.lbSignature + artists = listOf(), + titleColor = ListenBrainzTheme.colorScheme.lbSignature, + goToArtistPage = goToArtistPage ) Date( event = event, @@ -578,7 +589,8 @@ fun FollowListens( eventType = eventType ) } - } + }, + goToArtistPage = goToArtistPage ) { onPlay(event) } @@ -603,7 +615,8 @@ fun SimilarListens( review: (index: Int) -> Unit, pin: (index: Int) -> Unit, onPlay: (FeedEvent) -> Unit, - uriHandler: UriHandler = LocalUriHandler.current + uriHandler: UriHandler = LocalUriHandler.current, + goToArtistPage: (String) -> Unit ) { // Since, at most one drop down will be active at a time, then we only need to maintain one state variable. val dropdownItemIndex: MutableState = rememberSaveable { @@ -627,7 +640,7 @@ fun SimilarListens( vertical = ListenBrainzTheme.paddings.lazyListAdjacent ), trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", - artistName = event.metadata.trackMetadata?.artistName ?: "Unknown", + artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), coverArtUrl = Utils.getCoverArtUrl( caaReleaseMbid = event.metadata.trackMetadata?.mbidMapping?.caaReleaseMbid, @@ -680,7 +693,9 @@ fun SimilarListens( Column(modifier, horizontalAlignment = Alignment.End) { TitleAndSubtitle( title = event.username ?: "Unknown", - titleColor = ListenBrainzTheme.colorScheme.lbSignature + artists = listOf(), + titleColor = ListenBrainzTheme.colorScheme.lbSignature, + goToArtistPage = goToArtistPage ) Date( event = event, @@ -688,7 +703,8 @@ fun SimilarListens( eventType = eventType ) } - } + }, + goToArtistPage = goToArtistPage ) { onPlay(event) } @@ -890,7 +906,8 @@ private fun FeedScreenPreview() { searchFollower = {}, isCritiqueBrainzLinked = {true}, onPlay = {}, - goToUserPage = {} + goToUserPage = {}, + goToArtistPage = {} ) } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenFeedLayout.kt index 25f164bc..aabd3b6e 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenFeedLayout.kt @@ -29,6 +29,7 @@ fun ListenFeedLayout ( onPersonallyRecommend: () -> Unit, onReview: () -> Unit, goToUserPage: (String?) -> Unit, + goToArtistPage: (String) -> Unit, ) { BaseFeedLayout( eventType = FeedEventType.LISTEN, @@ -40,7 +41,7 @@ fun ListenFeedLayout ( ListenCardSmall( trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", - artistName = event.metadata.trackMetadata?.artistName ?: "Unknown", + artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), coverArtUrl = remember { Utils.getCoverArtUrl( caaReleaseMbid = event.metadata.trackMetadata?.mbidMapping?.caaReleaseMbid, @@ -61,7 +62,8 @@ fun ListenFeedLayout ( onReview = onReview ) }, - onClick = onClick + onClick = onClick, + goToArtistPage = goToArtistPage ) } } @@ -91,7 +93,8 @@ private fun ListenFeedLayoutPreview() { onRecommend = {}, onPersonallyRecommend = {}, onReview = {}, - goToUserPage = {} + goToUserPage = {}, + goToArtistPage = {} ) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenLikeFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenLikeFeedLayout.kt index 34490d99..05e18cce 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenLikeFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenLikeFeedLayout.kt @@ -28,7 +28,8 @@ fun ListenLikeFeedLayout( onRecommend: () -> Unit, onPersonallyRecommend: () -> Unit, onReview: () -> Unit, - goToUserPage: (String?) -> Unit + goToUserPage: (String?) -> Unit, + goToArtistPage: (String) -> Unit, ) { BaseFeedLayout( eventType = FeedEventType.LIKE, @@ -39,7 +40,7 @@ fun ListenLikeFeedLayout( ) { ListenCardSmall( trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", - artistName = event.metadata.trackMetadata?.artistName ?: "Unknown", + artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), coverArtUrl = remember { getCoverArtUrl( caaReleaseMbid = event.metadata.trackMetadata?.mbidMapping?.caaReleaseMbid, @@ -60,6 +61,7 @@ fun ListenLikeFeedLayout( onReview = onReview ) }, + goToArtistPage = goToArtistPage, onClick = onClick ) } @@ -90,7 +92,8 @@ private fun ListenLikeFeedLayoutPreview() { onRecommend = {}, onPersonallyRecommend = {}, onReview = {}, - goToUserPage = {} + goToUserPage = {}, + goToArtistPage = {} ) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PersonalRecommendationFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PersonalRecommendationFeedLayout.kt index 2a8b0a9b..ff97cf3f 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PersonalRecommendationFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PersonalRecommendationFeedLayout.kt @@ -41,6 +41,7 @@ fun PersonalRecommendationFeedLayout( onPersonallyRecommend: () -> Unit, onReview: () -> Unit, goToUserPage: (String?) -> Unit, + goToArtistPage: (String) -> Unit, ) { BaseFeedLayout( eventType = FeedEventType.PERSONAL_RECORDING_RECOMMENDATION, @@ -51,7 +52,7 @@ fun PersonalRecommendationFeedLayout( ) { ListenCardSmall( trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", - artistName = event.metadata.trackMetadata?.artistName ?: "Unknown", + artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), coverArtUrl = remember { Utils.getCoverArtUrl( caaReleaseMbid = event.metadata.trackMetadata?.mbidMapping?.caaReleaseMbid, @@ -110,6 +111,7 @@ fun PersonalRecommendationFeedLayout( } } }, + goToArtistPage = goToArtistPage, onClick = onClick ) @@ -145,7 +147,8 @@ private fun PersonalRecommendationFeedLayoutPreview() { onRecommend = {}, onPersonallyRecommend = {}, onReview = {}, - goToUserPage = {} + goToUserPage = {}, + goToArtistPage = {} ) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PinFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PinFeedLayout.kt index 51481550..24693861 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PinFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PinFeedLayout.kt @@ -32,6 +32,7 @@ fun PinFeedLayout( onPersonallyRecommend: () -> Unit, onReview: () -> Unit, goToUserPage: (String?) -> Unit, + goToArtistPage: (String) -> Unit, ) { BaseFeedLayout( eventType = FeedEventType.RECORDING_PIN, @@ -44,7 +45,7 @@ fun PinFeedLayout( ListenCardSmall( trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", - artistName = event.metadata.trackMetadata?.artistName ?: "Unknown", + artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), coverArtUrl = remember { Utils.getCoverArtUrl( caaReleaseMbid = event.metadata.trackMetadata?.mbidMapping?.caaReleaseMbid, @@ -77,6 +78,7 @@ fun PinFeedLayout( } } }, + goToArtistPage = goToArtistPage, onClick = onClick ) @@ -110,6 +112,7 @@ fun PinFeedLayoutPreview() { onPersonallyRecommend = {}, onReview = {}, goToUserPage = {}, + goToArtistPage = {}, ) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/RecordingRecommendationFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/RecordingRecommendationFeedLayout.kt index 2e3cc660..ff00dfb9 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/RecordingRecommendationFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/RecordingRecommendationFeedLayout.kt @@ -30,6 +30,7 @@ fun RecordingRecommendationFeedLayout( onPersonallyRecommend: () -> Unit, onReview: () -> Unit, goToUserPage: (String?) -> Unit, + goToArtistPage: (String) -> Unit, ) { BaseFeedLayout( eventType = FeedEventType.RECORDING_RECOMMENDATION, @@ -39,7 +40,7 @@ fun RecordingRecommendationFeedLayout( onDeleteOrHide = onDeleteOrHide, goToUserPage = goToUserPage) { ListenCardSmall( trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", - artistName = event.metadata.trackMetadata?.artistName ?: "Unknown", + artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), coverArtUrl = remember { Utils.getCoverArtUrl( caaReleaseMbid = event.metadata.trackMetadata?.mbidMapping?.caaReleaseMbid, @@ -60,6 +61,7 @@ fun RecordingRecommendationFeedLayout( onReview = onReview ) }, + goToArtistPage = goToArtistPage, onClick = onClick ) } @@ -91,7 +93,8 @@ private fun RecordingRecommendationFeedCardPreview() { onRecommend = {}, onPersonallyRecommend = {}, onReview = {}, - goToUserPage = {} + goToUserPage = {}, + goToArtistPage = {} ) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ReviewFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ReviewFeedLayout.kt index e7171706..4c6b95c4 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ReviewFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ReviewFeedLayout.kt @@ -50,6 +50,7 @@ fun ReviewFeedLayout( onPersonallyRecommend: () -> Unit, onReview: () -> Unit, goToUserPage: (String?) -> Unit, + goToArtistPage: (String) -> Unit, uriHandler: UriHandler = LocalUriHandler.current ) { BaseFeedLayout( @@ -62,7 +63,7 @@ fun ReviewFeedLayout( ListenCardSmall( trackName = event.metadata.entityName ?: "Unknown", - artistName = event.metadata.trackMetadata?.artistName ?: "", + artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), coverArtUrl = remember { Utils.getCoverArtUrl( caaReleaseMbid = event.metadata.trackMetadata?.mbidMapping?.caaReleaseMbid, @@ -140,6 +141,7 @@ fun ReviewFeedLayout( } }, + goToArtistPage = goToArtistPage, onClick = onClick ) @@ -176,7 +178,8 @@ private fun ReviewFeedLayoutPreview() { onRecommend = {}, onPersonallyRecommend = {}, onReview = {}, - goToUserPage = {} + goToUserPage = {}, + goToArtistPage = {} ) } } 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 4bfd0764..5801c1df 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 @@ -225,13 +225,15 @@ fun BaseProfileScreen( snackbarState = snackbarState, socialViewModel = socialViewModel, viewModel = viewModel, - feedViewModel = feedViewModel + feedViewModel = feedViewModel, + goToArtistPage = goToArtistPage ) ProfileScreenTab.TASTE -> TasteScreen( snackbarState = snackbarState, socialViewModel = socialViewModel, feedViewModel = feedViewModel, - viewModel = viewModel + viewModel = viewModel, + goToArtistPage = goToArtistPage ) else -> ListensScreen( scrollRequestState = false, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt index 10f062e3..8f6b8b74 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt @@ -270,7 +270,7 @@ fun ListensScreen( vertical = ListenBrainzTheme.paddings.lazyListAdjacent ), trackName = listen.trackMetadata.trackName, - artistName = listen.trackMetadata.artistName, + artists = metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), coverArtUrl = getCoverArtUrl( caaReleaseMbid = listen.trackMetadata.mbidMapping?.caaReleaseMbid, caaId = listen.trackMetadata.mbidMapping?.caaId @@ -279,6 +279,7 @@ fun ListensScreen( onDropdownIconClick = { dropdownItemIndex.value = index }, + goToArtistPage = goToArtistPage, dropDown = { SocialDropdown( isExpanded = dropdownItemIndex.value == index, @@ -456,8 +457,9 @@ private fun BuildSimilarArtists(similarArtists: List, onArtistClick: (St append("You both listen to ") } topSimilarArtists.forEachIndexed { index, artist -> - if(artist.artistMbid != null) - pushStringAnnotation(tag = "ARTIST", annotation = artist.artistMbid) + if(artist.artistMbid != null) { + pushStringAnnotation(tag = "ARTIST", annotation = artist.artistMbid) + } withStyle(style = SpanStyle(color = lb_purple_night)) { append(artist.artistName) } 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 fbb35fcf..fa933ec3 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 @@ -92,6 +92,7 @@ fun StatsScreen( socialViewModel: SocialViewModel, feedViewModel : FeedViewModel, snackbarState : SnackbarHostState, + goToArtistPage: (String) -> Unit ) { val uiState by viewModel.uiState.collectAsState() val socialUiState by socialViewModel.uiState.collectAsState() @@ -158,7 +159,8 @@ fun StatsScreen( }, onPersonallyRecommend = { metadata, users, blurbContent -> socialViewModel.personallyRecommend(metadata, users, blurbContent) - } + }, + goToArtistPage = goToArtistPage ) } @@ -187,6 +189,7 @@ fun StatsScreen( isCritiqueBrainzLinked: suspend () -> Boolean?, onReview: (type: ReviewEntityType, blurbContent: String, rating: Int?, locale: String, metadata: Metadata) -> Unit, onPersonallyRecommend: (metadata: Metadata, users: List, blurbContent: String) -> Unit, + goToArtistPage: (String) -> Unit, ) { val currentTabSelection: MutableState = remember { mutableStateOf(CategoryState.ARTISTS) @@ -442,7 +445,7 @@ fun StatsScreen( index, topAlbum -> ListenCardSmall( trackName = topAlbum.releaseName ?: "", - artistName = topAlbum.artistName ?: "", + artists = topAlbum.artists ?: listOf(), coverArtUrl = getCoverArtUrl(topAlbum.caaReleaseMbid, topAlbum.caaId), modifier = Modifier.padding(top = 10.dp, bottom = 10.dp, end = 10.dp), color = app_bg_secondary_dark, @@ -450,7 +453,8 @@ fun StatsScreen( subtitleColor = ListenBrainzTheme.colorScheme.listenText.copy(alpha = 0.7f), enableTrailingContent = true, listenCount = topAlbum.listenCount, - enableDropdownIcon = true + enableDropdownIcon = true, + goToArtistPage = goToArtistPage ) { @@ -488,7 +492,7 @@ fun StatsScreen( )) ListenCardSmall( trackName = topSong.trackName ?: "", - artistName = topSong.artistName ?: "", + artists = topSong.artists ?: listOf(), coverArtUrl = getCoverArtUrl(topSong.caaReleaseMbid, topSong.caaId), modifier = Modifier.padding(top = 10.dp, bottom = 10.dp, end = 10.dp), color = app_bg_secondary_dark, @@ -532,7 +536,8 @@ fun StatsScreen( ) }, enableTrailingContent = true, - listenCount = topSong.listenCount + listenCount = topSong.listenCount, + goToArtistPage = goToArtistPage ) { val trackMetadata = metadata.trackMetadata if(trackMetadata != null){ diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt index bbd71a87..6b7cae70 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt @@ -69,6 +69,7 @@ fun TasteScreen( socialViewModel: SocialViewModel, feedViewModel : FeedViewModel, snackbarState : SnackbarHostState, + goToArtistPage: (String) -> Unit, ) { val uiState by viewModel.uiState.collectAsState() val socialUiState by socialViewModel.uiState.collectAsState() @@ -111,7 +112,8 @@ fun TasteScreen( }, onMessageShown = { socialViewModel.clearMsgFlow() - } + }, + goToArtistPage = goToArtistPage, ) } @@ -132,6 +134,7 @@ fun TasteScreen( onPersonallyRecommend: (metadata: Metadata, users: List, blurbContent: String) -> Unit, onErrorShown : () -> Unit, onMessageShown : () -> Unit, + goToArtistPage: (String) -> Unit, ){ val lovedHatedState: MutableState = remember { mutableStateOf(LovedHated.loved) } @@ -179,8 +182,7 @@ fun TasteScreen( horizontal = 16.dp, vertical = ListenBrainzTheme.paddings.lazyListAdjacent ), - trackName = feedback.trackMetadata?.trackName ?: "", artistName = feedback.trackMetadata - ?.artistName ?: "", coverArtUrl = getCoverArtUrl( + trackName = feedback.trackMetadata?.trackName ?: "", artists = feedback.trackMetadata?.mbidMapping?.artists ?: listOf(), coverArtUrl = getCoverArtUrl( caaReleaseMbid = feedback.trackMetadata?.mbidMapping?.caaReleaseMbid, caaId = feedback.trackMetadata?.mbidMapping?.caaId ), @@ -221,7 +223,8 @@ fun TasteScreen( } ) - } + }, + goToArtistPage = goToArtistPage ) { if(feedback.trackMetadata != null){ playListen(feedback.trackMetadata) @@ -275,8 +278,7 @@ fun TasteScreen( vertical = ListenBrainzTheme.paddings.lazyListAdjacent ), trackName = recording.trackMetadata?.trackName ?: "", - artistName = recording.trackMetadata - ?.artistName ?: "", + artists = recording.trackMetadata?.mbidMapping?.artists ?: listOf(), coverArtUrl = getCoverArtUrl( caaReleaseMbid = recording.trackMetadata?.mbidMapping?.caaReleaseMbid, caaId = recording.trackMetadata?.mbidMapping?.caaId @@ -326,7 +328,8 @@ fun TasteScreen( } ) - } + }, + goToArtistPage = goToArtistPage ) { if (recording.trackMetadata != null) { playListen(recording.trackMetadata) From 2d8d736ea3b659ebc28137a7634878228c951660 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sun, 18 Aug 2024 23:18:41 +0530 Subject: [PATCH 88/97] Linked User and artist pages --- .../android/ui/components/SimilarUserCard.kt | 9 ++++- .../android/ui/navigation/AppNavigation.kt | 19 ++++++++-- .../android/ui/screens/artist/ArtistScreen.kt | 28 +++++++++----- .../android/ui/screens/feed/FeedScreen.kt | 7 +++- .../screens/feed/events/ListenFeedLayout.kt | 3 +- .../feed/events/ListenLikeFeedLayout.kt | 3 +- .../PersonalRecommendationFeedLayout.kt | 3 +- .../android/ui/screens/main/MainActivity.kt | 4 ++ .../ui/screens/profile/BaseProfileScreen.kt | 8 ++-- .../ui/screens/profile/ProfileScreen.kt | 5 ++- .../screens/profile/listens/ListensScreen.kt | 38 +++++++++++++------ .../ui/screens/profile/stats/StatsScreen.kt | 7 +++- .../ui/screens/yim/YearInMusicActivity.kt | 2 +- .../ui/screens/yim/YimDiscoverScreen.kt | 11 ++++-- .../screens/yim/navigation/YimNavigation.kt | 11 +++++- .../ui/screens/yim23/YearInMusic23Activity.kt | 2 +- .../screens/yim23/Yim23MusicBuddiesScreen.kt | 9 +++-- .../yim23/navigation/Yim23Navigation.kt | 21 +--------- 18 files changed, 121 insertions(+), 69 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/components/SimilarUserCard.kt b/app/src/main/java/org/listenbrainz/android/ui/components/SimilarUserCard.kt index c17dc41b..4d9a6a8b 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/components/SimilarUserCard.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/components/SimilarUserCard.kt @@ -2,6 +2,7 @@ package org.listenbrainz.android.ui.components import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -47,7 +48,8 @@ fun SimilarUserCard( similarity: Float, modifier: Modifier = Modifier .fillMaxWidth() - .padding(vertical = 4.dp) + .padding(vertical = 4.dp), + goToUserPage: (String) -> Unit, ){ Surface( modifier = modifier, @@ -91,6 +93,9 @@ fun SimilarUserCard( color = if (uiModeIsDark) MaterialTheme.colorScheme.onSurface else lb_purple, lineHeight = 14.sp ) , + modifier = Modifier.clickable { + goToUserPage(userName) + } ) } @@ -136,6 +141,6 @@ fun SimilarUserCard( @Composable fun Preview(){ ListenBrainzTheme { - SimilarUserCard(index = 0, userName = "jasje", similarity = 0.80f) + SimilarUserCard(index = 0, userName = "jasje", similarity = 0.80f, goToUserPage = {}) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt index aa5c3b25..ec64e15d 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt @@ -28,6 +28,7 @@ fun AppNavigation( snackbarState : SnackbarHostState, goToUserProfile: () -> Unit, goToArtistPage: (String) -> Unit, + goToUserPage: (String?) -> Unit, ) { NavHost( navController = navController as NavHostController, @@ -80,7 +81,8 @@ fun AppNavigation( username = username, snackbarState = snackbarState, goToUserProfile = goToUserProfile, - goToArtistPage = goToArtistPage + goToArtistPage = goToArtistPage, + goToUserPage = goToUserPage ) } composable(route = AppNavigationItem.Settings.route){ @@ -98,9 +100,20 @@ fun AppNavigation( } } else{ - ArtistScreen(artistMbid = artistMbid, goToArtistPage = goToArtistPage) + ArtistScreen(artistMbid = artistMbid, goToArtistPage = goToArtistPage, goToUserPage = {username : String? -> + if(username != null) { + navController.navigate("${AppNavigationItem.Profile.route}/$username"){ + // Avoid building large backstack + popUpTo(AppNavigationItem.Feed.route){ + saveState = true + } + // Avoid copies + launchSingleTop = true + // Restore previous state + restoreState = true + } + } }) } - } } } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt index e8b6d490..abe74e3d 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt @@ -64,6 +64,7 @@ import coil.compose.AsyncImage import coil.request.ImageRequest import org.listenbrainz.android.R import org.listenbrainz.android.model.artist.ReleaseGroup +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.LoadingAnimation import org.listenbrainz.android.ui.screens.profile.listens.LoadMoreButton @@ -82,20 +83,22 @@ import org.listenbrainz.android.viewmodel.ArtistViewModel fun ArtistScreen( artistMbid: String, viewModel: ArtistViewModel = hiltViewModel(), - goToArtistPage: (String) -> Unit + goToArtistPage: (String) -> Unit, + goToUserPage: (String?) -> Unit, ) { LaunchedEffect(Unit) { viewModel.fetchArtistData(artistMbid) } val uiState by viewModel.uiState.collectAsState() - ArtistScreen(artistMbid = artistMbid,uiState = uiState, goToArtistPage = goToArtistPage) + ArtistScreen(artistMbid = artistMbid,uiState = uiState, goToArtistPage = goToArtistPage, goToUserPage = goToUserPage) } @Composable private fun ArtistScreen( artistMbid: String, uiState: ArtistUIState, - goToArtistPage: (String) -> Unit + goToArtistPage: (String) -> Unit, + goToUserPage: (String?) -> Unit, ) { Box(modifier = Modifier.fillMaxSize()){ AnimatedVisibility( @@ -125,10 +128,10 @@ private fun ArtistScreen( AlbumsCard(header = "Appears On", albumsList = uiState.appearsOn) } item { - SimilarArtists(uiState = uiState) + SimilarArtists(uiState = uiState, goToArtistPage = goToArtistPage) } item { - TopListenersCard(uiState = uiState) + TopListenersCard(uiState = uiState, goToUserPage = goToUserPage) } } } @@ -381,7 +384,9 @@ private fun PopularTracks( Text("Popular Tracks", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) Spacer(modifier = Modifier.height(20.dp)) popularTracks.map { - ListenCardSmall(trackName = it?.recordingName ?: "", artists = it?.artists ?: listOf(), coverArtUrl = Utils.getCoverArtUrl(it?.caaReleaseMbid, it?.caaId), goToArtistPage = goToArtistPage) { + ListenCardSmall(trackName = it?.recordingName ?: "", artists = it?.artists ?: listOf( + FeedListenArtist(it?.artistName ?: "" , null, "") + ), coverArtUrl = Utils.getCoverArtUrl(it?.caaReleaseMbid, it?.caaId), goToArtistPage = goToArtistPage) { } Spacer(modifier = Modifier.height(12.dp)) @@ -445,7 +450,8 @@ private fun AlbumsCard( @Composable private fun SimilarArtists( - uiState: ArtistUIState + uiState: ArtistUIState, + goToArtistPage: (String) -> Unit ) { val similarArtistsCollapisbleState: MutableState = remember { mutableStateOf(true) @@ -463,7 +469,8 @@ private fun SimilarArtists( Text("Similar Artists", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) similarArtists.map { ArtistCard(artistName = it?.name ?: "") { - + if(it?.artistMbid != null) + goToArtistPage(it.artistMbid) } Spacer(modifier = Modifier.height(12.dp)) } @@ -481,7 +488,8 @@ private fun SimilarArtists( @Composable private fun TopListenersCard( - uiState: ArtistUIState + uiState: ArtistUIState, + goToUserPage: (String?) -> Unit, ) { val topListenersCollapsibleState: MutableState = remember { mutableStateOf(true) @@ -499,7 +507,7 @@ private fun TopListenersCard( Spacer(modifier = Modifier.height(20.dp)) topListeners.map { ArtistCard(artistName = it?.userName ?: "", listenCount = it?.listenCount ?: 0) { - + goToUserPage(it?.userName) } Spacer(modifier = Modifier.height(12.dp)) } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/FeedScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/FeedScreen.kt index 0f94a618..0d9ab7ff 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/FeedScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/FeedScreen.kt @@ -71,6 +71,7 @@ import kotlinx.coroutines.launch import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.model.feed.FeedEvent import org.listenbrainz.android.model.feed.FeedEventType +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.model.feed.ReviewEntityType import org.listenbrainz.android.ui.components.ErrorBar import org.listenbrainz.android.ui.components.ListenCardSmall @@ -536,7 +537,9 @@ fun FollowListens( vertical = ListenBrainzTheme.paddings.lazyListAdjacent ), trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", - artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), + artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf( + FeedListenArtist(event.metadata.trackMetadata?.artistName ?: "" , null, "") + ), coverArtUrl = Utils.getCoverArtUrl( caaReleaseMbid = event.metadata.trackMetadata?.mbidMapping?.caaReleaseMbid, @@ -640,7 +643,7 @@ fun SimilarListens( vertical = ListenBrainzTheme.paddings.lazyListAdjacent ), trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", - artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), + artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(FeedListenArtist(event.metadata.trackMetadata?.artistName ?: "" , null, "")), coverArtUrl = Utils.getCoverArtUrl( caaReleaseMbid = event.metadata.trackMetadata?.mbidMapping?.caaReleaseMbid, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenFeedLayout.kt index aabd3b6e..24072118 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenFeedLayout.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.tooling.preview.Preview import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.model.feed.FeedEvent import org.listenbrainz.android.model.feed.FeedEventType +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.screens.feed.BaseFeedLayout import org.listenbrainz.android.ui.screens.feed.SocialDropdown @@ -41,7 +42,7 @@ fun ListenFeedLayout ( ListenCardSmall( trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", - artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), + artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(FeedListenArtist(event.metadata.trackMetadata?.artistName ?: "" , null, "")), coverArtUrl = remember { Utils.getCoverArtUrl( caaReleaseMbid = event.metadata.trackMetadata?.mbidMapping?.caaReleaseMbid, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenLikeFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenLikeFeedLayout.kt index 05e18cce..c2dc851b 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenLikeFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ListenLikeFeedLayout.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.tooling.preview.Preview import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.model.feed.FeedEvent import org.listenbrainz.android.model.feed.FeedEventType +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.screens.feed.BaseFeedLayout import org.listenbrainz.android.ui.screens.feed.SocialDropdown @@ -40,7 +41,7 @@ fun ListenLikeFeedLayout( ) { ListenCardSmall( trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", - artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), + artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(FeedListenArtist(event.metadata.trackMetadata?.artistName ?: "" , null, "")), coverArtUrl = remember { getCoverArtUrl( caaReleaseMbid = event.metadata.trackMetadata?.mbidMapping?.caaReleaseMbid, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PersonalRecommendationFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PersonalRecommendationFeedLayout.kt index ff97cf3f..e4f0baf8 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PersonalRecommendationFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PersonalRecommendationFeedLayout.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.unit.dp import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.model.feed.FeedEvent import org.listenbrainz.android.model.feed.FeedEventType +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.dialogs.UserTag import org.listenbrainz.android.ui.screens.feed.BaseFeedLayout @@ -52,7 +53,7 @@ fun PersonalRecommendationFeedLayout( ) { ListenCardSmall( trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", - artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), + artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(FeedListenArtist(event.metadata.trackMetadata?.artistName ?: "" , null, "")), coverArtUrl = remember { Utils.getCoverArtUrl( caaReleaseMbid = event.metadata.trackMetadata?.mbidMapping?.caaReleaseMbid, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt index 91b609f5..099a004f 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt @@ -206,6 +206,10 @@ class MainActivity : ComponentActivity() { goToArtistPage = { mbid -> navController.navigate("artist/${mbid}") + }, + goToUserPage = { + user -> + navController.navigate("${AppNavigationItem.Profile.route}/$user") } ) } 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 5801c1df..98c8eef6 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 @@ -73,6 +73,7 @@ fun BaseProfileScreen( listensViewModel: ListensViewModel = hiltViewModel(), socialViewModel: SocialViewModel = hiltViewModel(), goToArtistPage: (String) -> Unit, + goToUserPage: (String?) -> Unit, ){ val currentTab : MutableState = remember { mutableStateOf(ProfileScreenTab.LISTENS) } @@ -218,7 +219,8 @@ fun BaseProfileScreen( feedViewModel = feedViewModel, socialViewModel = socialViewModel, viewModel = listensViewModel, - goToArtistPage = goToArtistPage + goToArtistPage = goToArtistPage, + goToUserPage = goToUserPage, ) ProfileScreenTab.STATS -> StatsScreen( username = username, @@ -244,10 +246,10 @@ fun BaseProfileScreen( feedViewModel = feedViewModel, socialViewModel = socialViewModel, viewModel = listensViewModel, - goToArtistPage = goToArtistPage + goToArtistPage = goToArtistPage, + goToUserPage = goToUserPage, ) } - } } if(mbOpeningErrorState.value != null){ diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt index 6816082c..b0fa8ecc 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileScreen.kt @@ -49,6 +49,7 @@ fun ProfileScreen( snackbarState: SnackbarHostState, goToUserProfile: () -> Unit, goToArtistPage: (String) -> Unit, + goToUserPage: (String?) -> Unit, ) { val scrollState = rememberScrollState() val uiState = viewModel.uiState.collectAsState() @@ -78,9 +79,9 @@ fun ProfileScreen( viewModel.unfollowUser(it) }, goToUserProfile = goToUserProfile, - goToArtistPage = goToArtistPage + goToArtistPage = goToArtistPage, + goToUserPage = goToUserPage ) - } else -> { Column( diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt index 8f6b8b74..2ca24513 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt @@ -62,6 +62,7 @@ import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.model.SimilarUser import org.listenbrainz.android.model.SocialUiState import org.listenbrainz.android.model.TrackMetadata +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.model.feed.ReviewEntityType import org.listenbrainz.android.model.user.Artist import org.listenbrainz.android.ui.components.ErrorBar @@ -99,6 +100,7 @@ fun ListensScreen( snackbarState : SnackbarHostState, username: String?, goToArtistPage: (String) -> Unit, + goToUserPage: (String?) -> Unit ) { val uiState by userViewModel.uiState.collectAsState() @@ -168,7 +170,8 @@ fun ListensScreen( } } }, - goToArtistPage = goToArtistPage + goToArtistPage = goToArtistPage, + goToUserPage = goToUserPage ) } @@ -213,6 +216,7 @@ fun ListensScreen( onPersonallyRecommend: (metadata: Metadata, users: List, blurbContent: String) -> Unit, onFollowButtonClick: (String?, Boolean) -> Unit, goToArtistPage: (String) -> Unit, + goToUserPage: (String?) -> Unit, ) { val listState = rememberLazyListState() @@ -270,7 +274,9 @@ fun ListensScreen( vertical = ListenBrainzTheme.paddings.lazyListAdjacent ), trackName = listen.trackMetadata.trackName, - artists = metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), + artists = metadata.trackMetadata?.mbidMapping?.artists ?: listOf( + FeedListenArtist(metadata.trackMetadata?.artistName ?: "" , null, "") + ), coverArtUrl = getCoverArtUrl( caaReleaseMbid = listen.trackMetadata.mbidMapping?.caaReleaseMbid, caaId = listen.trackMetadata.mbidMapping?.caaId @@ -371,7 +377,8 @@ fun ListensScreen( newMenuState-> followersMenuState.value = !newMenuState }, - onFollowButtonClick = onFollowButtonClick + onFollowButtonClick = onFollowButtonClick, + goToUserPage = goToUserPage ) if((uiState.listensTabUiState.followersCount ?: 0) > 5 || ((uiState.listensTabUiState.followingCount ?: 0) > 5)){ Spacer(modifier = Modifier.height(20.dp)) @@ -401,7 +408,7 @@ fun ListensScreen( SimilarUsersCard(similarUsers = when(similarUsersCollapsibleState.value){ true -> uiState.listensTabUiState.similarUsers?.take(5) ?: emptyList() false -> uiState.listensTabUiState.similarUsers ?: emptyList() - }) + }, goToUserPage = goToUserPage) if((uiState.listensTabUiState.similarUsers?.size ?: 0) > 5){ Spacer(modifier = Modifier.height(20.dp)) @@ -673,7 +680,9 @@ fun CompatibilityCard(compatibility: Float, similarArtists: List, goToAr } @Composable -private fun FollowersCard(followersCount: Int?, followingCount: Int?, followers: List>, following: List>, followersState: Boolean, onStateChange: (Boolean) -> Unit, onFollowButtonClick: (String?, Boolean) -> Unit) { +private fun FollowersCard(followersCount: Int?, followingCount: Int?, followers: List>, + following: List>, followersState: Boolean, onStateChange: (Boolean) -> Unit, + onFollowButtonClick: (String?, Boolean) -> Unit, goToUserPage: (String?) -> Unit) { Column(modifier = Modifier.padding(start = 16.dp , top = 30.dp)) { Text( "Followers", @@ -748,12 +757,12 @@ private fun FollowersCard(followersCount: Int?, followingCount: Int?, followers: when(followersState){ true -> followers.map { state -> - FollowCard(username = state.first, onFollowButtonClick = onFollowButtonClick, followStatus = state.second) + FollowCard(username = state.first, onFollowButtonClick = onFollowButtonClick, followStatus = state.second, goToUserPage = goToUserPage) Spacer(modifier = Modifier.height(10.dp)) } false -> following.map { state -> - FollowCard(username = state.first, onFollowButtonClick = onFollowButtonClick, followStatus = state.second) + FollowCard(username = state.first, onFollowButtonClick = onFollowButtonClick, followStatus = state.second, goToUserPage = goToUserPage) Spacer(modifier = Modifier.height(10.dp)) } } @@ -761,18 +770,19 @@ private fun FollowersCard(followersCount: Int?, followingCount: Int?, followers: } @Composable -private fun SimilarUsersCard(similarUsers: List){ +private fun SimilarUsersCard(similarUsers: List, goToUserPage: (String?) -> Unit){ Spacer(modifier = Modifier.height(20.dp)) Text("Similar Users", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), modifier = Modifier.padding(horizontal = 16.dp)) Spacer(modifier = Modifier.height(20.dp)) similarUsers.mapIndexed{ index , item -> - SimilarUserCard(index = index, userName = item.username, similarity = item.similarity.toFloat(), modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp)) + SimilarUserCard(index = index, userName = item.username, similarity = item.similarity.toFloat(), + modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), goToUserPage = goToUserPage) } } @Composable -private fun FollowCard(username: String?, onFollowButtonClick: (String?, Boolean) -> Unit, followStatus: Boolean) { +private fun FollowCard(username: String?, onFollowButtonClick: (String?, Boolean) -> Unit, followStatus: Boolean, goToUserPage: (String?) -> Unit) { Card(colors = CardDefaults.cardColors(containerColor = ListenBrainzTheme.colorScheme.followerCardColor)) { Row( modifier = Modifier @@ -785,7 +795,10 @@ private fun FollowCard(username: String?, onFollowButtonClick: (String?, Boolean Text( username ?: "", color = ListenBrainzTheme.colorScheme.followerCardTextColor, - style = MaterialTheme.typography.bodyLarge + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.clickable { + goToUserPage(username) + } ) TextButton( onClick = { @@ -841,6 +854,7 @@ fun ListensScreenPreview() { snackbarState = remember { SnackbarHostState() }, username = "pranavkonidena", onFollowButtonClick = {_,_ -> }, - goToArtistPage = {} + goToArtistPage = {}, + goToUserPage = {} ) } 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 fa933ec3..32ee91e0 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 @@ -421,7 +421,12 @@ fun StatsScreen( Column (horizontalAlignment = Alignment.CenterHorizontally) { topArtists.map { topArtist -> - ArtistCard(artistName = topArtist.artistName ?: "", listenCount = topArtist.listenCount){} + ArtistCard(artistName = topArtist.artistName ?: "", listenCount = topArtist.listenCount){ + if(topArtist.artistMbid != null){ + goToArtistPage(topArtist.artistMbid) + } + + } } Spacer(modifier = Modifier.height(10.dp)) if((uiState.statsTabUIState.topArtists?.size ?: 0) > 5){ diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/yim/YearInMusicActivity.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/yim/YearInMusicActivity.kt index 95983ad8..0572fd65 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/yim/YearInMusicActivity.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/yim/YearInMusicActivity.kt @@ -33,7 +33,7 @@ class YearInMusicActivity : ComponentActivity() { Toast.makeText(this, "Please Login to access your Year in Music!", Toast.LENGTH_LONG).show() finish() } - YimNavigation(yimViewModel = yimViewModel, networkConnectivityViewModel = networkConnectivityViewModel, activity = this) + YimNavigation(yimViewModel = yimViewModel, networkConnectivityViewModel = networkConnectivityViewModel, activity = this, goToUserPage = {}) } } } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/yim/YimDiscoverScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/yim/YimDiscoverScreen.kt index 9c09c803..9ec6b534 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/yim/YimDiscoverScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/yim/YimDiscoverScreen.kt @@ -52,7 +52,8 @@ import org.listenbrainz.android.viewmodel.YimViewModel fun YimDiscoverScreen( yimViewModel: YimViewModel, navController: NavController, - paddings: YimPaddings = LocalYimPaddings.current + paddings: YimPaddings = LocalYimPaddings.current, + goToUserPage: (String?) -> Unit, ){ YearInMusicTheme(redTheme = false) { var startAnim by remember{ @@ -167,7 +168,7 @@ fun YimDiscoverScreen( visible = startAnim, enter = expandVertically(animationSpec = tween(durationMillis = 700, delayMillis = 1900)) ) { - YimSimilarUsersList(yimViewModel = yimViewModel) + YimSimilarUsersList(yimViewModel = yimViewModel, goToUserPage = goToUserPage) } } @@ -189,7 +190,8 @@ fun YimDiscoverScreen( @Composable private fun YimSimilarUsersList( yimViewModel: YimViewModel, - paddings: YimPaddings = LocalYimPaddings.current + paddings: YimPaddings = LocalYimPaddings.current, + goToUserPage: (String?) -> Unit, ) { val similarUsers = remember { yimViewModel.getSimilarUsers() ?: listOf() @@ -211,7 +213,8 @@ private fun YimSimilarUsersList( userName = item.first, similarity = item.second.toFloat(), cardBackGround = MaterialTheme.colorScheme.surface, - uiModeIsDark = false + uiModeIsDark = false, + goToUserPage = goToUserPage ) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/yim/navigation/YimNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/yim/navigation/YimNavigation.kt index 42781086..ac784c17 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/yim/navigation/YimNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/yim/navigation/YimNavigation.kt @@ -15,7 +15,13 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import org.listenbrainz.android.model.yimdata.YimScreens -import org.listenbrainz.android.ui.screens.yim.* +import org.listenbrainz.android.ui.screens.yim.YimChartsScreen +import org.listenbrainz.android.ui.screens.yim.YimDiscoverScreen +import org.listenbrainz.android.ui.screens.yim.YimEndgameScreen +import org.listenbrainz.android.ui.screens.yim.YimHomeScreen +import org.listenbrainz.android.ui.screens.yim.YimRecommendedPlaylistsScreen +import org.listenbrainz.android.ui.screens.yim.YimStatisticsScreen +import org.listenbrainz.android.ui.screens.yim.YimTopAlbumsScreen import org.listenbrainz.android.util.connectivityobserver.NetworkConnectivityViewModel import org.listenbrainz.android.viewmodel.YimViewModel @@ -28,6 +34,7 @@ fun YimNavigation( yimViewModel: YimViewModel, activity: ComponentActivity, networkConnectivityViewModel: NetworkConnectivityViewModel, + goToUserPage: (String?) -> Unit, ) { val navController = rememberNavController() NavHost( @@ -73,7 +80,7 @@ fun YimNavigation( } addYimScreen( route = YimScreens.YimDiscoverScreen.name ){ - YimDiscoverScreen(yimViewModel = yimViewModel, navController = navController) + YimDiscoverScreen(yimViewModel = yimViewModel, navController = navController, goToUserPage = goToUserPage) } addYimScreen( route = YimScreens.YimEndgameScreen.name ){ diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/yim23/YearInMusic23Activity.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/yim23/YearInMusic23Activity.kt index 11dbfeb9..cd76b7cb 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/yim23/YearInMusic23Activity.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/yim23/YearInMusic23Activity.kt @@ -33,7 +33,7 @@ class YearInMusic23Activity : ComponentActivity() { Toast.LENGTH_LONG).show() finish() } - Yim23Navigation(yimViewModel = yim23ViewModel ,networkConnectivityViewModel = networkConnectivityViewModel, activity = this) + Yim23Navigation(yimViewModel = yim23ViewModel ,networkConnectivityViewModel = networkConnectivityViewModel, activity = this, goToUserPage = {}) } } } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/yim23/Yim23MusicBuddiesScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/yim23/Yim23MusicBuddiesScreen.kt index 99667d14..38e2a850 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/yim23/Yim23MusicBuddiesScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/yim23/Yim23MusicBuddiesScreen.kt @@ -26,7 +26,8 @@ import org.listenbrainz.android.viewmodel.Yim23ViewModel @Composable fun Yim23MusicBuddiesScreen ( viewModel: Yim23ViewModel, - navController: NavController + navController: NavController, + goToUserPage: (String?) -> Unit, ) { val followers = remember { viewModel.followers } val context = LocalContext.current @@ -41,7 +42,7 @@ fun Yim23MusicBuddiesScreen ( else -> Yim23Screens.YimFriendsScreen } ) { - Yim23MusicBuddies(viewModel = viewModel) + Yim23MusicBuddies(viewModel = viewModel, goToUserPage = goToUserPage) } } else{ @@ -57,7 +58,7 @@ fun Yim23MusicBuddiesScreen ( @Composable -private fun Yim23MusicBuddies (viewModel: Yim23ViewModel) { +private fun Yim23MusicBuddies (viewModel: Yim23ViewModel, goToUserPage: (String?) -> Unit,) { val musicBuddies = remember { viewModel.getSimilarUsers() ?: listOf() } @@ -75,7 +76,7 @@ private fun Yim23MusicBuddies (viewModel: Yim23ViewModel) { LazyColumn (state = rememberLazyListState()) { itemsIndexed(musicBuddies.toList()) {index , it -> SimilarUserCard(uiModeIsDark = false,index = index, userName = it.first, - similarity = it.second.toFloat() , cardBackGround = Color(0xFFe0e5de)) + similarity = it.second.toFloat() , cardBackGround = Color(0xFFe0e5de), goToUserPage = goToUserPage) } } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/yim23/navigation/Yim23Navigation.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/yim23/navigation/Yim23Navigation.kt index da15981a..e95b94c8 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/yim23/navigation/Yim23Navigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/yim23/navigation/Yim23Navigation.kt @@ -2,31 +2,16 @@ package org.listenbrainz.android.ui.screens.yim23.navigation import androidx.activity.ComponentActivity import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.AnimatedVisibilityScope -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import kotlinx.coroutines.launch -import org.listenbrainz.android.model.AppNavigationItem import org.listenbrainz.android.model.yimdata.Yim23Screens -import org.listenbrainz.android.model.yimdata.YimScreens -import org.listenbrainz.android.ui.screens.profile.ProfileScreen -import org.listenbrainz.android.ui.screens.yim.* import org.listenbrainz.android.ui.screens.yim.navigation.addYimScreen import org.listenbrainz.android.ui.screens.yim23.Yim23AlbumsListScreen import org.listenbrainz.android.ui.screens.yim23.Yim23ChartTitleScreen @@ -48,11 +33,8 @@ import org.listenbrainz.android.ui.screens.yim23.Yim23StatsTitleScreen import org.listenbrainz.android.ui.screens.yim23.Yim23TopAlbumsScreen import org.listenbrainz.android.ui.screens.yim23.Yim23TopArtistsScreen import org.listenbrainz.android.ui.screens.yim23.Yim23TopSongsScreen -import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.util.connectivityobserver.NetworkConnectivityViewModel -import org.listenbrainz.android.viewmodel.SocialViewModel import org.listenbrainz.android.viewmodel.Yim23ViewModel -import org.listenbrainz.android.viewmodel.YimViewModel // Transition Duration private const val screenTransitionDuration = 900 @@ -63,6 +45,7 @@ fun Yim23Navigation( yimViewModel: Yim23ViewModel, activity: ComponentActivity, networkConnectivityViewModel: NetworkConnectivityViewModel, + goToUserPage: (String?) -> Unit, ) { val navController = rememberNavController() NavHost( @@ -142,7 +125,7 @@ fun Yim23Navigation( Yim23NewAlbumsFromTopArtistsScreen(viewModel = yimViewModel, navController = navController) } addYimScreen( route = Yim23Screens.YimMusicBuddiesScreen.name){ - Yim23MusicBuddiesScreen(viewModel = yimViewModel, navController = navController) + Yim23MusicBuddiesScreen(viewModel = yimViewModel, navController = navController, goToUserPage = goToUserPage) } addYimScreen( route = Yim23Screens.YimFriendsScreen.name){ Yim23FriendsScreen(viewModel = yimViewModel, navController = navController) From 7e24bd4851c42661c8cfce042059713f921495c7 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sun, 18 Aug 2024 23:24:52 +0530 Subject: [PATCH 89/97] Fixes cases when MBID Mapping is null --- .../android/ui/screens/feed/events/PinFeedLayout.kt | 3 ++- .../feed/events/RecordingRecommendationFeedLayout.kt | 3 ++- .../android/ui/screens/feed/events/ReviewFeedLayout.kt | 3 ++- .../android/ui/screens/profile/stats/StatsScreen.kt | 5 +++-- .../android/ui/screens/profile/taste/TasteScreen.kt | 8 +++++--- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PinFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PinFeedLayout.kt index 24693861..85fbf244 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PinFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/PinFeedLayout.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.tooling.preview.Preview import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.model.feed.FeedEvent import org.listenbrainz.android.model.feed.FeedEventType +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.screens.feed.BaseFeedLayout import org.listenbrainz.android.ui.screens.feed.SocialDropdown @@ -45,7 +46,7 @@ fun PinFeedLayout( ListenCardSmall( trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", - artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), + artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(FeedListenArtist(event.metadata.trackMetadata?.artistName ?: "" , null, "")), coverArtUrl = remember { Utils.getCoverArtUrl( caaReleaseMbid = event.metadata.trackMetadata?.mbidMapping?.caaReleaseMbid, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/RecordingRecommendationFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/RecordingRecommendationFeedLayout.kt index ff00dfb9..14d5f16b 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/RecordingRecommendationFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/RecordingRecommendationFeedLayout.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.tooling.preview.Preview import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.model.feed.FeedEvent import org.listenbrainz.android.model.feed.FeedEventType +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.screens.feed.BaseFeedLayout import org.listenbrainz.android.ui.screens.feed.SocialDropdown @@ -40,7 +41,7 @@ fun RecordingRecommendationFeedLayout( onDeleteOrHide = onDeleteOrHide, goToUserPage = goToUserPage) { ListenCardSmall( trackName = event.metadata.trackMetadata?.trackName ?: "Unknown", - artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), + artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(FeedListenArtist(event.metadata.trackMetadata?.artistName ?: "" , null, "")), coverArtUrl = remember { Utils.getCoverArtUrl( caaReleaseMbid = event.metadata.trackMetadata?.mbidMapping?.caaReleaseMbid, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ReviewFeedLayout.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ReviewFeedLayout.kt index 4c6b95c4..9f6f10ec 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ReviewFeedLayout.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/feed/events/ReviewFeedLayout.kt @@ -29,6 +29,7 @@ import org.listenbrainz.android.R import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.model.feed.FeedEvent import org.listenbrainz.android.model.feed.FeedEventType +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.screens.feed.BaseFeedLayout import org.listenbrainz.android.ui.screens.feed.SocialDropdown @@ -63,7 +64,7 @@ fun ReviewFeedLayout( ListenCardSmall( trackName = event.metadata.entityName ?: "Unknown", - artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(), + artists = event.metadata.trackMetadata?.mbidMapping?.artists ?: listOf(FeedListenArtist(event.metadata.trackMetadata?.artistName ?: "" , null, "")), coverArtUrl = remember { Utils.getCoverArtUrl( caaReleaseMbid = event.metadata.trackMetadata?.mbidMapping?.caaReleaseMbid, 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 32ee91e0..9c519e64 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 @@ -65,6 +65,7 @@ import org.listenbrainz.android.model.MbidMapping import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.model.SocialUiState import org.listenbrainz.android.model.TrackMetadata +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.model.feed.ReviewEntityType import org.listenbrainz.android.ui.components.ErrorBar import org.listenbrainz.android.ui.components.ListenCardSmall @@ -450,7 +451,7 @@ fun StatsScreen( index, topAlbum -> ListenCardSmall( trackName = topAlbum.releaseName ?: "", - artists = topAlbum.artists ?: listOf(), + artists = topAlbum.artists ?: listOf(FeedListenArtist(topAlbum.artistName ?: "", null, "")), coverArtUrl = getCoverArtUrl(topAlbum.caaReleaseMbid, topAlbum.caaId), modifier = Modifier.padding(top = 10.dp, bottom = 10.dp, end = 10.dp), color = app_bg_secondary_dark, @@ -497,7 +498,7 @@ fun StatsScreen( )) ListenCardSmall( trackName = topSong.trackName ?: "", - artists = topSong.artists ?: listOf(), + artists = topSong.artists ?:listOf(FeedListenArtist(topSong.artistName ?: "", null, "")), coverArtUrl = getCoverArtUrl(topSong.caaReleaseMbid, topSong.caaId), modifier = Modifier.padding(top = 10.dp, bottom = 10.dp, end = 10.dp), color = app_bg_secondary_dark, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt index 6b7cae70..45b026b5 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/taste/TasteScreen.kt @@ -44,6 +44,7 @@ import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.SocialUiState import org.listenbrainz.android.model.TrackMetadata +import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.model.feed.ReviewEntityType import org.listenbrainz.android.ui.components.ErrorBar import org.listenbrainz.android.ui.components.ListenCardSmall @@ -182,7 +183,9 @@ fun TasteScreen( horizontal = 16.dp, vertical = ListenBrainzTheme.paddings.lazyListAdjacent ), - trackName = feedback.trackMetadata?.trackName ?: "", artists = feedback.trackMetadata?.mbidMapping?.artists ?: listOf(), coverArtUrl = getCoverArtUrl( + trackName = feedback.trackMetadata?.trackName ?: "", artists = feedback.trackMetadata?.mbidMapping?.artists ?: listOf( + FeedListenArtist(feedback.trackMetadata?.artistName ?: "", null, "") + ), coverArtUrl = getCoverArtUrl( caaReleaseMbid = feedback.trackMetadata?.mbidMapping?.caaReleaseMbid, caaId = feedback.trackMetadata?.mbidMapping?.caaId ), @@ -274,11 +277,10 @@ fun TasteScreen( }, modifier = Modifier .padding( - vertical = ListenBrainzTheme.paddings.lazyListAdjacent ), trackName = recording.trackMetadata?.trackName ?: "", - artists = recording.trackMetadata?.mbidMapping?.artists ?: listOf(), + artists = recording.trackMetadata?.mbidMapping?.artists ?: listOf(FeedListenArtist(recording.trackMetadata?.artistName ?: "", null, "")), coverArtUrl = getCoverArtUrl( caaReleaseMbid = recording.trackMetadata?.mbidMapping?.caaReleaseMbid, caaId = recording.trackMetadata?.mbidMapping?.caaId From 7a8bdec1fd48704504b324cc76b7c226bbe95dc8 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Mon, 19 Aug 2024 10:22:05 +0530 Subject: [PATCH 90/97] Adds review functionality on artist pages --- .../android/YearInMusicActivityTest.kt | 8 +- .../listenbrainz/android/service/CBService.kt | 5 +- .../ui/components/dialogs/ReviewDialog.kt | 6 +- .../components/dialogs/ReviewEnabledDialog.kt | 5 +- .../android/ui/navigation/AppNavigation.kt | 2 +- .../android/ui/screens/artist/ArtistScreen.kt | 125 +++++++++++++++++- .../screens/profile/listens/ListensScreen.kt | 11 +- .../android/viewmodel/SocialViewModel.kt | 13 +- app/src/main/res/values/strings.xml | 2 +- 9 files changed, 156 insertions(+), 21 deletions(-) diff --git a/app/src/androidTest/java/org/listenbrainz/android/YearInMusicActivityTest.kt b/app/src/androidTest/java/org/listenbrainz/android/YearInMusicActivityTest.kt index 1c1baa7a..d605acc0 100644 --- a/app/src/androidTest/java/org/listenbrainz/android/YearInMusicActivityTest.kt +++ b/app/src/androidTest/java/org/listenbrainz/android/YearInMusicActivityTest.kt @@ -2,8 +2,12 @@ package org.listenbrainz.android import androidx.activity.ComponentActivity import androidx.annotation.StringRes -import androidx.compose.ui.test.* import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performTouchInput import androidx.test.ext.junit.runners.AndroidJUnit4 import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -41,7 +45,7 @@ class YearInMusicActivityTest { val networkViewModel = MockNetworkConnectivityViewModel(ConnectivityObserver.NetworkStatus.AVAILABLE) rule.setContent { - YimNavigation(yimViewModel = yimViewModel, activity = activity, networkConnectivityViewModel = networkViewModel) + YimNavigation(yimViewModel = yimViewModel, activity = activity, networkConnectivityViewModel = networkViewModel, goToUserPage = {}) } } diff --git a/app/src/main/java/org/listenbrainz/android/service/CBService.kt b/app/src/main/java/org/listenbrainz/android/service/CBService.kt index da202e2f..113bea80 100644 --- a/app/src/main/java/org/listenbrainz/android/service/CBService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/CBService.kt @@ -3,10 +3,9 @@ package org.listenbrainz.android.service import org.listenbrainz.android.model.artist.ArtistReview import retrofit2.Response import retrofit2.http.GET -import retrofit2.http.Path import retrofit2.http.Query interface CBService { - @GET("ws/1/review/?limit=5&entity_id={artist_mbid}") - suspend fun getArtistReviews(@Path("artist_mbid") artistMbid: String?, @Query("entity_type") entityType: String? = "artist"): Response + @GET("ws/1/review/") + suspend fun getArtistReviews(@Query("entity_id") artistMbid: String?, @Query("entity_type") entityType: String? = "artist", @Query("limit") limit: Int = 5): Response } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/components/dialogs/ReviewDialog.kt b/app/src/main/java/org/listenbrainz/android/ui/components/dialogs/ReviewDialog.kt index b90bc0d5..e7988763 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/components/dialogs/ReviewDialog.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/components/dialogs/ReviewDialog.kt @@ -25,7 +25,8 @@ fun ReviewDialog( releaseName: String?, onDismiss: () -> Unit, isCritiqueBrainzLinked: suspend () -> Boolean?, - onSubmit: (type: ReviewEntityType, blurbContent: String, rating: Int?, locale: String) -> Unit + onSubmit: (type: ReviewEntityType, blurbContent: String, rating: Int?, locale: String) -> Unit, + reviewEntityType: ReviewEntityType = ReviewEntityType.RECORDING ) { var isLinked by rememberSaveable { mutableStateOf(null) } @@ -45,7 +46,8 @@ fun ReviewDialog( artistName = artistName, releaseName = releaseName, onDismiss = onDismiss, - onSubmit = onSubmit + onSubmit = onSubmit, + selectedEntityType = reviewEntityType ) false -> ReviewDisabledDialog(onDismiss = onDismiss) null -> Dialog(onDismissRequest = onDismiss) { diff --git a/app/src/main/java/org/listenbrainz/android/ui/components/dialogs/ReviewEnabledDialog.kt b/app/src/main/java/org/listenbrainz/android/ui/components/dialogs/ReviewEnabledDialog.kt index 7a7a7259..2300c4d4 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/components/dialogs/ReviewEnabledDialog.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/components/dialogs/ReviewEnabledDialog.kt @@ -59,13 +59,14 @@ fun ReviewEnabledDialog( onDismiss: () -> Unit, uriHandler: UriHandler = LocalUriHandler.current, keyboardController: SoftwareKeyboardController? = LocalSoftwareKeyboardController.current, - onSubmit: (type: ReviewEntityType, blurbContent: String, rating: Int?, locale: String) -> Unit + onSubmit: (type: ReviewEntityType, blurbContent: String, rating: Int?, locale: String) -> Unit, + selectedEntityType: ReviewEntityType = ReviewEntityType.RECORDING ){ var blurbContent by rememberSaveable { mutableStateOf("") } var selectedEntity by rememberSaveable { - mutableStateOf(ReviewEntityType.RECORDING) + mutableStateOf(selectedEntityType) } var rating by rememberSaveable { mutableStateOf(0f) diff --git a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt index ec64e15d..7192200e 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt @@ -100,7 +100,7 @@ fun AppNavigation( } } else{ - ArtistScreen(artistMbid = artistMbid, goToArtistPage = goToArtistPage, goToUserPage = {username : String? -> + ArtistScreen(artistMbid = artistMbid, goToArtistPage = goToArtistPage, snackBarState = snackbarState, goToUserPage = {username : String? -> if(username != null) { navController.navigate("${AppNavigationItem.Profile.route}/$username"){ // Avoid building large backstack diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt index abe74e3d..679e0397 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt @@ -34,8 +34,10 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SuggestionChipDefaults import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -62,35 +64,58 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.AsyncImage import coil.request.ImageRequest +import com.gowtham.ratingbar.RatingBar +import com.gowtham.ratingbar.RatingBarStyle import org.listenbrainz.android.R +import org.listenbrainz.android.model.Listen +import org.listenbrainz.android.model.MbidMapping +import org.listenbrainz.android.model.Metadata +import org.listenbrainz.android.model.TrackMetadata import org.listenbrainz.android.model.artist.ReleaseGroup import org.listenbrainz.android.model.feed.FeedListenArtist +import org.listenbrainz.android.model.feed.ReviewEntityType +import org.listenbrainz.android.ui.components.ErrorBar import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.LoadingAnimation +import org.listenbrainz.android.ui.components.SuccessBar +import org.listenbrainz.android.ui.components.dialogs.Dialog +import org.listenbrainz.android.ui.components.dialogs.rememberDialogsState +import org.listenbrainz.android.ui.screens.feed.FeedUiState +import org.listenbrainz.android.ui.screens.profile.listens.Dialogs +import org.listenbrainz.android.ui.screens.profile.listens.ListenDialogBundleKeys import org.listenbrainz.android.ui.screens.profile.listens.LoadMoreButton import org.listenbrainz.android.ui.screens.profile.stats.ArtistCard import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.ui.theme.app_bg_dark +import org.listenbrainz.android.ui.theme.app_bg_light import org.listenbrainz.android.ui.theme.app_bg_mid import org.listenbrainz.android.ui.theme.app_bg_secondary_dark +import org.listenbrainz.android.ui.theme.lb_purple import org.listenbrainz.android.ui.theme.lb_purple_night +import org.listenbrainz.android.ui.theme.new_app_bg_light import org.listenbrainz.android.util.Constants.MB_BASE_URL import org.listenbrainz.android.util.Utils import org.listenbrainz.android.viewmodel.ArtistViewModel +import org.listenbrainz.android.viewmodel.FeedViewModel +import org.listenbrainz.android.viewmodel.SocialViewModel @Composable fun ArtistScreen( artistMbid: String, viewModel: ArtistViewModel = hiltViewModel(), + socialViewModel: SocialViewModel = hiltViewModel(), + feedViewModel: FeedViewModel = hiltViewModel(), goToArtistPage: (String) -> Unit, goToUserPage: (String?) -> Unit, + snackBarState: SnackbarHostState ) { LaunchedEffect(Unit) { viewModel.fetchArtistData(artistMbid) } val uiState by viewModel.uiState.collectAsState() - ArtistScreen(artistMbid = artistMbid,uiState = uiState, goToArtistPage = goToArtistPage, goToUserPage = goToUserPage) + ArtistScreen(artistMbid = artistMbid,uiState = uiState, goToArtistPage = goToArtistPage, goToUserPage = goToUserPage, + socialViewModel = socialViewModel, feedViewModel = feedViewModel, snackBarState = snackBarState) } @Composable @@ -99,6 +124,9 @@ private fun ArtistScreen( uiState: ArtistUIState, goToArtistPage: (String) -> Unit, goToUserPage: (String?) -> Unit, + socialViewModel: SocialViewModel, + feedViewModel: FeedViewModel, + snackBarState: SnackbarHostState ) { Box(modifier = Modifier.fillMaxSize()){ AnimatedVisibility( @@ -133,6 +161,11 @@ private fun ArtistScreen( item { TopListenersCard(uiState = uiState, goToUserPage = goToUserPage) } + item { + ReviewsCard(uiState = uiState, goToUserPage = goToUserPage, + socialViewModel = socialViewModel, feedViewModel = feedViewModel, artistMbid = artistMbid, + snackBarState = snackBarState, onMessageShown = {socialViewModel.clearMsgFlow()}, onErrorShown = {socialViewModel.clearErrorFlow()}) + } } } @@ -423,7 +456,7 @@ private fun AlbumsCard( .horizontalScroll(rememberScrollState()) .padding(top = 20.dp)) { albumsList?.map { - Box (modifier = Modifier) { + Box (modifier = Modifier.width(150.dp)) { Column { val coverArt = Utils.getCoverArtUrl(it?.caaReleaseMbid, it?.caaId, 500) AsyncImage( @@ -438,7 +471,7 @@ private fun AlbumsCard( contentDescription = "Album Cover Art" ) Spacer(modifier = Modifier.height(10.dp)) - Text(it?.name ?: "", color = lb_purple_night, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 18.sp)) + Text(it?.name ?: "", color = lb_purple_night, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 18.sp), overflow = TextOverflow.Ellipsis) } } Spacer(modifier = Modifier.width(40.dp)) @@ -523,7 +556,91 @@ private fun TopListenersCard( } } +@Composable +private fun ReviewsCard( + uiState: ArtistUIState, + feedViewModel: FeedViewModel, + socialViewModel: SocialViewModel, + snackBarState: SnackbarHostState, + goToUserPage: (String?) -> Unit, + artistMbid: String, + onErrorShown : () -> Unit, + onMessageShown : () -> Unit, +) { + val reviews = uiState.reviews?.reviews?.take(2) ?: listOf() + val dialogsState = rememberDialogsState() + val socialUiState by socialViewModel.uiState.collectAsState() + Box(modifier = Modifier + .fillMaxWidth() + .background(brush = ListenBrainzTheme.colorScheme.gradientBrush) + .padding(23.dp)) { + Column { + Text("Reviews", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + if(reviews.isEmpty()){ + Spacer(modifier = Modifier.height(10.dp)) + Text("Be the first one to review this artist on CritiqueBrainz", color = app_bg_mid, style = MaterialTheme.typography.bodyMedium) + } + else{ + Spacer(modifier = Modifier.height(10.dp)) + reviews.map { + Box { + Column { + Row { + Text("Rating: ", color = app_bg_light) + RatingBar( + modifier = Modifier.padding(start = 2.dp), + value = (it?.rating ?: 0).toFloat(), + size = 19.dp, + spaceBetween = 2.dp, + style = RatingBarStyle.Default, + onValueChange = {}, + onRatingChanged = {} + ) + } + Spacer(modifier = Modifier.height(10.dp)) + Text(it?.text ?: "", color = app_bg_mid, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodyMedium) + Spacer(modifier = Modifier.height(5.dp)) + Text("By ${it?.user?.musicbrainzUsername ?: ""}", color = lb_purple_night, modifier = Modifier.clickable { + goToUserPage(it?.user?.musicbrainzUsername) + }) + Spacer(modifier = Modifier.height(10.dp)) + } + } + } + } + TextButton(onClick = { + dialogsState.activateDialog(Dialog.REVIEW , ListenDialogBundleKeys.listenDialogBundle(0, 0)) + }, modifier = Modifier.background(lb_purple)) { + Row (verticalAlignment = Alignment.CenterVertically) { + Text("Review", color = new_app_bg_light) + Spacer(modifier = Modifier.width(10.dp)) + Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_redirect), tint = new_app_bg_light, contentDescription = null, modifier = Modifier.size(12.dp)) + } + } + } + } + ErrorBar(error = socialUiState.error, onErrorShown = onErrorShown ) + SuccessBar(resId = socialUiState.successMsgId, onMessageShown = onMessageShown, snackbarState = snackBarState) + + Dialogs( + deactivateDialog = { + dialogsState.deactivateDialog() + }, + currentDialog = dialogsState.currentDialog, + currentIndex = dialogsState.metadata?.getInt(ListenDialogBundleKeys.EVENT_INDEX.name), + listens = listOf(Listen(insertedAt = "", recordingMsid = "", userName = "", trackMetadata = TrackMetadata(additionalInfo = null, mbidMapping = MbidMapping(artistMbids = listOf(artistMbid), recordingName = ""), artistName = uiState.name ?: "", trackName = "", releaseName = null))), + onPin = {meatadata: Metadata,blurbContent: String ->}, + searchUsers = { query -> }, + feedUiState = FeedUiState(), + isCritiqueBrainzLinked = {feedViewModel.isCritiqueBrainzLinked()}, + onReview = {type, blurbContent, rating, locale, metadata -> socialViewModel.review(metadata , type , blurbContent , rating , locale)}, + onPersonallyRecommend = {metadata, users, blurbContent -> }, + snackbarState = snackBarState, + socialUiState = socialUiState, + reviewEntityType = ReviewEntityType.ARTIST + ) +} @Composable private fun LinkCard( @@ -629,4 +746,4 @@ fun removeHtmlTags(input: String): String { val regex = "<[^>]*>".toRegex() // Replace all matches of the pattern with an empty string return input.replace(regex, "") -} \ No newline at end of file +} diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt index 2ca24513..6d862741 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/listens/ListensScreen.kt @@ -504,8 +504,9 @@ private fun BuildSimilarArtists(similarArtists: List, onArtistClick: (St append("You both listen to ") } similarArtists.forEachIndexed { index, artist -> - if(artist.artistMbid != null) - pushStringAnnotation(tag = "ARTIST", annotation = artist.artistMbid) + if(artist.artistMbid != null) { + pushStringAnnotation(tag = "ARTIST", annotation = artist.artistMbid) + } withStyle(style = SpanStyle(color = lb_purple_night)) { append(artist.artistName) } @@ -544,7 +545,8 @@ fun Dialogs( onReview : (type: ReviewEntityType, blurbContent: String, rating: Int?, locale: String , metadata : Metadata) -> Unit, onPersonallyRecommend : (metadata : Metadata , users : List , blurbContent : String) -> Unit, snackbarState: SnackbarHostState, - socialUiState: SocialUiState + socialUiState: SocialUiState, + reviewEntityType: ReviewEntityType = ReviewEntityType.RECORDING ) { val context = LocalContext.current when (currentDialog) { @@ -591,7 +593,8 @@ fun Dialogs( locale, Metadata(trackMetadata = listens[currentIndex].trackMetadata) ) - } + }, + reviewEntityType = reviewEntityType ) } } diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/SocialViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/SocialViewModel.kt index 99af8003..96fa4cca 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/SocialViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/SocialViewModel.kt @@ -1,6 +1,7 @@ package org.listenbrainz.android.viewmodel import android.net.Uri +import android.util.Log import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher @@ -10,6 +11,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.listenbrainz.android.R import org.listenbrainz.android.di.IoDispatcher import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.model.RecommendationData @@ -25,7 +27,6 @@ import org.listenbrainz.android.repository.remoteplayer.RemotePlaybackHandler import org.listenbrainz.android.repository.social.SocialRepository import org.listenbrainz.android.util.Resource import javax.inject.Inject -import org.listenbrainz.android.R @HiltViewModel class SocialViewModel @Inject constructor( @@ -149,7 +150,13 @@ class SocialViewModel @Inject constructor( data = Review( metadata = ReviewMetadata( entityName = metadata.trackMetadata?.trackName ?: return@launch, - entityId = (metadata.trackMetadata.mbidMapping?.recordingMbid ?: return@launch).toString(), + entityId = when(entityType) { + ReviewEntityType.RECORDING -> (metadata.trackMetadata.mbidMapping?.recordingMbid ?: return@launch).toString() + ReviewEntityType.ARTIST -> (when(metadata.trackMetadata.mbidMapping?.artistMbids?.size){ + 1 -> metadata.trackMetadata.mbidMapping.artistMbids[0] + else -> return@launch + }).toString() + ReviewEntityType.RELEASE_GROUP -> (metadata.trackMetadata.mbidMapping?.recordingMbid ?: return@launch).toString() }, entityType = entityType.code, text = blurbContent, rating = rating, @@ -157,6 +164,8 @@ class SocialViewModel @Inject constructor( ) ) ) + + Log.v("pranav", result.status.toString()) if (result.status == Resource.Status.FAILED){ emitError(result.error) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1695cdbb..be7ff9f9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -95,7 +95,7 @@ Search history deleted Song recommended successfully! Song recommendation sent successfully! - Song review submitted successfully! + Review submitted successfully! Song pinned successfully! Error occurred! Please try again later! From 4db985bb5d53161eb08af00c6d22eeb89a1961d6 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Mon, 19 Aug 2024 10:32:02 +0530 Subject: [PATCH 91/97] Fixes User Pages UI Tests --- .../androidTest/java/org/listenbrainz/android/UserPagesTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/androidTest/java/org/listenbrainz/android/UserPagesTest.kt b/app/src/androidTest/java/org/listenbrainz/android/UserPagesTest.kt index a761f378..d18189ea 100644 --- a/app/src/androidTest/java/org/listenbrainz/android/UserPagesTest.kt +++ b/app/src/androidTest/java/org/listenbrainz/android/UserPagesTest.kt @@ -106,6 +106,8 @@ class UserPagesTest { feedViewModel = feedViewModel, socialViewModel = socialViewModel, listensViewModel = listensViewModel, + goToUserPage = {}, + goToArtistPage = {} ) } From 3ef04308ccd1b53e6329b6ce2b61f6bd7be5369b Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:48:58 +0530 Subject: [PATCH 92/97] Adds themes in artist pages --- .../android/ui/screens/artist/ArtistScreen.kt | 38 +++++++++---------- .../ui/screens/profile/stats/StatsScreen.kt | 4 +- .../listenbrainz/android/ui/theme/Theme.kt | 6 ++- .../android/viewmodel/SocialViewModel.kt | 2 - 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt index 679e0397..67b213bf 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt @@ -86,10 +86,8 @@ import org.listenbrainz.android.ui.screens.profile.listens.ListenDialogBundleKey import org.listenbrainz.android.ui.screens.profile.listens.LoadMoreButton import org.listenbrainz.android.ui.screens.profile.stats.ArtistCard import org.listenbrainz.android.ui.theme.ListenBrainzTheme -import org.listenbrainz.android.ui.theme.app_bg_dark import org.listenbrainz.android.ui.theme.app_bg_light import org.listenbrainz.android.ui.theme.app_bg_mid -import org.listenbrainz.android.ui.theme.app_bg_secondary_dark import org.listenbrainz.android.ui.theme.lb_purple import org.listenbrainz.android.ui.theme.lb_purple_night import org.listenbrainz.android.ui.theme.new_app_bg_light @@ -180,11 +178,11 @@ private fun ArtistBioCard( Box(modifier = Modifier .fillMaxWidth() .clip(shape = RoundedCornerShape(bottomStart = 18.dp, bottomEnd = 18.dp)) - .background(Color(0xFF2B2E35)) + .background(ListenBrainzTheme.colorScheme.artistBioColor) .padding(23.dp)){ Column { Row (horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { - Text(uiState.name ?: "", color = Color.White, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 25.sp)) + Text(uiState.name ?: "", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 25.sp)) LbRadioButton { } @@ -202,7 +200,7 @@ private fun ArtistBioCard( Text(uiState.beginYear.toString(), color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) Text(uiState.area.toString(), color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) Spacer(modifier = Modifier.height(10.dp)) - HorizontalDivider(color = app_bg_dark, thickness = 3.dp, modifier = Modifier.padding(end = 50.dp)) + HorizontalDivider(color = ListenBrainzTheme.colorScheme.dividerColor, thickness = 3.dp, modifier = Modifier.padding(end = 50.dp)) Spacer(modifier = Modifier.height(10.dp)) Row { Icon( @@ -229,7 +227,7 @@ private fun ArtistBioCard( Text(removeHtmlTags(uiState.wikiExtract.wikipediaExtract.content).trim() , maxLines = 4, color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp), overflow = TextOverflow.Ellipsis) if(uiState.wikiExtract.wikipediaExtract.url != null){ val uriHandlder = LocalUriHandler.current - Text("read more", color = lb_purple_night, modifier = Modifier.clickable { + Text("read more", color = ListenBrainzTheme.colorScheme.followerChipSelected, modifier = Modifier.clickable { uriHandlder.openUri(uiState.wikiExtract.wikipediaExtract.url) }) } @@ -243,12 +241,12 @@ private fun ArtistBioCard( .clip( RoundedCornerShape((16.dp)) ) - .background(app_bg_secondary_dark) + .background(ListenBrainzTheme.colorScheme.followerCardColor) .padding(10.dp)) { Row { - Text(it.tag, color= lb_purple_night, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + Text(it.tag, color= ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) Spacer(modifier = Modifier.width(8.dp)) - Text((it.count ?: 0).toString(), color = Color.White ,style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + Text((it.count ?: 0).toString(), color= ListenBrainzTheme.colorScheme.textColor ,style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) } } Spacer(modifier = Modifier.width(10.dp)) @@ -316,7 +314,7 @@ private fun Links( .fillMaxWidth() .padding(23.dp)){ Column { - Text("Links", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Text("Links", color= ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) Row (modifier = Modifier .horizontalScroll(rememberScrollState()) .padding(top = 10.dp)) { @@ -414,7 +412,7 @@ private fun PopularTracks( .background(brush = ListenBrainzTheme.colorScheme.gradientBrush) .padding(start = 23.dp, end = 23.dp, top = 23.dp)){ Column { - Text("Popular Tracks", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Text("Popular Tracks", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) Spacer(modifier = Modifier.height(20.dp)) popularTracks.map { ListenCardSmall(trackName = it?.recordingName ?: "", artists = it?.artists ?: listOf( @@ -451,7 +449,7 @@ private fun AlbumsCard( .background(brush = ListenBrainzTheme.colorScheme.gradientBrush) .padding(23.dp)){ Column { - Text(header, color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Text(header, color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) Row (modifier = Modifier .horizontalScroll(rememberScrollState()) .padding(top = 20.dp)) { @@ -471,7 +469,9 @@ private fun AlbumsCard( contentDescription = "Album Cover Art" ) Spacer(modifier = Modifier.height(10.dp)) - Text(it?.name ?: "", color = lb_purple_night, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 18.sp), overflow = TextOverflow.Ellipsis) + Text(it?.name ?: "", color = ListenBrainzTheme.colorScheme.followerCardTextColor, + style = MaterialTheme.typography.bodyLarge.copy(fontSize = 18.sp), + maxLines = 2, overflow = TextOverflow.Ellipsis) } } Spacer(modifier = Modifier.width(40.dp)) @@ -499,7 +499,7 @@ private fun SimilarArtists( .background(brush = ListenBrainzTheme.colorScheme.gradientBrush) .padding(23.dp)){ Column { - Text("Similar Artists", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Text("Similar Artists", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) similarArtists.map { ArtistCard(artistName = it?.name ?: "") { if(it?.artistMbid != null) @@ -536,7 +536,7 @@ private fun TopListenersCard( .background(brush = ListenBrainzTheme.colorScheme.gradientBrush) .padding(23.dp)) { Column { - Text("Top Listeners", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Text("Top Listeners", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) Spacer(modifier = Modifier.height(20.dp)) topListeners.map { ArtistCard(artistName = it?.userName ?: "", listenCount = it?.listenCount ?: 0) { @@ -575,7 +575,7 @@ private fun ReviewsCard( .background(brush = ListenBrainzTheme.colorScheme.gradientBrush) .padding(23.dp)) { Column { - Text("Reviews", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Text("Reviews", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) if(reviews.isEmpty()){ Spacer(modifier = Modifier.height(10.dp)) Text("Be the first one to review this artist on CritiqueBrainz", color = app_bg_mid, style = MaterialTheme.typography.bodyMedium) @@ -653,7 +653,7 @@ private fun LinkCard( Box( modifier = Modifier .clip(RoundedCornerShape(8.dp)) - .background(color = app_bg_secondary_dark) + .background(color = ListenBrainzTheme.colorScheme.followerCardColor) .padding(top = 10.dp, bottom = 10.dp, start = 16.dp, end = 16.dp) .clickable { try { @@ -668,10 +668,10 @@ private fun LinkCard( Row (verticalAlignment = Alignment.CenterVertically) { Icon(imageVector = icon, contentDescription = null, tint = when(icon){ ImageVector.vectorResource(id = R.drawable.musicbrainz_logo) -> Color.Unspecified - else -> lb_purple_night + else -> ListenBrainzTheme.colorScheme.followerCardTextColor }) Spacer(modifier = Modifier.width(10.dp)) - Text(label, color = lb_purple_night, style = MaterialTheme.typography.bodyMedium) + Text(label, color = ListenBrainzTheme.colorScheme.followerCardTextColor, style = MaterialTheme.typography.bodyMedium) } } } 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 9c519e64..1df8fca2 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 @@ -599,7 +599,7 @@ fun ArtistCard( .clickable(enabled = true) { onClick() }, shape = ListenBrainzTheme.shapes.listenCardSmall, shadowElevation = 4.dp, - color = app_bg_secondary_dark + color = ListenBrainzTheme.colorScheme.followerCardColor ) { Column { Box( @@ -632,7 +632,7 @@ fun ArtistCard( ) { Text( text = listenCount.toString(), - color = Color.Black + color = ListenBrainzTheme.colorScheme.followerChipUnselected ) } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/theme/Theme.kt b/app/src/main/java/org/listenbrainz/android/ui/theme/Theme.kt index f8b06715..4f402f4a 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/theme/Theme.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/theme/Theme.kt @@ -74,6 +74,8 @@ data class ColorScheme( val followingButtonColor: Color, val followingButtonTextColor: Color, val followingButtonBorder: BorderStroke?, + /** Used for Artist Pages **/ + val artistBioColor: Color, ) @@ -138,6 +140,7 @@ private val colorSchemeDark = ColorScheme( followingButtonColor = app_bg_dark, followingButtonTextColor = Color.White, followingButtonBorder = null, + artistBioColor = Color(0xFF2B2E35) ) private val colorSchemeLight = ColorScheme( @@ -179,7 +182,8 @@ private val colorSchemeLight = ColorScheme( followerCardTextColor = lb_purple, followingButtonColor = Color.White, followingButtonTextColor = lb_purple, - followingButtonBorder = BorderStroke(width = 1.dp, color = lb_purple) + followingButtonBorder = BorderStroke(width = 1.dp, color = lb_purple), + artistBioColor = Color(0xFFD7D6EB) ) diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/SocialViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/SocialViewModel.kt index 96fa4cca..92529e98 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/SocialViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/SocialViewModel.kt @@ -164,8 +164,6 @@ class SocialViewModel @Inject constructor( ) ) ) - - Log.v("pranav", result.status.toString()) if (result.status == Resource.Status.FAILED){ emitError(result.error) From 4a33c603ed22c686e6a0e11a19e0e4d87c78d70c Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Mon, 19 Aug 2024 12:15:42 +0530 Subject: [PATCH 93/97] Adds Number formatting --- .../android/ui/screens/artist/ArtistScreen.kt | 17 +++++++-- .../ui/screens/profile/stats/StatsScreen.kt | 37 ++++++++++--------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt index 67b213bf..41d3e180 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt @@ -96,6 +96,7 @@ import org.listenbrainz.android.util.Utils import org.listenbrainz.android.viewmodel.ArtistViewModel import org.listenbrainz.android.viewmodel.FeedViewModel import org.listenbrainz.android.viewmodel.SocialViewModel +import kotlin.math.round @Composable @@ -209,7 +210,7 @@ private fun ArtistBioCard( tint = app_bg_mid ) Spacer(modifier = Modifier.width(5.dp)) - Text((uiState.totalPlays ?: 0).toString() + " plays", color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + Text(formatNumber(uiState.totalPlays ?: 0) + " plays", color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) } Row { Icon( @@ -218,7 +219,7 @@ private fun ArtistBioCard( tint = app_bg_mid ) Spacer(modifier = Modifier.width(5.dp)) - Text((uiState.totalListeners ?: 0).toString() + " listeners", color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + Text(formatNumber(uiState.totalListeners ?: 0) + " listeners", color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) } } } @@ -500,6 +501,7 @@ private fun SimilarArtists( .padding(23.dp)){ Column { Text("Similar Artists", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Spacer(modifier = Modifier.height(20.dp)) similarArtists.map { ArtistCard(artistName = it?.name ?: "") { if(it?.artistMbid != null) @@ -539,7 +541,7 @@ private fun TopListenersCard( Text("Top Listeners", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) Spacer(modifier = Modifier.height(20.dp)) topListeners.map { - ArtistCard(artistName = it?.userName ?: "", listenCount = it?.listenCount ?: 0) { + ArtistCard(artistName = it?.userName ?: "", listenCountLabel = formatNumber(it?.listenCount ?: 0)) { goToUserPage(it?.userName) } Spacer(modifier = Modifier.height(12.dp)) @@ -687,6 +689,7 @@ private fun LbRadioButton( imageVector = ImageVector.vectorResource(id = R.drawable.lb_radio_play_button), contentDescription = "" ) + Spacer(modifier = Modifier.width(5.dp)) Text("Radio") } } @@ -747,3 +750,11 @@ fun removeHtmlTags(input: String): String { // Replace all matches of the pattern with an empty string return input.replace(regex, "") } + +fun formatNumber(input: Int): String { + return when { + input >= 1_00_000 -> "${round(input/1_00_000f)}L" + input >= 1_000 -> "${round(input/1_000f)}K" + else -> input.toString() + } +} \ 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 1df8fca2..37b89a66 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 @@ -72,6 +72,7 @@ import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.SuccessBar import org.listenbrainz.android.ui.components.dialogs.Dialog import org.listenbrainz.android.ui.components.dialogs.rememberDialogsState +import org.listenbrainz.android.ui.screens.artist.formatNumber import org.listenbrainz.android.ui.screens.feed.FeedUiState import org.listenbrainz.android.ui.screens.feed.SocialDropdown import org.listenbrainz.android.ui.screens.profile.ProfileUiState @@ -422,7 +423,7 @@ fun StatsScreen( Column (horizontalAlignment = Alignment.CenterHorizontally) { topArtists.map { topArtist -> - ArtistCard(artistName = topArtist.artistName ?: "", listenCount = topArtist.listenCount){ + ArtistCard(artistName = topArtist.artistName ?: "", listenCountLabel = formatNumber(topArtist.listenCount ?: 0)){ if(topArtist.artistMbid != null){ goToArtistPage(topArtist.artistMbid) } @@ -589,7 +590,7 @@ fun StatsScreen( fun ArtistCard( modifier: Modifier = Modifier, artistName: String, - listenCount: Int? = 0, + listenCountLabel: String? = null, onClick: () -> Unit, ) { Surface( @@ -616,24 +617,25 @@ fun ArtistCard( maxLines = 1, overflow = TextOverflow.Ellipsis) } - - Box( - modifier = modifier - .align(Alignment.CenterEnd) - .padding(end = 10.dp), - contentAlignment = Alignment.Center - ) { + if(listenCountLabel != null){ Box( - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .background(ListenBrainzTheme.colorScheme.followerChipSelected) - .padding(6.dp), + modifier = modifier + .align(Alignment.CenterEnd) + .padding(end = 10.dp), contentAlignment = Alignment.Center ) { - Text( - text = listenCount.toString(), - color = ListenBrainzTheme.colorScheme.followerChipUnselected - ) + Box( + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .background(ListenBrainzTheme.colorScheme.followerChipSelected) + .padding(6.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = listenCountLabel, + color = ListenBrainzTheme.colorScheme.followerChipUnselected + ) + } } } @@ -689,7 +691,6 @@ private fun RangeBar( ) Spacer(modifier = Modifier.width(10.dp)) } - } } } From e8eaf9b49d8f00134d77843055b197d9e5674b3f Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:59:56 +0530 Subject: [PATCH 94/97] Adds service and repository set up for album pages --- .../android/di/AlbumRepositoryModule.kt | 16 ++++++++++ .../listenbrainz/android/di/ServiceModule.kt | 9 ++++++ .../listenbrainz/android/model/album/Album.kt | 14 ++++++++ .../android/model/album/AlbumArtist.kt | 10 ++++++ .../android/model/album/AlbumInfo.kt | 14 ++++++++ .../android/model/album/AlbumTags.kt | 9 ++++++ .../android/model/album/ListeningStats.kt | 20 ++++++++++++ .../android/model/album/Medium.kt | 8 +++++ .../android/model/album/Release.kt | 13 ++++++++ .../android/model/album/ReleaseGroupData.kt | 9 ++++++ .../model/album/ReleaseGroupMetadata.kt | 10 ++++++ .../listenbrainz/android/model/album/Tag.kt | 6 ++++ .../listenbrainz/android/model/album/Track.kt | 15 +++++++++ .../android/model/artist/Artist.kt | 2 ++ .../artist/{ArtistReview.kt => CBReview.kt} | 2 +- .../repository/album/AlbumRepository.kt | 13 ++++++++ .../repository/album/AlbumRepositoryImpl.kt | 32 +++++++++++++++++++ .../repository/artist/ArtistRepository.kt | 4 +-- .../repository/artist/ArtistRepositoryImpl.kt | 4 +-- .../android/service/AlbumService.kt | 11 +++++++ .../listenbrainz/android/service/CBService.kt | 4 +-- .../listenbrainz/android/service/MBService.kt | 6 ++++ .../ui/screens/artist/ArtistUIState.kt | 4 +-- 23 files changed, 226 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/di/AlbumRepositoryModule.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/album/Album.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/album/AlbumArtist.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/album/AlbumInfo.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/album/AlbumTags.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/album/ListeningStats.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/album/Medium.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/album/Release.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/album/ReleaseGroupData.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/album/ReleaseGroupMetadata.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/album/Tag.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/album/Track.kt rename app/src/main/java/org/listenbrainz/android/model/artist/{ArtistReview.kt => CBReview.kt} (92%) create mode 100644 app/src/main/java/org/listenbrainz/android/repository/album/AlbumRepository.kt create mode 100644 app/src/main/java/org/listenbrainz/android/repository/album/AlbumRepositoryImpl.kt create mode 100644 app/src/main/java/org/listenbrainz/android/service/AlbumService.kt diff --git a/app/src/main/java/org/listenbrainz/android/di/AlbumRepositoryModule.kt b/app/src/main/java/org/listenbrainz/android/di/AlbumRepositoryModule.kt new file mode 100644 index 00000000..9f0d8115 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/di/AlbumRepositoryModule.kt @@ -0,0 +1,16 @@ +package org.listenbrainz.android.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.listenbrainz.android.repository.album.AlbumRepository +import org.listenbrainz.android.repository.album.AlbumRepositoryImpl + +@Module +@InstallIn(SingletonComponent::class) +abstract class AlbumRepositoryModule { + + @Binds + abstract fun bindsAlbumRepository (repository: AlbumRepositoryImpl?) : AlbumRepository? +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt b/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt index 2c19c8a9..64c08611 100644 --- a/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt +++ b/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt @@ -14,6 +14,7 @@ import dagger.hilt.components.SingletonComponent import okhttp3.OkHttpClient import org.listenbrainz.android.model.yimdata.YimData import org.listenbrainz.android.repository.preferences.AppPreferences +import org.listenbrainz.android.service.AlbumService import org.listenbrainz.android.service.ArtistService import org.listenbrainz.android.service.BlogService import org.listenbrainz.android.service.CBService @@ -115,6 +116,14 @@ class ServiceModule { .addConverterFactory(GsonConverterFactory.create()) .build().create(CBService::class.java) + @Singleton + @Provides + fun providesAlbumService(): AlbumService = Retrofit.Builder() + .baseUrl(LB_BASE_URL) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build().create(AlbumService::class.java) + @Singleton @Provides fun providesYoutubeApiService(@ApplicationContext context: Context): YouTubeApiService = diff --git a/app/src/main/java/org/listenbrainz/android/model/album/Album.kt b/app/src/main/java/org/listenbrainz/android/model/album/Album.kt new file mode 100644 index 00000000..8c7a7b02 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/album/Album.kt @@ -0,0 +1,14 @@ +package org.listenbrainz.android.model.album + +import com.google.gson.annotations.SerializedName + +data class Album( + @SerializedName("caa_id") val caaId: Long? = null, + @SerializedName("caa_release_mbid") val caaReleaseMbid: String? = null, + @SerializedName("listening_stats") val listeningStats: ListeningStats? = null, + val mediums: List? = listOf(), + @SerializedName("recordings_release_mbid") val recordingsReleaseMbid: String? = null, + @SerializedName("release_group_mbid") val releaseGroupMbid: String? = null, + @SerializedName("release_group_metadata")val releaseGroupMetadata: ReleaseGroupMetadata? = null, + val type: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/album/AlbumArtist.kt b/app/src/main/java/org/listenbrainz/android/model/album/AlbumArtist.kt new file mode 100644 index 00000000..4365b344 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/album/AlbumArtist.kt @@ -0,0 +1,10 @@ +package org.listenbrainz.android.model.album + +import com.google.gson.annotations.SerializedName +import org.listenbrainz.android.model.artist.Artist + +data class AlbumArtist( + @SerializedName("artist_credit_id") val artistCreditId: Int? = null, + val artists: List? = listOf(), + val name: String? = null, +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/album/AlbumInfo.kt b/app/src/main/java/org/listenbrainz/android/model/album/AlbumInfo.kt new file mode 100644 index 00000000..f2cd9071 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/album/AlbumInfo.kt @@ -0,0 +1,14 @@ +package org.listenbrainz.android.model.album + +import com.google.gson.annotations.SerializedName + +data class AlbumInfo( + val disambiguation: String? = null, + @SerializedName("first-release-date") val firstReleaseDate: String? = null, + val id: String? = null, + @SerializedName("primary-type") val primaryType: String? = null, + @SerializedName("primary-type-id") val primaryTypeId: String? = null, + @SerializedName("secondary-type-ids") val secondaryTypeIds: List? = null, + @SerializedName("secondar-types") val secondaryTypes: List? = null, + val title: String? = null, +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/album/AlbumTags.kt b/app/src/main/java/org/listenbrainz/android/model/album/AlbumTags.kt new file mode 100644 index 00000000..fb98fc3f --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/album/AlbumTags.kt @@ -0,0 +1,9 @@ +package org.listenbrainz.android.model.album + +import com.google.gson.annotations.SerializedName +import org.listenbrainz.android.model.artist.ArtistWithTags + +data class AlbumTags( + val artist: List? = listOf(), + @SerializedName("release_group") val releaseGroup: List? = listOf() +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/album/ListeningStats.kt b/app/src/main/java/org/listenbrainz/android/model/album/ListeningStats.kt new file mode 100644 index 00000000..cfac82f7 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/album/ListeningStats.kt @@ -0,0 +1,20 @@ +package org.listenbrainz.android.model.album + +import com.google.gson.annotations.SerializedName +import org.listenbrainz.android.model.artist.Listeners + +data class ListeningStats( + @SerializedName("artist_mbids") val artistMbids: List? = null, + @SerializedName("artist_name") val artistName: String? = null, + @SerializedName("caa_id") val caaId: Long? = null, + @SerializedName("caa_release_mbid") val caaReleaseMbid: String? = null, + @SerializedName("from_ts") val fromTs: Int? = null, + @SerializedName("last_updated") val lastUpdated: Int? = null, + val listeners: List? = null, + @SerializedName("release_group_mbid") val releaseGroupMbid: String? = null, + @SerializedName("release_group_name") val releaseGroupName: String? = null, + @SerializedName("stats_range") val statsRange: String? = null, + @SerializedName("to_ts") val toTs: Int? = null, + @SerializedName("total_listen_count") val totalListenCount: Int? = null, + @SerializedName("total_user_count") val totalUserCount: Int? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/album/Medium.kt b/app/src/main/java/org/listenbrainz/android/model/album/Medium.kt new file mode 100644 index 00000000..de74d9bb --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/album/Medium.kt @@ -0,0 +1,8 @@ +package org.listenbrainz.android.model.album + +data class Medium( + val format: String? = null, + val name: String? = null, + val position: Int? = null, + val tracks: List? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/album/Release.kt b/app/src/main/java/org/listenbrainz/android/model/album/Release.kt new file mode 100644 index 00000000..cdd740d3 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/album/Release.kt @@ -0,0 +1,13 @@ +package org.listenbrainz.android.model.album + +import com.google.gson.annotations.SerializedName +import org.listenbrainz.android.model.artist.Rels + +data class Release( + @SerializedName("caa_id") val caaId: Long? = null, + @SerializedName("caa_release_mbid") val caaReleaseMbid: String? = null, + val date: String? = null, + val name: String? = null, + val rels: Rels? = null, + val type: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/album/ReleaseGroupData.kt b/app/src/main/java/org/listenbrainz/android/model/album/ReleaseGroupData.kt new file mode 100644 index 00000000..96bdc44a --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/album/ReleaseGroupData.kt @@ -0,0 +1,9 @@ +package org.listenbrainz.android.model.album + +import com.google.gson.annotations.SerializedName + +data class ReleaseGroupData( + val count: Int? = null, + @SerializedName("genre_mbid") val genreMbid: String? = null, + val tag: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/album/ReleaseGroupMetadata.kt b/app/src/main/java/org/listenbrainz/android/model/album/ReleaseGroupMetadata.kt new file mode 100644 index 00000000..dc3b4b65 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/album/ReleaseGroupMetadata.kt @@ -0,0 +1,10 @@ +package org.listenbrainz.android.model.album + +import com.google.gson.annotations.SerializedName + +data class ReleaseGroupMetadata( + val artist: AlbumArtist? = null, + val release: Release? = null, + @SerializedName("release_group") val releaseGroup: Release? = null, + val tag: AlbumTags? = null, +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/album/Tag.kt b/app/src/main/java/org/listenbrainz/android/model/album/Tag.kt new file mode 100644 index 00000000..d759f12b --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/album/Tag.kt @@ -0,0 +1,6 @@ +package org.listenbrainz.android.model.album + +data class Tag( + val count: Int? = null, + val name: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/album/Track.kt b/app/src/main/java/org/listenbrainz/android/model/album/Track.kt new file mode 100644 index 00000000..e534e043 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/album/Track.kt @@ -0,0 +1,15 @@ +package org.listenbrainz.android.model.album + +import com.google.gson.annotations.SerializedName +import org.listenbrainz.android.model.feed.FeedListenArtist + +data class Track( + @SerializedName("artist_mbids") val artistMbids: List? = null, + val artists: List? = null, + val length: Int? = null, + val name: String? = null, + val position: Int? = null, + @SerializedName("recording_mbid") val recordingMbid: String? = null, + @SerializedName("total_listen_count") val totalListenCount: Int? = null, + @SerializedName("total_user_count") val totalUserCount: Int? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/Artist.kt b/app/src/main/java/org/listenbrainz/android/model/artist/Artist.kt index 72820c28..dc9c0961 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/Artist.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/Artist.kt @@ -6,6 +6,8 @@ data class Artist( val area: String? = "", @SerializedName("artist_mbid") val artistMbid: String? = "", @SerializedName("begin_year") val beginYear: Int? = 0, + @SerializedName("end_year") val endYear: Int? = null, + @SerializedName("join_phrase") val joinPhrase: String? = null, val gender: String? = "", val mbid: String? = "", val name: String? = "", diff --git a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistReview.kt b/app/src/main/java/org/listenbrainz/android/model/artist/CBReview.kt similarity index 92% rename from app/src/main/java/org/listenbrainz/android/model/artist/ArtistReview.kt rename to app/src/main/java/org/listenbrainz/android/model/artist/CBReview.kt index 06048d13..65b6c8bd 100644 --- a/app/src/main/java/org/listenbrainz/android/model/artist/ArtistReview.kt +++ b/app/src/main/java/org/listenbrainz/android/model/artist/CBReview.kt @@ -2,7 +2,7 @@ package org.listenbrainz.android.model.artist import com.google.gson.annotations.SerializedName -data class ArtistReview( +data class CBReview( @SerializedName("average_rating") val averageRating: AverageRating? = null, val count: Int? = null, val limit: Int? = null, diff --git a/app/src/main/java/org/listenbrainz/android/repository/album/AlbumRepository.kt b/app/src/main/java/org/listenbrainz/android/repository/album/AlbumRepository.kt new file mode 100644 index 00000000..76c717ee --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/repository/album/AlbumRepository.kt @@ -0,0 +1,13 @@ +package org.listenbrainz.android.repository.album + + +import org.listenbrainz.android.model.album.Album +import org.listenbrainz.android.model.album.AlbumInfo +import org.listenbrainz.android.model.artist.CBReview +import org.listenbrainz.android.util.Resource + +interface AlbumRepository { + suspend fun fetchAlbumInfo(albumMbid: String?): Resource + suspend fun fetchAlbum(albumMbid: String?): Resource + suspend fun fetchAlbumReviews(albumMbid: String?): Resource +} diff --git a/app/src/main/java/org/listenbrainz/android/repository/album/AlbumRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/album/AlbumRepositoryImpl.kt new file mode 100644 index 00000000..5ad76ea8 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/repository/album/AlbumRepositoryImpl.kt @@ -0,0 +1,32 @@ +package org.listenbrainz.android.repository.album + +import org.listenbrainz.android.model.ResponseError +import org.listenbrainz.android.model.album.Album +import org.listenbrainz.android.model.album.AlbumInfo +import org.listenbrainz.android.model.artist.CBReview +import org.listenbrainz.android.service.AlbumService +import org.listenbrainz.android.service.CBService +import org.listenbrainz.android.service.MBService +import org.listenbrainz.android.util.Resource +import org.listenbrainz.android.util.Utils.parseResponse +import javax.inject.Inject + +class AlbumRepositoryImpl @Inject constructor( + val service: AlbumService, + val cbService: CBService, + val mbService: MBService +) : AlbumRepository { + override suspend fun fetchAlbumInfo(albumMbid: String?): Resource = parseResponse { + if(albumMbid.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() + mbService.getAlbumInfo(albumMbid) + } + + override suspend fun fetchAlbum(albumMbid: String?): Resource = parseResponse { + if(albumMbid.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() + service.getAlbumData(albumMbid) + } + override suspend fun fetchAlbumReviews(albumMbid: String?): Resource = parseResponse { + if(albumMbid.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() + cbService.getArtistReviews(albumMbid, entityType = "release_group") + } +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepository.kt b/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepository.kt index 6aa45e77..3e526688 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepository.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepository.kt @@ -2,12 +2,12 @@ package org.listenbrainz.android.repository.artist import org.listenbrainz.android.model.artist.ArtistPayload -import org.listenbrainz.android.model.artist.ArtistReview import org.listenbrainz.android.model.artist.ArtistWikiExtract +import org.listenbrainz.android.model.artist.CBReview import org.listenbrainz.android.util.Resource interface ArtistRepository { suspend fun fetchArtistData(artistMbid: String?): Resource suspend fun fetchArtistWikiExtract(artistMbid: String?): Resource - suspend fun fetchArtistReviews(artistMbid: String?): Resource + suspend fun fetchArtistReviews(artistMbid: String?): Resource } diff --git a/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepositoryImpl.kt index 5dba55cd..41fa81a1 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/artist/ArtistRepositoryImpl.kt @@ -2,8 +2,8 @@ package org.listenbrainz.android.repository.artist import org.listenbrainz.android.model.ResponseError import org.listenbrainz.android.model.artist.ArtistPayload -import org.listenbrainz.android.model.artist.ArtistReview import org.listenbrainz.android.model.artist.ArtistWikiExtract +import org.listenbrainz.android.model.artist.CBReview import org.listenbrainz.android.service.ArtistService import org.listenbrainz.android.service.CBService import org.listenbrainz.android.service.MBService @@ -26,7 +26,7 @@ class ArtistRepositoryImpl @Inject constructor( mbService.getArtistWikiExtract(artistMbid) } - override suspend fun fetchArtistReviews(artistMbid: String?): Resource = parseResponse { + override suspend fun fetchArtistReviews(artistMbid: String?): Resource = parseResponse { if(artistMbid.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() cbService.getArtistReviews(artistMbid) } diff --git a/app/src/main/java/org/listenbrainz/android/service/AlbumService.kt b/app/src/main/java/org/listenbrainz/android/service/AlbumService.kt new file mode 100644 index 00000000..43a7f012 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/service/AlbumService.kt @@ -0,0 +1,11 @@ +package org.listenbrainz.android.service + +import org.listenbrainz.android.model.album.Album +import retrofit2.Response +import retrofit2.http.POST +import retrofit2.http.Path + +interface AlbumService { + @POST("album/{album_mbid}/") + suspend fun getAlbumData(@Path("album_mbid") albumMbid: String?): Response +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/service/CBService.kt b/app/src/main/java/org/listenbrainz/android/service/CBService.kt index 113bea80..aa770463 100644 --- a/app/src/main/java/org/listenbrainz/android/service/CBService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/CBService.kt @@ -1,11 +1,11 @@ package org.listenbrainz.android.service -import org.listenbrainz.android.model.artist.ArtistReview +import org.listenbrainz.android.model.artist.CBReview import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Query interface CBService { @GET("ws/1/review/") - suspend fun getArtistReviews(@Query("entity_id") artistMbid: String?, @Query("entity_type") entityType: String? = "artist", @Query("limit") limit: Int = 5): Response + suspend fun getArtistReviews(@Query("entity_id") artistMbid: String?, @Query("entity_type") entityType: String? = "artist", @Query("limit") limit: Int = 5): Response } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/service/MBService.kt b/app/src/main/java/org/listenbrainz/android/service/MBService.kt index 01423909..a60abe51 100644 --- a/app/src/main/java/org/listenbrainz/android/service/MBService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/MBService.kt @@ -1,11 +1,17 @@ package org.listenbrainz.android.service +import org.listenbrainz.android.model.album.AlbumInfo import org.listenbrainz.android.model.artist.ArtistWikiExtract import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path +import retrofit2.http.Query interface MBService { @GET("artist/{artist_mbid}/wikipedia-extract") suspend fun getArtistWikiExtract(@Path("artist_mbid") artistMbid: String?): Response + + @GET("ws/2/release-group/{album_id}") + suspend fun getAlbumInfo(@Path("album_id") albumMbid: String?, @Query("fmt") format: String = "json") + : Response } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistUIState.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistUIState.kt index 2b9c97c7..842061d1 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistUIState.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistUIState.kt @@ -1,6 +1,6 @@ package org.listenbrainz.android.ui.screens.artist -import org.listenbrainz.android.model.artist.ArtistReview +import org.listenbrainz.android.model.artist.CBReview import org.listenbrainz.android.model.artist.ArtistWikiExtract import org.listenbrainz.android.model.artist.Listeners import org.listenbrainz.android.model.artist.PopularRecording @@ -25,5 +25,5 @@ data class ArtistUIState( val appearsOn: List? = listOf(), val similarArtists: List? = listOf(), val topListeners: List? = listOf(), - val reviews: ArtistReview? = null, + val reviews: CBReview? = null, ) \ No newline at end of file From 62107abe2f3dc5066af7796b759154700f7859dd Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Mon, 19 Aug 2024 23:13:30 +0530 Subject: [PATCH 95/97] Initialized Album Screen, set up ui state and data flow --- .../listenbrainz/android/di/ServiceModule.kt | 24 +++++-- .../android/model/AppNavigationItem.kt | 1 + .../android/model/album/AlbumTags.kt | 4 +- .../android/model/album/Release.kt | 3 +- .../repository/album/AlbumRepositoryImpl.kt | 6 +- .../android/ui/navigation/AppNavigation.kt | 46 ++++++-------- .../android/ui/navigation/TopBar.kt | 1 + .../android/ui/screens/album/AlbumScreen.kt | 63 +++++++++++++++++++ .../android/ui/screens/album/AlbumUIState.kt | 23 +++++++ .../android/ui/screens/artist/ArtistScreen.kt | 25 +++++--- .../android/ui/screens/main/MainActivity.kt | 4 ++ .../android/viewmodel/AlbumViewModel.kt | 55 ++++++++++++++++ 12 files changed, 210 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/album/AlbumScreen.kt create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/album/AlbumUIState.kt create mode 100644 app/src/main/java/org/listenbrainz/android/viewmodel/AlbumViewModel.kt diff --git a/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt b/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt index 64c08611..e1fdaa49 100644 --- a/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt +++ b/app/src/main/java/org/listenbrainz/android/di/ServiceModule.kt @@ -102,11 +102,25 @@ class ServiceModule { @Singleton @Provides - fun providesMBService(): MBService = Retrofit.Builder() - .baseUrl(MB_BASE_URL) - .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create()) - .build().create(MBService::class.java) + fun providesMBService(): MBService { + val okHttpClient = OkHttpClient.Builder() + .addInterceptor { chain -> + val original = chain.request() + val request = original.newBuilder() + .header("user-agent", "ListenBrainz Android") + .method(original.method, original.body) + .build() + chain.proceed(request) + } + .build() + + return Retrofit.Builder() + .baseUrl(MB_BASE_URL) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(MBService::class.java) + } @Singleton @Provides diff --git a/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt b/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt index cef7d658..8c2a572d 100644 --- a/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt +++ b/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt @@ -11,5 +11,6 @@ sealed class AppNavigationItem(val route: String, @DrawableRes val iconUnselecte object Settings: AppNavigationItem("settings", R.drawable.ic_settings, R.drawable.ic_settings, "Settings") object About: AppNavigationItem("about", R.drawable.ic_info, R.drawable.ic_info, "About") object Artist: AppNavigationItem("artist", R.drawable.ic_artist, R.drawable.ic_artist,"Artist") + object Album: AppNavigationItem("album", R.drawable.ic_album, R.drawable.ic_album, "Artist -> Album") } diff --git a/app/src/main/java/org/listenbrainz/android/model/album/AlbumTags.kt b/app/src/main/java/org/listenbrainz/android/model/album/AlbumTags.kt index fb98fc3f..cfaf1a5a 100644 --- a/app/src/main/java/org/listenbrainz/android/model/album/AlbumTags.kt +++ b/app/src/main/java/org/listenbrainz/android/model/album/AlbumTags.kt @@ -4,6 +4,6 @@ import com.google.gson.annotations.SerializedName import org.listenbrainz.android.model.artist.ArtistWithTags data class AlbumTags( - val artist: List? = listOf(), - @SerializedName("release_group") val releaseGroup: List? = listOf() + val artist: List? = listOf(), + @SerializedName("release_group") val releaseGroup: List = listOf() ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/album/Release.kt b/app/src/main/java/org/listenbrainz/android/model/album/Release.kt index cdd740d3..a58bf182 100644 --- a/app/src/main/java/org/listenbrainz/android/model/album/Release.kt +++ b/app/src/main/java/org/listenbrainz/android/model/album/Release.kt @@ -1,13 +1,12 @@ package org.listenbrainz.android.model.album import com.google.gson.annotations.SerializedName -import org.listenbrainz.android.model.artist.Rels data class Release( @SerializedName("caa_id") val caaId: Long? = null, @SerializedName("caa_release_mbid") val caaReleaseMbid: String? = null, val date: String? = null, val name: String? = null, - val rels: Rels? = null, + val rels: List = listOf(), val type: String? = null ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/repository/album/AlbumRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/album/AlbumRepositoryImpl.kt index 5ad76ea8..62b4898e 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/album/AlbumRepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/album/AlbumRepositoryImpl.kt @@ -12,9 +12,9 @@ import org.listenbrainz.android.util.Utils.parseResponse import javax.inject.Inject class AlbumRepositoryImpl @Inject constructor( - val service: AlbumService, - val cbService: CBService, - val mbService: MBService + private val service: AlbumService, + private val cbService: CBService, + private val mbService: MBService ) : AlbumRepository { override suspend fun fetchAlbumInfo(albumMbid: String?): Resource = parseResponse { if(albumMbid.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() diff --git a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt index 7192200e..62bcdab9 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt @@ -13,6 +13,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import org.listenbrainz.android.model.AppNavigationItem +import org.listenbrainz.android.ui.screens.album.AlbumScreen import org.listenbrainz.android.ui.screens.artist.ArtistScreen import org.listenbrainz.android.ui.screens.brainzplayer.BrainzPlayerScreen import org.listenbrainz.android.ui.screens.explore.ExploreScreen @@ -28,6 +29,7 @@ fun AppNavigation( snackbarState : SnackbarHostState, goToUserProfile: () -> Unit, goToArtistPage: (String) -> Unit, + goToAlbumPage: (String) -> Unit, goToUserPage: (String?) -> Unit, ) { NavHost( @@ -48,19 +50,7 @@ fun AppNavigation( // Restore previous state restoreState = true } - } }, goToArtistPage = { - mbid -> - navController.navigate("${AppNavigationItem.Artist.route}/$mbid"){ - // Avoid building large backstack - popUpTo(AppNavigationItem.Feed.route){ - saveState = true - } - // Avoid copies - launchSingleTop = true - // Restore previous state - restoreState = true - } - }) + } }, goToArtistPage = goToArtistPage) } composable(route = AppNavigationItem.BrainzPlayer.route){ BrainzPlayerScreen() @@ -100,19 +90,23 @@ fun AppNavigation( } } else{ - ArtistScreen(artistMbid = artistMbid, goToArtistPage = goToArtistPage, snackBarState = snackbarState, goToUserPage = {username : String? -> - if(username != null) { - navController.navigate("${AppNavigationItem.Profile.route}/$username"){ - // Avoid building large backstack - popUpTo(AppNavigationItem.Feed.route){ - saveState = true - } - // Avoid copies - launchSingleTop = true - // Restore previous state - restoreState = true - } - } }) + ArtistScreen(artistMbid = artistMbid, goToArtistPage = goToArtistPage, + snackBarState = snackbarState, goToUserPage = goToUserPage, goToAlbumPage = goToAlbumPage) + } + } + composable(route = "${AppNavigationItem.Album.route}/{mbid}", arguments = listOf( + navArgument("mbid"){ + type = NavType.StringType + } + )){ + val albumMbid = it.arguments?.getString("mbid") + if(albumMbid == null){ + LaunchedEffect(Unit) { + snackbarState.showSnackbar("The album page can't be loaded") + } + } + else{ + AlbumScreen(albumMbid = albumMbid) } } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/navigation/TopBar.kt b/app/src/main/java/org/listenbrainz/android/ui/navigation/TopBar.kt index 1aac418e..abafa740 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/navigation/TopBar.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/navigation/TopBar.kt @@ -42,6 +42,7 @@ fun TopBar( AppNavigationItem.Settings.route -> AppNavigationItem.Settings.title AppNavigationItem.About.route -> AppNavigationItem.About.title "${AppNavigationItem.Artist.route}/{mbid}" -> AppNavigationItem.Artist.title + "${AppNavigationItem.Album.route}/{mbid}" -> AppNavigationItem.Album.title else -> "" } } ?: "ListenBrainz" diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/album/AlbumScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/album/AlbumScreen.kt new file mode 100644 index 00000000..ce98fe33 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/album/AlbumScreen.kt @@ -0,0 +1,63 @@ +package org.listenbrainz.android.ui.screens.album + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import org.listenbrainz.android.ui.components.LoadingAnimation +import org.listenbrainz.android.ui.theme.ListenBrainzTheme +import org.listenbrainz.android.viewmodel.AlbumViewModel + +@Composable +fun AlbumScreen( + albumMbid: String, + viewModel: AlbumViewModel = hiltViewModel(), +) { + LaunchedEffect(Unit) { + viewModel.fetchAlbumData(albumMbid) + } + val uiState by viewModel.uiState.collectAsState() + AlbumScreen( + uiState = uiState + ) +} + +@Composable +private fun AlbumScreen( + uiState: AlbumUiState +) { + Box(modifier = Modifier.fillMaxSize()) { + AnimatedVisibility( + visible = uiState.isLoading, + modifier = Modifier.align(Alignment.Center), + enter = fadeIn(initialAlpha = 0.4f), + exit = fadeOut(animationSpec = tween(durationMillis = 250)) + ) { + LoadingAnimation() + } + AnimatedVisibility( + visible = !uiState.isLoading, + ) { + ListenBrainzTheme { + LazyColumn { + item { + Text(uiState.name.toString(), color = ListenBrainzTheme.colorScheme.textColor) + } + } + } + + } + } +} + diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/album/AlbumUIState.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/album/AlbumUIState.kt new file mode 100644 index 00000000..16887c09 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/album/AlbumUIState.kt @@ -0,0 +1,23 @@ +package org.listenbrainz.android.ui.screens.album + +import org.listenbrainz.android.model.album.ReleaseGroupData +import org.listenbrainz.android.model.album.Track +import org.listenbrainz.android.model.artist.Artist +import org.listenbrainz.android.model.artist.CBReview +import org.listenbrainz.android.model.artist.Listeners +import org.listenbrainz.android.model.artist.Rels + +data class AlbumUiState( + val isLoading: Boolean = true, + val name: String? = null, + val artists: List = listOf(), + val releaseDate: String? = null, + val totalPlays: Int? = null, + val totalListeners: Int? = null, + val tags: List = listOf(), + val links: Rels? = null, + val trackList: List = listOf(), + val topListeners: List = listOf(), + val reviews: CBReview? = null, + val type: String? = null, +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt index 41d3e180..b87d5ff8 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt @@ -107,6 +107,7 @@ fun ArtistScreen( feedViewModel: FeedViewModel = hiltViewModel(), goToArtistPage: (String) -> Unit, goToUserPage: (String?) -> Unit, + goToAlbumPage: (String) -> Unit, snackBarState: SnackbarHostState ) { LaunchedEffect(Unit) { @@ -114,7 +115,7 @@ fun ArtistScreen( } val uiState by viewModel.uiState.collectAsState() ArtistScreen(artistMbid = artistMbid,uiState = uiState, goToArtistPage = goToArtistPage, goToUserPage = goToUserPage, - socialViewModel = socialViewModel, feedViewModel = feedViewModel, snackBarState = snackBarState) + socialViewModel = socialViewModel, feedViewModel = feedViewModel, snackBarState = snackBarState, goToAlbumPage = goToAlbumPage) } @Composable @@ -125,7 +126,8 @@ private fun ArtistScreen( goToUserPage: (String?) -> Unit, socialViewModel: SocialViewModel, feedViewModel: FeedViewModel, - snackBarState: SnackbarHostState + snackBarState: SnackbarHostState, + goToAlbumPage: (String) -> Unit, ) { Box(modifier = Modifier.fillMaxSize()){ AnimatedVisibility( @@ -149,10 +151,10 @@ private fun ArtistScreen( PopularTracks(uiState = uiState, goToArtistPage = goToArtistPage) } item { - AlbumsCard(header = "Albums", albumsList = uiState.albums) + AlbumsCard(header = "Albums", albumsList = uiState.albums, goToAlbumPage = goToAlbumPage) } item { - AlbumsCard(header = "Appears On", albumsList = uiState.appearsOn) + AlbumsCard(header = "Appears On", albumsList = uiState.appearsOn, goToAlbumPage = goToAlbumPage) } item { SimilarArtists(uiState = uiState, goToArtistPage = goToArtistPage) @@ -443,7 +445,8 @@ private fun PopularTracks( @Composable private fun AlbumsCard( header: String, - albumsList: List? + albumsList: List?, + goToAlbumPage: (String) -> Unit, ) { Box(modifier = Modifier .fillMaxWidth() @@ -455,7 +458,11 @@ private fun AlbumsCard( .horizontalScroll(rememberScrollState()) .padding(top = 20.dp)) { albumsList?.map { - Box (modifier = Modifier.width(150.dp)) { + Box (modifier = Modifier.width(150.dp).clickable { + if(it?.mbid != null){ + goToAlbumPage(it.mbid) + } + }) { Column { val coverArt = Utils.getCoverArtUrl(it?.caaReleaseMbid, it?.caaId, 500) AsyncImage( @@ -472,7 +479,11 @@ private fun AlbumsCard( Spacer(modifier = Modifier.height(10.dp)) Text(it?.name ?: "", color = ListenBrainzTheme.colorScheme.followerCardTextColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 18.sp), - maxLines = 2, overflow = TextOverflow.Ellipsis) + maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.clickable { + if(it?.mbid != null){ + goToAlbumPage(it.mbid) + } + }) } } Spacer(modifier = Modifier.width(40.dp)) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt index 099a004f..3e334f27 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/main/MainActivity.kt @@ -210,6 +210,10 @@ class MainActivity : ComponentActivity() { goToUserPage = { user -> navController.navigate("${AppNavigationItem.Profile.route}/$user") + }, + goToAlbumPage = { + mbid -> + navController.navigate("album/${mbid}") } ) } diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/AlbumViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/AlbumViewModel.kt new file mode 100644 index 00000000..bf905724 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/AlbumViewModel.kt @@ -0,0 +1,55 @@ +package org.listenbrainz.android.viewmodel + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import org.listenbrainz.android.repository.album.AlbumRepository +import org.listenbrainz.android.ui.screens.album.AlbumUiState +import javax.inject.Inject + +@HiltViewModel +class AlbumViewModel @Inject constructor( + private val repository: AlbumRepository +) : BaseViewModel() { + private val albumUIStateFlow: MutableStateFlow = MutableStateFlow(AlbumUiState()) + + suspend fun fetchAlbumData(albumMbid: String?) { + val albumInfo = repository.fetchAlbumInfo(albumMbid).data + val albumData = repository.fetchAlbum(albumMbid).data + val albumReviews = repository.fetchAlbumReviews(albumMbid).data + + val albumUiState = AlbumUiState( + isLoading = false, + name = albumInfo?.title, + artists = albumData?.releaseGroupMetadata?.artist?.artists ?: listOf(), + releaseDate = albumInfo?.firstReleaseDate, + totalPlays = albumData?.listeningStats?.totalListenCount, + totalListeners = albumData?.listeningStats?.totalUserCount, + tags = albumData?.releaseGroupMetadata?.tag?.releaseGroup ?: listOf(), + links = albumData?.releaseGroupMetadata?.artist?.artists?.get(0)?.rels, + trackList = albumData?.mediums?.get(0)?.tracks ?: listOf(), + topListeners = albumData?.listeningStats?.listeners ?: listOf(), + reviews = albumReviews, + type = albumData?.type + ) + albumUIStateFlow.emit(albumUiState) + } + + override val uiState: StateFlow = createUiStateFlow() + + override fun createUiStateFlow(): StateFlow { + return combine( + albumUIStateFlow + ) { + it[0] + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + AlbumUiState() + ) + } +} \ No newline at end of file From d52217e6158dcf2c857af9e2e43fcbf574652da4 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:45:07 +0530 Subject: [PATCH 96/97] Adds UI for Album Pages, review of albums --- .../android/model/AppNavigationItem.kt | 2 +- .../android/ui/components/ListenCardSmall.kt | 8 +- .../android/ui/navigation/AppNavigation.kt | 2 +- .../android/ui/screens/album/AlbumScreen.kt | 204 +++++++++++++++++- .../android/ui/screens/album/AlbumUIState.kt | 1 + .../android/ui/screens/artist/ArtistScreen.kt | 190 ++++++++++++---- .../android/viewmodel/AlbumViewModel.kt | 2 + 7 files changed, 356 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt b/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt index 8c2a572d..9b755c7b 100644 --- a/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt +++ b/app/src/main/java/org/listenbrainz/android/model/AppNavigationItem.kt @@ -11,6 +11,6 @@ sealed class AppNavigationItem(val route: String, @DrawableRes val iconUnselecte object Settings: AppNavigationItem("settings", R.drawable.ic_settings, R.drawable.ic_settings, "Settings") object About: AppNavigationItem("about", R.drawable.ic_info, R.drawable.ic_info, "About") object Artist: AppNavigationItem("artist", R.drawable.ic_artist, R.drawable.ic_artist,"Artist") - object Album: AppNavigationItem("album", R.drawable.ic_album, R.drawable.ic_album, "Artist -> Album") + object Album: AppNavigationItem("album", R.drawable.ic_album, R.drawable.ic_album, "Artist > Album") } 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 e6d1c685..9804c260 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 @@ -53,7 +53,7 @@ import org.listenbrainz.android.ui.theme.ListenBrainzTheme fun ListenCardSmall( modifier: Modifier = Modifier, trackName: String, - artists: List, + artists: List, coverArtUrl: String?, listenCount: Int? = null, @DrawableRes errorAlbumArt: Int = R.drawable.ic_coverartarchive_logo_no_text, @@ -205,7 +205,7 @@ private fun AlbumArt( fun TitleAndSubtitle( modifier: Modifier = Modifier, title: String, - artists: List, + artists: List, alignment: Alignment.Horizontal = Alignment.Start, titleColor: Color = ListenBrainzTheme.colorScheme.listenText, subtitleColor: Color = titleColor.copy(alpha = 0.7f), @@ -221,11 +221,11 @@ fun TitleAndSubtitle( ) Row { artists.map { - Text(it.artistCreditName + it.joinPhrase, style = ListenBrainzTheme.textStyles.listenSubtitle, + Text((it?.artistCreditName ?: "") + (it?.joinPhrase ?: ""), style = ListenBrainzTheme.textStyles.listenSubtitle, color = subtitleColor, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.clickable { - if(it.artistMbid != null){ + if(it?.artistMbid != null){ goToArtistPage(it.artistMbid) } }) diff --git a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt index 62bcdab9..febe0c5c 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/navigation/AppNavigation.kt @@ -106,7 +106,7 @@ fun AppNavigation( } } else{ - AlbumScreen(albumMbid = albumMbid) + AlbumScreen(albumMbid = albumMbid, snackBarState = snackbarState) } } } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/album/AlbumScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/album/AlbumScreen.kt index ce98fe33..fe389f78 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/album/AlbumScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/album/AlbumScreen.kt @@ -4,38 +4,84 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +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.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +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.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowDropDown +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState 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.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import org.listenbrainz.android.R +import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.LoadingAnimation +import org.listenbrainz.android.ui.screens.artist.BioCard +import org.listenbrainz.android.ui.screens.artist.Links +import org.listenbrainz.android.ui.screens.artist.ReviewsCard +import org.listenbrainz.android.ui.screens.artist.formatNumber +import org.listenbrainz.android.ui.screens.profile.listens.LoadMoreButton +import org.listenbrainz.android.ui.screens.profile.stats.ArtistCard import org.listenbrainz.android.ui.theme.ListenBrainzTheme +import org.listenbrainz.android.ui.theme.new_app_bg_light import org.listenbrainz.android.viewmodel.AlbumViewModel +import org.listenbrainz.android.viewmodel.FeedViewModel +import org.listenbrainz.android.viewmodel.SocialViewModel @Composable fun AlbumScreen( albumMbid: String, viewModel: AlbumViewModel = hiltViewModel(), + feedViewModel: FeedViewModel = hiltViewModel(), + socialViewModel: SocialViewModel = hiltViewModel(), + snackBarState: SnackbarHostState, ) { LaunchedEffect(Unit) { viewModel.fetchAlbumData(albumMbid) } val uiState by viewModel.uiState.collectAsState() AlbumScreen( - uiState = uiState + uiState = uiState, + feedViewModel = feedViewModel, + socialViewModel = socialViewModel, + snackBarState = snackBarState, + albumMbid = albumMbid ) } @Composable private fun AlbumScreen( - uiState: AlbumUiState + uiState: AlbumUiState, + feedViewModel: FeedViewModel, + socialViewModel: SocialViewModel, + snackBarState: SnackbarHostState, + albumMbid: String, ) { Box(modifier = Modifier.fillMaxSize()) { AnimatedVisibility( @@ -52,7 +98,57 @@ private fun AlbumScreen( ListenBrainzTheme { LazyColumn { item { - Text(uiState.name.toString(), color = ListenBrainzTheme.colorScheme.textColor) + BioCard( + header = uiState.name, + coverArt = uiState.coverArt, + displayRadioButton = false, + useWebView = false, + totalPlays = uiState.totalPlays, + totalListeners = uiState.totalListeners, + artists = uiState.artists, + albumType = uiState.type, + albumReleaseDate = uiState.releaseDate, + albumTags = uiState.tags + ) + } + item { + ArtistRadio() + } + item { + val artistMbid = when(uiState.artists.isNotEmpty()){ + true -> uiState.artists[0]?.artistMbid + false -> null + } + val links = when(uiState.artists.isNotEmpty()){ + true -> uiState.artists[0]?.rels + false -> null + } + Links( + artistMbid = artistMbid, + links = links + ) + } + item { + TrackListCard(uiState = uiState) + } + item { + TopListenersCard(uiState = uiState) + } + item { + if(uiState.name != null){ + ReviewsCard( + reviewOfEntity = uiState.reviews, + feedViewModel = feedViewModel, + socialViewModel = socialViewModel, + snackBarState = snackBarState, + goToUserPage = {}, + onErrorShown = {socialViewModel.clearErrorFlow()}, + onMessageShown = {socialViewModel.clearMsgFlow()}, + albumMbid = albumMbid, + albumName = uiState.name + ) + } + } } } @@ -61,3 +157,105 @@ private fun AlbumScreen( } } +@Composable +private fun ArtistRadio () { + Box(modifier = Modifier + .fillMaxWidth() + .background( + brush = Brush.linearGradient( + start = Offset.Zero, + end = Offset(0f, Float.POSITIVE_INFINITY), + colors = listOf( + Color(0xFF1E1E24), + Color(0xFF1F1E25), + Color(0xFF201F28), + Color(0xFF1F1F27), + Color(0xFF201F29), + Color(0xFF21202C), + Color(0xFF232233), + Color(0xFF242235) + ) + ) + ) + .padding(start = 23.dp, top = 18.dp, bottom = 18.dp)) { + Row (verticalAlignment = Alignment.CenterVertically) { + Icon(imageVector = ImageVector.vectorResource(id = R.drawable.lb_radio_play_button), contentDescription = null, tint = new_app_bg_light, modifier = Modifier + .width(24.dp) + .height(24.dp)) + Spacer(modifier = Modifier.width(14.dp)) + Text("Artist Radio", color = new_app_bg_light, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 25.sp)) + Spacer(modifier = Modifier.fillMaxWidth(0.8f)) + Icon(imageVector = Icons.Rounded.ArrowDropDown, contentDescription = null, tint = new_app_bg_light, modifier = Modifier + .width(24.dp) + .height(24.dp)) + } + } +} + +@Composable +private fun TrackListCard( + uiState: AlbumUiState +) { + val trackListCollapsibleState: MutableState = remember { + mutableStateOf(true) + } + val trackList = when(trackListCollapsibleState.value){ + true -> uiState.trackList.take(5) + false -> uiState.trackList + } + Box(modifier = Modifier + .background(brush = ListenBrainzTheme.colorScheme.gradientBrush) + .fillMaxWidth() + .padding(23.dp)){ + Column { + Text("Tracklist", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 25.sp)) + Spacer(modifier = Modifier.height(20.dp)) + trackList.map { + ListenCardSmall(trackName = it?.name ?: "", artists = it?.artists ?: listOf(), coverArtUrl = uiState.coverArt, goToArtistPage = {}){} + Spacer(modifier = Modifier.height(12.dp)) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + if(uiState.trackList.size > 5){ + LoadMoreButton(state = trackListCollapsibleState.value) { + trackListCollapsibleState.value = !trackListCollapsibleState.value + } + } + } + } + } +} + +@Composable +private fun TopListenersCard( + uiState: AlbumUiState +) { + val topListenersCollapsibleState: MutableState = remember { + mutableStateOf(true) + } + val topListeners = when(topListenersCollapsibleState.value){ + true -> uiState.topListeners.take(5) + false -> uiState.topListeners + } + Box(modifier = Modifier + .background(brush = ListenBrainzTheme.colorScheme.gradientBrush) + .fillMaxWidth() + .padding(23.dp)){ + Column { + Text("Top listeners", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 25.sp)) + Spacer(modifier = Modifier.height(20.dp)) + topListeners.map { + ArtistCard(artistName = it?.userName ?: "", listenCountLabel = formatNumber(it?.listenCount ?: 0)) { + + } + Spacer(modifier = Modifier.height(12.dp)) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + if(uiState.topListeners.size > 5){ + LoadMoreButton(state = topListenersCollapsibleState.value) { + topListenersCollapsibleState.value = !topListenersCollapsibleState.value + } + } + } + } + } +} diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/album/AlbumUIState.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/album/AlbumUIState.kt index 16887c09..97e7895d 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/album/AlbumUIState.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/album/AlbumUIState.kt @@ -10,6 +10,7 @@ import org.listenbrainz.android.model.artist.Rels data class AlbumUiState( val isLoading: Boolean = true, val name: String? = null, + val coverArt: String? = null, val artists: List = listOf(), val releaseDate: String? = null, val totalPlays: Int? = null, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt index b87d5ff8..65422c5c 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/artist/ArtistScreen.kt @@ -71,7 +71,13 @@ import org.listenbrainz.android.model.Listen import org.listenbrainz.android.model.MbidMapping import org.listenbrainz.android.model.Metadata import org.listenbrainz.android.model.TrackMetadata +import org.listenbrainz.android.model.album.ReleaseGroupData +import org.listenbrainz.android.model.artist.Artist +import org.listenbrainz.android.model.artist.ArtistWikiExtract +import org.listenbrainz.android.model.artist.CBReview import org.listenbrainz.android.model.artist.ReleaseGroup +import org.listenbrainz.android.model.artist.Rels +import org.listenbrainz.android.model.artist.Tag import org.listenbrainz.android.model.feed.FeedListenArtist import org.listenbrainz.android.model.feed.ReviewEntityType import org.listenbrainz.android.ui.components.ErrorBar @@ -142,10 +148,20 @@ private fun ArtistScreen( ListenBrainzTheme { LazyColumn { item { - ArtistBioCard(uiState = uiState) + BioCard( + header = uiState.name, + coverArt = uiState.coverArt, + displayRadioButton = true, + beginYear = uiState.beginYear, + area = uiState.area, + totalPlays = uiState.totalPlays, + totalListeners = uiState.totalListeners, + wikiExtract = uiState.wikiExtract, + artistTags = uiState.tags + ) } item { - Links(uiState = uiState, artistMbid = artistMbid) + Links(artistMbid = artistMbid, links = uiState.links) } item { PopularTracks(uiState = uiState, goToArtistPage = goToArtistPage) @@ -163,9 +179,11 @@ private fun ArtistScreen( TopListenersCard(uiState = uiState, goToUserPage = goToUserPage) } item { - ReviewsCard(uiState = uiState, goToUserPage = goToUserPage, - socialViewModel = socialViewModel, feedViewModel = feedViewModel, artistMbid = artistMbid, - snackBarState = snackBarState, onMessageShown = {socialViewModel.clearMsgFlow()}, onErrorShown = {socialViewModel.clearErrorFlow()}) + if(uiState.name != null){ + ReviewsCard(reviewOfEntity = uiState.reviews, goToUserPage = goToUserPage, + socialViewModel = socialViewModel, feedViewModel = feedViewModel, artistMbid = artistMbid, artistName = uiState.name, + snackBarState = snackBarState, onMessageShown = {socialViewModel.clearMsgFlow()}, onErrorShown = {socialViewModel.clearErrorFlow()}) + } } } } @@ -175,8 +193,21 @@ private fun ArtistScreen( } @Composable -private fun ArtistBioCard( - uiState: ArtistUIState +fun BioCard( + header: String? = null, + coverArt: String? = null, + useWebView: Boolean = true, + displayRadioButton: Boolean = false, + beginYear: Int? = null, + area: String? = null, + totalPlays: Int? = 0, + totalListeners: Int? = 0, + wikiExtract: ArtistWikiExtract? = null, + artistTags: Tag? = null, + artists: List? = null, + albumType: String? = null, + albumReleaseDate: String? = null, + albumTags: List? = null ) { Box(modifier = Modifier .fillMaxWidth() @@ -185,23 +216,63 @@ private fun ArtistBioCard( .padding(23.dp)){ Column { Row (horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { - Text(uiState.name ?: "", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 25.sp)) - LbRadioButton { + Text(header ?: "", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 25.sp)) + if(displayRadioButton){ + LbRadioButton { + } + } + else{ + Spacer(modifier = Modifier.height(40.dp)) } } Row { - if (uiState.coverArt != null) { - SvgWithWebView( - svgContent = uiState.coverArt, - width = 200.dp, - height = 200.dp - ) + if (coverArt != null) { + if(useWebView){ + SvgWithWebView( + svgContent = coverArt, + width = 200.dp, + height = 200.dp + ) + } + else{ + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(coverArt) + .build(), + fallback = painterResource(id = R.drawable.ic_coverartarchive_logo_no_text), + modifier = Modifier.size(ListenBrainzTheme.sizes.listenCardHeight * 3f), + contentScale = ContentScale.Fit, + placeholder = painterResource(id = R.drawable.ic_coverartarchive_logo_no_text), + filterQuality = FilterQuality.Low, + contentDescription = "Album Cover Art" + ) + Spacer(modifier = Modifier.height(40.dp)) + } + } Spacer(modifier = Modifier.width(20.dp)) Column { - Text(uiState.beginYear.toString(), color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) - Text(uiState.area.toString(), color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + if(beginYear != null){ + Text(beginYear.toString(), color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + } + else if(artists != null){ + Row { + artists.map { + Text((it?.name ?: "") + (it?.joinPhrase ?: ""), color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp), maxLines = 1, + overflow = TextOverflow.Ellipsis) + } + } + } + if(area != null){ + Text(area.toString(), color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + } + if(albumType != null){ + Text(albumType.toString(), color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + } + if(albumReleaseDate != null){ + Text(albumReleaseDate.toString(), color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + } Spacer(modifier = Modifier.height(10.dp)) HorizontalDivider(color = ListenBrainzTheme.colorScheme.dividerColor, thickness = 3.dp, modifier = Modifier.padding(end = 50.dp)) Spacer(modifier = Modifier.height(10.dp)) @@ -212,7 +283,7 @@ private fun ArtistBioCard( tint = app_bg_mid ) Spacer(modifier = Modifier.width(5.dp)) - Text(formatNumber(uiState.totalPlays ?: 0) + " plays", color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + Text(formatNumber(totalPlays ?: 0) + " plays", color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) } Row { Icon( @@ -221,25 +292,44 @@ private fun ArtistBioCard( tint = app_bg_mid ) Spacer(modifier = Modifier.width(5.dp)) - Text(formatNumber(uiState.totalListeners ?: 0) + " listeners", color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + Text(formatNumber(totalListeners ?: 0) + " listeners", color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) } } } - if(uiState.wikiExtract?.wikipediaExtract?.content != null){ + if(wikiExtract?.wikipediaExtract?.content != null){ Spacer(modifier = Modifier.height(20.dp)) - Text(removeHtmlTags(uiState.wikiExtract.wikipediaExtract.content).trim() , maxLines = 4, color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp), overflow = TextOverflow.Ellipsis) - if(uiState.wikiExtract.wikipediaExtract.url != null){ + Text(removeHtmlTags(wikiExtract.wikipediaExtract.content).trim() , maxLines = 4, color = app_bg_mid, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp), overflow = TextOverflow.Ellipsis) + if(wikiExtract.wikipediaExtract.url != null){ val uriHandlder = LocalUriHandler.current Text("read more", color = ListenBrainzTheme.colorScheme.followerChipSelected, modifier = Modifier.clickable { - uriHandlder.openUri(uiState.wikiExtract.wikipediaExtract.url) + uriHandlder.openUri(wikiExtract.wikipediaExtract.url) }) } } Row (modifier = Modifier .horizontalScroll(rememberScrollState()) .padding(top = 10.dp)) { - uiState.tags?.artist?.map { - if(it.tag != null){ + if(artistTags != null){ + artistTags.artist?.map { + if(it.tag != null){ + Box (modifier = Modifier + .clip( + RoundedCornerShape((16.dp)) + ) + .background(ListenBrainzTheme.colorScheme.followerCardColor) + .padding(10.dp)) { + Row { + Text(it.tag, color= ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + Spacer(modifier = Modifier.width(8.dp)) + Text((it.count ?: 0).toString(), color= ListenBrainzTheme.colorScheme.textColor ,style = MaterialTheme.typography.bodyMedium.copy(fontSize = 18.sp)) + } + } + Spacer(modifier = Modifier.width(10.dp)) + } + } + } + albumTags?.map { + if(it?.tag != null){ Box (modifier = Modifier .clip( RoundedCornerShape((16.dp)) @@ -254,10 +344,8 @@ private fun ArtistBioCard( } Spacer(modifier = Modifier.width(10.dp)) } - } } - } } } @@ -265,9 +353,9 @@ private fun ArtistBioCard( class LinkCardData (val iconResId: ImageVector, val label: String, val url: String) {} @Composable -private fun Links( - artistMbid: String, - uiState: ArtistUIState +fun Links( + artistMbid: String?, + links: Rels? = null, ) { //TODO: Move this logic to vm and get map to ui state val allLinkCards: MutableList = mutableListOf() @@ -275,7 +363,6 @@ private fun Links( val streamingLinkCards: MutableList = mutableListOf() val socialMediaLinkCards: MutableList = mutableListOf() val lyricsLinkCards: MutableList = mutableListOf() - val links = uiState.links if(links?.wikidata != null){ val wikidata = LinkCardData(ImageVector.vectorResource(id = R.drawable.wiki_data), "Wikidata", links.wikidata) allLinkCards.add(wikidata) @@ -301,7 +388,10 @@ private fun Links( allLinkCards.add(mailOrder) streamingLinkCards.add(mailOrder) } - mainLinkCards.add(LinkCardData(ImageVector.vectorResource(id = R.drawable.musicbrainz_logo), "Edit", MB_BASE_URL + "artist/${artistMbid}")) + if(artistMbid != null){ + mainLinkCards.add(LinkCardData(ImageVector.vectorResource(id = R.drawable.musicbrainz_logo), "Edit", MB_BASE_URL + "artist/${artistMbid}")) + } + val linksMap: Map> = mapOf( ArtistLinksEnum.ALL to allLinkCards, ArtistLinksEnum.MAIN to mainLinkCards, @@ -317,7 +407,7 @@ private fun Links( .fillMaxWidth() .padding(23.dp)){ Column { - Text("Links", color= ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Text("Links", color= ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 25.sp)) Row (modifier = Modifier .horizontalScroll(rememberScrollState()) .padding(top = 10.dp)) { @@ -458,11 +548,13 @@ private fun AlbumsCard( .horizontalScroll(rememberScrollState()) .padding(top = 20.dp)) { albumsList?.map { - Box (modifier = Modifier.width(150.dp).clickable { - if(it?.mbid != null){ - goToAlbumPage(it.mbid) - } - }) { + Box (modifier = Modifier + .width(150.dp) + .clickable { + if (it?.mbid != null) { + goToAlbumPage(it.mbid) + } + }) { Column { val coverArt = Utils.getCoverArtUrl(it?.caaReleaseMbid, it?.caaId, 500) AsyncImage( @@ -570,17 +662,20 @@ private fun TopListenersCard( } @Composable -private fun ReviewsCard( - uiState: ArtistUIState, +fun ReviewsCard( + reviewOfEntity: CBReview?, feedViewModel: FeedViewModel, socialViewModel: SocialViewModel, snackBarState: SnackbarHostState, goToUserPage: (String?) -> Unit, - artistMbid: String, + artistMbid: String? = null, + artistName: String? = null, onErrorShown : () -> Unit, onMessageShown : () -> Unit, + albumMbid: String? = null, + albumName: String? = null, ) { - val reviews = uiState.reviews?.reviews?.take(2) ?: listOf() + val reviews = reviewOfEntity?.reviews?.take(2) ?: listOf() val dialogsState = rememberDialogsState() val socialUiState by socialViewModel.uiState.collectAsState() Box(modifier = Modifier @@ -635,6 +730,10 @@ private fun ReviewsCard( ErrorBar(error = socialUiState.error, onErrorShown = onErrorShown ) SuccessBar(resId = socialUiState.successMsgId, onMessageShown = onMessageShown, snackbarState = snackBarState) + val listensList = when(artistName != null && artistMbid != null){ + true -> listOf(Listen(insertedAt = "", recordingMsid = "", userName = "", trackMetadata = TrackMetadata(additionalInfo = null, mbidMapping = MbidMapping(artistMbids = listOf(artistMbid), recordingName = ""), artistName = artistName ?: "", trackName = "", releaseName = null))) + false -> listOf(Listen(insertedAt = "", recordingMsid = "", userName = "", trackMetadata = TrackMetadata(additionalInfo = null, mbidMapping = MbidMapping(recordingMbid = albumMbid, artistMbids = listOf(), recordingName = ""), artistName = "", trackName = "", releaseName = albumName))) + } Dialogs( deactivateDialog = { @@ -642,7 +741,7 @@ private fun ReviewsCard( }, currentDialog = dialogsState.currentDialog, currentIndex = dialogsState.metadata?.getInt(ListenDialogBundleKeys.EVENT_INDEX.name), - listens = listOf(Listen(insertedAt = "", recordingMsid = "", userName = "", trackMetadata = TrackMetadata(additionalInfo = null, mbidMapping = MbidMapping(artistMbids = listOf(artistMbid), recordingName = ""), artistName = uiState.name ?: "", trackName = "", releaseName = null))), + listens = listensList, onPin = {meatadata: Metadata,blurbContent: String ->}, searchUsers = { query -> }, feedUiState = FeedUiState(), @@ -651,7 +750,10 @@ private fun ReviewsCard( onPersonallyRecommend = {metadata, users, blurbContent -> }, snackbarState = snackBarState, socialUiState = socialUiState, - reviewEntityType = ReviewEntityType.ARTIST + reviewEntityType = when(artistMbid){ + null -> ReviewEntityType.RELEASE_GROUP + else -> ReviewEntityType.ARTIST + } ) } diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/AlbumViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/AlbumViewModel.kt index bf905724..f7b543ee 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/AlbumViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/AlbumViewModel.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import org.listenbrainz.android.repository.album.AlbumRepository import org.listenbrainz.android.ui.screens.album.AlbumUiState +import org.listenbrainz.android.util.Utils import javax.inject.Inject @HiltViewModel @@ -25,6 +26,7 @@ class AlbumViewModel @Inject constructor( val albumUiState = AlbumUiState( isLoading = false, name = albumInfo?.title, + coverArt = Utils.getCoverArtUrl(albumData?.caaReleaseMbid, albumData?.caaId), artists = albumData?.releaseGroupMetadata?.artist?.artists ?: listOf(), releaseDate = albumInfo?.firstReleaseDate, totalPlays = albumData?.listeningStats?.totalListenCount, From 80ef9b34a6c8fd21c7d124c33e8ba44da47cfee6 Mon Sep 17 00:00:00 2001 From: Akshat Tiwari Date: Wed, 21 Aug 2024 08:41:44 +0530 Subject: [PATCH 97/97] Beta Release 2.7.0 --- app/build.gradle.kts | 4 ++-- .../metadata/android/en-US/changelogs/52.txt | 1 + gradle/libs.versions.toml | 20 +++++++++---------- 3 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/52.txt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a9d8f0ac..31483576 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,8 +22,8 @@ android { applicationId = "org.listenbrainz.android" minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() - versionCode = 51 - versionName = "2.6.1" + versionCode = 52 + versionName = "2.7.0" multiDexEnabled = true testInstrumentationRunner = "org.listenbrainz.android.di.CustomTestRunner" vectorDrawables { diff --git a/fastlane/metadata/android/en-US/changelogs/52.txt b/fastlane/metadata/android/en-US/changelogs/52.txt new file mode 100644 index 00000000..b8010023 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/52.txt @@ -0,0 +1 @@ +Dashboard, Album and Artist Pages \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1c24018a..ededa15e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,19 +1,19 @@ [versions] kotlin = "2.0.0" navigation = "2.7.7" -hilt = "2.51.1" +hilt = "2.52" compose = "1.6.8" room = "2.6.1" accompanist = "0.34.0" -work = "2.9.0" +work = "2.9.1" exoplayer = "2.19.1" -paging = "3.3.0" -androidGradlePlugin = "8.5.1" +paging = "3.3.2" +androidGradlePlugin = "8.5.2" sentry = "4.10.0" ksp = "2.0.0-1.0.23" composeBom = "2024.06.00" appcompat = "1.7.0" -lifecycle = "2.8.3" +lifecycle = "2.8.4" browser = "1.8.0" preference = "1.2.1" coreSplashscreen = "1.0.1" @@ -23,10 +23,10 @@ retrofit = "2.11.0" okhttp = "5.0.0-alpha.14" glide = "4.16.0" glideCompose = "4.14.0" -coil = "2.6.0" +coil = "2.7.0" androidsvg = "1.4" material = "1.12.0" -lottie = "6.4.1" +lottie = "6.5.0" onboarding = "1.1.3" shareAndroid = "1.0.0" hiltNavigationCompose = "1.2.0" @@ -37,8 +37,8 @@ junit = "4.13.2" archCoreTesting = "2.2.0" mockito = "5.12.0" mockitoKotlin = "5.4.0" -testMonitor = "1.7.1" -testRunner = "1.6.1" +testMonitor = "1.7.2" +testRunner = "1.6.2" testExtJunit = "1.2.1" espresso = "3.6.1" turbine = "1.1.0" @@ -46,7 +46,7 @@ vicoCompose = "2.0.0-alpha.22" composeRatingbar = "1.3.12" loggerAndroid = "1.0.0" compileSdk = "34" -targetSdk = "34" +targetSdk = "35" minSdk = "21" [libraries]