0.2.97
This commit is contained in:
@@ -16,6 +16,7 @@ import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.withFrameNanos
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.ponzischeme89.memby.R
|
||||
import com.ponzischeme89.memby.BuildConfig
|
||||
@@ -457,14 +458,16 @@ internal fun AppRoot(
|
||||
else -> FirstRunScreen(onGetStarted = { startingFirstRun = true })
|
||||
}
|
||||
if (confirmingExit) {
|
||||
ExitMembyConfirmation(
|
||||
ExitMembyScreen(
|
||||
onStay = { confirmingExit = false },
|
||||
onExit = onCloseSettings,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(20f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ExitMembyConfirmation lives in ui/ExitConfirmation.kt — it is drawn from the design
|
||||
// tokens rather than from raw Material buttons, and is screenshot-tested on its own.
|
||||
// ExitMembyScreen is a small, stateless component and is screenshot-tested on its own.
|
||||
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
package com.ponzischeme89.memby.ui
|
||||
|
||||
import com.ponzischeme89.memby.ui.theme.MembyIcon
|
||||
import com.ponzischeme89.memby.ui.theme.mark
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
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.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.tv.material3.Icon
|
||||
import androidx.tv.material3.Text
|
||||
import com.ponzischeme89.memby.ui.theme.MembyAccentBright
|
||||
import com.ponzischeme89.memby.ui.theme.MembyAccentMuted
|
||||
import com.ponzischeme89.memby.ui.theme.MembyHairline
|
||||
import com.ponzischeme89.memby.ui.theme.MembyMutedText
|
||||
import com.ponzischeme89.memby.ui.theme.MembyOnSurface
|
||||
import com.ponzischeme89.memby.ui.theme.MembyPanelCorner
|
||||
import com.ponzischeme89.memby.ui.theme.MembyQuietText
|
||||
import com.ponzischeme89.memby.ui.theme.MembySplashTint
|
||||
import com.ponzischeme89.memby.ui.theme.MembySurfaceRaised
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/** The two answers the panel offers, named so a capture can say which one is under focus. */
|
||||
internal enum class ExitChoice { STAY, CLOSE }
|
||||
|
||||
/**
|
||||
* "Close Memby?" — the last thing between a press of Back on the launcher and the TV's own
|
||||
* home screen. Only drawn when the viewer has asked for it (Settings → `confirmExitMemby`).
|
||||
*
|
||||
* It is deliberately the only dialog in the app drawn from the design tokens rather than
|
||||
* from raw `androidx.tv.material3.Button`s. Those buttons carry Material's own colour
|
||||
* scheme, which nothing else on the launcher uses, so the one panel that appears *over*
|
||||
* Memby was the one thing on screen that did not look like it — and the destructive choice
|
||||
* and the safe one were drawn identically, on a screen read from three metres away by
|
||||
* somebody who pressed a key by accident.
|
||||
*
|
||||
* Three things are worth keeping:
|
||||
*
|
||||
* - **The two actions do not look alike.** Stay is the accent fill and takes focus first;
|
||||
* closing is a quiet outline. A remote has no pointer, so the loud shape is the whole of
|
||||
* what says which one the viewer probably wants.
|
||||
* - **Nothing here recomposes while it appears.** The entrance is one `animateFloatAsState`
|
||||
* read inside `graphicsLayer`/`drawBehind` lambdas — the draw and layer phases — never in
|
||||
* a composable body. This app ships to weak TV boxes and a dialog that recomposed the
|
||||
* panel per frame would stutter on the way in.
|
||||
* - **Back means stay.** It is the key that raised this panel, and pressing it again must
|
||||
* not be the thing that closes the app.
|
||||
*
|
||||
* The last two parameters exist only so the panel can be captured: `ExitConfirmationScreenshotTest`
|
||||
* needs the settled frame rather than whatever the animation clock happened to be at, and
|
||||
* Robolectric's window never takes focus, so the focus ring — the whole of what says which
|
||||
* action a press would take — cannot be photographed without being asked for.
|
||||
*/
|
||||
@Composable
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
internal fun ExitMembyConfirmation(
|
||||
onStay: () -> Unit,
|
||||
onExit: () -> Unit,
|
||||
animateIn: Boolean = true,
|
||||
focusedForCapture: ExitChoice? = null,
|
||||
) {
|
||||
val stayFocus = remember { FocusRequester() }
|
||||
val exitFocus = remember { FocusRequester() }
|
||||
var appeared by remember { mutableStateOf(!animateIn) }
|
||||
val appear by animateFloatAsState(
|
||||
targetValue = if (appeared) 1f else 0f,
|
||||
animationSpec = tween(durationMillis = 180),
|
||||
label = "exit-dialog-appear",
|
||||
)
|
||||
BackHandler(onBack = onStay)
|
||||
// The delay is the one the panel this replaced used: a FocusRequester attached on the
|
||||
// frame it is requested on is not placed yet and the request is dropped — which on a
|
||||
// dialog leaves a television with nothing focused and no way out of it.
|
||||
LaunchedEffect(Unit) {
|
||||
appeared = true
|
||||
delay(16.milliseconds)
|
||||
runCatching { stayFocus.requestFocus() }
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(20f)
|
||||
// A wash rather than a flat 82% black: the launcher stays faintly readable
|
||||
// underneath, so this reads as a question asked over your library rather than
|
||||
// as a screen the app has navigated to.
|
||||
.graphicsLayer { alpha = appear }
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
listOf(
|
||||
MembySplashTint.copy(alpha = 0.80f),
|
||||
Color.Black.copy(alpha = 0.93f),
|
||||
),
|
||||
),
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val panelShape = RoundedCornerShape(MembyPanelCorner)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(520.dp)
|
||||
// Settles the last few pixels down onto the scrim as it fades in. Read in
|
||||
// the layer phase, so the panel itself is composed once.
|
||||
.graphicsLayer {
|
||||
val t = appear
|
||||
scaleX = 0.96f + 0.04f * t
|
||||
scaleY = 0.96f + 0.04f * t
|
||||
translationY = (1f - t) * 14.dp.toPx()
|
||||
}
|
||||
.shadow(30.dp, panelShape)
|
||||
.clip(panelShape)
|
||||
.background(MembySurfaceRaised)
|
||||
.border(1.dp, MembyHairline, panelShape)
|
||||
.padding(horizontal = 40.dp, vertical = 34.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(58.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MembyAccentMuted)
|
||||
.border(1.dp, MembyAccentBright.copy(alpha = 0.45f), CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
MembyIcon.Power.mark,
|
||||
contentDescription = null,
|
||||
tint = MembyAccentBright,
|
||||
modifier = Modifier.size(27.dp),
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(20.dp))
|
||||
Text(
|
||||
"Close Memby?",
|
||||
color = MembyOnSurface,
|
||||
fontSize = 30.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text(
|
||||
"Your place is saved. You can pick up where you left off next time you " +
|
||||
"open Memby.",
|
||||
color = MembyMutedText,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 23.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.height(26.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
MembyDialogAction(
|
||||
label = "Stay in Memby",
|
||||
primary = true,
|
||||
onClick = onStay,
|
||||
focusedForCapture = focusedForCapture == ExitChoice.STAY,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.focusRequester(stayFocus)
|
||||
.focusProperties {
|
||||
left = FocusRequester.Cancel
|
||||
right = exitFocus
|
||||
up = FocusRequester.Cancel
|
||||
down = FocusRequester.Cancel
|
||||
},
|
||||
)
|
||||
MembyDialogAction(
|
||||
label = "Close Memby",
|
||||
primary = false,
|
||||
onClick = onExit,
|
||||
focusedForCapture = focusedForCapture == ExitChoice.CLOSE,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.focusRequester(exitFocus)
|
||||
.focusProperties {
|
||||
left = stayFocus
|
||||
right = FocusRequester.Cancel
|
||||
up = FocusRequester.Cancel
|
||||
down = FocusRequester.Cancel
|
||||
},
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(
|
||||
"Press Back to stay",
|
||||
color = MembyQuietText,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.ponzischeme89.memby.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.ponzischeme89.memby.ui.theme.MembyHairline
|
||||
import com.ponzischeme89.memby.ui.theme.MembyOnSurface
|
||||
import com.ponzischeme89.memby.ui.theme.MembyPanelCorner
|
||||
import com.ponzischeme89.memby.ui.theme.MembySurfaceRaised
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/** The two answers the screen offers, named so a capture can say which one is under focus. */
|
||||
internal enum class ExitChoice { STAY, CLOSE }
|
||||
|
||||
/** A restrained confirmation shown before Back closes Memby from the launcher. */
|
||||
@Composable
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
internal fun ExitMembyScreen(
|
||||
onStay: () -> Unit,
|
||||
onExit: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
focusedForCapture: ExitChoice? = null,
|
||||
) {
|
||||
val stayFocus = remember { FocusRequester() }
|
||||
val exitFocus = remember { FocusRequester() }
|
||||
|
||||
BackHandler(onBack = onStay)
|
||||
LaunchedEffect(Unit) {
|
||||
// Wait until the focus target has been placed before requesting focus.
|
||||
delay(16.milliseconds)
|
||||
runCatching { stayFocus.requestFocus() }
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier.background(Color.Black.copy(alpha = 0.78f)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val panelShape = RoundedCornerShape(MembyPanelCorner)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(430.dp)
|
||||
.background(MembySurfaceRaised, panelShape)
|
||||
.border(1.dp, MembyHairline, panelShape)
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
androidx.tv.material3.Text(
|
||||
"Exit Memby?",
|
||||
color = MembyOnSurface,
|
||||
fontSize = 26.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
MembyDialogAction(
|
||||
label = "Stay",
|
||||
primary = true,
|
||||
onClick = onStay,
|
||||
focusedForCapture = focusedForCapture == ExitChoice.STAY,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.focusRequester(stayFocus)
|
||||
.focusProperties {
|
||||
left = FocusRequester.Cancel
|
||||
right = exitFocus
|
||||
up = FocusRequester.Cancel
|
||||
down = FocusRequester.Cancel
|
||||
},
|
||||
)
|
||||
MembyDialogAction(
|
||||
label = "Exit Memby",
|
||||
primary = false,
|
||||
onClick = onExit,
|
||||
focusedForCapture = focusedForCapture == ExitChoice.CLOSE,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.focusRequester(exitFocus)
|
||||
.focusProperties {
|
||||
left = stayFocus
|
||||
right = FocusRequester.Cancel
|
||||
up = FocusRequester.Cancel
|
||||
down = FocusRequester.Cancel
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -830,6 +830,28 @@ class HomeViewModel(private val repository: EmbyRepository) : ViewModel() {
|
||||
*/
|
||||
internal fun focusedItemWithMetadata(item: BaseItem, metadata: BaseItem?): BaseItem =
|
||||
(metadata ?: item).copy(
|
||||
// Detail requests enrich the facts panel, but their Fields list does not ask Emby
|
||||
// for artwork. Keep the row card's image identity when that sparse response arrives
|
||||
// after focus settles. This matters again after the 32-entry metadata cache turns
|
||||
// over: revisiting an early row fetches details again, and replacing the card
|
||||
// wholesale used to drop its Logo tag and switch the hero back to a text heading.
|
||||
// Merge ImageTags individually because a detail response may carry Primary while
|
||||
// still omitting Logo; treating a non-empty map as authoritative loses the latter.
|
||||
imageTags = item.imageTags + metadata?.imageTags.orEmpty(),
|
||||
backdropImageTags = metadata?.backdropImageTags
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: item.backdropImageTags,
|
||||
primaryImageAspectRatio = metadata?.primaryImageAspectRatio
|
||||
?: item.primaryImageAspectRatio,
|
||||
parentBackdropItemId = metadata?.parentBackdropItemId
|
||||
?: item.parentBackdropItemId,
|
||||
parentBackdropImageTags = metadata?.parentBackdropImageTags
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: item.parentBackdropImageTags,
|
||||
parentLogoItemId = metadata?.parentLogoItemId
|
||||
?: item.parentLogoItemId,
|
||||
parentLogoImageTag = metadata?.parentLogoImageTag
|
||||
?: item.parentLogoImageTag,
|
||||
userData = item.userData ?: metadata?.userData,
|
||||
membyAiringToday = item.membyAiringToday || metadata?.membyAiringToday == true,
|
||||
membyRecommendationReason = item.membyRecommendationReason
|
||||
|
||||
@@ -250,7 +250,7 @@ internal fun MembyChoiceChip(
|
||||
}
|
||||
|
||||
/**
|
||||
* One answer in a full-stop dialog: the shape [ExitMembyConfirmation] draws and the shape
|
||||
* One answer in a full-stop dialog: the shape [ExitMembyScreen] draws and the shape
|
||||
* every panel that asks a yes/no question over the app should draw.
|
||||
*
|
||||
* The two answers must never look alike — a remote has no pointer, so the loud shape is the
|
||||
|
||||
@@ -163,7 +163,7 @@ internal fun ProfileChooser(
|
||||
* Draws the screen as though the remote were on this card, so a capture can show the
|
||||
* focused avatar and the revealed remove control at once. Robolectric's window never
|
||||
* takes focus and the focus state is most of what this screen has to say — the flag
|
||||
* `ViewerActionButton` and `ExitMembyConfirmation` carry, for the same reason. The index
|
||||
* `ViewerActionButton` and `ExitMembyScreen` carry, for the same reason. The index
|
||||
* one past the last profile is the Add card.
|
||||
*/
|
||||
focusedCardForCapture: Int? = null,
|
||||
@@ -654,7 +654,7 @@ private fun ProfileRemoveButton(
|
||||
* "Remove Matt from this TV?" — the full stop between one accidental press and a sign-in
|
||||
* somebody has to type back in on a remote.
|
||||
*
|
||||
* Drawn from the design tokens in the shape `ExitMembyConfirmation` set, and for the same
|
||||
* Drawn from the design tokens in the shape `ExitMembyScreen` set, and for the same
|
||||
* reason: the two answers must not look alike. Keeping them is the accent fill and takes
|
||||
* focus first; removing is a quiet outline that turns red only once the remote is on it.
|
||||
* Back cancels — it is the key most likely to be pressed by somebody who did not mean to be
|
||||
|
||||
@@ -195,7 +195,7 @@ internal enum class SettingsPage(
|
||||
// its download URL with it.
|
||||
DEVICES("Devices", "TVs signed in to your account", MembyIcon.Devices),
|
||||
STORAGE("Storage", "Artwork Memby keeps on this TV", MembyIcon.Storage),
|
||||
ABOUT("About", "Version and release notes", MembyIcon.Info),
|
||||
ABOUT("About", "Version/changelog", MembyIcon.Info),
|
||||
}
|
||||
|
||||
// Black, and one lit thing at a time.
|
||||
@@ -1111,7 +1111,7 @@ internal fun SettingsPanelContent(
|
||||
) {
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
Text(
|
||||
stringResource(R.string.app_name),
|
||||
"Version",
|
||||
color = TextPrimary,
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -1131,7 +1131,7 @@ internal fun SettingsPanelContent(
|
||||
}
|
||||
SettingDivider()
|
||||
VersionRow(
|
||||
label = "Memby gateway",
|
||||
label = "Gateway server version",
|
||||
version = gatewayVersionLabel(state.gatewayVersion, state.embyVersion),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ import com.ponzischeme89.memby.ui.theme.mark
|
||||
* [emphasised] marks the answer the screen is *for* — adding the person, saving the name —
|
||||
* so that on a row of two the destructive or the neutral one is never the one wearing the
|
||||
* accent. It is a quiet outline the rest of the time, which is the same distinction
|
||||
* `ExitConfirmation` draws between staying and closing.
|
||||
* `ExitMembyScreen` draws between staying and closing.
|
||||
*/
|
||||
@Composable
|
||||
internal fun ViewerActionButton(
|
||||
@@ -59,7 +59,7 @@ internal fun ViewerActionButton(
|
||||
/**
|
||||
* Draws the button as though the remote were on it. Robolectric's window never takes
|
||||
* focus and the ring is the whole of what says which answer a press would take, so a
|
||||
* capture of a two-answer row would otherwise prove nothing — the flag `ExitConfirmation`
|
||||
* capture of a two-answer row would otherwise prove nothing — the flag `ExitMembyScreen`
|
||||
* carries, for the same reason.
|
||||
*/
|
||||
focusedForCapture: Boolean = false,
|
||||
|
||||
@@ -262,7 +262,7 @@ private fun ViewerManageRow(
|
||||
* The full-stop question, because removing somebody deletes everything Memby kept for them
|
||||
* and there is nothing to undo it with.
|
||||
*
|
||||
* It follows `ExitConfirmation`'s rules, which are the app's rules for a question asked over
|
||||
* It follows `ExitMembyScreen`'s rules, which are the app's rules for a question asked over
|
||||
* the thing it is about: the two answers **do not look alike**, the safe one takes focus
|
||||
* first, and **Back means keep** — it is the key that raised the panel, and pressing it
|
||||
* again must not be what deletes a person.
|
||||
|
||||
@@ -13,6 +13,42 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class ContinueWatchingResumeTest {
|
||||
@Test
|
||||
fun `detail metadata keeps artwork carried by the row card`() {
|
||||
val card = BaseItem(
|
||||
id = "episode",
|
||||
name = "The Episode",
|
||||
type = "Episode",
|
||||
imageTags = mapOf("Primary" to "card-primary", "Logo" to "logo-tag"),
|
||||
backdropImageTags = listOf("backdrop-tag"),
|
||||
primaryImageAspectRatio = 0.67,
|
||||
parentBackdropItemId = "series",
|
||||
parentBackdropImageTags = listOf("parent-backdrop-tag"),
|
||||
parentLogoItemId = "series",
|
||||
parentLogoImageTag = "parent-logo-tag",
|
||||
)
|
||||
val sparseDetails = BaseItem(
|
||||
id = "episode",
|
||||
name = "The Episode",
|
||||
type = "Episode",
|
||||
overview = "The richer record fetched after focus.",
|
||||
// A detail response can still carry one image type without carrying Logo.
|
||||
imageTags = mapOf("Primary" to "detail-primary"),
|
||||
)
|
||||
|
||||
val focused = focusedItemWithMetadata(card, sparseDetails)
|
||||
|
||||
assertEquals("The richer record fetched after focus.", focused.overview)
|
||||
assertEquals("detail-primary", focused.imageTags["Primary"])
|
||||
assertEquals("logo-tag", focused.imageTags["Logo"])
|
||||
assertEquals(listOf("backdrop-tag"), focused.backdropImageTags)
|
||||
assertEquals(0.67, focused.primaryImageAspectRatio)
|
||||
assertEquals("series", focused.parentBackdropItemId)
|
||||
assertEquals(listOf("parent-backdrop-tag"), focused.parentBackdropImageTags)
|
||||
assertEquals("series", focused.parentLogoItemId)
|
||||
assertEquals("parent-logo-tag", focused.parentLogoImageTag)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `detail metadata keeps the continue watching playhead and resume label`() {
|
||||
val card = BaseItem(
|
||||
|
||||
@@ -35,9 +35,8 @@ import org.robolectric.annotation.GraphicsMode
|
||||
* .\gradlew.bat :app:testDebugUnitTest --tests "*ExitConfirmationScreenshotTest"
|
||||
* ```
|
||||
*
|
||||
* It is drawn over a stand-in launcher rather than over black, because the whole point of
|
||||
* the wash behind it is that the library stays faintly readable underneath — a capture on
|
||||
* a blank background would prove nothing about the one thing worth looking at.
|
||||
* It is drawn over a stand-in launcher so the simple dimmed backdrop can be judged in its
|
||||
* real context.
|
||||
*
|
||||
* The second capture repaints under a different palette. This panel is the only full-stop
|
||||
* dialog in the app and it is now drawn entirely from the tokens, so a theme that cannot
|
||||
@@ -92,12 +91,10 @@ class ExitConfirmationScreenshotTest {
|
||||
compose.setContent {
|
||||
Box(Modifier.fillMaxSize().background(MembySurface)) {
|
||||
StandInLauncher()
|
||||
// Settled rather than mid-entrance: the frame worth looking at is the one
|
||||
// somebody actually reads.
|
||||
ExitMembyConfirmation(
|
||||
ExitMembyScreen(
|
||||
onStay = {},
|
||||
onExit = {},
|
||||
animateIn = false,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
// Robolectric's window never takes focus, so a capture that pressed
|
||||
// its way onto a button would photograph every action unfocused.
|
||||
focusedForCapture = focused,
|
||||
|
||||
Reference in New Issue
Block a user