diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/BackdropService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/BackdropService.kt index e0c48cca..832d4137 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/BackdropService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/BackdropService.kt @@ -114,7 +114,7 @@ class BackdropService } } - private suspend fun extractColorsFromBackdrop(imageUrl: String?): ExtractedColors = + suspend fun extractColorsFromBackdrop(imageUrl: String?): ExtractedColors = withContext(Dispatchers.IO) { if (imageUrl.isNullOrBlank()) { return@withContext ExtractedColors.DEFAULT diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt index 17fddb0c..f4cb9a48 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt @@ -54,6 +54,7 @@ import coil3.compose.AsyncImage import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.AudioItem import com.github.damontecres.wholphin.preferences.AppPreferences +import com.github.damontecres.wholphin.preferences.BackdropStyle import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.rememberQueue import com.github.damontecres.wholphin.ui.components.BasicDialog @@ -61,6 +62,7 @@ import com.github.damontecres.wholphin.ui.components.DialogParams import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.findActivity +import com.github.damontecres.wholphin.ui.nav.Backdrop import com.github.damontecres.wholphin.ui.playback.BottomDialog import com.github.damontecres.wholphin.ui.playback.BottomDialogItem import com.github.damontecres.wholphin.ui.playback.PlaybackKeyHandler @@ -159,6 +161,15 @@ fun NowPlayingPage( } Box(modifier) { + Backdrop( + backdrop = state.backdropResult, + drawerIsOpen = false, + backdropStyle = BackdropStyle.BACKDROP_DYNAMIC_COLOR, + modifier = Modifier.fillMaxSize(), + enableTopScrim = false, + useExistingImageAsPlaceholder = true, + crossfadeDuration = 1.5.seconds, + ) Box( modifier = Modifier diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingState.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingState.kt index 39552c59..f6fe40e4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingState.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingState.kt @@ -1,6 +1,7 @@ package com.github.damontecres.wholphin.ui.detail.music import androidx.compose.runtime.Stable +import com.github.damontecres.wholphin.services.BackdropResult import com.github.damontecres.wholphin.services.MusicServiceState import org.jellyfin.sdk.model.api.LyricDto @@ -10,6 +11,7 @@ data class NowPlayingState( val lyrics: LyricDto? = null, val currentLyricIndex: Int? = null, val visualizerPermissions: Boolean = false, + val backdropResult: BackdropResult = BackdropResult.NONE, ) { val hasLyrics: Boolean get() = lyrics != null && lyrics.lyrics.isNotEmpty() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt index 1b087bad..2ad7a58a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt @@ -16,7 +16,9 @@ import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.updateMusicPreferences +import com.github.damontecres.wholphin.services.BackdropResult import com.github.damontecres.wholphin.services.BackdropService +import com.github.damontecres.wholphin.services.ImageUrlService import com.github.damontecres.wholphin.services.MusicService import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.NowPlayingStatus @@ -45,6 +47,7 @@ import kotlinx.coroutines.sync.withLock import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.lyricsApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi +import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.LyricDto import org.jellyfin.sdk.model.extensions.ticks import timber.log.Timber @@ -63,6 +66,7 @@ class NowPlayingViewModel private val api: ApiClient, private val musicService: MusicService, private val backdropService: BackdropService, + private val imageUrlService: ImageUrlService, private val preferencesDataStore: DataStore, val navigationManager: NavigationManager, val userPreferencesService: UserPreferencesService, @@ -123,6 +127,7 @@ class NowPlayingViewModel controllerViewState.pulseControls() } viewModelScope.launchDefault { + backdropService.clearBackdrop() updateBackdrop(getCurrent()) } playbackLoop() @@ -230,19 +235,18 @@ class NowPlayingViewModel val backdropItem = getBackdropItemForAlbum(api, BaseItem(it, false)) if (backdropItem != null) { - backdropService.submit(backdropItem) + doUpdateBackdrop(backdropItem) } else { - backdropService.clearBackdrop() + clearBackdrop() } } } catch (ex: Exception) { Timber.e(ex, "Error fetching backdrop") - backdropService.clearBackdrop() + clearBackdrop() } } else { - backdropService.clearBackdrop() + clearBackdrop() } - // TODO needs smoother transition delay(60.seconds) try { val index = Random.nextInt(state.value.musicServiceState.queueSize) @@ -257,6 +261,27 @@ class NowPlayingViewModel } } + private suspend fun doUpdateBackdrop(item: BaseItem) { + val imageUrl = imageUrlService.getItemImageUrl(item, ImageType.BACKDROP) + val (primaryColor, secondaryColor, tertiaryColor) = + backdropService.extractColorsFromBackdrop( + imageUrl, + ) + val backdropResult = + BackdropResult( + itemId = item.id.toString(), + imageUrl = imageUrl, + primaryColor = primaryColor, + secondaryColor = secondaryColor, + tertiaryColor = tertiaryColor, + ) + state.update { it.copy(backdropResult = backdropResult) } + } + + private fun clearBackdrop() { + state.update { it.copy(backdropResult = BackdropResult.NONE) } + } + fun moveQueue( index: Int, direction: MoveDirection, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Backdrop.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Backdrop.kt index 2402ce48..21520447 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Backdrop.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Backdrop.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalCoilApi::class) + package com.github.damontecres.wholphin.ui.nav import androidx.compose.animation.animateColorAsState @@ -24,11 +26,15 @@ import androidx.compose.ui.platform.LocalContext import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.tv.material3.MaterialTheme +import coil3.annotation.ExperimentalCoilApi import coil3.compose.AsyncImage +import coil3.compose.useExistingImageAsPlaceholder import coil3.request.ImageRequest import coil3.request.transitionFactory import com.github.damontecres.wholphin.preferences.BackdropStyle +import com.github.damontecres.wholphin.services.BackdropResult import com.github.damontecres.wholphin.ui.CrossFadeFactory +import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @Composable @@ -38,8 +44,31 @@ fun Backdrop( viewModel: ApplicationContentViewModel = hiltViewModel(), modifier: Modifier = Modifier, enableTopScrim: Boolean = true, + useExistingImageAsPlaceholder: Boolean = false, + crossfadeDuration: Duration = 800.milliseconds, ) { val backdrop by viewModel.backdropService.backdropFlow.collectAsStateWithLifecycle() + Backdrop( + backdrop = backdrop, + drawerIsOpen = drawerIsOpen, + backdropStyle = backdropStyle, + modifier = modifier, + enableTopScrim = enableTopScrim, + useExistingImageAsPlaceholder = useExistingImageAsPlaceholder, + crossfadeDuration = crossfadeDuration, + ) +} + +@Composable +fun Backdrop( + backdrop: BackdropResult, + drawerIsOpen: Boolean, + backdropStyle: BackdropStyle, + modifier: Modifier = Modifier, + enableTopScrim: Boolean = true, + useExistingImageAsPlaceholder: Boolean = false, + crossfadeDuration: Duration = 800.milliseconds, +) { val baseBackgroundColor = MaterialTheme.colorScheme.background if (backdrop.hasColors && (backdropStyle == BackdropStyle.BACKDROP_DYNAMIC_COLOR || backdropStyle == BackdropStyle.UNRECOGNIZED) @@ -117,7 +146,8 @@ fun Backdrop( ImageRequest .Builder(LocalContext.current) .data(backdrop.imageUrl) - .transitionFactory(CrossFadeFactory(800.milliseconds)) + .useExistingImageAsPlaceholder(useExistingImageAsPlaceholder) + .transitionFactory(CrossFadeFactory(crossfadeDuration)) .build(), contentDescription = null, contentScale = ContentScale.Fit,