mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Smoother now playing backdrop transition
This commit is contained in:
parent
8ddd3fc7eb
commit
b70a7bc7bb
5 changed files with 75 additions and 7 deletions
|
|
@ -114,7 +114,7 @@ class BackdropService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun extractColorsFromBackdrop(imageUrl: String?): ExtractedColors =
|
suspend fun extractColorsFromBackdrop(imageUrl: String?): ExtractedColors =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
if (imageUrl.isNullOrBlank()) {
|
if (imageUrl.isNullOrBlank()) {
|
||||||
return@withContext ExtractedColors.DEFAULT
|
return@withContext ExtractedColors.DEFAULT
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ import coil3.compose.AsyncImage
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.AudioItem
|
import com.github.damontecres.wholphin.data.model.AudioItem
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
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.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.services.rememberQueue
|
import com.github.damontecres.wholphin.services.rememberQueue
|
||||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
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.DialogPopup
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.findActivity
|
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.BottomDialog
|
||||||
import com.github.damontecres.wholphin.ui.playback.BottomDialogItem
|
import com.github.damontecres.wholphin.ui.playback.BottomDialogItem
|
||||||
import com.github.damontecres.wholphin.ui.playback.PlaybackKeyHandler
|
import com.github.damontecres.wholphin.ui.playback.PlaybackKeyHandler
|
||||||
|
|
@ -159,6 +161,15 @@ fun NowPlayingPage(
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier) {
|
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(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail.music
|
package com.github.damontecres.wholphin.ui.detail.music
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.github.damontecres.wholphin.services.BackdropResult
|
||||||
import com.github.damontecres.wholphin.services.MusicServiceState
|
import com.github.damontecres.wholphin.services.MusicServiceState
|
||||||
import org.jellyfin.sdk.model.api.LyricDto
|
import org.jellyfin.sdk.model.api.LyricDto
|
||||||
|
|
||||||
|
|
@ -10,6 +11,7 @@ data class NowPlayingState(
|
||||||
val lyrics: LyricDto? = null,
|
val lyrics: LyricDto? = null,
|
||||||
val currentLyricIndex: Int? = null,
|
val currentLyricIndex: Int? = null,
|
||||||
val visualizerPermissions: Boolean = false,
|
val visualizerPermissions: Boolean = false,
|
||||||
|
val backdropResult: BackdropResult = BackdropResult.NONE,
|
||||||
) {
|
) {
|
||||||
val hasLyrics: Boolean get() = lyrics != null && lyrics.lyrics.isNotEmpty()
|
val hasLyrics: Boolean get() = lyrics != null && lyrics.lyrics.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updateMusicPreferences
|
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.BackdropService
|
||||||
|
import com.github.damontecres.wholphin.services.ImageUrlService
|
||||||
import com.github.damontecres.wholphin.services.MusicService
|
import com.github.damontecres.wholphin.services.MusicService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.NowPlayingStatus
|
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.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.lyricsApi
|
import org.jellyfin.sdk.api.client.extensions.lyricsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
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.api.LyricDto
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -63,6 +66,7 @@ class NowPlayingViewModel
|
||||||
private val api: ApiClient,
|
private val api: ApiClient,
|
||||||
private val musicService: MusicService,
|
private val musicService: MusicService,
|
||||||
private val backdropService: BackdropService,
|
private val backdropService: BackdropService,
|
||||||
|
private val imageUrlService: ImageUrlService,
|
||||||
private val preferencesDataStore: DataStore<AppPreferences>,
|
private val preferencesDataStore: DataStore<AppPreferences>,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
val userPreferencesService: UserPreferencesService,
|
val userPreferencesService: UserPreferencesService,
|
||||||
|
|
@ -123,6 +127,7 @@ class NowPlayingViewModel
|
||||||
controllerViewState.pulseControls()
|
controllerViewState.pulseControls()
|
||||||
}
|
}
|
||||||
viewModelScope.launchDefault {
|
viewModelScope.launchDefault {
|
||||||
|
backdropService.clearBackdrop()
|
||||||
updateBackdrop(getCurrent())
|
updateBackdrop(getCurrent())
|
||||||
}
|
}
|
||||||
playbackLoop()
|
playbackLoop()
|
||||||
|
|
@ -230,19 +235,18 @@ class NowPlayingViewModel
|
||||||
val backdropItem =
|
val backdropItem =
|
||||||
getBackdropItemForAlbum(api, BaseItem(it, false))
|
getBackdropItemForAlbum(api, BaseItem(it, false))
|
||||||
if (backdropItem != null) {
|
if (backdropItem != null) {
|
||||||
backdropService.submit(backdropItem)
|
doUpdateBackdrop(backdropItem)
|
||||||
} else {
|
} else {
|
||||||
backdropService.clearBackdrop()
|
clearBackdrop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.e(ex, "Error fetching backdrop")
|
Timber.e(ex, "Error fetching backdrop")
|
||||||
backdropService.clearBackdrop()
|
clearBackdrop()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
backdropService.clearBackdrop()
|
clearBackdrop()
|
||||||
}
|
}
|
||||||
// TODO needs smoother transition
|
|
||||||
delay(60.seconds)
|
delay(60.seconds)
|
||||||
try {
|
try {
|
||||||
val index = Random.nextInt(state.value.musicServiceState.queueSize)
|
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(
|
fun moveQueue(
|
||||||
index: Int,
|
index: Int,
|
||||||
direction: MoveDirection,
|
direction: MoveDirection,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@file:OptIn(ExperimentalCoilApi::class)
|
||||||
|
|
||||||
package com.github.damontecres.wholphin.ui.nav
|
package com.github.damontecres.wholphin.ui.nav
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
|
@ -24,11 +26,15 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import coil3.annotation.ExperimentalCoilApi
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import coil3.compose.useExistingImageAsPlaceholder
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import coil3.request.transitionFactory
|
import coil3.request.transitionFactory
|
||||||
import com.github.damontecres.wholphin.preferences.BackdropStyle
|
import com.github.damontecres.wholphin.preferences.BackdropStyle
|
||||||
|
import com.github.damontecres.wholphin.services.BackdropResult
|
||||||
import com.github.damontecres.wholphin.ui.CrossFadeFactory
|
import com.github.damontecres.wholphin.ui.CrossFadeFactory
|
||||||
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -38,8 +44,31 @@ fun Backdrop(
|
||||||
viewModel: ApplicationContentViewModel = hiltViewModel(),
|
viewModel: ApplicationContentViewModel = hiltViewModel(),
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enableTopScrim: Boolean = true,
|
enableTopScrim: Boolean = true,
|
||||||
|
useExistingImageAsPlaceholder: Boolean = false,
|
||||||
|
crossfadeDuration: Duration = 800.milliseconds,
|
||||||
) {
|
) {
|
||||||
val backdrop by viewModel.backdropService.backdropFlow.collectAsStateWithLifecycle()
|
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
|
val baseBackgroundColor = MaterialTheme.colorScheme.background
|
||||||
if (backdrop.hasColors &&
|
if (backdrop.hasColors &&
|
||||||
(backdropStyle == BackdropStyle.BACKDROP_DYNAMIC_COLOR || backdropStyle == BackdropStyle.UNRECOGNIZED)
|
(backdropStyle == BackdropStyle.BACKDROP_DYNAMIC_COLOR || backdropStyle == BackdropStyle.UNRECOGNIZED)
|
||||||
|
|
@ -117,7 +146,8 @@ fun Backdrop(
|
||||||
ImageRequest
|
ImageRequest
|
||||||
.Builder(LocalContext.current)
|
.Builder(LocalContext.current)
|
||||||
.data(backdrop.imageUrl)
|
.data(backdrop.imageUrl)
|
||||||
.transitionFactory(CrossFadeFactory(800.milliseconds))
|
.useExistingImageAsPlaceholder(useExistingImageAsPlaceholder)
|
||||||
|
.transitionFactory(CrossFadeFactory(crossfadeDuration))
|
||||||
.build(),
|
.build(),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue