mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Better video grid, poster images, & home page images (#238)
- General video library grid now shows 16:9 cards - Ensures the posters on Movie/TV show grid pages fill the bounds (TVDB posters are often very slightly wider than 2x3) - Always crossfade home page backdrop image Related to #66
This commit is contained in:
parent
4d34c977c8
commit
98fde1b65e
7 changed files with 128 additions and 32 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String?>(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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue