Preserve check state after refreshing installed identity

This commit is contained in:
justinzeus 2026-11-07 16:55:14 +01:00
parent 19df50c0f1
commit ee3f3ae11e
9 changed files with 31 additions and 9 deletions

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performKeyInput
import androidx.compose.ui.test.pressKey
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.semantics.SemanticsProperties
import com.github.takahirom.roborazzi.captureRoboImage
import java.io.File
@ -141,6 +142,15 @@ class UpdatesCaptureTest {
assertEquals(1, (shell.contentFocus as UpdateFocus).actionIndex)
}
@Test fun refreshingInstalledIdentityCannotMaskAnOngoingCheck() {
launch(UpdateUiState.UpToDate)
rule.runOnIdle { controller.showInstalled(InstalledVersion("1.1.1", 100001)) }
rule.mainClock.advanceTimeBy(320)
rule.runOnIdle { controller.show(UpdateUiState.Checking) }
rule.mainClock.advanceTimeBy(320)
rule.onNodeWithTag("update-progress").assertIsDisplayed()
}
@Test fun nativeProgressAndStateTransitionAtSixteenMillisecondFrames() {
launch(UpdateUiState.Checking)
val target = directory

View file

@ -51,6 +51,7 @@ import org.justinvisser.wolfskin.design.focusRing
import org.justinvisser.wolfskin.design.focusScale
import org.justinvisser.wolfskin.design.motionDuration
import org.justinvisser.wolfskin.design.reducedMotion
import org.justinvisser.wolfskin.design.resolveUpdateAvatarColor
import org.justinvisser.wolfskin.design.rowReveal
import org.justinvisser.wolfskin.design.WolfskinTokens.Color as C
import org.justinvisser.wolfskin.design.WolfskinTokens.Layout as L
@ -68,7 +69,9 @@ fun UpdatesScreen(shell: ShellState, controller: UpdateController, onBack: () ->
val autoCheck by controller.autoCheck.collectAsStateWithLifecycle()
val initialInstalled = remember { installed.versionName }
val updatedVersion = installed.versionName.takeIf { it != initialInstalled }
val presentation = presentUpdate(if (updatedVersion != null) UpdateUiState.UpToDate else state, release, updatedVersion)
val presentation = remember(state, release, updatedVersion) {
presentUpdate(state, release, updatedVersion.takeIf { state == UpdateUiState.UpToDate })
}
val scroll = rememberScrollState()
val scope = rememberCoroutineScope()
val density = LocalDensity.current
@ -90,7 +93,7 @@ fun UpdatesScreen(shell: ShellState, controller: UpdateController, onBack: () ->
Box(modifier.fillMaxSize().testTag("updates-screen")) {
Row(Modifier.offset(L.safeSide, U.headerTop).height(U.headerHeight),
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(S.settingsHeaderGap)) {
Avatar(shell.person, S.settingsHeaderAvatar, T.avatarLetterLibHeader, letterColor = C.muted)
Avatar(shell.person, S.settingsHeaderAvatar, T.avatarLetterLibHeader, letterColor = C.muted, backgroundColor = resolveUpdateAvatarColor(shell.person))
Text("Updates", Modifier.alignByBaseline().exactLineBox(T.screenTitle), style = T.screenTitle, color = C.fg)
Text("Installed ${installed.versionName}", Modifier.alignByBaseline(), style = T.nav.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Normal), color = C.muted)
}

View file

@ -21,6 +21,10 @@ fun personColor(key: String): Color = when (key.lowercase()) {
else -> WolfskinTokens.Color.accent
}
/** Updates has its own locked avatar swatch; other destinations retain household colours. */
fun resolveUpdateAvatarColor(person: ShellPerson): Color =
if (person.key.equals("justin", ignoreCase = true)) WolfskinTokens.Update.avatarJustin else personColor(person.key)
/** The same mark anchors the picker and the persistent navigation. */
@Composable
fun Wordmark(modifier: Modifier = Modifier, prefix: String = "Wolf", textStyle: TextStyle = WolfskinTokens.Type.wordmark) {
@ -32,8 +36,9 @@ fun Wordmark(modifier: Modifier = Modifier, prefix: String = "Wolf", textStyle:
/** Initials are drawn over opaque person colour at the exact slot size. */
@Composable
fun Avatar(person: ShellPerson, diameter: Dp, textStyle: TextStyle, modifier: Modifier = Modifier, letterColor: Color = WolfskinTokens.Color.onFocus) {
Box(modifier.size(diameter).background(personColor(person.key), CircleShape), contentAlignment = Alignment.Center) {
fun Avatar(person: ShellPerson, diameter: Dp, textStyle: TextStyle, modifier: Modifier = Modifier, letterColor: Color = WolfskinTokens.Color.onFocus,
backgroundColor: Color = personColor(person.key)) {
Box(modifier.size(diameter).background(backgroundColor, CircleShape), contentAlignment = Alignment.Center) {
Text(person.initial, color = letterColor, style = textStyle)
}
}

View file

@ -117,6 +117,7 @@ fun NavBar(state: ShellState, scrolled: Boolean, modifier: Modifier = Modifier)
Modifier.offset(avatar.left, avatar.top)
.focusRing(avatarFocused, L.navAvatar / 2, WolfskinTokens.Focus.ringAvatarRing)
.focusProperties { canFocus = false }
.clickable { state.focusNav(state.tabs.size); state.key(ShellKey.Ok) })
.clickable { state.focusNav(state.tabs.size); state.key(ShellKey.Ok) },
backgroundColor = if (hasSelectedTab) personColor(state.person.key) else resolveUpdateAvatarColor(state.person))
}
}

View file

@ -70,7 +70,7 @@ fun SettingsList(state: ShellState, modifier: Modifier = Modifier, requestCount:
Box(modifier.fillMaxSize()) {
Row(Modifier.offset(L.safeSide, WolfskinTokens.Update.headerTop).height(WolfskinTokens.Update.headerHeight),
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(S.settingsHeaderGap)) {
Avatar(state.person, S.settingsHeaderAvatar, T.avatarLetterLibHeader, letterColor = C.muted)
Avatar(state.person, S.settingsHeaderAvatar, T.avatarLetterLibHeader, letterColor = C.muted, backgroundColor = resolveUpdateAvatarColor(state.person))
Text("Settings", Modifier.alignByBaseline().exactLineBox(T.screenTitle), style = T.screenTitle, color = C.fg)
Text("This TV · ${state.person.name}", Modifier.alignByBaseline(), style = T.nav.copy(fontWeight = FontWeight.Normal), color = C.muted)
}

View file

@ -702,6 +702,7 @@ object WolfskinTokens {
const val ambientBrightness = 0.55f
}
object Update {
val avatarJustin = ComposeColor(0xFF7AB8FF)
val heading = TextStyle(fontSize = 18.0.sp, fontWeight = FontWeight(800), lineHeight = 20.160000000000004.sp, letterSpacing = -0.02.em)
const val settingsVisibleRows = 6.0f
val headerTop = 75.0.dp

View file

@ -516,6 +516,7 @@
--discover-stageGap: 10.0px;
--discover-ambientInset: 80.0px;
--discover-ambientBlur: 60.0px;
--update-avatarJustin: #7ab8ff;
--update-heading: 36.0px;
--update-headerTop: 150.0px;
--update-headerHeight: 84.0px;

View file

@ -2,8 +2,8 @@ b3f096b692c395534177cc82ac68f5b801870bc7e28f0dd5cd7c17e2103920a3 discover-token
993d933950e17fb72d0902ffc440e1e4ed4dc088c96a8d12974431c77dc31634 generate_tokens.py
25985087986a46f00e2819917d0421ff2b437ec21c54f6ce6412b84c0eb05302 home-tokens.json
be3af0ddcb0d67abef0805d19d7a33cdb55428bcace656b50f0a38935f212028 shell-tokens.json
18c08c6f4d77ee0690e63ec2e1ce14854eced539746c41c948942460b37a38b2 src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt
289935944db0aa2f43224d7532331ad9ac5c666dbe40f1c44c9b7ee725f972ad src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt
e0a2f5df2f249bce3f6e13f4a0320c7b18b85777eaf04a27a428d6683a6aad28 title-tokens.json
eed527dac5e09b209619cd67990556a5f9223d05e7e2516f300cb4760a198ca7 tokens.css
5fa37cdbb5ab1bf0dbb0f83b8facabcd601ffb22492ad54f8aa9fc86cb729ee4 tokens.css
2e5e0476341adec1ec05331af38bc67cd02ba013a0988aea40a70eff21513264 tokens.json
824cb58d1507bc05212323ecfa66664f8b2bde23438940eed8717266161cac55 update-tokens.json
20c17f4e980baccce555b0cff8f76a2cfbc0db4e88d5a11f43abf73f0d936eb2 update-tokens.json

View file

@ -1,5 +1,6 @@
{
"update": {
"avatarJustin": {"$type": "color", "$value": "#7ab8ff"},
"heading": {
"$type": "typography",
"$value": {"fontWeight": 800},