0.2.87
This commit is contained in:
@@ -64,7 +64,7 @@ val projectNoticeText =
|
|||||||
|
|
||||||
// A release workflow can derive the app version from its Git tag without editing the
|
// A release workflow can derive the app version from its Git tag without editing the
|
||||||
// source tree. Local builds keep using the checked-in default.
|
// source tree. Local builds keep using the checked-in default.
|
||||||
val defaultVersionName = "0.2.86"
|
val defaultVersionName = "0.2.87"
|
||||||
val membyVersionName: String =
|
val membyVersionName: String =
|
||||||
(project.findProperty("memby.versionName") as String?)
|
(project.findProperty("memby.versionName") as String?)
|
||||||
?.trim()
|
?.trim()
|
||||||
|
|||||||
@@ -1106,6 +1106,41 @@ fun MediaMetadataPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The quiet surface between the focused title details and the scrolling shelves.
|
||||||
|
*
|
||||||
|
* The backdrop remains visible through the tint, while the fade at the lower edge keeps
|
||||||
|
* this integrated with the rows rather than turning it into an opaque panel.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun MetadataHero(
|
||||||
|
item: BaseItem?,
|
||||||
|
loading: Boolean,
|
||||||
|
sectionLabel: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.clipToBounds()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
0f to MembySurface.copy(alpha = 0.52f),
|
||||||
|
0.72f to MembySurface.copy(alpha = 0.30f),
|
||||||
|
1f to Color.Transparent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
MediaMetadataPanel(
|
||||||
|
item = item,
|
||||||
|
loading = loading,
|
||||||
|
sectionLabel = sectionLabel,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(start = 36.dp, end = 36.dp, top = 24.dp, bottom = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal fun metadataPanelContentWidth(availableWidth: Dp, compact: Boolean): Dp =
|
internal fun metadataPanelContentWidth(availableWidth: Dp, compact: Boolean): Dp =
|
||||||
(availableWidth * if (compact) 0.84f else 0.76f)
|
(availableWidth * if (compact) 0.84f else 0.76f)
|
||||||
.coerceIn(280.dp, 720.dp)
|
.coerceIn(280.dp, 720.dp)
|
||||||
@@ -1804,14 +1839,7 @@ internal fun MediaRow(
|
|||||||
// can dispose that item during a focus transfer and make Compose
|
// can dispose that item during a focus transfer and make Compose
|
||||||
// release the same pin twice. LazyListState already preserves the
|
// release the same pin twice. LazyListState already preserves the
|
||||||
// row position; native TV spatial search safely handles row changes.
|
// row position; native TV spatial search safely handles row changes.
|
||||||
// A horizontal lazy list clips along its scroll axis, but focused cards
|
modifier = Modifier.fillMaxWidth().focusGroup(),
|
||||||
// also draw from a scaled layer in the vertical axis. Its measured height
|
|
||||||
// already contains artwork, title and secondary metadata; clip that
|
|
||||||
// complete unit here so the outgoing card cannot paint into another row.
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clipToBounds()
|
|
||||||
.focusGroup(),
|
|
||||||
) {
|
) {
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
row.items,
|
row.items,
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ internal fun FocusedHomeMetadata(
|
|||||||
// whole state: this panel sits beside the hero and redraws on every focus change as
|
// whole state: this panel sits beside the hero and redraws on every focus change as
|
||||||
// it is.
|
// it is.
|
||||||
val homeContent by homeViewModel.content.collectAsStateWithLifecycle()
|
val homeContent by homeViewModel.content.collectAsStateWithLifecycle()
|
||||||
MediaMetadataPanel(
|
MetadataHero(
|
||||||
item = focusedItem,
|
item = focusedItem,
|
||||||
loading = homeContent.loading.isNotEmpty(),
|
loading = homeContent.loading.isNotEmpty(),
|
||||||
sectionLabel = sectionLabel,
|
sectionLabel = sectionLabel,
|
||||||
|
|||||||
@@ -165,7 +165,10 @@ internal fun homeHeaderHeight(viewportHeight: Dp, showHero: Boolean): Dp =
|
|||||||
if (showHero) {
|
if (showHero) {
|
||||||
(viewportHeight * 0.46f).coerceIn(232.dp, 300.dp)
|
(viewportHeight * 0.46f).coerceIn(232.dp, 300.dp)
|
||||||
} else {
|
} else {
|
||||||
(viewportHeight * 0.42f).coerceIn(220.dp, 300.dp)
|
// The focused metadata surface is also the clean upper boundary for shelves as
|
||||||
|
// they move vertically. About 20dp more coverage at common TV heights keeps a
|
||||||
|
// departing card's last text line from being exposed without costing another row.
|
||||||
|
(viewportHeight * 0.46f).coerceIn(240.dp, 320.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val LazyListStateMapSaver: Saver<MutableMap<String, LazyListState>, Any> = listSaver(
|
private val LazyListStateMapSaver: Saver<MutableMap<String, LazyListState>, Any> = listSaver(
|
||||||
@@ -1377,7 +1380,9 @@ internal fun HomeScreen(
|
|||||||
sectionLabel = selectedDestination.label,
|
sectionLabel = selectedDestination.label,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(metadataHeight)
|
.height(metadataHeight)
|
||||||
.padding(start = 36.dp, end = 36.dp, top = 24.dp, bottom = 10.dp),
|
// LazyColumn is drawn later as a sibling. Keep the hero
|
||||||
|
// above any transient row draw overflow during D-pad moves.
|
||||||
|
.zIndex(1f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ponzischeme89.memby.ui
|
package com.ponzischeme89.memby.ui
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -19,12 +20,14 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.test.junit4.createComposeRule
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
import androidx.compose.ui.test.onAllNodesWithText
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
import androidx.compose.ui.test.onRoot
|
import androidx.compose.ui.test.onRoot
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.takahirom.roborazzi.captureRoboImage
|
import com.github.takahirom.roborazzi.captureRoboImage
|
||||||
@@ -141,6 +144,80 @@ class HomeMovieHeroScreenshotTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `metadata hero separates focused details from the shelves`() {
|
||||||
|
val artwork = requireNotNull(javaClass.getResourceAsStream("/home_hero_preview_art.png"))
|
||||||
|
.use(BitmapFactory::decodeStream)
|
||||||
|
.asImageBitmap()
|
||||||
|
val item = movie(
|
||||||
|
"The Last Horizon",
|
||||||
|
2026,
|
||||||
|
"Beyond the mapped worlds, one explorer finds an ocean that remembers every visitor.",
|
||||||
|
8.7,
|
||||||
|
)
|
||||||
|
|
||||||
|
compose.setContent {
|
||||||
|
PreviewSurface(alignment = Alignment.TopStart) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
Image(
|
||||||
|
bitmap = artwork,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
Column(Modifier.fillMaxSize()) {
|
||||||
|
MetadataHero(
|
||||||
|
item = item,
|
||||||
|
loading = false,
|
||||||
|
sectionLabel = "Movies",
|
||||||
|
modifier = Modifier
|
||||||
|
.height(homeHeaderHeight(540.dp, showHero = false))
|
||||||
|
.zIndex(1f),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Matt's Favourites",
|
||||||
|
color = Color(0xFFF1F3F4),
|
||||||
|
fontSize = 18.sp,
|
||||||
|
modifier = Modifier.padding(start = 36.dp, top = 8.dp, bottom = 10.dp),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 36.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
repeat(5) { index ->
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(16f / 9f)
|
||||||
|
.clip(RoundedCornerShape(7.dp))
|
||||||
|
.background(Color(0xFF27343D)),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Favourite ${index + 1}",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
modifier = Modifier.padding(top = 6.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"2026 • 1h 47m",
|
||||||
|
color = Color(0xFF8F9AA3),
|
||||||
|
fontSize = 11.sp,
|
||||||
|
modifier = Modifier.padding(top = 2.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compose.onRoot().captureRoboImage(
|
||||||
|
"build/screenshots/home-movie-hero/metadata-hero-row-boundary.png",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun capture(name: String, movies: List<HomeHeroPick>) {
|
private fun capture(name: String, movies: List<HomeHeroPick>) {
|
||||||
val artwork = requireNotNull(javaClass.getResourceAsStream("/home_hero_preview_art.png"))
|
val artwork = requireNotNull(javaClass.getResourceAsStream("/home_hero_preview_art.png"))
|
||||||
.use(BitmapFactory::decodeStream)
|
.use(BitmapFactory::decodeStream)
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ class HomeMovieHeroTest {
|
|||||||
assertTrue(540.dp - heroHeight >= 288.dp)
|
assertTrue(540.dp - heroHeight >= 288.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `metadata hero adds a modest scalable row boundary`() {
|
||||||
|
val compactTvHeight = homeHeaderHeight(540.dp, showHero = false)
|
||||||
|
|
||||||
|
assertEquals(248.4f, compactTvHeight.value, 0.01f)
|
||||||
|
assertTrue(540.dp - compactTvHeight >= 288.dp)
|
||||||
|
assertEquals(320f, homeHeaderHeight(720.dp, showHero = false).value, 0.01f)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `hero alternates new releases and popular movies`() {
|
fun `hero alternates new releases and popular movies`() {
|
||||||
val rows = listOf(
|
val rows = listOf(
|
||||||
|
|||||||
Reference in New Issue
Block a user