Restore household colours on the cold-start profile picker
All checks were successful
review/code-claude Code APPROVE: key change only, auth ids untouched
review/design-claude Design PASS: Justin blue, Mindel orange, Shared green

This commit is contained in:
justinzeus 2026-11-07 23:38:37 +01:00
parent 4af1898fd2
commit 27b7554fc1
3 changed files with 34 additions and 1 deletions

View file

@ -2,6 +2,7 @@
package org.justinvisser.wolfskin.tv
import android.app.Application
import android.graphics.BitmapFactory
import android.content.Intent
import android.os.Looper
import android.view.View
@ -11,6 +12,7 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.Snapshot
import org.robolectric.Shadows
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.datastore.core.DataStore
@ -46,6 +48,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.justinvisser.wolfskin.conductarr.MemoryCacheStore
import org.robolectric.RobolectricTestRunner
import org.justinvisser.wolfskin.design.WolfskinTokens
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
@ -137,6 +140,34 @@ class WolfskinSetupNavigationTest {
coVerify(exactly = 0) { repository.switchServerOrUser() }
}
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Test fun coldStartSavedPeopleKeepTheirOwnColorsWithoutConductarr() {
val saved = listOf(user, user.copy(id = UUID.randomUUID(), name = "Mindel"),
user.copy(id = UUID.randomUUID(), name = "Shared"))
every { repository.serverDao.getServer(server.id) } returns JellyfinServerUsers(server, saved)
runBlocking { appModel().handleAppStart() }
mount()
assertOwnPicker()
saved.forEach { rule.onNodeWithText(it.name!!).assertIsDisplayed() }
rule.mainClock.advanceTimeBy(1_000)
rule.waitForIdle()
val people = model.picker!!.people
val bounds = people.map { rule.onNodeWithTag("picker-person:${it.key}").fetchSemanticsNode().boundsInRoot }
val directory = File(requireNotNull(System.getProperty("wolfskin.captureDirectory")), "setup").apply { mkdirs() }
val frame = File(directory, "cold-start-people.png")
rule.onRoot().captureRoboImage(frame)
val bitmap = BitmapFactory.decodeFile(frame.absolutePath)
try {
val fills = bounds.map { bitmap.getPixel(it.center.x.toInt(), (it.top + it.width / 5).toInt()) }
assertEquals(listOf(WolfskinTokens.PersonColor.justin.toArgb(),
WolfskinTokens.PersonColor.mindel.toArgb(), WolfskinTokens.PersonColor.shared.toArgb()), fills)
} finally {
bitmap.recycle()
}
assertNull(conductarr.identity.value)
assertEquals(saved.map { it.id.toString() }, people.map { it.jellyfinUserId })
}
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Test fun unreachableSelectionRendersWolfskinRecoveryFrame() {
stack[0] = SetupDestination.UserList(server)

View file

@ -80,7 +80,7 @@ class WolfskinSetupViewModel @Inject constructor(
if (users.isEmpty()) {
showLogin(null)
} else {
val people = users.map { ShellPerson(it.id.toString(), it.name.orEmpty(), it.name.orEmpty().take(1).uppercase(), false, it.id.toString()) }
val people = users.map { ShellPerson(it.name.orEmpty().lowercase(), it.name.orEmpty(), it.name.orEmpty().take(1).uppercase(), false, it.id.toString()) }
picker = ShellState(people, people.first().key) { onAction(it) }
stage = Stage.Picker
}

View file

@ -10,6 +10,8 @@ Down from any avatar reaches the gear; Up returns to its origin, including after
Source — Justin (2026-11-07): “the settings icon moved, but pressing down only works from the last profile icon. I want it to always move there when pressing down in that screen”.
Saved profiles retain their approved household colours before Conductarr answers: Justin **#62c4ff**, Mindel **#ff9d72**, Shared **#9be38f**. Setup uses the same lowercase-name presentation key as the signed-in picker; the separate Jellyfin user ID remains the authentication identity. `WolfskinSetupNavigationTest.coldStartSavedPeopleKeepTheirOwnColorsWithoutConductarr` exercises the real cold-start setup boundary and checks the native avatar-fill pixels without a Conductarr session.
Root content Back moves to the nav; the next Back **finishes and removes Wolfskin’s Android task**, returning to the launcher without visiting an old Home/setup interface. This applies to Home, Discover, Movies, Shows and Anime. Cold-launch picker Back exits; a reopened picker first dismisses to its opener’s nav. Pushed title/person/settings/Updates pages pop their parent stack, and menus/sheets/unsaved-settings prompts get first refusal. A singleton full-screen page exits rather than becoming a dead end.
Native Android Back (`KEYCODE_BACK`) belongs exclusively to the activity’s system dispatcher, including Navigation 3 predictive completion; Compose key previews must not also navigate on its raw key-down. Escape remains the existing local keyboard Back shortcut. Each completed system Back has one consumer. A retained title/person callback checks the live navigation entry at invocation and hands Back to the incoming entry if its own entry has already popped; it does not depend on an enabled flag catching up with the exit animation. Root handling synchronizes the shell route to the current entry before applying content → nav → task exit. Back ownership is exercised on both SDK 34 (Android 14) and SDK 36.