Skip to content
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
9 changes: 6 additions & 3 deletions app/src/main/kotlin/com/arflix/tv/ui/components/MediaCard.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import com.arflix.tv.data.model.MediaType
import com.arflix.tv.ui.skin.ArvioFocusableSurface
import com.arflix.tv.ui.skin.ArvioSkin
import com.arflix.tv.ui.skin.rememberArvioCardShape
import com.arflix.tv.util.LocalDeviceType
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
import androidx.compose.ui.zIndex
Expand Down Expand Up @@ -102,6 +103,7 @@ fun MediaCard(

var isFocused by remember { mutableStateOf(false) }
val visualFocused = isFocusedOverride || isFocused
val isMobile = LocalDeviceType.current.isTouchDevice()

val aspectRatio = if (isLandscape) 16f / 9f else 2f / 3f
// Landscape cards should prefer wide artwork/backdrops.
Expand Down Expand Up @@ -136,8 +138,7 @@ fun MediaCard(
}
val shape = rememberArvioCardShape(ArvioSkin.radius.md)

val showFocusOutline = visualFocused
val jumpBorderWidth = if (showFocusOutline) 2.5.dp else 0.dp
val jumpBorderWidth = 2.5.dp

val context = LocalContext.current
val density = LocalDensity.current
Expand Down Expand Up @@ -192,6 +193,7 @@ fun MediaCard(
backgroundColor = ArvioSkin.colors.surface,
outlineColor = ArvioSkin.colors.focusOutline,
outlineWidth = jumpBorderWidth,
showRestBorder = true,
focusedScale = focusedScale,
pressedScale = 0.97f,
focusedTransformOriginX = 0.5f,
Expand Down Expand Up @@ -446,7 +448,8 @@ fun MediaCard(

Text(
text = item.title,
style = ArvioSkin.typography.cardTitle,
style = if (isMobile) ArvioSkin.typography.cardTitle.copy(fontSize = 14.sp)
else ArvioSkin.typography.cardTitle,
color = if (visualFocused) {
ArvioSkin.colors.textPrimary
} else {
Expand Down
26 changes: 13 additions & 13 deletions app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2617,12 +2617,12 @@ private fun MobileHomeRowsLayer(
onItemLongClick: ((MediaItem, Boolean) -> Unit)? = null,
onCategoryVisiblePosition: (String, Int) -> Unit = { _, _ -> }
) {
val mobileItemSpacing = 10.dp
val mobileItemSpacing = 14.dp

LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 80.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
// Hero carousel — profile/search row + banner card pager
item(key = "mobile_hero", contentType = "mobile_hero") {
Expand All @@ -2646,7 +2646,7 @@ private fun MobileHomeRowsLayer(
val isCollectionRow = category.id.startsWith("collection_row_")
val rowKey = remember(category.id) { "home:${category.id}" }
val rowUsePosterCards = rememberCatalogueRowLayoutMode(rowKey) == CardLayoutMode.POSTER
val rowMobileItemWidth = if (rowUsePosterCards) 124.dp else 200.dp
val rowMobileItemWidth = if (rowUsePosterCards) 120.dp else 200.dp
val rowState = rememberLazyListState()

LaunchedEffect(rowState, category.id) {
Expand All @@ -2661,12 +2661,12 @@ private fun MobileHomeRowsLayer(
}
}

Column(modifier = Modifier.padding(bottom = 8.dp)) {
Column(modifier = Modifier.padding(bottom = 0.dp)) {
// Section title
Row(
modifier = Modifier.padding(
start = contentStartPadding,
bottom = 8.dp
bottom = 4.dp
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
Expand Down Expand Up @@ -2854,9 +2854,9 @@ private fun TvHomeRowsLayer(
rememberCatalogueRowLayoutMode("home:${category.id}") == CardLayoutMode.POSTER
}
val categoryHeightsPx = remember(renderedCategories, rowLayoutModes, density) {
renderedCategories.mapIndexed { idx, category ->
renderedCategories.mapIndexed { idx, _ ->
val usePoster = rowLayoutModes.getOrNull(idx) ?: false
val heightDp = if (usePoster) 252.dp else 202.dp
val heightDp = if (usePoster) 245.dp else 202.dp
with(density) { heightDp.toPx() }
}
}
Expand Down Expand Up @@ -2972,7 +2972,7 @@ private fun TvHomeRowsLayer(
val rowIsFocused = !focusState.isSidebarFocused && actualRowIndex == focusState.currentRowIndex
val rowKey = remember(category.id) { "home:${category.id}" }
val rowUsePosterCards = rememberCatalogueRowLayoutMode(rowKey) == CardLayoutMode.POSTER
val rowHeight = if (rowUsePosterCards) 252.dp else 202.dp
val rowHeight = if (rowUsePosterCards) 245.dp else 202.dp
Box(
modifier = Modifier
.fillMaxWidth()
Expand Down Expand Up @@ -3218,7 +3218,7 @@ private fun ContentRow(
usePosterCards
}
val cardAspectRatio = if (effectivePosterMode) 2f / 3f else 16f / 9f
val itemWidth = if (effectivePosterMode) 119.dp else 210.dp
val itemWidth = if (effectivePosterMode) 105.dp else 210.dp
val itemSpacing = 14.dp
val totalItems = category.items.size
val maxFirstIndex = remember(totalItems) {
Expand Down Expand Up @@ -3323,7 +3323,7 @@ private fun ContentRow(
) {
// Section title - clean white text, aligned with cards
Row(
modifier = Modifier.padding(start = startPadding, bottom = 12.dp),
modifier = Modifier.padding(start = startPadding, bottom = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Expand Down Expand Up @@ -3363,8 +3363,8 @@ private fun ContentRow(
contentPadding = PaddingValues(
start = startPadding,
end = railEndPadding,
top = 14.dp,
bottom = 14.dp
top = 8.dp,
bottom = 8.dp
),
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
userScrollEnabled = false
Expand Down Expand Up @@ -3464,7 +3464,7 @@ private fun ContentRow(
if (railFocusOverlayActive) {
ArvioFocusableSurface(
modifier = Modifier
.padding(start = startPadding, top = 14.dp)
.padding(start = startPadding, top = 8.dp)
.width(itemWidth)
.aspectRatio(cardAspectRatio)
.zIndex(4f),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -913,16 +913,16 @@ private fun RowsLayer(
val itemWidth = if (isTouchDevice) {
if (rowUsePosterCards) 110.dp else 170.dp
} else {
if (rowUsePosterCards) 134.dp else 260.dp
if (rowUsePosterCards) 105.dp else 210.dp
}
val baseRowHeight = if (isTouchDevice) {
if (rowUsePosterCards) 260.dp else 190.dp
} else if (rowUsePosterCards) {
// Poster cards (2:3) need extra vertical room for title + date below the image
if (screenHeight <= 640) 314.dp else 352.dp
if (screenHeight <= 640) 271.dp else 309.dp
} else {
// Landscape cards still render title + subtitle below artwork.
if (screenHeight <= 640) 238.dp else 302.dp
if (screenHeight <= 640) 210.dp else 274.dp
}
val rowHeight = baseRowHeight + focusBleedPadding
// Fade non-current rows
Expand All @@ -934,7 +934,7 @@ private fun RowsLayer(
Box(modifier = Modifier.fillMaxWidth().height(rowHeight).graphicsLayer { alpha = rowAlpha }) {
Column {
Row(
modifier = Modifier.padding(start = focusBleedPadding, bottom = 8.dp, top = 4.dp),
modifier = Modifier.padding(start = focusBleedPadding, bottom = 4.dp, top = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Expand Down Expand Up @@ -998,7 +998,7 @@ private fun RowsLayer(
contentPadding = PaddingValues(
start = focusBleedPadding,
end = itemWidth + 56.dp,
top = focusBleedPadding,
top = 8.dp,
bottom = focusBleedPadding + 12.dp
),
horizontalArrangement = Arrangement.spacedBy(18.dp)
Expand Down Expand Up @@ -1039,7 +1039,7 @@ private fun RowsLayer(
@Composable
private fun ContentGrid(items: List<MediaItem>, usePosterCards: Boolean, isLoading: Boolean, isTouchDevice: Boolean, onItemClick: (MediaItem) -> Unit, onLoadMore: () -> Unit) {
val screenHeight = LocalConfiguration.current.screenHeightDp
val itemWidth = if (usePosterCards) 134.dp else 260.dp
val itemWidth = if (usePosterCards) 105.dp else 210.dp
val gridState = rememberLazyGridState()
LaunchedEffect(gridState.firstVisibleItemIndex, items.size) { val lv = gridState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0; if (items.isNotEmpty() && lv >= items.size - 8) onLoadMore() }

Expand Down
23 changes: 18 additions & 5 deletions app/src/main/kotlin/com/arflix/tv/ui/skin/ArvioFocus.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.drawscope.Stroke
Expand All @@ -49,6 +50,7 @@ fun Modifier.arvioFocusable(
useGradientBorder: Boolean = false, // Arctic Fuse 2: SOLID border, not gradient
gradientStartColor: Color = Color(0xFFFF00FF), // Magenta (unused when solid)
gradientEndColor: Color = Color(0xFF00D4FF), // Cyan (unused when solid)
showRestBorder: Boolean = false,
animateFocus: Boolean = true,
onClick: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null,
Expand Down Expand Up @@ -92,6 +94,13 @@ fun Modifier.arvioFocusable(
}
val highlightAlpha = if (visualFocused) 1f else animatedHighlightAlpha

// Subtle luminous edge always visible on cards that opt in (glass morphism).
val restBorderAlpha by animateFloatAsState(
targetValue = if (showRestBorder && !visualFocused) 0.4f else 0f,
animationSpec = tween(durationMillis = 150, easing = tokens.easing),
label = "arvio_rest_border",
)

val originX = if (visualFocused) focusedTransformOriginX.coerceIn(0f, 1f) else 0.5f
val focusTransformOrigin = TransformOrigin(originX, 0.5f)

Expand Down Expand Up @@ -146,13 +155,14 @@ fun Modifier.arvioFocusable(
Modifier
}

val borderModifier = if (highlightAlpha > 0.01f) {
val borderModifier = if (highlightAlpha > 0.01f || restBorderAlpha > 0.01f) {
Modifier.drawWithCache {
val outline = shape.createOutline(size, layoutDirection, this)
val borderWidth = outlineWidth.toPx()
val ringColor = resolvedOutlineColor.copy(alpha = highlightAlpha)
val borderWidth = if (highlightAlpha > 0f) outlineWidth.toPx() else 0.5.dp.toPx()
val ringAlpha = if (highlightAlpha > 0f) highlightAlpha else restBorderAlpha * 0.5f
val ringColor = resolvedOutlineColor.copy(alpha = ringAlpha)
val glowStrokeWidth = glowWidth.toPx()
val drawGlow = glowStrokeWidth > 0.01f && glowAlpha > 0.01f
val drawGlow = highlightAlpha > 0.3f && glowStrokeWidth > 0.01f && glowAlpha > 0.01f
val glowColor = resolvedOutlineColor.copy(alpha = highlightAlpha * glowAlpha)

onDrawWithContent {
Expand Down Expand Up @@ -181,7 +191,8 @@ fun Modifier.arvioFocusable(
}
is Outline.Generic -> {
if (drawGlow) {
drawPath(path = outline.path, color = glowColor, style = Stroke(width = borderWidth + glowStrokeWidth))
val path = Path().apply { addPath(outline.path) }
drawPath(path = path, color = glowColor, style = Stroke(width = borderWidth + glowStrokeWidth))
}
drawPath(path = outline.path, color = ringColor, style = Stroke(width = borderWidth))
}
Expand Down Expand Up @@ -215,6 +226,7 @@ fun ArvioFocusableSurface(
useGradientBorder: Boolean = false, // Arctic Fuse 2: SOLID border, not gradient
gradientStartColor: Color = ArvioSkin.colors.focusGradientStart,
gradientEndColor: Color = ArvioSkin.colors.focusGradientEnd,
showRestBorder: Boolean = false,
animateFocus: Boolean = true,
enabled: Boolean = true,
enableSystemFocus: Boolean = true,
Expand Down Expand Up @@ -246,6 +258,7 @@ fun ArvioFocusableSurface(
useGradientBorder = useGradientBorder,
gradientStartColor = gradientStartColor,
gradientEndColor = gradientEndColor,
showRestBorder = showRestBorder,
animateFocus = animateFocus,
onClick = onClick,
onLongClick = onLongClick,
Expand Down
Loading