diff --git a/app/src/test/java/com/github/damontecres/wholphin/ui/TestModule.kt b/app/src/test/java/com/github/damontecres/wholphin/ui/TestModule.kt index 3b4d3efa..6fb56cd0 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/ui/TestModule.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/ui/TestModule.kt @@ -73,6 +73,11 @@ import javax.inject.Singleton object TestModule { val testDispatcher = StandardTestDispatcher() + @Provides + @Singleton + fun updateController(): org.justinvisser.wolfskin.tv.update.UpdateController = + org.justinvisser.wolfskin.tv.update.FakeUpdateController() + @Provides @Singleton fun deviceInfo( diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/UpdateBehaviorTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/update/UpdateBehaviorTest.kt similarity index 98% rename from app/src/test/java/org/justinvisser/wolfskin/tv/UpdateBehaviorTest.kt rename to app/src/test/java/org/justinvisser/wolfskin/tv/update/UpdateBehaviorTest.kt index 72c07539..561b625a 100644 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/UpdateBehaviorTest.kt +++ b/app/src/test/java/org/justinvisser/wolfskin/tv/update/UpdateBehaviorTest.kt @@ -1,10 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.tv +package org.justinvisser.wolfskin.tv.update import org.junit.Assert.* import org.junit.Test import org.justinvisser.wolfskin.design.ShellKey -import org.justinvisser.wolfskin.tv.update.* /** Behavior boundaries are controller outcomes, not incidental UI wording. */ class UpdateBehaviorTest { diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/UpdatesCaptureTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/update/UpdatesCaptureTest.kt similarity index 90% rename from app/src/test/java/org/justinvisser/wolfskin/tv/UpdatesCaptureTest.kt rename to app/src/test/java/org/justinvisser/wolfskin/tv/update/UpdatesCaptureTest.kt index 347c3855..951e0eba 100644 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/UpdatesCaptureTest.kt +++ b/app/src/test/java/org/justinvisser/wolfskin/tv/update/UpdatesCaptureTest.kt @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-only -package org.justinvisser.wolfskin.tv +package org.justinvisser.wolfskin.tv.update import android.app.Application import android.view.View @@ -25,7 +25,6 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.justinvisser.wolfskin.design.* -import org.justinvisser.wolfskin.tv.update.* import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode @@ -78,7 +77,7 @@ class UpdatesCaptureTest { rule.onRoot().captureRoboImage(file.absolutePath) } - private fun press(vararg keys: Key) { + private fun press(keys: List) { keys.forEach { key -> rule.onNodeWithTag("wolfskin-shell").performKeyInput { pressKey(key) } } rule.mainClock.advanceTimeBy(320) rule.waitForIdle() @@ -87,7 +86,7 @@ class UpdatesCaptureTest { @Test fun settings() { launch(UpdateUiState.Checking, settings = true); capture("settings") } @Test fun classic() { launch(UpdateUiState.Checking, settings = true) - repeat(8) { press(Key.DirectionDown) } + repeat(8) { press(listOf(Key.DirectionDown)) } assertEquals(9, shell.settingsRow) assertEquals(4, shell.view.listState.firstVisibleItemIndex) capture("classic") @@ -95,8 +94,8 @@ class UpdatesCaptureTest { @Test fun checking() { launch(UpdateUiState.Checking); capture("checking") } @Test fun current() { launch(UpdateUiState.UpToDate); capture("current") } @Test fun available() { launch(updateReleaseFixture()); capture("available") } - @Test fun notes() { launch(updateReleaseFixture()); press(Key.DirectionRight); capture("notes") } - @Test fun notesOpen() { launch(updateReleaseFixture()); press(Key.DirectionRight, Key.DirectionCenter); capture("notes-open") } + @Test fun notes() { launch(updateReleaseFixture()); press(listOf(Key.DirectionRight)); capture("notes") } + @Test fun notesOpen() { launch(updateReleaseFixture()); press(listOf(Key.DirectionRight, Key.DirectionCenter)); capture("notes-open") } @Test fun downloading() { launch(UpdateUiState.Downloading(20_100_000, 47_800_000)); capture("downloading") } @Test fun ready() { launch(UpdateUiState.Ready("1.1.1")); capture("ready") } @Test fun permission() { launch(UpdateUiState.PermissionNeeded); capture("permission") } @@ -107,8 +106,8 @@ class UpdatesCaptureTest { @Test fun noInstaller() { launch(UpdateUiState.NoInstaller); capture("no-installer") } @Test fun noReleasePublished() { launch(UpdateUiState.NoReleasePublished); capture("no-release") } @Test fun unknownDownloadLength() { launch(UpdateUiState.Downloading(20_100_000, null)); capture("unknown-total") } - @Test fun autoOn() { launch(updateReleaseFixture()); press(Key.DirectionDown); capture("auto-on") } - @Test fun autoOff() { launch(updateReleaseFixture()); press(Key.DirectionDown, Key.DirectionCenter); capture("auto-off") } + @Test fun autoOn() { launch(updateReleaseFixture()); press(listOf(Key.DirectionDown)); capture("auto-on") } + @Test fun autoOff() { launch(updateReleaseFixture()); press(listOf(Key.DirectionDown, Key.DirectionCenter)); capture("auto-off") } @Test fun updated() { launch(UpdateUiState.UpToDate) rule.runOnIdle { controller.showInstalled(InstalledVersion("1.1.1", 100001)) } @@ -118,14 +117,14 @@ class UpdatesCaptureTest { @Test fun notesTrapRealKeysAndBackPreservesSettingsRowAndScroller() { launch(updateReleaseFixture()) - press(Key.DirectionRight, Key.DirectionCenter, Key.DirectionUp, Key.DirectionRight, Key.DirectionDown) + press(listOf(Key.DirectionRight, Key.DirectionCenter, Key.DirectionUp, Key.DirectionRight, Key.DirectionDown)) assertTrue(shell.contentOverlayVisible) assertFalse(shell.navFocused) assertTrue(controller.autoCheck.value) - press(Key.Back) + press(listOf(Key.Back)) assertFalse(shell.contentOverlayVisible) assertEquals(1, (shell.contentFocus as UpdateFocus).actionIndex) - press(Key.Back) + press(listOf(Key.Back)) assertEquals("settings", shell.route) assertEquals(1, shell.settingsRow) assertEquals(0, shell.view.listState.firstVisibleItemIndex) @@ -133,12 +132,12 @@ class UpdatesCaptureTest { @Test fun longNotesScrollWithoutEscapingToNavOrAutoCheck() { launch(updateReleaseFixture().copy(notes = (1..30).joinToString("\n") { "Release detail $it" })) - press(Key.DirectionRight, Key.DirectionCenter, Key.DirectionDown, Key.DirectionDown) + press(listOf(Key.DirectionRight, Key.DirectionCenter, Key.DirectionDown, Key.DirectionDown)) val range = rule.onNodeWithTag("update-notes").fetchSemanticsNode().config[SemanticsProperties.VerticalScrollAxisRange] assertTrue(range.value() > 0f) assertFalse(shell.navFocused) assertFalse((shell.contentFocus as UpdateFocus).autoFocused) - press(Key.Back) + press(listOf(Key.Back)) assertEquals(1, (shell.contentFocus as UpdateFocus).actionIndex) } @@ -167,7 +166,7 @@ class UpdatesCaptureTest { if (target != null) { repeat(101) { frame -> rule.mainClock.advanceTimeBy(16); capture("motion/download-%03d".format(frame)) } } - press(Key.DirectionDown) + press(listOf(Key.DirectionDown)) rule.runOnIdle { controller.show(UpdateUiState.Downloading(31_000_000, 47_800_000)) } rule.mainClock.advanceTimeBy(16) assertTrue((shell.contentFocus as UpdateFocus).autoFocused) diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/update/UpdatesScreen.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/update/UpdatesScreen.kt index a33341aa..725f591a 100644 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/update/UpdatesScreen.kt +++ b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/update/UpdatesScreen.kt @@ -165,18 +165,22 @@ private fun AutoCheckRow(enabled: Boolean, focused: Boolean, modifier: Modifier, @Composable private fun NotesPanel(release: UpdateUiState.Available?, scroll: androidx.compose.foundation.ScrollState) { val shape = RoundedCornerShape(U.radius) - Column(Modifier.offset(L.safeSide, U.contentTop).size(U.width, U.notesHeight).testTag("update-notes") + val paragraphs = remember(release?.notes) { release?.notes?.lineSequence()?.filter { it.isNotBlank() }?.toList().orEmpty() } + val notesStyle = T.nav.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Normal) + CssColumn(Modifier.offset(L.safeSide, U.contentTop).size(U.width, U.notesHeight).testTag("update-notes") .focusRing(true, U.radius, WolfskinTokens.Focus.ringControlRing) .background(C.surface, shape).border(U.border, C.border, shape) .verticalScroll(scroll).padding(U.padding + U.border)) { Text(updateMetadata(release), Modifier.exactLineBox(T.stateTitle), style = T.stateTitle.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), color = C.fg) - Spacer(Modifier.height(U.notesTitleBottom)) - release?.notes?.lineSequence()?.filter { it.isNotBlank() }?.forEach { paragraph -> - Row(Modifier.padding(vertical = U.notesGap / 2)) { - Text("•", Modifier.width(U.padding / 2), style = T.nav, color = C.muted) - Text(paragraph.removePrefix("- "), Modifier.exactLineBox(T.nav), style = T.nav.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Normal), color = C.muted) + Spacer(Modifier.height(U.notesListTop)) + paragraphs.forEachIndexed { index, paragraph -> + Row(Modifier.padding(start = U.notesMarkerOffset).exactLineBox(notesStyle)) { + Text("•", Modifier.width(U.notesMarkerWidth).exactLineBox(notesStyle), style = notesStyle, color = C.muted) + Text(paragraph.removePrefix("- "), Modifier.exactLineBox(notesStyle), style = notesStyle, color = C.muted) } + if (index != paragraphs.lastIndex) Spacer(Modifier.height(U.notesGap)) } - Text("Sign-in stays on this TV. Android asks before installing.", Modifier.padding(top = U.notesGap), style = T.body, color = C.muted) + Spacer(Modifier.height(U.notesListBottom)) + Text("Sign-in stays on this TV. Android asks before installing.", Modifier.exactLineBox(T.body), style = T.body, color = C.muted) } } diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt index bb8cfdc6..5c231d6a 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt @@ -703,6 +703,10 @@ object WolfskinTokens { } object Update { val avatarJustin = ComposeColor(0xFF7AB8FF) + val notesListTop = 14.0.dp + val notesListBottom = 14.0.dp + val notesMarkerOffset = 5.5.dp + val notesMarkerWidth = 14.5.dp 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 @@ -728,7 +732,6 @@ object WolfskinTokens { val toggleThumb = 15.0.dp val toggleInset = 3.0.dp val notesHeight = 256.0.dp - val notesTitleBottom = 10.0.dp val notesGap = 9.0.dp val notesScrollStep = 40.0.dp val backTop = 92.0.dp diff --git a/wolfskin-design/tokens.css b/wolfskin-design/tokens.css index fdc284bd..de9f79e7 100644 --- a/wolfskin-design/tokens.css +++ b/wolfskin-design/tokens.css @@ -517,6 +517,10 @@ --discover-ambientInset: 80.0px; --discover-ambientBlur: 60.0px; --update-avatarJustin: #7ab8ff; + --update-notesListTop: 28.0px; + --update-notesListBottom: 28.0px; + --update-notesMarkerOffset: 11.0px; + --update-notesMarkerWidth: 29.0px; --update-heading: 36.0px; --update-headerTop: 150.0px; --update-headerHeight: 84.0px; @@ -541,7 +545,6 @@ --update-toggleThumb: 30.0px; --update-toggleInset: 6.0px; --update-notesHeight: 512.0px; - --update-notesTitleBottom: 20.0px; --update-notesGap: 18.0px; --update-notesScrollStep: 80.0px; --update-backTop: 184.0px; diff --git a/wolfskin-design/tokens.sha256 b/wolfskin-design/tokens.sha256 index ae81afd2..5b6dd97f 100644 --- a/wolfskin-design/tokens.sha256 +++ b/wolfskin-design/tokens.sha256 @@ -2,8 +2,8 @@ b3f096b692c395534177cc82ac68f5b801870bc7e28f0dd5cd7c17e2103920a3 discover-token 993d933950e17fb72d0902ffc440e1e4ed4dc088c96a8d12974431c77dc31634 generate_tokens.py 25985087986a46f00e2819917d0421ff2b437ec21c54f6ce6412b84c0eb05302 home-tokens.json be3af0ddcb0d67abef0805d19d7a33cdb55428bcace656b50f0a38935f212028 shell-tokens.json -289935944db0aa2f43224d7532331ad9ac5c666dbe40f1c44c9b7ee725f972ad src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt +1b8222d01dc30cbe2591097218a4a93c6ac4b24704cf81b18174b7839eb57e31 src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt e0a2f5df2f249bce3f6e13f4a0320c7b18b85777eaf04a27a428d6683a6aad28 title-tokens.json -5fa37cdbb5ab1bf0dbb0f83b8facabcd601ffb22492ad54f8aa9fc86cb729ee4 tokens.css +9a4c316e83af3207dd21fa7a3b4a70711460f38b740ee1de3ec9b042b3d4f9a8 tokens.css 2e5e0476341adec1ec05331af38bc67cd02ba013a0988aea40a70eff21513264 tokens.json -20c17f4e980baccce555b0cff8f76a2cfbc0db4e88d5a11f43abf73f0d936eb2 update-tokens.json +1bedb8e8079b4a954651d8761d18b8be4adb3933370e5f296be27287401ae460 update-tokens.json diff --git a/wolfskin-design/update-tokens.json b/wolfskin-design/update-tokens.json index b5e59c6f..a6065c06 100644 --- a/wolfskin-design/update-tokens.json +++ b/wolfskin-design/update-tokens.json @@ -1,6 +1,10 @@ { "update": { "avatarJustin": {"$type": "color", "$value": "#7ab8ff"}, + "notesListTop": {"$type": "dimension", "px": 28, "dp": 14}, + "notesListBottom": {"$type": "dimension", "px": 28, "dp": 14}, + "notesMarkerOffset": {"$type": "dimension", "px": 11, "dp": 5.5}, + "notesMarkerWidth": {"$type": "dimension", "px": 29, "dp": 14.5}, "heading": { "$type": "typography", "$value": {"fontWeight": 800}, @@ -126,11 +130,6 @@ "px": 512, "dp": 256.0 }, - "notesTitleBottom": { - "$type": "dimension", - "px": 20, - "dp": 10.0 - }, "notesGap": { "$type": "dimension", "px": 18,