From 5d47a8beafa6f2a24247b4120d43661ca1dfc6ea Mon Sep 17 00:00:00 2001 From: Gautam Shorewala <123801344+GautamCoder4019k@users.noreply.github.com> Date: Sat, 4 Jan 2025 16:59:41 +0530 Subject: [PATCH 1/3] feat: Made the player screen background dynamic --- .../android/ui/navigation/TopBar.kt | 3 +- .../BrainzPlayerBackDropScreen.kt | 48 +++++++++++---- .../android/ui/screens/main/MainActivity.kt | 15 ++++- .../viewmodel/BrainzPlayerViewModel.kt | 58 +++++++++++++++++++ 4 files changed, 110 insertions(+), 14 deletions(-) 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 abafa740..9960681a 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,6 +29,7 @@ import org.listenbrainz.android.ui.theme.ListenBrainzTheme fun TopBar( navController: NavController = rememberNavController(), searchBarState: SearchBarState, + backgroundColor: Color=Color.Transparent, context: Context = LocalContext.current, ) { val navBackStackEntry by navController.currentBackStackEntryAsState() @@ -58,7 +59,7 @@ fun TopBar( tint = Color.Unspecified) } }, - backgroundColor = Color.Transparent, + backgroundColor =backgroundColor, contentColor = MaterialTheme.colorScheme.onSurface, elevation = 0.dp, actions = { 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 2e8ef071..a89c937b 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 @@ -6,6 +6,7 @@ import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -63,10 +64,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight @@ -118,11 +121,21 @@ fun BrainzPlayerBackDropScreen( mutableFloatStateOf(0F) } val repeatMode by brainzPlayerViewModel.repeatMode.collectAsStateWithLifecycle() + val context = LocalContext.current + val defaultBackgroundColor = MaterialTheme.colorScheme.background + val isSystemInDarkTheme = isSystemInDarkTheme() /** 56.dp is default bottom navigation height. 70.dp is our mini player's height. */ val headerHeight by animateDpAsState(targetValue = if (currentlyPlayingSong.title == "null" && currentlyPlayingSong.artist == "null") 56.dp else 126.dp) val isPlaying = brainzPlayerViewModel.isPlaying.collectAsState().value - + LaunchedEffect(currentlyPlayingSong) { + brainzPlayerViewModel.getBackGroundColorForPlayer( + currentlyPlayingSong.albumArt, + defaultBackgroundColor, + context, + isSystemInDarkTheme = isSystemInDarkTheme + ) + } BackdropScaffold( modifier = Modifier.padding(top = paddingValues.calculateTopPadding()), frontLayerShape = RectangleShape, @@ -136,7 +149,7 @@ fun BrainzPlayerBackDropScreen( backLayerContent() } }, - frontLayerBackgroundColor = MaterialTheme.colorScheme.background, + frontLayerBackgroundColor = defaultBackgroundColor, appBar = {}, persistentAppBar = false, frontLayerContent = { @@ -149,7 +162,14 @@ fun BrainzPlayerBackDropScreen( currentlyPlayingSong = currentlyPlayingSong, isShuffled = isShuffled, repeatMode = repeatMode, - backdropScaffoldState = backdropScaffoldState + backdropScaffoldState = backdropScaffoldState, + backgroundBrush = Brush.verticalGradient( + colors = listOf( + brainzPlayerViewModel.playerBackGroundColor, + defaultBackgroundColor + ) + ), + dynamicBackground = brainzPlayerViewModel.playerBackGroundColor ) val songList = brainzPlayerViewModel.mediaItem.collectAsState().value.data ?: listOf() SongViewPager( @@ -172,6 +192,8 @@ fun PlayerScreen( isShuffled: Boolean, repeatMode: RepeatMode, backdropScaffoldState: BackdropScaffoldState, + backgroundBrush: Brush, + dynamicBackground: Color = MaterialTheme.colorScheme.background ) { val coroutineScope = rememberCoroutineScope() val playlistViewModel = hiltViewModel() @@ -189,17 +211,17 @@ fun PlayerScreen( println("Playlist is empty") } - if(backdropScaffoldState.isConcealed){ + if (backdropScaffoldState.isConcealed) { BackHandler { coroutineScope.launch { backdropScaffoldState.reveal() } } } - LazyColumn { + LazyColumn(modifier = Modifier.background(brush = backgroundBrush)) { item { songList.data?.let { - AlbumArtViewPager(currentlyPlayingSong, pagerState) + AlbumArtViewPager(currentlyPlayingSong, pagerState, dynamicBackground) } } item { @@ -539,12 +561,15 @@ fun PlayerScreen( @OptIn(ExperimentalFoundationApi::class) @Composable -fun AlbumArtViewPager(currentlyPlayingSong: Song, pagerState: PagerState) { +fun AlbumArtViewPager( + currentlyPlayingSong: Song, + pagerState: PagerState, + dynamicBackground: Color +) { HorizontalPager( state = pagerState, modifier = Modifier .fillMaxWidth() - .background(ListenBrainzTheme.colorScheme.background), ) { page -> Column( Modifier @@ -556,7 +581,7 @@ fun AlbumArtViewPager(currentlyPlayingSong: Song, pagerState: PagerState) { .padding(top = 20.dp) .width(300.dp) .clip(RoundedCornerShape(20.dp)) - .background(MaterialTheme.colorScheme.background) + .background(dynamicBackground) .graphicsLayer { // Calculate the absolute offset for the current page from the // scroll position. We use the absolute value which allows us to mirror @@ -583,7 +608,7 @@ fun AlbumArtViewPager(currentlyPlayingSong: Song, pagerState: PagerState) { ) { AsyncImage( modifier = Modifier - .background(MaterialTheme.colorScheme.background) + .background(dynamicBackground) .fillMaxSize() .padding() .clip(shape = RoundedCornerShape(20.dp)) @@ -607,7 +632,8 @@ fun AlbumArtViewPager(currentlyPlayingSong: Song, pagerState: PagerState) { fun AlbumArtViewPagerPreview() { AlbumArtViewPager( currentlyPlayingSong = Song.preview(), - pagerState = rememberPagerState { 3 } + pagerState = rememberPagerState { 3 }, + dynamicBackground = MaterialTheme.colorScheme.background ) } 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 370c9813..4a1fa133 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 @@ -6,6 +6,9 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.safeDrawingPadding @@ -25,6 +28,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope @@ -48,6 +52,7 @@ import org.listenbrainz.android.ui.screens.search.rememberSearchBarState import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.util.Utils.isServiceRunning import org.listenbrainz.android.util.Utils.openAppSystemSettings +import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel import org.listenbrainz.android.viewmodel.DashBoardViewModel @AndroidEntryPoint @@ -158,7 +163,11 @@ class MainActivity : ComponentActivity() { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination val username = dashBoardViewModel.username - + val brainzPlayerViewModel: BrainzPlayerViewModel by viewModels() + val animatedBackgroundColor by animateColorAsState( + targetValue = if (backdropScaffoldState.isConcealed) brainzPlayerViewModel.playerBackGroundColor else Color.Transparent, + animationSpec = tween(durationMillis = 500) + ) Scaffold( modifier = Modifier.safeDrawingPadding(), topBar = { @@ -167,7 +176,8 @@ class MainActivity : ComponentActivity() { searchBarState = when (currentDestination?.route) { AppNavigationItem.BrainzPlayer.route -> brainzplayerSearchBarState else -> searchBarState - } + }, + backgroundColor = animatedBackgroundColor ) }, bottomBar = { @@ -199,6 +209,7 @@ class MainActivity : ComponentActivity() { BrainzPlayerBackDropScreen( backdropScaffoldState = backdropScaffoldState, paddingValues = it, + brainzPlayerViewModel = brainzPlayerViewModel ) { AppNavigation( navController = navController, 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 8f8cd120..faa59dfe 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt @@ -1,5 +1,8 @@ package org.listenbrainz.android.viewmodel +import android.content.Context +import android.graphics.Color.parseColor +import android.graphics.drawable.BitmapDrawable import android.os.Build import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.session.PlaybackStateCompat.REPEAT_MODE_ALL @@ -12,8 +15,13 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.palette.graphics.Palette +import coil.ImageLoader +import coil.request.ImageRequest +import coil.request.SuccessResult import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -67,6 +75,8 @@ class BrainzPlayerViewModel @Inject constructor( val repeatMode = brainzPlayerServiceConnection.repeatModeState var isSearching by mutableStateOf(false) + var playerBackGroundColor by mutableStateOf(Color.White) + init { updatePlayerPosition() _mediaItems.value = Resource.loading() @@ -94,6 +104,54 @@ class BrainzPlayerViewModel @Inject constructor( } } + fun getBackGroundColorForPlayer( + albumArtUrl: String?, + defaultColor: Color, + context: Context, + isSystemInDarkTheme: Boolean = true + ) { + viewModelScope.launch { + var dominantColor: Color = defaultColor + val loader = ImageLoader(context) + val request = ImageRequest.Builder(context) + .data(albumArtUrl) + .allowHardware(false) + .build() + val result = loader.execute(request) + val bitmap = (result as? SuccessResult)?.drawable?.let { drawable -> + (drawable as? BitmapDrawable)?.bitmap + } + bitmap?.let { + dominantColor = if (isSystemInDarkTheme) + Color( + parseColor( + parseColorSwatch( + Palette.from(it).generate().darkMutedSwatch + ) + ) + ) + else + Color( + parseColor( + parseColorSwatch( + Palette.from(it).generate().lightMutedSwatch + ) + ) + ) + } + playerBackGroundColor = dominantColor + } + } + + private fun parseColorSwatch(color: Palette.Swatch?): String { + return if (color != null) { + val parsedColor = Integer.toHexString(color.rgb) + return "#$parsedColor" + } else { + "#FFFFFF" + } + } + fun skipToNextSong() { brainzPlayerServiceConnection.transportControls.skipToNext() // Updating currently playing song. From c0d56533f485af8909c39b6f66dfa8fc119e979f Mon Sep 17 00:00:00 2001 From: Gautam Shorewala <123801344+GautamCoder4019k@users.noreply.github.com> Date: Tue, 7 Jan 2025 22:29:16 +0530 Subject: [PATCH 2/3] Fix: minor bugs related to theme and ui --- .../ui/screens/brainzplayer/BrainzPlayerBackDropScreen.kt | 7 ++++--- .../listenbrainz/android/ui/screens/main/MainActivity.kt | 6 +----- .../android/viewmodel/BrainzPlayerViewModel.kt | 6 +++--- 3 files changed, 8 insertions(+), 11 deletions(-) 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 a89c937b..96e66942 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 @@ -97,6 +97,7 @@ import org.listenbrainz.android.ui.components.PlayPauseIcon import org.listenbrainz.android.ui.components.SeekBar import org.listenbrainz.android.ui.screens.brainzplayer.ui.components.basicMarquee import org.listenbrainz.android.ui.theme.ListenBrainzTheme +import org.listenbrainz.android.ui.theme.onScreenUiModeIsDark import org.listenbrainz.android.util.BrainzPlayerExtensions.toSong import org.listenbrainz.android.util.CacheService import org.listenbrainz.android.util.Constants.RECENTLY_PLAYED_KEY @@ -123,17 +124,17 @@ fun BrainzPlayerBackDropScreen( val repeatMode by brainzPlayerViewModel.repeatMode.collectAsStateWithLifecycle() val context = LocalContext.current val defaultBackgroundColor = MaterialTheme.colorScheme.background - val isSystemInDarkTheme = isSystemInDarkTheme() + val isDarkThemeEnabled = onScreenUiModeIsDark() /** 56.dp is default bottom navigation height. 70.dp is our mini player's height. */ val headerHeight by animateDpAsState(targetValue = if (currentlyPlayingSong.title == "null" && currentlyPlayingSong.artist == "null") 56.dp else 126.dp) val isPlaying = brainzPlayerViewModel.isPlaying.collectAsState().value - LaunchedEffect(currentlyPlayingSong) { + LaunchedEffect(currentlyPlayingSong, isDarkThemeEnabled) { brainzPlayerViewModel.getBackGroundColorForPlayer( currentlyPlayingSong.albumArt, defaultBackgroundColor, context, - isSystemInDarkTheme = isSystemInDarkTheme + isDarkThemeEnabled = isDarkThemeEnabled ) } BackdropScaffold( 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 4a1fa133..0a96c759 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 @@ -164,10 +164,6 @@ class MainActivity : ComponentActivity() { val currentDestination = navBackStackEntry?.destination val username = dashBoardViewModel.username val brainzPlayerViewModel: BrainzPlayerViewModel by viewModels() - val animatedBackgroundColor by animateColorAsState( - targetValue = if (backdropScaffoldState.isConcealed) brainzPlayerViewModel.playerBackGroundColor else Color.Transparent, - animationSpec = tween(durationMillis = 500) - ) Scaffold( modifier = Modifier.safeDrawingPadding(), topBar = { @@ -177,7 +173,7 @@ class MainActivity : ComponentActivity() { AppNavigationItem.BrainzPlayer.route -> brainzplayerSearchBarState else -> searchBarState }, - backgroundColor = animatedBackgroundColor + backgroundColor = if (backdropScaffoldState.isConcealed) brainzPlayerViewModel.playerBackGroundColor else Color.Transparent ) }, bottomBar = { 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 faa59dfe..1f2919e5 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt @@ -75,7 +75,7 @@ class BrainzPlayerViewModel @Inject constructor( val repeatMode = brainzPlayerServiceConnection.repeatModeState var isSearching by mutableStateOf(false) - var playerBackGroundColor by mutableStateOf(Color.White) + var playerBackGroundColor by mutableStateOf(Color.Transparent) init { updatePlayerPosition() @@ -108,7 +108,7 @@ class BrainzPlayerViewModel @Inject constructor( albumArtUrl: String?, defaultColor: Color, context: Context, - isSystemInDarkTheme: Boolean = true + isDarkThemeEnabled: Boolean = true ) { viewModelScope.launch { var dominantColor: Color = defaultColor @@ -122,7 +122,7 @@ class BrainzPlayerViewModel @Inject constructor( (drawable as? BitmapDrawable)?.bitmap } bitmap?.let { - dominantColor = if (isSystemInDarkTheme) + dominantColor = if (isDarkThemeEnabled) Color( parseColor( parseColorSwatch( From 89a669f62ca02dcc8d1c3ebb7956ef7859df3d3d Mon Sep 17 00:00:00 2001 From: Jasjeet Singh <98077881+07jasjeet@users.noreply.github.com> Date: Sat, 11 Jan 2025 19:13:09 +0530 Subject: [PATCH 3/3] Improvements and bug fixes 1. Add fallback swatches 2. Better animation for background --- .../ui/navigation/BottomNavigationBar.kt | 4 + .../android/ui/navigation/TopBar.kt | 37 ++++-- .../BrainzPlayerBackDropScreen.kt | 31 +++-- .../android/ui/screens/main/MainActivity.kt | 63 ++++++++-- .../listenbrainz/android/ui/theme/Theme.kt | 5 +- .../org/listenbrainz/android/util/Utils.kt | 110 +++++++++++++++++- .../viewmodel/BrainzPlayerViewModel.kt | 51 +++----- 7 files changed, 224 insertions(+), 77 deletions(-) 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 01d02210..12fdf650 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 @@ -1,5 +1,6 @@ package org.listenbrainz.android.ui.navigation +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.BackdropScaffoldState @@ -31,6 +32,7 @@ import org.listenbrainz.android.ui.theme.ListenBrainzTheme @OptIn(ExperimentalMaterialApi::class) @Composable fun BottomNavigationBar( + modifier: Modifier = Modifier, navController: NavController = rememberNavController(), backdropScaffoldState: BackdropScaffoldState = rememberBackdropScaffoldState(initialValue = BackdropValue.Revealed), scrollToTop: () -> Unit, @@ -43,6 +45,7 @@ fun BottomNavigationBar( AppNavigationItem.Profile ) BottomNavigation( + modifier = modifier, backgroundColor = ListenBrainzTheme.colorScheme.nav, elevation = 0.dp ) { @@ -52,6 +55,7 @@ fun BottomNavigationBar( val currentDestination = navBackStackEntry?.destination val selected = currentDestination?.route?.startsWith("${item.route}/") == true || currentDestination?.route == item.route BottomNavigationItem( + modifier = Modifier.navigationBarsPadding(), icon = { Icon( painterResource(id = selected 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 9960681a..0ccc4513 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 @@ -11,6 +11,7 @@ import androidx.compose.material.TopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -27,9 +28,10 @@ import org.listenbrainz.android.ui.theme.ListenBrainzTheme @Composable fun TopBar( + modifier: Modifier = Modifier, navController: NavController = rememberNavController(), searchBarState: SearchBarState, - backgroundColor: Color=Color.Transparent, + backgroundColor: Color = Color.Transparent, context: Context = LocalContext.current, ) { val navBackStackEntry by navController.currentBackStackEntryAsState() @@ -43,32 +45,43 @@ 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 + "${AppNavigationItem.Album.route}/{mbid}" -> AppNavigationItem.Album.title else -> "" } } ?: "ListenBrainz" - + TopAppBar( + modifier = modifier, title = { Text(text = title) }, - navigationIcon = { + navigationIcon = { IconButton(onClick = { - context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://listenbrainz.org"))) + context.startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("https://listenbrainz.org") + ) + ) }) { - Icon(painterResource(id = R.drawable.ic_listenbrainz_logo_icon), + Icon( + painterResource(id = R.drawable.ic_listenbrainz_logo_icon), "ListenBrainz", - tint = Color.Unspecified) + tint = Color.Unspecified + ) } }, - backgroundColor =backgroundColor, + backgroundColor = backgroundColor, contentColor = MaterialTheme.colorScheme.onSurface, elevation = 0.dp, actions = { IconButton(onClick = { searchBarState.activate() }) { - Icon(painterResource(id = R.drawable.ic_search), contentDescription = "Search users") + Icon( + painterResource(id = R.drawable.ic_search), + contentDescription = "Search users" + ) } IconButton(onClick = { - if (navBackStackEntry?.destination?.route == AppNavigationItem.Settings.route){ + if (navBackStackEntry?.destination?.route == AppNavigationItem.Settings.route) { navController.popBackStack() } else { navController.navigate(AppNavigationItem.Settings.route) { @@ -83,11 +96,11 @@ fun TopBar( } } }) { - Icon(painterResource(id = R.drawable.ic_settings),"Settings") + Icon(painterResource(id = R.drawable.ic_settings), "Settings") } } ) - + } @Preview 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 96e66942..53ea1db5 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 @@ -6,7 +6,6 @@ import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -85,22 +84,17 @@ import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import kotlinx.coroutines.launch import org.listenbrainz.android.R -import org.listenbrainz.android.application.App 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.CustomSeekBar import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.components.PlayPauseIcon -import org.listenbrainz.android.ui.components.SeekBar import org.listenbrainz.android.ui.screens.brainzplayer.ui.components.basicMarquee import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.ui.theme.onScreenUiModeIsDark import org.listenbrainz.android.util.BrainzPlayerExtensions.toSong -import org.listenbrainz.android.util.CacheService -import org.listenbrainz.android.util.Constants.RECENTLY_PLAYED_KEY import org.listenbrainz.android.util.SongViewPager import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel import org.listenbrainz.android.viewmodel.PlaylistViewModel @@ -110,6 +104,7 @@ import kotlin.math.max @ExperimentalMaterialApi @Composable fun BrainzPlayerBackDropScreen( + modifier: Modifier = Modifier, backdropScaffoldState: BackdropScaffoldState, brainzPlayerViewModel: BrainzPlayerViewModel = viewModel(), paddingValues: PaddingValues, @@ -123,14 +118,18 @@ fun BrainzPlayerBackDropScreen( } val repeatMode by brainzPlayerViewModel.repeatMode.collectAsStateWithLifecycle() val context = LocalContext.current - val defaultBackgroundColor = MaterialTheme.colorScheme.background + val defaultBackgroundColor = ListenBrainzTheme.colorScheme.background val isDarkThemeEnabled = onScreenUiModeIsDark() - /** 56.dp is default bottom navigation height. 70.dp is our mini player's height. */ - val headerHeight by animateDpAsState(targetValue = if (currentlyPlayingSong.title == "null" && currentlyPlayingSong.artist == "null") 56.dp else 126.dp) - val isPlaying = brainzPlayerViewModel.isPlaying.collectAsState().value + /** 56.dp is default bottom navigation height */ + val headerHeight by animateDpAsState( + targetValue = if (currentlyPlayingSong.title == "null" && currentlyPlayingSong.artist == "null") + 56.dp + else + 56.dp + ListenBrainzTheme.sizes.brainzPlayerPeekHeight + ) LaunchedEffect(currentlyPlayingSong, isDarkThemeEnabled) { - brainzPlayerViewModel.getBackGroundColorForPlayer( + brainzPlayerViewModel.updateBackgroundColorForPlayer( currentlyPlayingSong.albumArt, defaultBackgroundColor, context, @@ -138,18 +137,14 @@ fun BrainzPlayerBackDropScreen( ) } BackdropScaffold( - modifier = Modifier.padding(top = paddingValues.calculateTopPadding()), + modifier = modifier.padding(top = paddingValues.calculateTopPadding()), frontLayerShape = RectangleShape, - backLayerBackgroundColor = MaterialTheme.colorScheme.background, + backLayerBackgroundColor = Color.Transparent, frontLayerScrimColor = Color.Unspecified, headerHeight = headerHeight, // 126.dp is optimal header height. peekHeight = 0.dp, scaffoldState = backdropScaffoldState, - backLayerContent = { - Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) { - backLayerContent() - } - }, + backLayerContent = backLayerContent, frontLayerBackgroundColor = defaultBackgroundColor, appBar = {}, persistentAppBar = false, 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 0a96c759..54fe92c5 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 @@ -7,13 +7,17 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material.BackdropValue import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Surface import androidx.compose.material.rememberBackdropScaffoldState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -22,13 +26,16 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf 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.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope @@ -52,6 +59,7 @@ import org.listenbrainz.android.ui.screens.search.rememberSearchBarState import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.util.Utils.isServiceRunning import org.listenbrainz.android.util.Utils.openAppSystemSettings +import org.listenbrainz.android.util.Utils.toPx import org.listenbrainz.android.viewmodel.BrainzPlayerViewModel import org.listenbrainz.android.viewmodel.DashBoardViewModel @@ -164,16 +172,52 @@ class MainActivity : ComponentActivity() { val currentDestination = navBackStackEntry?.destination val username = dashBoardViewModel.username val brainzPlayerViewModel: BrainzPlayerViewModel by viewModels() + + val isBackdropInitialised by remember { + derivedStateOf { + val currentOffset = runCatching { + backdropScaffoldState.requireOffset() + }.getOrNull() + + currentOffset != null + } + } + + var maxOffset by remember { + mutableFloatStateOf(0f) + } + + val playerHeight = ListenBrainzTheme.sizes.brainzPlayerPeekHeight.toPx() + LaunchedEffect(isBackdropInitialised) { + if (isBackdropInitialised) { + maxOffset = maxOf(maxOffset, backdropScaffoldState.requireOffset() - playerHeight) + println(maxOffset) + } + } + + val desiredBackgroundColor by remember { + derivedStateOf { + brainzPlayerViewModel.playerBackGroundColor.copy( + alpha = runCatching { + 1 - (backdropScaffoldState.requireOffset() / maxOffset).coerceIn(0f, 1f) + }.getOrElse { 0f } + ) + } + } + Scaffold( - modifier = Modifier.safeDrawingPadding(), + modifier = Modifier + .fillMaxSize() + .background(ListenBrainzTheme.colorScheme.background) + .background(desiredBackgroundColor), topBar = { TopBar( + modifier = Modifier.statusBarsPadding(), navController = navController, searchBarState = when (currentDestination?.route) { AppNavigationItem.BrainzPlayer.route -> brainzplayerSearchBarState else -> searchBarState }, - backgroundColor = if (backdropScaffoldState.isConcealed) brainzPlayerViewModel.playerBackGroundColor else Color.Transparent ) }, bottomBar = { @@ -185,7 +229,10 @@ class MainActivity : ComponentActivity() { ) }, snackbarHost = { - SnackbarHost(hostState = snackbarState) { snackbarData -> + SnackbarHost( + modifier = Modifier.safeDrawingPadding(), + hostState = snackbarState + ) { snackbarData -> Snackbar( snackbarData = snackbarData, containerColor = MaterialTheme.colorScheme.background, @@ -195,14 +242,12 @@ class MainActivity : ComponentActivity() { ) } }, - containerColor = MaterialTheme.colorScheme.background, + containerColor = Color.Transparent, contentWindowInsets = WindowInsets.captionBar - ) { - if (isGrantedPerms == PermissionStatus.GRANTED.name) { - BrainzPlayerBackDropScreen( + modifier = Modifier.navigationBarsPadding(), backdropScaffoldState = backdropScaffoldState, paddingValues = it, brainzPlayerViewModel = brainzPlayerViewModel 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 feab3433..99304e23 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 @@ -242,7 +242,8 @@ val LocalPaddings = staticCompositionLocalOf { Paddings() } data class Sizes( val listenCardHeight: Dp = 60.dp, val listenCardCorner: Dp = 8.dp, - val dropdownItem: Dp = 20.dp + val dropdownItem: Dp = 20.dp, + val brainzPlayerPeekHeight: Dp = 70.dp ) val LocalSizes = staticCompositionLocalOf { Sizes() } @@ -343,7 +344,6 @@ fun ListenBrainzTheme( val view = LocalView.current if (!view.isInEditMode) { SideEffect { - (view.context as Activity).window.statusBarColor = localColorScheme.background.toArgb() val isDark = when (uiMode){ UiMode.DARK -> false UiMode.LIGHT -> true @@ -351,7 +351,6 @@ fun ListenBrainzTheme( } systemUiController.statusBarDarkContentEnabled = isDark systemUiController.navigationBarDarkContentEnabled = isDark - systemUiController.setNavigationBarColor(color = colorScheme.tertiaryContainer) } } diff --git a/app/src/main/java/org/listenbrainz/android/util/Utils.kt b/app/src/main/java/org/listenbrainz/android/util/Utils.kt index 32e4dade..66fe5b99 100644 --- a/app/src/main/java/org/listenbrainz/android/util/Utils.kt +++ b/app/src/main/java/org/listenbrainz/android/util/Utils.kt @@ -8,6 +8,7 @@ import android.content.Context import android.content.ContextWrapper import android.content.Intent import android.content.pm.PackageManager +import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.drawable.BitmapDrawable @@ -22,10 +23,28 @@ import android.widget.Toast import androidx.activity.ComponentActivity import androidx.annotation.RequiresApi import androidx.annotation.WorkerThread -import androidx.compose.ui.geometry.Size +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.DisposableEffectResult +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp import androidx.core.app.NotificationCompat +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import okhttp3.* import org.listenbrainz.android.R @@ -129,6 +148,95 @@ object Utils { } } + @Composable + fun LaunchedEffectUnit(block: suspend CoroutineScope.() -> Unit) = LaunchedEffect(Unit, block) + + @Composable + fun LaunchedEffectUnitMainThread(block: () -> Unit) = DisposableEffect(Unit) { + block() + EmptyDisposableEffectResult + } + + @Composable + fun LaunchedEffectMainThread(vararg keys: Any?, block: () -> Unit) = DisposableEffect(*keys) { + block() + EmptyDisposableEffectResult + } + + @Composable + fun LaunchedEffectMainThread(key1: Any?, block: () -> Unit) = DisposableEffect(key1) { + block() + EmptyDisposableEffectResult + } + + private val EmptyDisposableEffectResult = object : DisposableEffectResult { + override fun dispose() = Unit + } + + fun Context.getNavigationBarHeightInPixels(): Int { + val resourceId: Int = resources.getIdentifier("navigation_bar_height", "dimen", "android") + return if (resourceId > 0) { + resources.getDimensionPixelSize(resourceId) + } else { + 0 + } + } + + /** Works for cases where the status bar may change, i.e., foldables.*/ + fun Context.getStatusBarHeightInPixels(): Int { + val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") + return if (resourceId > 0) { + resources.getDimensionPixelSize(resourceId) + } else { + 0 + } + } + + @Composable + fun getStatusBarHeight() = + androidx.compose.foundation.layout.WindowInsets.statusBars.asPaddingValues() + .calculateTopPadding() + + @Composable + fun getDisplayCutoutHeight() = + if (LocalConfiguration.current.orientation == ORIENTATION_PORTRAIT) { + androidx.compose.foundation.layout.WindowInsets.displayCutout.asPaddingValues() + .calculateTopPadding() + } else { + androidx.compose.foundation.layout.WindowInsets.displayCutout.asPaddingValues() + .calculateStartPadding(LocalLayoutDirection.current) + } + + @Composable + fun getNavigationBarHeight() = + androidx.compose.foundation.layout.WindowInsets.navigationBars.asPaddingValues() + .calculateBottomPadding() + + @Composable + fun HorizontalSpacer(width: Dp) = Spacer(Modifier.width(width)) + + @Composable + fun VerticalSpacer(height: Dp) = Spacer(Modifier.height(height)) + + @Composable + fun Int.toDp() = with(LocalDensity.current) { this@toDp.toDp() } + @Composable + fun Float.toDp() = with(LocalDensity.current) { this@toDp.toDp() } + + @Composable + fun Int.toSp() = with(LocalDensity.current) { this@toSp.toSp() } + @Composable + fun Float.toSp() = with(LocalDensity.current) { this@toSp.toSp() } + + @Composable + fun Dp.toPx() = with(LocalDensity.current) { this@toPx.toPx() } + + fun Number.toDp(context: Context) = (this.toFloat() / context.resources.displayMetrics.density).dp + fun Number.toDp(density: Int) = (this.toFloat() / density).dp + + fun Dp.toPx(context: Context) = this.value * context.resources.displayMetrics.density + fun Dp.toPx(density: Int) = this.value * density + fun emailIntent(recipient: String, subject: String?): Intent { val uri = Uri.parse("mailto:$recipient") val intent = Intent(Intent.ACTION_SENDTO, uri) 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 1f2919e5..7fba0ed9 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt @@ -3,19 +3,17 @@ package org.listenbrainz.android.viewmodel import android.content.Context import android.graphics.Color.parseColor import android.graphics.drawable.BitmapDrawable -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 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.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.palette.graphics.Palette @@ -23,7 +21,6 @@ import coil.ImageLoader import coil.request.ImageRequest import coil.request.SuccessResult import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -46,8 +43,6 @@ 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 @@ -104,11 +99,11 @@ class BrainzPlayerViewModel @Inject constructor( } } - fun getBackGroundColorForPlayer( + fun updateBackgroundColorForPlayer( albumArtUrl: String?, defaultColor: Color, context: Context, - isDarkThemeEnabled: Boolean = true + isDarkThemeEnabled: Boolean ) { viewModelScope.launch { var dominantColor: Color = defaultColor @@ -121,37 +116,25 @@ class BrainzPlayerViewModel @Inject constructor( val bitmap = (result as? SuccessResult)?.drawable?.let { drawable -> (drawable as? BitmapDrawable)?.bitmap } - bitmap?.let { - dominantColor = if (isDarkThemeEnabled) - Color( - parseColor( - parseColorSwatch( - Palette.from(it).generate().darkMutedSwatch - ) - ) - ) - else - Color( - parseColor( - parseColorSwatch( - Palette.from(it).generate().lightMutedSwatch - ) - ) - ) + bitmap?.let { bitmap -> + val palette = Palette.from(bitmap).generate() + val swatch = run { + if (isDarkThemeEnabled) { + palette.darkMutedSwatch ?: palette.darkVibrantSwatch ?: palette.lightMutedSwatch ?: palette.swatches.firstOrNull() + } else { + palette.lightMutedSwatch ?: palette.lightVibrantSwatch ?: palette.darkMutedSwatch ?: palette.swatches.firstOrNull() + } + } + dominantColor = if (swatch != null) { + Color(swatch.rgb) + } else { + defaultColor + } } playerBackGroundColor = dominantColor } } - private fun parseColorSwatch(color: Palette.Swatch?): String { - return if (color != null) { - val parsedColor = Integer.toHexString(color.rgb) - return "#$parsedColor" - } else { - "#FFFFFF" - } - } - fun skipToNextSong() { brainzPlayerServiceConnection.transportControls.skipToNext() // Updating currently playing song.