diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/WolfskinSetupNavigationTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/WolfskinSetupNavigationTest.kt index 4c7adc37..046168fa 100644 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/WolfskinSetupNavigationTest.kt +++ b/app/src/test/java/org/justinvisser/wolfskin/tv/WolfskinSetupNavigationTest.kt @@ -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) diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinSetupViewModel.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinSetupViewModel.kt index 2bf9119d..6d1caffb 100644 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinSetupViewModel.kt +++ b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinSetupViewModel.kt @@ -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 } diff --git a/data/tv-spec/spec.md b/data/tv-spec/spec.md index 0a22d47c..29ac8a5d 100644 --- a/data/tv-spec/spec.md +++ b/data/tv-spec/spec.md @@ -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.