From 37ad87d05822ec21f2eeaae6f7e8047493d83b3e Mon Sep 17 00:00:00 2001 From: justinzeus Date: Sat, 7 Nov 2026 17:55:09 +0100 Subject: [PATCH] Match native updater frames and preserve D-pad focus behavior --- README.md | 8 +++ ...datesCaptureTest.kt => UpdatesKeysTest.kt} | 57 +++++++++---------- .../wolfskin/tv/update/UpdateFocus.kt | 1 - .../wolfskin/tv/update/UpdatesScreen.kt | 24 ++++++-- .../update-font-license.txt} | 0 .../org/justinvisser/wolfskin/design/Glyph.kt | 3 + .../justinvisser/wolfskin/design/NavBar.kt | 19 ++++--- .../wolfskin/design/SettingsList.kt | 11 ++-- .../wolfskin/design/UpdateTypography.kt | 30 ++++++---- .../wolfskin/design/WolfskinTokens.kt | 6 ++ .../drawable/wolfskin_settings_chevron.xml | 3 + .../wolfskin/design/ShellCaptureTest.kt | 2 +- .../wolfskin/design/ShellKeysTest.kt | 16 ++++++ wolfskin-design/tokens.css | 6 ++ wolfskin-design/tokens.sha256 | 6 +- wolfskin-design/update-tokens.json | 6 ++ 16 files changed, 131 insertions(+), 67 deletions(-) rename app/src/test/java/org/justinvisser/wolfskin/tv/update/{UpdatesCaptureTest.kt => UpdatesKeysTest.kt} (72%) rename wolfskin-design/src/main/{res/raw/update_font_license.txt => assets/update-font-license.txt} (100%) create mode 100644 wolfskin-design/src/main/res/drawable/wolfskin_settings_chevron.xml diff --git a/README.md b/README.md index b8f04e12..ba3b9a44 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,14 @@ Wolfskin is an Android TV client for one household. It is released through Forge The app uses Wolfskin launcher artwork and a lilac-on-`#07070a` theme by default on a fresh install. Existing theme choices and all upstream colour themes remain available under Settings → App theme. Settings → About credits and links to Wholphin by damontecres. +## Wolfskin TV updates + +Settings opens on Requests. Press Down, then OK to open Updates; the row and screen show the installed app version. Left/Right selects an action, Down reaches Auto-check, and Up returns to the remembered action. Release notes keep focus inside the notes panel: Up/Down scrolls, and Back returns to the Release notes button. + +Auto-check uses the same preference as classic settings and only checks for updates. Manual checks remain available when it is off. Download and Install update are separate, explicit actions; Android owns installation permission and confirmation. Back can leave a download running, and returning to Updates shows the controller's current state. + +Classic settings (trial) is the final Settings row. It opens the full original settings, including About → Update and More/Advanced → Updates. All existing settings groups remain available. + --- # Wholphin - an OSS Android TV client for Jellyfin diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/update/UpdatesCaptureTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/update/UpdatesKeysTest.kt similarity index 72% rename from app/src/test/java/org/justinvisser/wolfskin/tv/update/UpdatesCaptureTest.kt rename to app/src/test/java/org/justinvisser/wolfskin/tv/update/UpdatesKeysTest.kt index 951e0eba..e593675d 100644 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/update/UpdatesCaptureTest.kt +++ b/app/src/test/java/org/justinvisser/wolfskin/tv/update/UpdatesKeysTest.kt @@ -29,11 +29,11 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode -/** Native TV frames use the real shell, updater controller seam and Android key adapter. */ +/** Native D-pad behavior preserves the retained shell, action focus and real scroller. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi") @GraphicsMode(GraphicsMode.Mode.NATIVE) -class UpdatesCaptureTest { +class UpdatesKeysTest { @get:Rule val rule = createAndroidComposeRule() private val shell = ShellState(listOf(ShellPerson("justin", "Justin", "J", true)), "justin").apply { pickerVisible = false @@ -78,41 +78,24 @@ class UpdatesCaptureTest { } private fun press(keys: List) { - keys.forEach { key -> rule.onNodeWithTag("wolfskin-shell").performKeyInput { pressKey(key) } } + keys.forEach { key -> + rule.onNodeWithTag("wolfskin-shell").performKeyInput { pressKey(key) } + rule.runOnIdle { androidx.compose.runtime.snapshots.Snapshot.sendApplyNotifications() } + rule.mainClock.advanceTimeByFrame() + } rule.mainClock.advanceTimeBy(320) rule.waitForIdle() } - @Test fun settings() { launch(UpdateUiState.Checking, settings = true); capture("settings") } - @Test fun classic() { + @Test fun classicRequiresEveryRetainedRowAndTheRealScroller() { + shell.settingsRow = 0 launch(UpdateUiState.Checking, settings = true) - repeat(8) { press(listOf(Key.DirectionDown)) } + repeat(9) { press(listOf(Key.DirectionDown)) } assertEquals(9, shell.settingsRow) assertEquals(4, shell.view.listState.firstVisibleItemIndex) capture("classic") - } - @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(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") } - @Test fun handoff() { launch(UpdateUiState.HandingOff); capture("handoff") } - @Test fun cancelled() { launch(UpdateUiState.InstallCancelled); capture("cancelled") } - @Test fun failed() { launch(UpdateUiState.Failed(UpdateFailure.HttpRefused, "The download server refused the request (403).")); capture("failed") } - @Test fun offline() { launch(UpdateUiState.Offline); capture("offline") } - @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(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)) } - rule.mainClock.advanceTimeBy(320) - capture("updated") + press(listOf(Key.DirectionCenter)) + assertEquals("classic", shell.handOff) } @Test fun notesTrapRealKeysAndBackPreservesSettingsRowAndScroller() { @@ -141,6 +124,19 @@ class UpdatesCaptureTest { assertEquals(1, (shell.contentFocus as UpdateFocus).actionIndex) } + @Test fun navAndAutoCheckReturnToTheRememberedActionWithoutToggling() { + launch(updateReleaseFixture()) + press(listOf(Key.DirectionRight, Key.DirectionDown, Key.DirectionUp)) + assertEquals(1, (shell.contentFocus as UpdateFocus).actionIndex) + assertTrue(controller.autoCheck.value) + press(listOf(Key.DirectionUp)) + assertTrue(shell.navFocused) + press(listOf(Key.DirectionDown)) + assertFalse(shell.navFocused) + assertEquals(1, (shell.contentFocus as UpdateFocus).actionIndex) + assertTrue(controller.autoCheck.value) + } + @Test fun refreshingInstalledIdentityCannotMaskAnOngoingCheck() { launch(UpdateUiState.UpToDate) rule.runOnIdle { controller.showInstalled(InstalledVersion("1.1.1", 100001)) } @@ -150,7 +146,7 @@ class UpdatesCaptureTest { rule.onNodeWithTag("update-progress").assertIsDisplayed() } - @Test fun nativeProgressAndStateTransitionAtSixteenMillisecondFrames() { + @Test fun progressKeepsAutoCheckFocusAcrossByteAndReducedMotionChanges() { launch(UpdateUiState.Checking) val target = directory if (target != null) { @@ -162,7 +158,6 @@ class UpdatesCaptureTest { rule.runOnIdle { controller.show(UpdateUiState.Downloading(20_100_000, 47_800_000)) } rule.mainClock.advanceTimeByFrame() repeat(24) { frame -> rule.mainClock.advanceTimeBy(16); capture("motion/state-%03d".format(frame)) } - assertEquals(12f, rule.onNodeWithTag("update-progress").fetchSemanticsNode().boundsInRoot.height, .01f) if (target != null) { repeat(101) { frame -> rule.mainClock.advanceTimeBy(16); capture("motion/download-%03d".format(frame)) } } diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/update/UpdateFocus.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/update/UpdateFocus.kt index 67d22cd2..cd959f76 100644 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/update/UpdateFocus.kt +++ b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/update/UpdateFocus.kt @@ -39,7 +39,6 @@ class UpdateFocus( } fun focusAction(index: Int) { actionIndex = index; autoFocused = false } - fun focusAuto() { autoFocused = true } fun activate(index: Int) { focusAction(index) 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 725f591a..1d270736 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 @@ -12,7 +12,6 @@ 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.offset import androidx.compose.foundation.layout.padding @@ -28,8 +27,14 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.Role @@ -47,7 +52,6 @@ import org.justinvisser.wolfskin.design.PickerButton import org.justinvisser.wolfskin.design.ShellState import org.justinvisser.wolfskin.design.WolfskinTokens import org.justinvisser.wolfskin.design.exactLineBox -import org.justinvisser.wolfskin.design.focusRing import org.justinvisser.wolfskin.design.focusScale import org.justinvisser.wolfskin.design.motionDuration import org.justinvisser.wolfskin.design.reducedMotion @@ -76,10 +80,11 @@ fun UpdatesScreen(shell: ShellState, controller: UpdateController, onBack: () -> val scope = rememberCoroutineScope() val density = LocalDensity.current val reduced = reducedMotion() + val scrollDuration by rememberUpdatedState(motionDuration(M.scroll, reduced)) val focus = remember(controller, shell) { UpdateFocus(controller, onBack, onClassic, { shell.navFocused = true; shell.navIndex = shell.currentTab }) { direction -> scope.launch { scroll.animateScrollBy(with(density) { U.notesScrollStep.toPx() } * direction, - tween(motionDuration(M.scroll, reduced), easing = M.easeOut)) } + tween(scrollDuration, easing = M.easeOut)) } } } LaunchedEffect(state, presentation.actions) { focus.update(state, presentation.actions) } @@ -168,7 +173,18 @@ private fun NotesPanel(release: UpdateUiState.Available?, scroll: androidx.compo 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) + .drawWithCache { + val ring = WolfskinTokens.Focus.ringControlRing.toPx() / 2f + val radius = U.radius.toPx() + val inset = U.border.toPx() + ring / 2f + onDrawWithContent { + drawContent() + drawRoundRect(C.accent, Offset(-ring / 2f, -ring / 2f), Size(size.width + ring, size.height + ring), + CornerRadius(radius + ring / 2f), style = Stroke(ring)) + drawRoundRect(C.accent, Offset(inset, inset), Size(size.width - inset * 2, size.height - inset * 2), + CornerRadius(radius - inset), style = Stroke(ring)) + } + } .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) diff --git a/wolfskin-design/src/main/res/raw/update_font_license.txt b/wolfskin-design/src/main/assets/update-font-license.txt similarity index 100% rename from wolfskin-design/src/main/res/raw/update_font_license.txt rename to wolfskin-design/src/main/assets/update-font-license.txt diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/Glyph.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/Glyph.kt index 5a9227d3..0039ab4a 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/Glyph.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/Glyph.kt @@ -11,6 +11,9 @@ import androidx.tv.material3.Icon fun Glyph(name: String, color: Color, modifier: Modifier = Modifier) { val resource = when (name) { "gear" -> R.drawable.wolfskin_gear + "updates" -> R.drawable.wolfskin_update + "settings-list" -> R.drawable.wolfskin_settings_list + "settings-right" -> R.drawable.wolfskin_settings_chevron "requests" -> R.drawable.wolfskin_tray "interface" -> R.drawable.wolfskin_grid "play", "playback" -> R.drawable.wolfskin_play diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/NavBar.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/NavBar.kt index e30392ae..17b0af02 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/NavBar.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/NavBar.kt @@ -40,15 +40,16 @@ import org.justinvisser.wolfskin.design.WolfskinTokens.SavedIndicator as V /** Blob bounds retain their measurement while their draw coordinates spring between targets. */ data class NavBox(val left: Dp, val top: Dp, val width: Dp, val height: Dp) -fun navBoxes(tabs: List): List { - var left = L.safeSide + S.wordmarkWidth + L.wordmarkGap + L.navItemGap +fun navBoxes(tabs: List, isUpdatesLayout: Boolean = false): List { + val updates = WolfskinTokens.Update + var left = L.safeSide + (if (isUpdatesLayout) updates.wordmarkWidth else S.wordmarkWidth) + L.wordmarkGap + L.navItemGap return tabs.map { tab -> val width = when (tab) { - "home" -> S.navHomeWidth - "discover" -> S.navDiscoverWidth - "movies" -> S.navMoviesWidth - "shows" -> S.navShowsWidth - else -> S.navAnimeWidth + "home" -> if (isUpdatesLayout) updates.navHomeWidth else S.navHomeWidth + "discover" -> if (isUpdatesLayout) updates.navDiscoverWidth else S.navDiscoverWidth + "movies" -> if (isUpdatesLayout) updates.navMoviesWidth else S.navMoviesWidth + "shows" -> if (isUpdatesLayout) updates.navShowsWidth else S.navShowsWidth + else -> if (isUpdatesLayout) updates.navAnimeWidth else S.navAnimeWidth } NavBox(left, S.navItemTop, width, L.navItemHeight).also { left += width + L.navItemGap } } + NavBox(S.stageWidth - L.safeSide - L.navAvatar, S.navItemTop, L.navAvatar, L.navAvatar) @@ -57,11 +58,11 @@ fun navBoxes(tabs: List): List { /** There is one blob, including when focus wraps around the nav avatar. */ @Composable fun NavBar(state: ShellState, scrolled: Boolean, modifier: Modifier = Modifier) { - val boxes = remember(state.person.hasAnime) { navBoxes(state.tabs) } + val hasSelectedTab = state.route != "settings" && state.route != "updates" + val boxes = remember(state.person.hasAnime, hasSelectedTab) { navBoxes(state.tabs, isUpdatesLayout = !hasSelectedTab) } val reduced = reducedMotion() val labelFade = tween(motionDuration(S.navLabelFade, reduced), easing = CubicBezierEasing(0f, 0f, .58f, 1f)) val index = if (state.navFocused) state.navIndex else state.currentTab - val hasSelectedTab = state.route != "settings" && state.route != "updates" val box = boxes[index] val avatarFocused = state.navFocused && index == state.tabs.size val pad = if (avatarFocused) L.navBlobAvatarPad else S.zero diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/SettingsList.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/SettingsList.kt index e04c344f..fe7adad1 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/SettingsList.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/SettingsList.kt @@ -9,14 +9,12 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues 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.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape @@ -40,11 +38,11 @@ import org.justinvisser.wolfskin.design.WolfskinTokens.Shell as S import org.justinvisser.wolfskin.design.UpdateTypography as T /** Settings routes identify real upstream destinations rather than stand-in panels. */ -data class SettingsGroup(val key: String, val title: String, val description: String, val wolfskin: Boolean = false) +data class SettingsGroup(val key: String, val title: String, val description: String) val SETTINGS_GROUPS = listOf( SettingsGroup("requests", "Requests", "Coming in: what the household asked for"), - SettingsGroup("updates", "Updates", "Check and install", true), + SettingsGroup("updates", "Updates", "Check and install"), SettingsGroup("interface", "Interface", "Sign-in · Theme music · Subtitles"), SettingsGroup("playback", "Playback", "Skip forward · Skip back · Resume"), SettingsGroup("nextup", "Next up", "Auto-play · Delay · Passout protection"), @@ -74,7 +72,8 @@ fun SettingsList(state: ShellState, modifier: Modifier = Modifier, requestCount: 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) } - LazyColumn(Modifier.fillMaxWidth().offset(y = WolfskinTokens.Update.rowsTop).height(WolfskinTokens.Update.rowsViewport + S.settingsRowGap), + LazyColumn(Modifier.fillMaxWidth().offset(y = WolfskinTokens.Update.rowsTop) + .height(WolfskinTokens.Update.rowsViewport + L.settingsRowHeight * (WolfskinTokens.Focus.scaleRow - 1f)), state = list, contentPadding = PaddingValues(bottom = S.settingsRowGap)) { itemsIndexed(SETTINGS_GROUPS, key = { _, group -> group.key }) { index, group -> Box(Modifier.height(L.row_srow)) { @@ -104,7 +103,7 @@ fun SettingsRow(group: SettingsGroup, focused: Boolean, requestCount: Int?, modi Text(group.description, Modifier.exactLineBox(T.caption), color = if (focused) C.onFocus else C.muted, style = T.caption, maxLines = 1, overflow = TextOverflow.Ellipsis) } SettingsPill(if (group.key == "requests" || group.key == "updates") "Wolfskin screen" else "upstream screen", focused) - Glyph("right", if (focused) C.onFocus else C.accentText, Modifier.size(S.settingsChevron)) + Glyph("settings-right", if (focused) C.onFocus else C.accentText, Modifier.size(S.settingsIcon)) } } diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/UpdateTypography.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/UpdateTypography.kt index 92ad49f0..e777372e 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/UpdateTypography.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/UpdateTypography.kt @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-only package org.justinvisser.wolfskin.design +import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -8,16 +11,19 @@ import androidx.compose.ui.text.font.FontWeight /** The approved Updates frames resolve system-ui to DejaVu Sans; other screens keep their fonts. */ object UpdateTypography { val family = FontFamily(Font(R.font.update_dejavu_sans), Font(R.font.update_dejavu_sans_bold, FontWeight.Bold)) - val screenTitle = WolfskinTokens.Type.screenTitle.copy(fontFamily = family) - val stateTitle = WolfskinTokens.Type.stateTitle.copy(fontFamily = family) - val heading = WolfskinTokens.Update.heading.copy(fontFamily = family) - val nav = WolfskinTokens.Type.nav.copy(fontFamily = family) - val wordmark = WolfskinTokens.Type.wordmark.copy(fontFamily = family) - val listTitle = WolfskinTokens.Type.listTitle.copy(fontFamily = family) - val body = WolfskinTokens.Type.body.copy(fontFamily = family) - val button = WolfskinTokens.Type.button.copy(fontFamily = family) - val label = WolfskinTokens.Type.label.copy(fontFamily = family) - val caption = WolfskinTokens.Type.caption.copy(fontFamily = family) - val avatarLetterNav = WolfskinTokens.Type.avatarLetterNav.copy(fontFamily = family) - val avatarLetterLibHeader = WolfskinTokens.Type.avatarLetterLibHeader.copy(fontFamily = family) + private val lineBox = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None) + private val platform = PlatformTextStyle(includeFontPadding = false) + private fun resolveStyle(style: TextStyle) = style.copy(fontFamily = family, lineHeightStyle = lineBox, platformStyle = platform) + val screenTitle = resolveStyle(WolfskinTokens.Type.screenTitle) + val stateTitle = resolveStyle(WolfskinTokens.Type.stateTitle) + val heading = resolveStyle(WolfskinTokens.Update.heading) + val nav = resolveStyle(WolfskinTokens.Type.nav) + val wordmark = resolveStyle(WolfskinTokens.Type.wordmark) + val listTitle = resolveStyle(WolfskinTokens.Type.listTitle) + val body = resolveStyle(WolfskinTokens.Type.body) + val button = resolveStyle(WolfskinTokens.Type.button) + val label = resolveStyle(WolfskinTokens.Type.label) + val caption = resolveStyle(WolfskinTokens.Type.caption) + val avatarLetterNav = resolveStyle(WolfskinTokens.Type.avatarLetterNav) + val avatarLetterLibHeader = resolveStyle(WolfskinTokens.Type.avatarLetterLibHeader) } 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 5c231d6a..d7e40012 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,12 @@ object WolfskinTokens { } object Update { val avatarJustin = ComposeColor(0xFF7AB8FF) + val wordmarkWidth = 81.796875.dp + val navHomeWidth = 74.6640625.dp + val navDiscoverWidth = 99.0703125.dp + val navMoviesWidth = 85.2578125.dp + val navShowsWidth = 80.578125.dp + val navAnimeWidth = 79.234375.dp val notesListTop = 14.0.dp val notesListBottom = 14.0.dp val notesMarkerOffset = 5.5.dp diff --git a/wolfskin-design/src/main/res/drawable/wolfskin_settings_chevron.xml b/wolfskin-design/src/main/res/drawable/wolfskin_settings_chevron.xml new file mode 100644 index 00000000..bbf83f40 --- /dev/null +++ b/wolfskin-design/src/main/res/drawable/wolfskin_settings_chevron.xml @@ -0,0 +1,3 @@ + + + diff --git a/wolfskin-design/src/test/java/org/justinvisser/wolfskin/design/ShellCaptureTest.kt b/wolfskin-design/src/test/java/org/justinvisser/wolfskin/design/ShellCaptureTest.kt index 6b508fd3..21a783d8 100644 --- a/wolfskin-design/src/test/java/org/justinvisser/wolfskin/design/ShellCaptureTest.kt +++ b/wolfskin-design/src/test/java/org/justinvisser/wolfskin/design/ShellCaptureTest.kt @@ -68,7 +68,7 @@ class ShellCaptureTest { @Test fun st014_discoverNav() = capture("st-014") { it.pickerVisible = false; it.navFocused = true; it.navIndex = 1 } @Test fun st091_settings() = capture("st-091") { it.pickerVisible = false; it.enterDestination("settings") } @Test fun st092_requestsCountFixture() = capture("st-092", requestCount = 5) { it.pickerVisible = false; it.enterDestination("settings") } - @Test fun st093_profileFocus() = capture("st-093") { it.pickerVisible = false; it.enterDestination("settings"); it.settingsRow = 4 } + @Test fun st093_profileFocus() = capture("st-093") { it.pickerVisible = false; it.enterDestination("settings"); it.settingsRow = 5 } } /** Tests and debug builds share the same shell-only projection of the locked fixture. */ diff --git a/wolfskin-design/src/test/java/org/justinvisser/wolfskin/design/ShellKeysTest.kt b/wolfskin-design/src/test/java/org/justinvisser/wolfskin/design/ShellKeysTest.kt index ea452a66..3cb4445c 100644 --- a/wolfskin-design/src/test/java/org/justinvisser/wolfskin/design/ShellKeysTest.kt +++ b/wolfskin-design/src/test/java/org/justinvisser/wolfskin/design/ShellKeysTest.kt @@ -46,6 +46,22 @@ class ShellKeysTest { assertFalse("Requests content, not the nav, must own focus", state.navFocused) } + @Test fun updatesUseTwoPressesFromRequestsWithoutClassicHandOff() { + val actions = mutableListOf() + val shell = ShellState(fixturePeople(), "justin", actions::add).apply { + pickerVisible = false + enterDestination("settings") + } + rule.setContent { WolfskinTheme { ShellSurface(shell) } } + rule.onNodeWithTag("wolfskin-shell").performKeyInput { + pressKey(Key.DirectionDown) + pressKey(Key.DirectionCenter) + } + assertEquals(1, shell.settingsRow) + assertNull(shell.handOff) + assertEquals(listOf(ShellAction.Destination("updates")), actions) + } + @Test fun settingsBackBackInTenPresses() { replay(settings + listOf(Key.Back, Key.Back), 10) assertEquals("home", state.route) diff --git a/wolfskin-design/tokens.css b/wolfskin-design/tokens.css index de9f79e7..7804b4f5 100644 --- a/wolfskin-design/tokens.css +++ b/wolfskin-design/tokens.css @@ -517,6 +517,12 @@ --discover-ambientInset: 80.0px; --discover-ambientBlur: 60.0px; --update-avatarJustin: #7ab8ff; + --update-wordmarkWidth: 163.59375px; + --update-navHomeWidth: 149.328125px; + --update-navDiscoverWidth: 198.140625px; + --update-navMoviesWidth: 170.515625px; + --update-navShowsWidth: 161.15625px; + --update-navAnimeWidth: 158.46875px; --update-notesListTop: 28.0px; --update-notesListBottom: 28.0px; --update-notesMarkerOffset: 11.0px; diff --git a/wolfskin-design/tokens.sha256 b/wolfskin-design/tokens.sha256 index 5b6dd97f..2a9dd865 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 -1b8222d01dc30cbe2591097218a4a93c6ac4b24704cf81b18174b7839eb57e31 src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt +a951ee1e5c433789e22cc995a7ed279d170b24d5bfbd1672fa9799c9282c6fb8 src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt e0a2f5df2f249bce3f6e13f4a0320c7b18b85777eaf04a27a428d6683a6aad28 title-tokens.json -9a4c316e83af3207dd21fa7a3b4a70711460f38b740ee1de3ec9b042b3d4f9a8 tokens.css +27b0e44afaece06f732d24ff847140da6175ea352de620fa6de9d7e9e4c101fa tokens.css 2e5e0476341adec1ec05331af38bc67cd02ba013a0988aea40a70eff21513264 tokens.json -1bedb8e8079b4a954651d8761d18b8be4adb3933370e5f296be27287401ae460 update-tokens.json +19728f99b90bc40c0ebec37c5bcc45b383b89b023211490bb3235c1690237ae5 update-tokens.json diff --git a/wolfskin-design/update-tokens.json b/wolfskin-design/update-tokens.json index a6065c06..57d2454b 100644 --- a/wolfskin-design/update-tokens.json +++ b/wolfskin-design/update-tokens.json @@ -1,6 +1,12 @@ { "update": { "avatarJustin": {"$type": "color", "$value": "#7ab8ff"}, + "wordmarkWidth": {"$type": "dimension", "px": 163.59375, "dp": 81.796875}, + "navHomeWidth": {"$type": "dimension", "px": 149.328125, "dp": 74.6640625}, + "navDiscoverWidth": {"$type": "dimension", "px": 198.140625, "dp": 99.0703125}, + "navMoviesWidth": {"$type": "dimension", "px": 170.515625, "dp": 85.2578125}, + "navShowsWidth": {"$type": "dimension", "px": 161.15625, "dp": 80.578125}, + "navAnimeWidth": {"$type": "dimension", "px": 158.46875, "dp": 79.234375}, "notesListTop": {"$type": "dimension", "px": 28, "dp": 14}, "notesListBottom": {"$type": "dimension", "px": 28, "dp": 14}, "notesMarkerOffset": {"$type": "dimension", "px": 11, "dp": 5.5},