diff --git a/CLAUDE.md b/CLAUDE.md index 39804d8..a8f8763 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2895,21 +2895,18 @@ thumbnails had three copies of the same lift-and-grow at 1.06, 1.06 and 1.05 ove value is read only inside the `graphicsLayer` block, so travelling a grid redraws two cards rather than recomposing every card in it — which is why it is a modifier and not a wrapper. -**"Close Memby?" is the one full-stop dialog**, `ui/ExitConfirmation.kt`, drawn when the +**"Exit Memby?" is the one full-stop dialog**, `ui/ExitMembyScreen.kt`, drawn when the viewer has asked to be asked (`confirmExitMemby`). It is the panel that appears *over* Memby, so it was the one thing on screen that must not look like somebody else's app — and it was two identical raw `androidx.tv.material3.Button`s wearing Material's own colours, which meant the destructive answer and the safe one were the same shape at three metres. Things to preserve: the two actions **do not look alike** (Stay is the accent fill and takes -focus first, closing is a quiet outline), the entrance is read only inside `graphicsLayer` -lambdas so nothing recomposes while the panel arrives, and **Back means stay** — it is the -key that raised the panel and pressing it again must not be what closes the app. The scrim -is a wash rather than a flat black, so the library underneath stays faintly readable and -this reads as a question asked over it. `focusedForCapture` exists because Robolectric's -window never takes focus and the focus ring is the whole of what says which action a press -would take; `ExitConfirmationScreenshotTest` renders both answers under it, plus one -capture under a foreign palette — this is the only dialog drawn entirely from the tokens, -so a theme that could not reach it would show here and nowhere else. +focus first, closing is a quiet outline), and **Back means stay** — it is the key that raised +the panel and pressing it again must not be what closes the app. The treatment stays plain: +a dimmed backdrop, a compact panel, one question and two actions. `focusedForCapture` exists +because Robolectric's window never takes focus and the focus ring is the whole of what says +which action a press would take; `ExitConfirmationScreenshotTest` renders both answers under +it, plus one capture under a foreign palette. **Settings is black, flat, and says one thing once.** `ui/settings/SettingsSheet.kt` had four stacked surfaces to show two switches — the page, the rail, a titled section card, and the diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b198b09..90e0ae8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -64,7 +64,7 @@ val projectNoticeText = // 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. -val defaultVersionName = "0.2.96" +val defaultVersionName = "0.2.97" val membyVersionName: String = (project.findProperty("memby.versionName") as String?) ?.trim() diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/AppRoot.kt b/app/src/main/java/com/ponzischeme89/memby/ui/AppRoot.kt index ed96477..419a7c6 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/AppRoot.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/AppRoot.kt @@ -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. diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/ExitConfirmation.kt b/app/src/main/java/com/ponzischeme89/memby/ui/ExitConfirmation.kt deleted file mode 100644 index 6c85b4f..0000000 --- a/app/src/main/java/com/ponzischeme89/memby/ui/ExitConfirmation.kt +++ /dev/null @@ -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, - ) - } - } -} diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/ExitMembyScreen.kt b/app/src/main/java/com/ponzischeme89/memby/ui/ExitMembyScreen.kt new file mode 100644 index 0000000..5d18755 --- /dev/null +++ b/app/src/main/java/com/ponzischeme89/memby/ui/ExitMembyScreen.kt @@ -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 + }, + ) + } + } + } +} diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/HomeViewModel.kt b/app/src/main/java/com/ponzischeme89/memby/ui/HomeViewModel.kt index ca74122..066c8b0 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/HomeViewModel.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/HomeViewModel.kt @@ -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 diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/MembyButtons.kt b/app/src/main/java/com/ponzischeme89/memby/ui/MembyButtons.kt index 729bbda..f21cfcd 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/MembyButtons.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/MembyButtons.kt @@ -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 diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/profiles/ProfileChooser.kt b/app/src/main/java/com/ponzischeme89/memby/ui/profiles/ProfileChooser.kt index e7db32f..df38cea 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/profiles/ProfileChooser.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/profiles/ProfileChooser.kt @@ -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 diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/settings/SettingsSheet.kt b/app/src/main/java/com/ponzischeme89/memby/ui/settings/SettingsSheet.kt index 3543f7e..462026a 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/settings/SettingsSheet.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/settings/SettingsSheet.kt @@ -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), ) } diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/viewers/ViewerControls.kt b/app/src/main/java/com/ponzischeme89/memby/ui/viewers/ViewerControls.kt index e0f91bc..9a10f40 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/viewers/ViewerControls.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/viewers/ViewerControls.kt @@ -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, diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/viewers/ViewerManageScreen.kt b/app/src/main/java/com/ponzischeme89/memby/ui/viewers/ViewerManageScreen.kt index 3039800..a36dbd8 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/viewers/ViewerManageScreen.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/viewers/ViewerManageScreen.kt @@ -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. diff --git a/app/src/test/java/com/ponzischeme89/memby/ui/ContinueWatchingResumeTest.kt b/app/src/test/java/com/ponzischeme89/memby/ui/ContinueWatchingResumeTest.kt index cebb184..fbec561 100644 --- a/app/src/test/java/com/ponzischeme89/memby/ui/ContinueWatchingResumeTest.kt +++ b/app/src/test/java/com/ponzischeme89/memby/ui/ContinueWatchingResumeTest.kt @@ -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( diff --git a/app/src/test/java/com/ponzischeme89/memby/ui/ExitConfirmationScreenshotTest.kt b/app/src/test/java/com/ponzischeme89/memby/ui/ExitConfirmationScreenshotTest.kt index 0cefbde..caac215 100644 --- a/app/src/test/java/com/ponzischeme89/memby/ui/ExitConfirmationScreenshotTest.kt +++ b/app/src/test/java/com/ponzischeme89/memby/ui/ExitConfirmationScreenshotTest.kt @@ -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, diff --git a/server/internal/api/sonarr_status.go b/server/internal/api/sonarr_status.go index 107387d..94993c8 100644 --- a/server/internal/api/sonarr_status.go +++ b/server/internal/api/sonarr_status.go @@ -132,7 +132,7 @@ func (s *Server) scanSonarrLifecycle(ctx context.Context) (sonarrLifecycleResult UserID: user.ID, Username: user.Username, Title: "Show cancelled", - Body: change.Current.Title + " is now listed as cancelled in Sonarr.", + Body: change.Current.Title + " has been cancelled.", SourceKey: sourceKey, EventAt: &eventAt, Metadata: map[string]any{"series": change.Current.Title, "status": change.Current.Status},