Match native updater frames and preserve D-pad focus behavior
This commit is contained in:
parent
2b8c729f62
commit
37ad87d058
16 changed files with 131 additions and 67 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<ComponentActivity>()
|
||||
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<Key>) {
|
||||
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)) }
|
||||
}
|
||||
|
|
@ -39,7 +39,6 @@ class UpdateFocus(
|
|||
}
|
||||
|
||||
fun focusAction(index: Int) { actionIndex = index; autoFocused = false }
|
||||
fun focusAuto() { autoFocused = true }
|
||||
|
||||
fun activate(index: Int) {
|
||||
focusAction(index)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<String>): List<NavBox> {
|
||||
var left = L.safeSide + S.wordmarkWidth + L.wordmarkGap + L.navItemGap
|
||||
fun navBoxes(tabs: List<String>, isUpdatesLayout: Boolean = false): List<NavBox> {
|
||||
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<String>): List<NavBox> {
|
|||
/** 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<Color>(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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="32dp" android:height="32dp" android:viewportWidth="32" android:viewportHeight="32">
|
||||
<path android:pathData="M12,6 L22,16 L12,26" android:strokeColor="#FFFFFFFF" android:strokeWidth="2.3" android:fillColor="@android:color/transparent" />
|
||||
</vector>
|
||||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -46,6 +46,22 @@ class ShellKeysTest {
|
|||
assertFalse("Requests content, not the nav, must own focus", state.navFocused)
|
||||
}
|
||||
|
||||
@Test fun updatesUseTwoPressesFromRequestsWithoutClassicHandOff() {
|
||||
val actions = mutableListOf<ShellAction>()
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue