Skip to content

Commit

Permalink
Rewrite usages of QueueEntry in UI
Browse files Browse the repository at this point in the history
  • Loading branch information
nielsvanvelzen committed Oct 28, 2024
1 parent 81aa372 commit 67d470f
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 161 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,15 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import org.jellyfin.androidtv.integration.dream.model.DreamContent
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.playback.core.PlaybackManager
import org.jellyfin.playback.core.queue.QueueEntry
import org.jellyfin.playback.core.queue.queue
import org.jellyfin.playback.jellyfin.lyricsFlow
import org.jellyfin.playback.jellyfin.queue.baseItemFlow
import org.jellyfin.playback.jellyfin.queue.baseItem
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.ApiClientException
import org.jellyfin.sdk.api.client.extensions.imageApi
Expand All @@ -46,25 +43,14 @@ class DreamViewModel(
playbackManager: PlaybackManager,
private val userPreferences: UserPreferences,
) : ViewModel() {
private val QueueEntry.nowPlayingFlow
get() = combine(baseItemFlow, lyricsFlow) { baseItem, lyrics ->
baseItem?.let {
DreamContent.NowPlaying(
item = baseItem,
lyrics = lyrics,
)
}
}

@OptIn(ExperimentalCoroutinesApi::class)
private val _mediaContent = playbackManager.queue.entry
.flatMapLatest { entry -> entry?.nowPlayingFlow ?: emptyFlow() }
.distinctUntilChanged()
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(),
null,
)
.map { entry ->
entry?.baseItem?.let { baseItem ->
DreamContent.NowPlaying(entry, baseItem)
}
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)

private val _libraryContent = flow {
// Load first library item after 2 seconds
Expand Down Expand Up @@ -108,7 +94,7 @@ class DreamViewModel(
hasParentalRating = if (requireParentalRating) true else null,
)

val item = response.items?.firstOrNull { item ->
val item = response.items.firstOrNull { item ->
!item.backdropImageTags.isNullOrEmpty()
} ?: return null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
Expand All @@ -31,22 +27,22 @@ import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Text
import kotlinx.coroutines.delay
import org.jellyfin.androidtv.integration.dream.model.DreamContent
import org.jellyfin.androidtv.ui.composable.AsyncImage
import org.jellyfin.androidtv.ui.composable.LyricsDtoBox
import org.jellyfin.androidtv.ui.composable.blurHashPainter
import org.jellyfin.androidtv.ui.composable.modifier.fadingEdges
import org.jellyfin.androidtv.ui.composable.modifier.overscan
import org.jellyfin.androidtv.ui.composable.rememberPlayerProgress
import org.jellyfin.playback.core.PlaybackManager
import org.jellyfin.playback.core.model.PlayState
import org.jellyfin.playback.jellyfin.lyrics
import org.jellyfin.playback.jellyfin.lyricsFlow
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.imageApi
import org.jellyfin.sdk.model.api.ImageFormat
import org.jellyfin.sdk.model.api.ImageType
import org.koin.compose.koinInject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

@Composable
fun DreamContentNowPlaying(
Expand All @@ -56,21 +52,8 @@ fun DreamContentNowPlaying(
) {
val api = koinInject<ApiClient>()
val playbackManager = koinInject<PlaybackManager>()

// Track playback position & duration
var playbackPosition by remember { mutableStateOf(Duration.ZERO) }
var playbackDuration by remember { mutableStateOf(Duration.ZERO) }
val playState by remember { playbackManager.state.playState }.collectAsState()

LaunchedEffect(playState) {
while (true) {
val positionInfo = playbackManager.state.positionInfo
playbackPosition = positionInfo.active
playbackDuration = positionInfo.duration

delay(1.seconds)
}
}
val lyrics = content.entry.run { lyricsFlow.collectAsState(lyrics) }.value
val progress = rememberPlayerProgress(playbackManager)

val primaryImageTag = content.item.imageTags?.get(ImageType.PRIMARY)
val (imageItemId, imageTag) = when {
Expand All @@ -96,11 +79,12 @@ fun DreamContentNowPlaying(
}

// Lyrics overlay (on top of background)
if (content.lyrics != null) {
if (lyrics != null) {
val playState by playbackManager.state.playState.collectAsState()
LyricsDtoBox(
lyricDto = content.lyrics,
currentTimestamp = playbackPosition,
duration = playbackDuration,
lyricDto = lyrics,
currentTimestamp = playbackManager.state.positionInfo.active,
duration = playbackManager.state.positionInfo.duration,
paused = playState != PlayState.PLAYING,
fontSize = 22.sp,
color = Color.White,
Expand Down Expand Up @@ -177,7 +161,7 @@ fun DreamContentNowPlaying(
// Foreground
drawRect(
Color.White,
size = size.copy(width = size.width * (playbackPosition.inWholeMilliseconds.toFloat() / playbackDuration.inWholeMilliseconds.toFloat()))
size = size.copy(width = progress * size.width)
)
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package org.jellyfin.androidtv.integration.dream.model

import android.graphics.Bitmap
import org.jellyfin.playback.core.queue.QueueEntry
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.LyricDto

sealed interface DreamContent {
data object Logo : DreamContent
data class LibraryShowcase(val item: BaseItemDto, val backdrop: Bitmap, val logo: Bitmap?) : DreamContent
data class NowPlaying(val item: BaseItemDto, val lyrics: LyricDto?) : DreamContent
data class NowPlaying(val entry: QueueEntry, val item: BaseItemDto) : DreamContent
}
17 changes: 12 additions & 5 deletions app/src/main/java/org/jellyfin/androidtv/ui/NowPlayingView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
Expand All @@ -36,21 +38,26 @@ import androidx.tv.material3.Surface
import androidx.tv.material3.Text
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.ui.composable.AsyncImage
import org.jellyfin.androidtv.ui.composable.rememberMediaItem
import org.jellyfin.androidtv.ui.composable.rememberPlayerProgress
import org.jellyfin.androidtv.ui.composable.rememberQueueEntry
import org.jellyfin.androidtv.ui.navigation.Destinations
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
import org.jellyfin.androidtv.ui.playback.MediaManager
import org.jellyfin.androidtv.util.ImageHelper
import org.jellyfin.playback.core.PlaybackManager
import org.jellyfin.playback.jellyfin.queue.baseItem
import org.jellyfin.playback.jellyfin.queue.baseItemFlow
import org.jellyfin.sdk.model.api.ImageType
import org.koin.compose.koinInject

@Composable
fun NowPlayingComposable() {
val mediaManager = koinInject<MediaManager>()
val playbackManager = koinInject<PlaybackManager>()
val navigationRepository = koinInject<NavigationRepository>()
val imageHelper = koinInject<ImageHelper>()

val (item, progress) = rememberMediaItem(mediaManager)
val entry by rememberQueueEntry(playbackManager)
val item = entry?.run { baseItemFlow.collectAsState(baseItem) }?.value
val progress = rememberPlayerProgress(playbackManager)

AnimatedVisibility(
visible = item != null,
Expand Down Expand Up @@ -81,7 +88,7 @@ fun NowPlayingComposable() {
// Background
drawRect(Color.White, alpha = 0.4f)
// Foreground
drawRect(Color.White, size = size.copy(width = size.width * progress))
drawRect(Color.White, size = size.copy(width = progress * size.width))
}
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package org.jellyfin.androidtv.ui.composable

import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
Expand Down Expand Up @@ -164,32 +163,12 @@ fun LyricsBox(
color: Color = LocalTextStyle.current.color,
) = Box(modifier) {
var totalHeight by remember { mutableFloatStateOf(0f) }
val activeLineOffsetAnimation = remember { Animatable(0f) }

LaunchedEffect(paused, duration, totalHeight) {
if (duration == Duration.ZERO) {
activeLineOffsetAnimation.snapTo(0f)
} else {
val progress = (currentTimestamp.inWholeMilliseconds.toFloat() / duration.inWholeMilliseconds.toFloat()).coerceIn(0f, 1f)

activeLineOffsetAnimation.snapTo(-(progress * totalHeight))
}

if (!paused) {
activeLineOffsetAnimation.animateTo(
targetValue = -totalHeight,
animationSpec = tween(
durationMillis = (duration - currentTimestamp).inWholeMilliseconds.toInt(),
easing = LinearEasing,
)
)
}
}
val progress = rememberPlayerProgress(!paused, currentTimestamp, duration)

LyricsBoxContent(
items = lines,
modifier = Modifier.graphicsLayer {
translationY = activeLineOffsetAnimation.value
translationY = -(progress * totalHeight)
},
onMeasured = { measurements -> totalHeight = measurements.size.height }
) { line, index ->
Expand Down

This file was deleted.

67 changes: 67 additions & 0 deletions app/src/main/java/org/jellyfin/androidtv/ui/composable/playback.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.jellyfin.androidtv.ui.composable

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import org.jellyfin.playback.core.PlaybackManager
import org.jellyfin.playback.core.model.PlayState
import org.jellyfin.playback.core.queue.queue
import org.koin.compose.koinInject
import kotlin.math.roundToInt
import kotlin.time.Duration

@Composable
fun rememberQueueEntry(
playbackManager: PlaybackManager = koinInject(),
) = remember(playbackManager) {
playbackManager.queue.entry
}.collectAsState()

@Composable
fun rememberPlayerProgress(
playbackManager: PlaybackManager = koinInject(),
): Float {
val playState by playbackManager.state.playState.collectAsState()
val active = playbackManager.state.positionInfo.active
val duration = playbackManager.state.positionInfo.duration

return rememberPlayerProgress(
playing = playState == PlayState.PLAYING,
active = active,
duration = duration,
)
}

@Composable
fun rememberPlayerProgress(
playing: Boolean,
active: Duration,
duration: Duration,
): Float {
val animatable = remember { Animatable(0f) }

LaunchedEffect(playing, duration) {
val activeMs = active.inWholeMilliseconds.toFloat()
val durationMs = duration.inWholeMilliseconds.toFloat()

if (active == Duration.ZERO) animatable.snapTo(0f)
else animatable.snapTo((activeMs / durationMs).coerceIn(0f, 1f))

if (playing) {
animatable.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = (durationMs - activeMs).roundToInt(),
easing = LinearEasing,
)
)
}
}

return animatable.value
}
Loading

0 comments on commit 67d470f

Please sign in to comment.