diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/CrossFadeFactory.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/CrossFadeFactory.kt new file mode 100644 index 00000000..7afbcd7d --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/CrossFadeFactory.kt @@ -0,0 +1,26 @@ +package com.github.damontecres.wholphin.ui + +import coil3.request.ImageResult +import coil3.request.SuccessResult +import coil3.transition.CrossfadeTransition +import coil3.transition.Transition +import coil3.transition.TransitionTarget +import kotlin.time.Duration +import kotlin.time.DurationUnit + +/** + * Similar to [coil3.transition.CrossfadeTransition.Factory] but always cross fades even if loading from memory + */ +class CrossFadeFactory( + val duration: Duration, +) : Transition.Factory { + override fun create( + target: TransitionTarget, + result: ImageResult, + ): Transition = + if (result is SuccessResult) { + CrossfadeTransition(target, result, duration.toInt(DurationUnit.MILLISECONDS), false) + } else { + Transition.Factory.NONE.create(target, result) + } +} 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 e5e30a3c..66377a3c 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 @@ -17,6 +17,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -40,6 +41,7 @@ fun GridCard( modifier: Modifier = Modifier, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, imageAspectRatio: Float = AspectRatios.TALL, + imageContentScale: ContentScale = ContentScale.Fit, ) { val dto = item?.data val focused by interactionSource.collectIsFocusedAsState() @@ -85,6 +87,7 @@ fun GridCard( unwatchedCount = dto?.userData?.unplayedItemCount ?: -1, watchedPercent = dto?.userData?.playedPercentage, useFallbackText = false, + contentScale = imageContentScale, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemCardImage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemCardImage.kt index 1aa8fb58..86296197 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemCardImage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemCardImage.kt @@ -52,6 +52,7 @@ fun ItemCardImage( watchedPercent: Double?, modifier: Modifier = Modifier, useFallbackText: Boolean = true, + contentScale: ContentScale = ContentScale.Fit, ) { var imageError by remember { mutableStateOf(false) } Box(modifier = modifier) { @@ -59,7 +60,7 @@ fun ItemCardImage( AsyncImage( model = imageUrl, contentDescription = name, - contentScale = ContentScale.Fit, + contentScale = contentScale, alignment = Alignment.Center, onError = { logCoilError(imageUrl, it.result) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index 17ddd578..0373a188 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -20,9 +20,11 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.MutableLiveData @@ -41,6 +43,7 @@ import com.github.damontecres.wholphin.services.FavoriteWatchManager import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.SlimItemFields +import com.github.damontecres.wholphin.ui.cards.GridCard import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.detail.CardGrid @@ -310,6 +313,7 @@ fun CollectionFolderGrid( initialSortAndDirection: SortAndDirection? = null, showTitle: Boolean = true, positionCallback: ((columns: Int, position: Int) -> Unit)? = null, + params: CollectionFolderGridParameters = CollectionFolderGridParameters(), ) = CollectionFolderGrid( preferences, itemId.toServerString(), @@ -321,6 +325,7 @@ fun CollectionFolderGrid( initialSortAndDirection = initialSortAndDirection, showTitle = showTitle, positionCallback = positionCallback, + params = params, ) @Composable @@ -337,6 +342,7 @@ fun CollectionFolderGrid( initialSortAndDirection: SortAndDirection? = null, showTitle: Boolean = true, positionCallback: ((columns: Int, position: Int) -> Unit)? = null, + params: CollectionFolderGridParameters = CollectionFolderGridParameters(), ) { val context = LocalContext.current OneTimeLaunchedEffect { @@ -376,6 +382,7 @@ fun CollectionFolderGrid( sortOptions = sortOptions, positionCallback = positionCallback, letterPosition = { viewModel.positionOfLetter(it) ?: -1 }, + params = params, ) } } @@ -445,6 +452,7 @@ fun CollectionFolderGridContent( modifier: Modifier = Modifier, showTitle: Boolean = true, positionCallback: ((columns: Int, position: Int) -> Unit)? = null, + params: CollectionFolderGridParameters = CollectionFolderGridParameters(), ) { val title = item?.name ?: item?.data?.collectionType?.name ?: stringResource(R.string.collection) @@ -493,6 +501,9 @@ fun CollectionFolderGridContent( showHeader = position < columns positionCallback?.invoke(columns, position) }, + cardContent = params.cardContent, + columns = params.columns, + spacing = params.spacing, ) } } @@ -501,3 +512,22 @@ data class PositionItem( val position: Int, val item: BaseItem, ) + +data class CollectionFolderGridParameters( + val columns: Int = 6, + val spacing: Dp = 16.dp, + val cardContent: @Composable ( + item: BaseItem?, + onClick: () -> Unit, + onLongClick: () -> Unit, + mod: Modifier, + ) -> Unit = { item, onClick, onLongClick, mod -> + GridCard( + item = item, + onClick = onClick, + onLongClick = onLongClick, + imageContentScale = ContentScale.FillBounds, + modifier = mod, + ) + }, +) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt index 3f6f5a40..b83ee031 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.type +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp @@ -91,6 +92,7 @@ fun CardGrid( item = item, onClick = onClick, onLongClick = onLongClick, + imageContentScale = ContentScale.FillBounds, modifier = mod, ) }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderGeneric.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderGeneric.kt index 5898f9d5..003e5b15 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderGeneric.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderGeneric.kt @@ -7,12 +7,16 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.ui.AspectRatios +import com.github.damontecres.wholphin.ui.cards.GridCard import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid +import com.github.damontecres.wholphin.ui.components.CollectionFolderGridParameters import com.github.damontecres.wholphin.ui.data.VideoSortOptions import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel import java.util.UUID @@ -28,6 +32,21 @@ fun CollectionFolderGeneric( preferencesViewModel: PreferencesViewModel = hiltViewModel(), ) { var showHeader by remember { mutableStateOf(true) } + val params = + CollectionFolderGridParameters( + columns = 4, + spacing = 24.dp, + cardContent = { item, onClick, onLongClick, mod -> + GridCard( + item = item, + onClick = onClick, + onLongClick = onLongClick, + imageContentScale = ContentScale.Crop, + imageAspectRatio = AspectRatios.WIDE, + modifier = mod, + ) + }, + ) CollectionFolderGrid( preferences = preferences, onClickItem = { _, item -> preferencesViewModel.navigationManager.navigateTo(item.destination()) }, @@ -42,5 +61,6 @@ fun CollectionFolderGeneric( positionCallback = { columns, position -> showHeader = position < columns }, + params = params, ) } 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 e5729620..53779cf9 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 @@ -44,11 +44,14 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import coil3.request.transitionFactory 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.CrossFadeFactory import com.github.damontecres.wholphin.ui.cards.BannerCard import com.github.damontecres.wholphin.ui.cards.ItemRow import com.github.damontecres.wholphin.ui.components.CircularProgress @@ -78,6 +81,7 @@ import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.extensions.ticks import java.util.UUID +import kotlin.time.Duration.Companion.milliseconds @Composable fun HomePage( @@ -198,6 +202,7 @@ fun HomePageContent( onFocusPosition: ((RowColumn) -> Unit)? = null, loadingState: LoadingState? = null, ) { + val context = LocalContext.current val scope = rememberCoroutineScope() val firstRow = remember { @@ -239,38 +244,47 @@ fun HomePageContent( LaunchedEffect(position) { listState.animateScrollToItem(position.row) } + var backdropImageUrl by remember { mutableStateOf(null) } + LaunchedEffect(focusedItem) { + backdropImageUrl = null + delay(150) + backdropImageUrl = focusedItem?.backdropImageUrl + } Box(modifier = modifier) { - if (focusedItem?.backdropImageUrl.isNotNullOrBlank()) { - val gradientColor = MaterialTheme.colorScheme.background - AsyncImage( - model = focusedItem?.backdropImageUrl, - contentDescription = null, - contentScale = ContentScale.Fit, - alignment = Alignment.TopEnd, - modifier = - Modifier - .fillMaxHeight(.7f) - .fillMaxWidth(.7f) - .alpha(.75f) - .align(Alignment.TopEnd) - .drawWithContent { - drawContent() - drawRect( - Brush.verticalGradient( - colors = listOf(Color.Transparent, gradientColor), - startY = size.height * .33f, - ), - ) - drawRect( - Brush.horizontalGradient( - colors = listOf(gradientColor, Color.Transparent), - startX = 0f, - endX = size.width * .5f, - ), - ) - }, - ) - } + val gradientColor = MaterialTheme.colorScheme.background + AsyncImage( + model = + ImageRequest + .Builder(context) + .data(backdropImageUrl) + .transitionFactory(CrossFadeFactory(250.milliseconds)) + .build(), + contentDescription = null, + contentScale = ContentScale.Fit, + alignment = Alignment.TopEnd, + modifier = + Modifier + .fillMaxHeight(.7f) + .fillMaxWidth(.7f) + .alpha(.75f) + .align(Alignment.TopEnd) + .drawWithContent { + drawContent() + drawRect( + Brush.verticalGradient( + colors = listOf(Color.Transparent, gradientColor), + startY = size.height * .33f, + ), + ) + drawRect( + Brush.horizontalGradient( + colors = listOf(gradientColor, Color.Transparent), + startX = 0f, + endX = size.width * .5f, + ), + ) + }, + ) Column(modifier = Modifier.fillMaxSize()) { HomePageHeader(