diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt index 490ad701..ec385b53 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt @@ -5,13 +5,13 @@ import android.content.SharedPreferences import androidx.core.content.edit import androidx.datastore.core.DataStore import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.map import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.toServerString +import com.github.damontecres.wholphin.util.EqualityMutableLiveData import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -42,7 +42,7 @@ class ServerRepository ) { private val sharedPreferences = getServerSharedPreferences(context) - private var _current = MutableLiveData(null) + private var _current = EqualityMutableLiveData(null) val current: LiveData = _current val currentServer: LiveData get() = _current.map { it?.server } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt index c0678736..cc4791e2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt @@ -74,6 +74,12 @@ object Cards { val playedPercentHeight = 6.dp } +object AspectRatios { + const val WIDE = 16f / 9f + const val FOUR_THREE = 4f / 3f + const val TALL = 2f / 3f +} + @Preview( device = "spec:parent=tv_1080p", backgroundColor = 0xFF383535, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt index 06f994f5..28de1f7b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt @@ -34,6 +34,7 @@ import androidx.tv.material3.Text import coil3.compose.AsyncImage import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.ui.AppColors +import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.isNotNullOrBlank @@ -53,7 +54,7 @@ fun BannerCard( favorite: Boolean = false, playPercent: Double = 0.0, cardHeight: Dp = 140.dp * .85f, - aspectRatio: Float = 16f / 9, + aspectRatio: Float = AspectRatios.WIDE, interactionSource: MutableInteractionSource? = null, ) { var imageError by remember { mutableStateOf(false) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterCard.kt index aad0ddd0..4739b80c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterCard.kt @@ -30,9 +30,9 @@ fun ChapterCard( position: Duration, imageUrl: String?, onClick: () -> Unit, + aspectRatio: Float, modifier: Modifier = Modifier, cardHeight: Dp = 140.dp * .85f, - aspectRatio: Float = 16f / 9, onLongClick: (() -> Unit)? = null, interactionSource: MutableInteractionSource? = null, ) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterRow.kt index 1c206990..cf799c54 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterRow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ChapterRow.kt @@ -21,6 +21,7 @@ import com.github.damontecres.wholphin.data.model.Chapter @Composable fun ChapterRow( chapters: List, + aspectRatio: Float, onClick: (Chapter) -> Unit, modifier: Modifier = Modifier, onLongClick: ((Chapter) -> Unit)? = null, @@ -50,6 +51,7 @@ fun ChapterRow( position = item.position, imageUrl = item.imageUrl, onClick = { onClick(item) }, + aspectRatio = aspectRatio, modifier = Modifier, onLongClick = onLongClick?.let { { it(item) } }, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/EpisodeCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/EpisodeCard.kt index 9eafb1ca..6d25aa80 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/EpisodeCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/EpisodeCard.kt @@ -30,6 +30,7 @@ import androidx.tv.material3.CardDefaults import androidx.tv.material3.Text import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.ui.AppColors +import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.enableMarquee import com.github.damontecres.wholphin.ui.seasonEpisode import kotlinx.coroutines.delay @@ -63,7 +64,7 @@ fun EpisodeCard( } else { focusedAfterDelay = false } - val aspectRatio = dto?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f) + val aspectRatio = dto?.primaryImageAspectRatio?.toFloat() ?: AspectRatios.TALL val width = imageHeight * aspectRatio val height = imageWidth * (1f / aspectRatio) Column( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/GridCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/GridCard.kt index 39cb13f2..e5e30a3c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/GridCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/GridCard.kt @@ -25,6 +25,7 @@ import androidx.tv.material3.CardDefaults import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.enableMarquee import kotlinx.coroutines.delay @@ -38,7 +39,7 @@ fun GridCard( onLongClick: () -> Unit, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - imageAspectRatio: Float = 2f / 3f, + imageAspectRatio: Float = AspectRatios.TALL, ) { val dto = item?.data val focused by interactionSource.collectIsFocusedAsState() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemRow.kt index 71df426b..e629fb97 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemRow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemRow.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.util.FocusPair @Composable @@ -112,7 +113,8 @@ fun BannerItemRow( name = title, imageUrl = item?.imageUrl, aspectRatio = - aspectRatioOverride ?: item?.data?.primaryImageAspectRatio?.toFloat() ?: (16f / 9), + aspectRatioOverride ?: item?.data?.primaryImageAspectRatio?.toFloat() + ?: AspectRatios.WIDE, cornerText = item?.data?.indexNumber?.let { "E$it" }, played = item?.data?.userData?.played ?: false, playPercent = item?.data?.userData?.playedPercentage ?: 0.0, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonCard.kt index dcde2ff0..83d72010 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonCard.kt @@ -22,6 +22,7 @@ import androidx.tv.material3.Card import androidx.tv.material3.CardDefaults import androidx.tv.material3.Text import com.github.damontecres.wholphin.data.model.Person +import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.enableMarquee import kotlinx.coroutines.delay @@ -81,7 +82,7 @@ fun PersonCard( modifier = Modifier .fillMaxWidth() - .aspectRatio(2f / 3f), // TODO, + .aspectRatio(AspectRatios.TALL), // TODO, ) } Column( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/SeasonCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/SeasonCard.kt index 22ad8b1b..ed9d01cd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/SeasonCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/SeasonCard.kt @@ -26,6 +26,7 @@ import androidx.tv.material3.Card import androidx.tv.material3.CardDefaults import androidx.tv.material3.Text import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.enableMarquee import kotlinx.coroutines.delay @@ -42,7 +43,7 @@ fun SeasonCard( imageWidth: Dp = Dp.Unspecified, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, showImageOverlay: Boolean = false, - aspectRatio: Float = item?.data?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f), + aspectRatio: Float = item?.data?.primaryImageAspectRatio?.toFloat() ?: AspectRatios.TALL, ) = SeasonCard( title = item?.title, subtitle = item?.subtitle, @@ -82,7 +83,7 @@ fun SeasonCard( imageWidth: Dp = Dp.Unspecified, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, showImageOverlay: Boolean = false, - aspectRatio: Float = (2f / 3f), + aspectRatio: Float = AspectRatios.TALL, ) { val focused by interactionSource.collectIsFocusedAsState() val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp) @@ -102,7 +103,6 @@ fun SeasonCard( } else { focusedAfterDelay = false } -// val aspectRatio = dto?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f) val width = imageHeight * aspectRatio val height = imageWidth * (1f / aspectRatio) Column( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt index 22cd6175..06159073 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt @@ -12,7 +12,7 @@ import com.github.damontecres.wholphin.services.FavoriteWatchManager import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.data.RowColumn -import com.github.damontecres.wholphin.ui.launchIO +import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler @@ -26,6 +26,8 @@ import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -95,8 +97,8 @@ class RecommendedMovieViewModel ), ) - withContext(Dispatchers.Main) { - loading.value = LoadingState.Success + if (resumeItems.isNotEmpty()) { + loading.setValueOnMain(LoadingState.Success) } } catch (ex: Exception) { Timber.e(ex, "Exception fetching movie recommendations") @@ -159,23 +161,31 @@ class RecommendedMovieViewModel R.string.suggestions to suggestedItems, R.string.top_unwatched to unwatchedTopRatedItems, ) - - rows.forEachIndexed { index, (title, pager) -> - viewModelScope.launchIO { - val title = context.getString(title) - val result = - try { - pager.init() - if (pager.isNotEmpty()) { - pager.getBlocking(0) + rows + .mapIndexed { index, (title, pager) -> + viewModelScope.async(Dispatchers.IO + ExceptionHandler()) { + val title = context.getString(title) + val result = + try { + pager.init() + if (pager.isNotEmpty()) { + pager.getBlocking(0) + } + HomeRowLoadingState.Success(title, pager) + } catch (ex: Exception) { + Timber.e(ex, "Error fetching %s", title) + HomeRowLoadingState.Error(title, null, ex) } - HomeRowLoadingState.Success(title, pager) - } catch (ex: Exception) { - Timber.e(ex, "Error fetching %s", title) - HomeRowLoadingState.Error(title, null, ex) + update(index + 1, result) + if (result is HomeRowLoadingState.Success && result.items.isNotEmpty() && + (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) + ) { + loading.setValueOnMain(LoadingState.Success) } - update(index + 1, result) - } + } + }.awaitAll() + if (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) { + loading.setValueOnMain(LoadingState.Success) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt index 7144d010..e1a18517 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt @@ -12,7 +12,7 @@ import com.github.damontecres.wholphin.services.FavoriteWatchManager import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.data.RowColumn -import com.github.damontecres.wholphin.ui.launchIO +import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler @@ -27,6 +27,8 @@ import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -123,8 +125,8 @@ class RecommendedTvShowViewModel ), ) - withContext(Dispatchers.Main) { - loading.value = LoadingState.Success + if (resumeItems.isNotEmpty() || nextUpItems.isNotEmpty()) { + loading.setValueOnMain(LoadingState.Success) } } catch (ex: Exception) { Timber.e(ex, "Exception fetching tv recommendations") @@ -188,19 +190,31 @@ class RecommendedTvShowViewModel R.string.top_unwatched to unwatchedTopRatedItems, ) - rows.forEachIndexed { index, (title, pager) -> - viewModelScope.launchIO { - val title = context.getString(title) - val result = - try { - pager.init() - HomeRowLoadingState.Success(title, pager) - } catch (ex: Exception) { - Timber.e(ex, "Error fetching %s", title) - HomeRowLoadingState.Error(title, null, ex) + rows + .mapIndexed { index, (title, pager) -> + viewModelScope.async(Dispatchers.IO + ExceptionHandler()) { + val title = context.getString(title) + val result = + try { + pager.init() + if (pager.isNotEmpty()) { + pager.getBlocking(0) + } + HomeRowLoadingState.Success(title, pager) + } catch (ex: Exception) { + Timber.e(ex, "Error fetching %s", title) + HomeRowLoadingState.Error(title, null, ex) + } + update(index + 2, result) + if (result is HomeRowLoadingState.Success && result.items.isNotEmpty() && + (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) + ) { + loading.setValueOnMain(LoadingState.Success) } - update(index + 2, result) - } + } + }.awaitAll() + if (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) { + loading.setValueOnMain(LoadingState.Success) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index c7d329a9..346ef4a6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -51,9 +51,11 @@ import com.github.damontecres.wholphin.data.model.LocalTrailer import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.data.model.RemoteTrailer import com.github.damontecres.wholphin.data.model.Trailer +import com.github.damontecres.wholphin.data.model.aspectRatioFloat import com.github.damontecres.wholphin.data.model.chooseSource import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.TrailerService +import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.cards.ChapterRow import com.github.damontecres.wholphin.ui.cards.ItemRow @@ -486,6 +488,7 @@ fun MovieDetailsContent( item { ChapterRow( chapters = chapters, + aspectRatio = movie.data.aspectRatioFloat ?: AspectRatios.WIDE, onClick = { position = CHAPTER_ROW playOnClick.invoke(it.position) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt index 302da4e3..bb98f97c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt @@ -42,8 +42,8 @@ import coil3.compose.AsyncImage import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ChosenStreams import com.github.damontecres.wholphin.data.model.BaseItem -import com.github.damontecres.wholphin.data.model.aspectRatioFloat import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.cards.BannerCard import com.github.damontecres.wholphin.ui.components.ErrorMessage @@ -216,10 +216,8 @@ fun SeriesOverviewContent( ?.data ?.primaryImageAspectRatio ?.toFloat() - ?.coerceAtLeast( - episode.data.aspectRatioFloat ?: (4f / 3f), - ) - ?: (16f / 9), // TODO some episode images don't match the file's aspect ratio + ?.coerceAtLeast(AspectRatios.FOUR_THREE) + ?: (AspectRatios.WIDE), cornerText = cornerText, played = episode?.data?.userData?.played ?: false, playPercent = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index 72b84490..4d7f4566 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -46,6 +46,7 @@ import coil3.compose.AsyncImage import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.cards.BannerCard import com.github.damontecres.wholphin.ui.cards.ItemRow @@ -197,8 +198,20 @@ fun HomePageContent( loadingState: LoadingState? = null, ) { val scope = rememberCoroutineScope() + val firstRow = + remember { + homeRows + .indexOfFirst { + when (it) { + is HomeRowLoadingState.Error -> false + is HomeRowLoadingState.Loading -> true + is HomeRowLoadingState.Pending -> true + is HomeRowLoadingState.Success -> it.items.isNotEmpty() + } + }.coerceAtLeast(0) + } var position by rememberSaveable(stateSaver = RowColumnSaver) { - mutableStateOf(RowColumn(0, 0)) + mutableStateOf(RowColumn(firstRow, 0)) } var focusedItem = position.let { @@ -208,11 +221,19 @@ fun HomePageContent( val listState = rememberLazyListState() val focusRequester = remember { FocusRequester() } val positionFocusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { - positionFocusRequester.tryRequestFocus() - // Hacky, but mostly works - delay(50) - listState.animateScrollToItem(position.row) + var focused by remember { mutableStateOf(false) } + LaunchedEffect(homeRows) { + if (!focused) { + homeRows + .indexOfFirst { it is HomeRowLoadingState.Success && it.items.isNotEmpty() } + .takeIf { it >= 0 } + ?.let { + positionFocusRequester.tryRequestFocus() + delay(50) + listState.animateScrollToItem(position.row) + focused = true + } + } } LaunchedEffect(position) { listState.animateScrollToItem(position.row) @@ -333,11 +354,10 @@ fun HomePageContent( .fillMaxWidth() .animateItem(), cardContent = { index, item, cardModifier, onClick, onLongClick -> - // TODO better aspect ration handling? BannerCard( name = item?.data?.seriesName ?: item?.name, imageUrl = item?.imageUrl, - aspectRatio = (2f / 3f), + aspectRatio = AspectRatios.TALL, cornerText = item?.data?.indexNumber?.let { "E$it" } ?: item?.data?.childCount?.let { if (it > 0) it.toString() else null }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/NextUpEpisode.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/NextUpEpisode.kt index 5596a864..44048a40 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/NextUpEpisode.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/NextUpEpisode.kt @@ -45,6 +45,7 @@ import coil3.compose.AsyncImagePreviewHandler import coil3.compose.LocalAsyncImagePreviewHandler import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.ui.AppColors +import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.tryRequestFocus @@ -62,7 +63,7 @@ fun NextUpEpisode( onClick: () -> Unit, timeLeft: Duration?, modifier: Modifier = Modifier, - aspectRatio: Float = 16f / 9, + aspectRatio: Float = AspectRatios.WIDE, ) { val interactionSource = remember { MutableInteractionSource() } val focusRequester = remember { FocusRequester() } @@ -202,7 +203,7 @@ private fun NextUpEpisodePreview() { "But it just keeps going and going.", imageUrl = "", onClick = {}, - aspectRatio = 4f / 3, + aspectRatio = AspectRatios.FOUR_THREE, timeLeft = 30.seconds, modifier = Modifier diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt index b50c279b..60abc1bc 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt @@ -50,7 +50,9 @@ import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.Chapter import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.Playlist +import com.github.damontecres.wholphin.data.model.aspectRatioFloat import com.github.damontecres.wholphin.ui.AppColors +import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.TimeFormatter import com.github.damontecres.wholphin.ui.cards.ChapterCard import com.github.damontecres.wholphin.ui.cards.SeasonCard @@ -75,6 +77,7 @@ private val subtitleTextSize = 18.sp */ @Composable fun PlaybackOverlay( + item: BaseItem?, title: String?, subtitleStreams: List, chapters: List, @@ -236,6 +239,7 @@ fun PlaybackOverlay( name = chapter.name, position = chapter.position, imageUrl = chapter.imageUrl, + aspectRatio = item?.data?.aspectRatioFloat ?: AspectRatios.WIDE, onClick = { playerControls.seekTo(chapter.position.inWholeMilliseconds) controllerViewState.hideControls() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt index d9d1003e..9d89bf93 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt @@ -69,6 +69,7 @@ import com.github.damontecres.wholphin.data.model.Playlist import com.github.damontecres.wholphin.preferences.PlayerBackend import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.skipBackOnResume +import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.LoadingPage @@ -332,6 +333,7 @@ fun PlaybackPage( .padding(WindowInsets.systemBars.asPaddingValues()) .fillMaxSize() .background(Color.Transparent), + item = currentPlayback?.item, title = title, subtitle = subtitle, subtitleStreams = subtitleStreams, @@ -488,7 +490,9 @@ fun PlaybackPage( ).joinToString(" - "), description = it.data.overview, imageUrl = it.imageUrl, - aspectRatio = it.data.primaryImageAspectRatio?.toFloat() ?: (16f / 9), + aspectRatio = + it.data.primaryImageAspectRatio?.toFloat() + ?: AspectRatios.WIDE, onClick = { viewModel.reportInteraction() viewModel.playNextUp() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index 368746e6..5d069e1e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -75,7 +75,6 @@ import org.jellyfin.sdk.api.client.extensions.trickplayApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.api.client.extensions.videosApi import org.jellyfin.sdk.api.sockets.subscribe -import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.api.MediaSegmentDto @@ -142,7 +141,7 @@ class PlaybackViewModel private lateinit var preferences: UserPreferences private lateinit var deviceProfile: DeviceProfile private lateinit var itemId: UUID - private lateinit var dto: BaseItemDto + private lateinit var item: BaseItem private var activityListener: TrackActivityPlaybackListener? = null val nextUp = MutableLiveData() @@ -204,10 +203,11 @@ class PlaybackViewModel } else { queriedItem } + val item = BaseItem.from(base, api) val played = play( - base, + item, destination.positionMs, destination.itemPlayback, destination.forceTranscoding, @@ -228,25 +228,25 @@ class PlaybackViewModel } private suspend fun play( - base: BaseItemDto, + item: BaseItem, positionMs: Long, itemPlayback: ItemPlayback? = null, forceTranscoding: Boolean = false, ): Boolean = withContext(Dispatchers.IO) { - Timber.i("Playing ${base.id}") + Timber.i("Playing ${item.id}") autoSkippedSegments.clear() - if (base.type !in supportItemKinds) { + if (item.type !in supportItemKinds) { showToast( context, - "Unsupported type '${base.type}', skipping...", + "Unsupported type '${item.type}', skipping...", Toast.LENGTH_SHORT, ) return@withContext false } - val isLiveTv = base.type == BaseItemKind.TV_CHANNEL + val isLiveTv = item.type == BaseItemKind.TV_CHANNEL - dto = base + val base = item.data val title = if (base.type == BaseItemKind.EPISODE) { base.seriesName @@ -358,7 +358,7 @@ class PlaybackViewModel this@PlaybackViewModel.subtitleStreams.value = subtitleStreams changeStreams( - base, + item, itemPlaybackToUse, audioIndex, subtitleIndex, @@ -379,7 +379,7 @@ class PlaybackViewModel @OptIn(UnstableApi::class) private suspend fun changeStreams( - item: BaseItemDto, + item: BaseItem, currentItemPlayback: ItemPlayback = this@PlaybackViewModel.currentItemPlayback.value!!, audioIndex: Int?, subtitleIndex: Int?, @@ -392,7 +392,7 @@ class PlaybackViewModel val playerBackend = preferences.appPreferences.playbackPreferences.playerBackend val currentPlayback = this@PlaybackViewModel.currentPlayback.value - if (currentPlayback != null && currentPlayback.itemId == item.id && currentPlayback.playMethod == PlayMethod.DIRECT_PLAY) { + if (currentPlayback != null && currentPlayback.item.id == item.id && currentPlayback.playMethod == PlayMethod.DIRECT_PLAY) { // If direct playing, can try to switch tracks without playback restarting // Except for external subtitles // TODO there's probably no reason why we can't add external subtitles? @@ -549,9 +549,9 @@ class PlaybackViewModel val playback = CurrentPlayback( - itemId = item.id, - backend = preferences.appPreferences.playbackPreferences.playerBackend, + item = item, tracks = listOf(), + backend = preferences.appPreferences.playbackPreferences.playerBackend, playMethod = transcodeType, playSessionId = response.playSessionId, liveStreamId = source.liveStreamId, @@ -623,7 +623,7 @@ class PlaybackViewModel } } val trickPlayInfo = - item.trickplay + item.data.trickplay ?.get(source.id) ?.values ?.firstOrNull() @@ -637,7 +637,7 @@ class PlaybackViewModel fun changeAudioStream(index: Int) { viewModelScope.launchIO { changeStreams( - dto, + item, currentItemPlayback.value!!, index, currentItemPlayback.value?.subtitleIndex, @@ -650,7 +650,7 @@ class PlaybackViewModel fun changeSubtitleStream(index: Int?): Job = viewModelScope.launchIO { changeStreams( - dto, + item, currentItemPlayback.value!!, currentItemPlayback.value?.audioIndex, index, @@ -660,7 +660,7 @@ class PlaybackViewModel } fun getTrickplayUrl(index: Int): String? { - val itemId = dto.id + val itemId = item.id val mediaSourceId = currentItemPlayback.value?.sourceId val trickPlayInfo = trickplay.value ?: return null return api.trickplayApi.getTrickplayTileImageUrl( @@ -792,7 +792,7 @@ class PlaybackViewModel viewModelScope.launchIO { cancelUpNextEpisode() val item = it.getAndAdvance() - val played = play(item.data, 0) + val played = play(item, 0) if (!played) { playNextUp() } @@ -807,7 +807,7 @@ class PlaybackViewModel viewModelScope.launchIO { cancelUpNextEpisode() val item = it.getPreviousAndReverse() - val played = play(item.data, 0) + val played = play(item, 0) if (!played) { playPrevious() } @@ -825,7 +825,7 @@ class PlaybackViewModel viewModelScope.launchIO { val toPlay = playlist.advanceTo(item.id) if (toPlay != null) { - val played = play(toPlay.data, 0) + val played = play(toPlay, 0) if (!played) { playNextUp() } @@ -859,7 +859,7 @@ class PlaybackViewModel PlayMethod.DIRECT_STREAM, PlayMethod.DIRECT_PLAY -> { Timber.w("Playback error during ${it.playMethod}, falling back to transcoding") changeStreams( - dto, + item, currentItemPlayback.value!!, currentItemPlayback.value?.audioIndex, currentItemPlayback.value?.subtitleIndex, @@ -978,8 +978,12 @@ class PlaybackViewModel while (maxAttempts > 0 && subtitleCount == newCount) { maxAttempts-- delay(1500) - dto = api.userLibraryApi.getItem(itemId = dto.id).content - val mediaSource = chooseSource(dto, it) + item = + BaseItem.from( + api.userLibraryApi.getItem(itemId = item.id).content, + api, + ) + val mediaSource = chooseSource(item.data, it) if (mediaSource == null) { // This shouldn't happen, but just in case showToast( @@ -1036,7 +1040,7 @@ class PlaybackViewModel audioIndex += 1 } changeStreams( - dto, + item, currentItemPlayback.value!!, audioIndex, newStream.index, @@ -1066,9 +1070,9 @@ class PlaybackViewModel } data class CurrentPlayback( - val itemId: UUID, - val backend: PlayerBackend, + val item: BaseItem, val tracks: List, + val backend: PlayerBackend, val playMethod: PlayMethod, val playSessionId: String?, val liveStreamId: String?, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt index c90dbe41..4cb05d43 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt @@ -53,6 +53,7 @@ fun SwitchServerContent( var showAddServer by remember { mutableStateOf(false) } LaunchedEffect(Unit) { + viewModel.init() viewModel.discoverServers() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt index 11202a31..d75dafaa 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt @@ -52,10 +52,6 @@ class SwitchServerViewModel addServerState.value = LoadingState.Pending } - init { - init() - } - fun init() { viewModelScope.launchIO { withContext(Dispatchers.Main) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt index f747a612..a33479cb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt @@ -58,6 +58,9 @@ fun SwitchUserContent( ), ) { val context = LocalContext.current + LaunchedEffect(Unit) { + viewModel.init() + } // val currentServer by viewModel.serverRepository.currentServer.observeAsState() val currentUser by viewModel.serverRepository.currentUser.observeAsState() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt index cd1d6daf..56d7ab98 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt @@ -48,12 +48,6 @@ class SwitchUserViewModel fun create(server: JellyfinServer): SwitchUserViewModel } - init { - viewModelScope.launch(Dispatchers.Main + ExceptionHandler()) { - serverRepository.switchServerOrUser() - } - } - val serverQuickConnect = MutableLiveData(false) val users = MutableLiveData>(listOf()) @@ -78,6 +72,9 @@ class SwitchUserViewModel } fun init() { + viewModelScope.launch(Dispatchers.Main + ExceptionHandler()) { + serverRepository.switchServerOrUser() + } quickConnectJob?.cancel() viewModelScope.launchIO { users.setValueOnMain(listOf())