Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite usages of QueueEntry in UI #4110

Merged
merged 1 commit into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading