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 abafa740..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,8 +28,10 @@ import org.listenbrainz.android.ui.theme.ListenBrainzTheme @Composable fun TopBar( + modifier: Modifier = Modifier, navController: NavController = rememberNavController(), searchBarState: SearchBarState, + backgroundColor: Color = Color.Transparent, context: Context = LocalContext.current, ) { val navBackStackEntry by navController.currentBackStackEntryAsState() @@ -42,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 = Color.Transparent, + 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) { @@ -82,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 2e8ef071..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 @@ -63,10 +63,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 @@ -82,21 +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 @@ -106,6 +104,7 @@ import kotlin.math.max @ExperimentalMaterialApi @Composable fun BrainzPlayerBackDropScreen( + modifier: Modifier = Modifier, backdropScaffoldState: BackdropScaffoldState, brainzPlayerViewModel: BrainzPlayerViewModel = viewModel(), paddingValues: PaddingValues, @@ -118,25 +117,35 @@ fun BrainzPlayerBackDropScreen( mutableFloatStateOf(0F) } val repeatMode by brainzPlayerViewModel.repeatMode.collectAsStateWithLifecycle() + val context = LocalContext.current + 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.updateBackgroundColorForPlayer( + currentlyPlayingSong.albumArt, + defaultBackgroundColor, + context, + isDarkThemeEnabled = isDarkThemeEnabled + ) + } 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() - } - }, - frontLayerBackgroundColor = MaterialTheme.colorScheme.background, + backLayerContent = backLayerContent, + frontLayerBackgroundColor = defaultBackgroundColor, appBar = {}, persistentAppBar = false, frontLayerContent = { @@ -149,7 +158,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 +188,8 @@ fun PlayerScreen( isShuffled: Boolean, repeatMode: RepeatMode, backdropScaffoldState: BackdropScaffoldState, + backgroundBrush: Brush, + dynamicBackground: Color = MaterialTheme.colorScheme.background ) { val coroutineScope = rememberCoroutineScope() val playlistViewModel = hiltViewModel() @@ -189,17 +207,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 +557,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 +577,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 +604,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 +628,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..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 @@ -6,11 +6,18 @@ 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.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 @@ -19,12 +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 @@ -48,6 +59,8 @@ 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 @AndroidEntryPoint @@ -158,16 +171,53 @@ class MainActivity : ComponentActivity() { val navBackStackEntry by navController.currentBackStackEntryAsState() 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 - } + }, ) }, bottomBar = { @@ -179,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, @@ -189,16 +242,15 @@ 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 ) { AppNavigation( navController = navController, 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 8f8cd120..7fba0ed9 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt @@ -1,21 +1,26 @@ package org.listenbrainz.android.viewmodel -import android.os.Build +import android.content.Context +import android.graphics.Color.parseColor +import android.graphics.drawable.BitmapDrawable 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 +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 @@ -38,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 @@ -67,6 +70,8 @@ class BrainzPlayerViewModel @Inject constructor( val repeatMode = brainzPlayerServiceConnection.repeatModeState var isSearching by mutableStateOf(false) + var playerBackGroundColor by mutableStateOf(Color.Transparent) + init { updatePlayerPosition() _mediaItems.value = Resource.loading() @@ -94,6 +99,42 @@ class BrainzPlayerViewModel @Inject constructor( } } + fun updateBackgroundColorForPlayer( + albumArtUrl: String?, + defaultColor: Color, + context: Context, + isDarkThemeEnabled: Boolean + ) { + 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 { 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 + } + } + fun skipToNextSong() { brainzPlayerServiceConnection.transportControls.skipToNext() // Updating currently playing song.