mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +00:00
Refactor TV home media cards smaller
This commit is contained in:
@@ -18,12 +18,11 @@ import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.LibraryKind
|
||||
import hu.bbara.purefin.core.model.MediaKind
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.core.ui.model.EpisodeUiModel
|
||||
import hu.bbara.purefin.core.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.core.ui.model.MovieUiModel
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import hu.bbara.purefin.ui.screen.home.TvHomeScreen
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -68,7 +67,6 @@ class TvHomeContentTest {
|
||||
libraryContent = sampleLibraryContent(),
|
||||
continueWatching = sampleContinueWatching(),
|
||||
nextUp = emptyList(),
|
||||
serverUrl = "",
|
||||
onMovieSelected = {},
|
||||
onSeriesSelected = {},
|
||||
onEpisodeSelected = { _, _, _ -> }
|
||||
@@ -80,7 +78,6 @@ class TvHomeContentTest {
|
||||
|
||||
composeRule.onNodeWithTag(TvHomeInitialFocusTag).assertIsFocused()
|
||||
composeRule.onNodeWithTag(TvHomeHeroTitleTag).assertTextEquals("Blade Runner 2049")
|
||||
composeRule.onNodeWithTag(TvHomeHeroProgressLabelTag).assertTextEquals("42%")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -92,7 +89,6 @@ class TvHomeContentTest {
|
||||
libraryContent = sampleLibraryContent(),
|
||||
continueWatching = sampleContinueWatchingRow(),
|
||||
nextUp = emptyList(),
|
||||
serverUrl = "",
|
||||
onMovieSelected = {},
|
||||
onSeriesSelected = {},
|
||||
onEpisodeSelected = { _, _, _ -> }
|
||||
@@ -126,7 +122,6 @@ class TvHomeContentTest {
|
||||
libraryContent = sampleLibraryContent(),
|
||||
continueWatching = sampleContinueWatching(),
|
||||
nextUp = emptyList(),
|
||||
serverUrl = "",
|
||||
onMovieSelected = {},
|
||||
onSeriesSelected = {},
|
||||
onEpisodeSelected = { _, _, _ -> }
|
||||
@@ -156,7 +151,6 @@ class TvHomeContentTest {
|
||||
libraryContent = sampleLibraryContent(),
|
||||
continueWatching = sampleContinueWatching(),
|
||||
nextUp = sampleNextUp(),
|
||||
serverUrl = "",
|
||||
onMovieSelected = {},
|
||||
onSeriesSelected = {},
|
||||
onEpisodeSelected = { _, _, _ -> }
|
||||
@@ -196,7 +190,6 @@ class TvHomeContentTest {
|
||||
libraryContent = sampleLibraryContent(),
|
||||
continueWatching = sampleContinueWatching(),
|
||||
nextUp = emptyList(),
|
||||
serverUrl = "",
|
||||
onMovieSelected = {},
|
||||
onSeriesSelected = {},
|
||||
onEpisodeSelected = { _, _, _ -> }
|
||||
@@ -228,7 +221,6 @@ class TvHomeContentTest {
|
||||
libraryContent = sampleLibraryContent(),
|
||||
continueWatching = sampleContinueWatching(),
|
||||
nextUp = sampleNextUp(),
|
||||
serverUrl = "",
|
||||
onMovieSelected = {},
|
||||
onSeriesSelected = {},
|
||||
onEpisodeSelected = { _, _, _ -> }
|
||||
@@ -266,7 +258,6 @@ class TvHomeContentTest {
|
||||
libraryContent = sampleLibraryContent(),
|
||||
continueWatching = sampleContinueWatching(),
|
||||
nextUp = sampleNextUpRow(),
|
||||
serverUrl = "",
|
||||
onMovieSelected = {},
|
||||
onSeriesSelected = {},
|
||||
onEpisodeSelected = { _, _, _ -> }
|
||||
@@ -295,11 +286,49 @@ class TvHomeContentTest {
|
||||
assertEquals(alignedTopBefore, alignedTopAfter, 2f)
|
||||
}
|
||||
|
||||
private fun sampleContinueWatching(): List<ContinueWatchingItem> {
|
||||
@Test
|
||||
fun tvHomeScreen_usesReducedMediaCardWidths() {
|
||||
composeRule.setContent {
|
||||
AppTheme {
|
||||
Box(
|
||||
modifier = Modifier.size(width = 960.dp, height = 540.dp)
|
||||
) {
|
||||
TvHomeScreen(
|
||||
libraries = sampleLibraries(),
|
||||
libraryContent = sampleLibraryContent(),
|
||||
continueWatching = sampleContinueWatching(),
|
||||
nextUp = emptyList(),
|
||||
onMovieSelected = {},
|
||||
onSeriesSelected = {},
|
||||
onEpisodeSelected = { _, _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeRule.waitForIdle()
|
||||
|
||||
val expectedLandscapeWidth = with(composeRule.density) { 223.2.dp.toPx() }
|
||||
assertEquals(expectedLandscapeWidth, nodeWidth(TvHomeInitialFocusTag), 2f)
|
||||
|
||||
composeRule.onRoot().performKeyInput {
|
||||
pressKey(Key.DirectionDown)
|
||||
}
|
||||
composeRule.waitForIdle()
|
||||
|
||||
val expectedPosterWidth = with(composeRule.density) { 122.4.dp.toPx() }
|
||||
composeRule.onNodeWithTag(tvHomeLibraryFirstItemTag(sampleLibraryId())).assertIsDisplayed()
|
||||
assertEquals(
|
||||
expectedPosterWidth,
|
||||
nodeWidth(tvHomeLibraryFirstItemTag(sampleLibraryId())),
|
||||
2f
|
||||
)
|
||||
}
|
||||
|
||||
private fun sampleContinueWatching(): List<MediaUiModel> {
|
||||
return listOf(
|
||||
ContinueWatchingItem(
|
||||
type = MediaKind.MOVIE,
|
||||
movie = sampleMovie(
|
||||
MovieUiModel(
|
||||
sampleMovie(
|
||||
id = "11111111-1111-1111-1111-111111111111",
|
||||
title = "Blade Runner 2049",
|
||||
progress = 42.0
|
||||
@@ -308,19 +337,17 @@ class TvHomeContentTest {
|
||||
)
|
||||
}
|
||||
|
||||
private fun sampleContinueWatchingRow(): List<ContinueWatchingItem> {
|
||||
private fun sampleContinueWatchingRow(): List<MediaUiModel> {
|
||||
return listOf(
|
||||
ContinueWatchingItem(
|
||||
type = MediaKind.MOVIE,
|
||||
movie = sampleMovie(
|
||||
MovieUiModel(
|
||||
sampleMovie(
|
||||
id = "11111111-1111-1111-1111-111111111111",
|
||||
title = "Blade Runner 2049",
|
||||
progress = 42.0
|
||||
)
|
||||
),
|
||||
ContinueWatchingItem(
|
||||
type = MediaKind.MOVIE,
|
||||
movie = sampleMovie(
|
||||
MovieUiModel(
|
||||
sampleMovie(
|
||||
id = "55555555-5555-5555-5555-555555555555",
|
||||
title = "Mad Max: Fury Road",
|
||||
progress = 8.0
|
||||
@@ -329,10 +356,10 @@ class TvHomeContentTest {
|
||||
)
|
||||
}
|
||||
|
||||
private fun sampleNextUp(): List<NextUpItem> {
|
||||
private fun sampleNextUp(): List<MediaUiModel> {
|
||||
return listOf(
|
||||
NextUpItem(
|
||||
episode = sampleEpisode(
|
||||
EpisodeUiModel(
|
||||
sampleEpisode(
|
||||
id = "66666666-6666-6666-6666-666666666666",
|
||||
title = "The Train Job"
|
||||
)
|
||||
@@ -340,16 +367,16 @@ class TvHomeContentTest {
|
||||
)
|
||||
}
|
||||
|
||||
private fun sampleNextUpRow(): List<NextUpItem> {
|
||||
private fun sampleNextUpRow(): List<MediaUiModel> {
|
||||
return listOf(
|
||||
NextUpItem(
|
||||
episode = sampleEpisode(
|
||||
EpisodeUiModel(
|
||||
sampleEpisode(
|
||||
id = "66666666-6666-6666-6666-666666666666",
|
||||
title = "The Train Job"
|
||||
)
|
||||
),
|
||||
NextUpItem(
|
||||
episode = sampleEpisode(
|
||||
EpisodeUiModel(
|
||||
sampleEpisode(
|
||||
id = "77777777-7777-7777-7777-777777777777",
|
||||
title = "Safe",
|
||||
releaseDate = "2024-02-09"
|
||||
@@ -370,14 +397,13 @@ class TvHomeContentTest {
|
||||
)
|
||||
}
|
||||
|
||||
private fun sampleLibraryContent(): Map<UUID, List<PosterItem>> {
|
||||
private fun sampleLibraryContent(): Map<UUID, List<MediaUiModel>> {
|
||||
val libraryId = sampleLibraryId()
|
||||
|
||||
return mapOf(
|
||||
libraryId to listOf(
|
||||
PosterItem(
|
||||
type = MediaKind.MOVIE,
|
||||
movie = sampleMovie(
|
||||
MovieUiModel(
|
||||
sampleMovie(
|
||||
id = "44444444-4444-4444-4444-444444444444",
|
||||
title = "Arrival",
|
||||
progress = null,
|
||||
@@ -462,4 +488,11 @@ class TvHomeContentTest {
|
||||
.boundsInRoot
|
||||
.top
|
||||
}
|
||||
|
||||
private fun nodeWidth(tag: String): Float {
|
||||
return composeRule.onNodeWithTag(tag, useUnmergedTree = true)
|
||||
.fetchSemanticsNode()
|
||||
.boundsInRoot
|
||||
.width
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ fun PosterCard(
|
||||
modifier: Modifier = Modifier,
|
||||
imageModifier: Modifier = Modifier,
|
||||
posterWidth: Dp = 144.dp,
|
||||
contentScale: Float = 1f,
|
||||
showSecondaryText: Boolean = false,
|
||||
indicatorSize: Int = 28,
|
||||
indicatorPadding: Dp = 8.dp,
|
||||
@@ -39,6 +40,7 @@ fun PosterCard(
|
||||
modifier = modifier,
|
||||
imageModifier = imageModifier,
|
||||
posterWidth = posterWidth,
|
||||
contentScale = contentScale,
|
||||
showSecondaryText = showSecondaryText,
|
||||
indicatorSize = indicatorSize,
|
||||
indicatorPadding = indicatorPadding,
|
||||
|
||||
@@ -13,7 +13,11 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@@ -43,8 +47,21 @@ fun TvHomeContent(
|
||||
val topOffsetPx = with(LocalDensity.current) { TvHomeFocusedItemTopOffset.toPx() }
|
||||
val hasContinueWatching = continueWatching.isNotEmpty()
|
||||
val hasNextUp = nextUp.isNotEmpty()
|
||||
val hasLibraryItems = libraries.any { libraryContent[it.id].orEmpty().isNotEmpty() }
|
||||
val firstLibraryWithItemsId = libraries.firstOrNull {
|
||||
libraryContent[it.id].orEmpty().isNotEmpty()
|
||||
}?.id
|
||||
val hasVisibleContent = hasContinueWatching || hasNextUp
|
||||
val hasInitialFocusableItem = hasContinueWatching || hasNextUp || hasLibraryItems
|
||||
val initialFocusRequester = remember { FocusRequester() }
|
||||
var initialFocusApplied by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(hasInitialFocusableItem, initialFocusApplied) {
|
||||
if (!initialFocusApplied && hasInitialFocusableItem) {
|
||||
initialFocusRequester.requestFocus()
|
||||
initialFocusApplied = true
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalBringIntoViewSpec provides tvHomeColumnBringIntoViewSpec(topOffsetPx)
|
||||
@@ -94,7 +111,10 @@ fun TvHomeContent(
|
||||
title = library.name,
|
||||
items = libraryContent[library.id].orEmpty(),
|
||||
onFocusedItem = onMediaFocused,
|
||||
firstItemFocusRequester = initialFocusRequester,
|
||||
firstItemFocusRequester = initialFocusRequester.takeIf {
|
||||
!hasContinueWatching && !hasNextUp && library.id == firstLibraryWithItemsId
|
||||
},
|
||||
firstItemTestTag = tvHomeLibraryFirstItemTag(library.id),
|
||||
onMovieSelected = onMovieSelected,
|
||||
onSeriesSelected = onSeriesSelected,
|
||||
onEpisodeSelected = onEpisodeSelected
|
||||
|
||||
@@ -50,12 +50,25 @@ import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||
import hu.bbara.purefin.ui.common.card.PosterCard
|
||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||
import java.util.UUID
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val TvHomeSectionsThumbShape = RoundedCornerShape(20.dp)
|
||||
private const val TvHomeMediaCardScale = 0.9f
|
||||
private val TvHomeSectionsThumbShape = RoundedCornerShape((20f * TvHomeMediaCardScale).dp)
|
||||
private val TvHomeSectionsHorizontalPadding = 32.dp
|
||||
private val TvHomeSectionsRowSpacing = 18.dp
|
||||
private val TvHomeLandscapeCardWidth = 248.dp
|
||||
private val TvHomePosterCardWidth = 136.dp
|
||||
private val TvHomeLandscapeCardWidth = (248f * TvHomeMediaCardScale).dp
|
||||
private val TvHomePosterCardWidth = (136f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeFocusedBorderWidth = (2f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeUnfocusedBorderWidth = (1f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeProgressBarHeight = (6f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeProgressBarPadding = (8f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeMetadataSpacing = (4f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeMetadataTopPadding = (12f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeMetadataHorizontalPadding = (4f * TvHomeMediaCardScale).dp
|
||||
private val TvHomeLandscapeTitleFontSize = (15f * TvHomeMediaCardScale).sp
|
||||
private val TvHomeLandscapeSupportingFontSize = (11f * TvHomeMediaCardScale).sp
|
||||
private val TvHomePosterIndicatorSize = (24f * TvHomeMediaCardScale).roundToInt()
|
||||
private val TvHomePosterIndicatorPadding = (6f * TvHomeMediaCardScale).dp
|
||||
internal const val TvHomeSectionRowTagPrefix = "tv-home-section-row-"
|
||||
internal const val TvHomeContinueWatchingRowTag = "${TvHomeSectionRowTagPrefix}continue-watching"
|
||||
internal const val TvHomeNextUpRowTag = "${TvHomeSectionRowTagPrefix}next-up"
|
||||
@@ -63,6 +76,9 @@ internal const val TvHomeNextUpRowTag = "${TvHomeSectionRowTagPrefix}next-up"
|
||||
internal fun tvHomeLibraryRowTag(libraryId: UUID): String =
|
||||
"${TvHomeSectionRowTagPrefix}library-$libraryId"
|
||||
|
||||
internal fun tvHomeLibraryFirstItemTag(libraryId: UUID): String =
|
||||
"${TvHomeSectionRowTagPrefix}library-item-$libraryId"
|
||||
|
||||
@Composable
|
||||
fun TvContinueWatchingSection(
|
||||
items: List<MediaUiModel>,
|
||||
@@ -190,9 +206,10 @@ fun TvLibraryPosterSection(
|
||||
PosterCard(
|
||||
item = item,
|
||||
posterWidth = TvHomePosterCardWidth,
|
||||
contentScale = TvHomeMediaCardScale,
|
||||
showSecondaryText = true,
|
||||
indicatorSize = 24,
|
||||
indicatorPadding = 6.dp,
|
||||
indicatorSize = TvHomePosterIndicatorSize,
|
||||
indicatorPadding = TvHomePosterIndicatorPadding,
|
||||
imageModifier = Modifier
|
||||
.then(
|
||||
if (index == 0 && firstItemFocusRequester != null) {
|
||||
@@ -296,7 +313,7 @@ private fun TvHomeLandscapeCard(
|
||||
.aspectRatio(16f / 9f)
|
||||
.clip(TvHomeSectionsThumbShape)
|
||||
.border(
|
||||
width = if (isFocused) 2.dp else 1.dp,
|
||||
width = if (isFocused) TvHomeLandscapeFocusedBorderWidth else TvHomeLandscapeUnfocusedBorderWidth,
|
||||
color = if (isFocused) scheme.primary else scheme.outlineVariant.copy(alpha = 0.6f),
|
||||
shape = TvHomeSectionsThumbShape
|
||||
)
|
||||
@@ -335,21 +352,28 @@ private fun TvHomeLandscapeCard(
|
||||
foregroundColor = scheme.primary,
|
||||
backgroundColor = scheme.surfaceVariant,
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
barHeight = 6.dp,
|
||||
barHeight = TvHomeLandscapeProgressBarHeight,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||
.padding(
|
||||
horizontal = TvHomeLandscapeProgressBarPadding,
|
||||
vertical = TvHomeLandscapeProgressBarPadding
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.padding(top = 12.dp, start = 4.dp, end = 4.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(TvHomeLandscapeMetadataSpacing),
|
||||
modifier = Modifier.padding(
|
||||
top = TvHomeLandscapeMetadataTopPadding,
|
||||
start = TvHomeLandscapeMetadataHorizontalPadding,
|
||||
end = TvHomeLandscapeMetadataHorizontalPadding
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
color = scheme.onBackground,
|
||||
fontSize = 15.sp,
|
||||
fontSize = TvHomeLandscapeTitleFontSize,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
@@ -358,7 +382,7 @@ private fun TvHomeLandscapeCard(
|
||||
Text(
|
||||
text = supporting,
|
||||
color = scheme.onSurfaceVariant,
|
||||
fontSize = 11.sp,
|
||||
fontSize = TvHomeLandscapeSupportingFontSize,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
|
||||
Reference in New Issue
Block a user