Round 2 candidate 2: merge origin/main
This commit is contained in:
commit
ed103bcb96
74 changed files with 4267 additions and 201 deletions
|
|
@ -19,18 +19,24 @@ android {
|
|||
}
|
||||
kotlin.compilerOptions.jvmTarget = JvmTarget.JVM_11
|
||||
|
||||
val robolectricSdk by configurations.creating {
|
||||
isCanBeResolved = true
|
||||
isCanBeConsumed = false
|
||||
isTransitive = false
|
||||
}
|
||||
val cacheRobolectricSdk by tasks.registering(Copy::class) {
|
||||
from(robolectricSdk)
|
||||
val versions = listOf(
|
||||
"9-robolectric-4913185-2-i7",
|
||||
"11-robolectric-6757853-i7",
|
||||
"14-robolectric-10818077-i7",
|
||||
"16-robolectric-13921718-i7",
|
||||
)
|
||||
// One configuration would evict the older SDKs through version conflict resolution.
|
||||
from(versions.map { version ->
|
||||
project.configurations.detachedConfiguration(
|
||||
project.dependencies.create("org.robolectric:android-all-instrumented:$version"),
|
||||
).apply { isTransitive = false }
|
||||
})
|
||||
into(layout.buildDirectory.dir("robolectric-sdk"))
|
||||
}
|
||||
|
||||
val checkTokenDrift by tasks.registering {
|
||||
inputs.files(fileTree(projectDir) { include("tokens.json", "shell-tokens.json", "title-tokens.json", "home-tokens.json", "discover-tokens.json", "generate_tokens.py", "tokens.css", "tokens.sha256", "src/main/java/**/WolfskinTokens.kt") })
|
||||
inputs.files(fileTree(projectDir) { include("tokens.json", "shell-tokens.json", "title-tokens.json", "home-tokens.json", "discover-tokens.json", "update-tokens.json", "generate_tokens.py", "tokens.css", "tokens.sha256", "src/main/java/**/WolfskinTokens.kt") })
|
||||
doLast {
|
||||
file("tokens.sha256").readLines().forEach { line ->
|
||||
val (expected, name) = line.split(" ", limit = 2)
|
||||
|
|
@ -50,10 +56,6 @@ tasks.withType<Test>().configureEach {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
robolectricSdk("org.robolectric:android-all-instrumented:9-robolectric-4913185-2-i7")
|
||||
robolectricSdk("org.robolectric:android-all-instrumented:11-robolectric-6757853-i7")
|
||||
robolectricSdk("org.robolectric:android-all-instrumented:14-robolectric-10818077-i7")
|
||||
robolectricSdk("org.robolectric:android-all-instrumented:16-robolectric-13921718-i7")
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.tv.material)
|
||||
|
|
|
|||
|
|
@ -121,9 +121,10 @@ def main() -> None:
|
|||
tokens.update(json.loads((ROOT / "title-tokens.json").read_text()))
|
||||
tokens.update(json.loads((ROOT / "home-tokens.json").read_text()))
|
||||
tokens.update(json.loads((ROOT / "discover-tokens.json").read_text()))
|
||||
tokens.update(json.loads((ROOT / "update-tokens.json").read_text()))
|
||||
kotlin, css = render_tokens(tokens)
|
||||
outputs = {str(KOTLIN): kotlin, "tokens.css": css}
|
||||
inputs = ["tokens.json", "shell-tokens.json", "title-tokens.json", "home-tokens.json", "discover-tokens.json", "generate_tokens.py"]
|
||||
inputs = ["tokens.json", "shell-tokens.json", "title-tokens.json", "home-tokens.json", "discover-tokens.json", "update-tokens.json", "generate_tokens.py"]
|
||||
checksums = {name: hashlib.sha256((ROOT / name).read_bytes()).hexdigest() for name in inputs}
|
||||
checksums.update({name: hashlib.sha256(content.encode()).hexdigest() for name, content in outputs.items()})
|
||||
outputs["tokens.sha256"] = "".join(f"{checksum} {name}\n" for name, checksum in sorted(checksums.items()))
|
||||
|
|
|
|||
47
wolfskin-design/src/main/assets/update-font-license.txt
Normal file
47
wolfskin-design/src/main/assets/update-font-license.txt
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
DejaVu Sans and DejaVu Sans Bold
|
||||
https://dejavu-fonts.github.io/
|
||||
|
||||
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.
|
||||
Bitstream Vera is a trademark of Bitstream, Inc.
|
||||
DejaVu changes are in public domain.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of the fonts accompanying this license ("Fonts") and associated
|
||||
documentation files (the "Font Software"), to reproduce and distribute the
|
||||
Font Software, including without limitation the rights to use, copy, merge,
|
||||
publish, distribute, and/or sell copies of the Font Software, and to permit
|
||||
persons to whom the Font Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright and trademark notices and this permission notice shall
|
||||
be included in all copies of one or more of the Font Software typefaces.
|
||||
|
||||
The Font Software may be modified, altered, or added to, and in particular
|
||||
the designs of glyphs or characters in the Fonts may be modified and
|
||||
additional glyphs or characters may be added to the Fonts, only if the fonts
|
||||
are renamed to names not containing either the words "Bitstream" or the word
|
||||
"Vera".
|
||||
|
||||
This License becomes null and void to the extent applicable to Fonts or Font
|
||||
Software that has been modified and is distributed under the "Bitstream
|
||||
Vera" names.
|
||||
|
||||
The Font Software may be sold as part of a larger software package but no
|
||||
copy of one or more of the Font Software typefaces may be sold by itself.
|
||||
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
|
||||
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
|
||||
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
|
||||
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
|
||||
FONT SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the names of Gnome, the Gnome
|
||||
Foundation, and Bitstream Inc., shall not be used in advertising or
|
||||
otherwise to promote the sale, use or other dealings in this Font Software
|
||||
without prior written authorization from the Gnome Foundation or Bitstream
|
||||
Inc., respectively. For further information, contact: fonts at gnome dot
|
||||
org.
|
||||
|
|
@ -40,7 +40,7 @@ private class CssColumnHeight : ParentDataModifier {
|
|||
|
||||
/** Fractional line heights accumulate before rounding, rather than adding a rounded pixel per row. */
|
||||
@Composable
|
||||
internal fun CssColumn(modifier: Modifier = Modifier, gap: Dp = WolfskinTokens.Shell.zero, content: @Composable () -> Unit) {
|
||||
fun CssColumn(modifier: Modifier = Modifier, gap: Dp = WolfskinTokens.Shell.zero, content: @Composable () -> Unit) {
|
||||
val exact = remember { CssColumnHeight() }
|
||||
Layout(content, modifier.then(exact)) { children, constraints ->
|
||||
val measured = Array(children.size) { children[it].measure(constraints.copy(minWidth = 0, minHeight = 0)) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -21,19 +21,24 @@ fun personColor(key: String): Color = when (key.lowercase()) {
|
|||
else -> WolfskinTokens.Color.accent
|
||||
}
|
||||
|
||||
/** Updates has its own locked avatar swatch; other destinations retain household colours. */
|
||||
fun resolveUpdateAvatarColor(person: ShellPerson): Color =
|
||||
if (person.key.equals("justin", ignoreCase = true)) WolfskinTokens.Update.avatarJustin else personColor(person.key)
|
||||
|
||||
/** The same mark anchors the picker and the persistent navigation. */
|
||||
@Composable
|
||||
fun Wordmark(modifier: Modifier = Modifier) {
|
||||
fun Wordmark(modifier: Modifier = Modifier, prefix: String = "Wolf", textStyle: TextStyle = WolfskinTokens.Type.wordmark) {
|
||||
Row(modifier) {
|
||||
Text("Wolf", color = WolfskinTokens.Color.fg, style = WolfskinTokens.Type.wordmark)
|
||||
Text("skin", color = WolfskinTokens.Color.accent, style = WolfskinTokens.Type.wordmark)
|
||||
Text(prefix, color = WolfskinTokens.Color.fg, style = textStyle)
|
||||
Text("skin", color = WolfskinTokens.Color.accent, style = textStyle)
|
||||
}
|
||||
}
|
||||
|
||||
/** Initials are drawn over opaque person colour at the exact slot size. */
|
||||
@Composable
|
||||
fun Avatar(person: ShellPerson, diameter: Dp, textStyle: TextStyle, modifier: Modifier = Modifier, letterColor: Color = WolfskinTokens.Color.onFocus) {
|
||||
Box(modifier.size(diameter).background(personColor(person.key), CircleShape), contentAlignment = Alignment.Center) {
|
||||
fun Avatar(person: ShellPerson, diameter: Dp, textStyle: TextStyle, modifier: Modifier = Modifier, letterColor: Color = WolfskinTokens.Color.onFocus,
|
||||
backgroundColor: Color = personColor(person.key)) {
|
||||
Box(modifier.size(diameter).background(backgroundColor, CircleShape), contentAlignment = Alignment.Center) {
|
||||
Text(person.initial, color = letterColor, style = textStyle)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,7 +58,8 @@ 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
|
||||
|
|
@ -79,14 +81,15 @@ fun NavBar(state: ShellState, scrolled: Boolean, modifier: Modifier = Modifier)
|
|||
Box(modifier.fillMaxWidth().height(L.navBand)) {
|
||||
Canvas(Modifier.matchParentSize()) {
|
||||
if (scrolled) drawRect(C.bg) else drawRect(band)
|
||||
drawRoundRect(color, Offset(x * density, y * density), Size(width * density, height * density), CornerRadius(height * density / 2))
|
||||
if (state.navFocused || hasSelectedTab) drawRoundRect(color, Offset(x * density, y * density), Size(width * density, height * density), CornerRadius(height * density / 2))
|
||||
}
|
||||
Wordmark(Modifier.offset(L.safeSide, S.wordmarkTop))
|
||||
Wordmark(Modifier.offset(L.safeSide, S.wordmarkTop), if (hasSelectedTab) "Wolf" else "wolf",
|
||||
if (hasSelectedTab) WolfskinTokens.Type.wordmark else UpdateTypography.wordmark)
|
||||
state.tabs.forEachIndexed { itemIndex, tab ->
|
||||
val item = boxes[itemIndex]
|
||||
val focused = state.navFocused && state.navIndex == itemIndex
|
||||
val labelColor by animateColorAsState(
|
||||
if (focused) C.onFocus else if (itemIndex == state.currentTab) C.fg else C.muted,
|
||||
if (focused) C.onFocus else if (hasSelectedTab && itemIndex == state.currentTab) C.fg else C.muted,
|
||||
animationSpec = labelFade,
|
||||
label = "nav-label-$tab",
|
||||
)
|
||||
|
|
@ -95,8 +98,8 @@ fun NavBar(state: ShellState, scrolled: Boolean, modifier: Modifier = Modifier)
|
|||
.clickable {
|
||||
state.focusNav(itemIndex); state.key(ShellKey.Ok)
|
||||
}, contentAlignment = Alignment.Center) {
|
||||
Text(tab.replaceFirstChar { it.uppercase() }, style = WolfskinTokens.Type.nav, color = labelColor)
|
||||
if (itemIndex == state.currentTab && !focused) {
|
||||
Text(tab.replaceFirstChar { it.uppercase() }, style = if (hasSelectedTab) WolfskinTokens.Type.nav else UpdateTypography.nav, color = labelColor)
|
||||
if (hasSelectedTab && itemIndex == state.currentTab && !focused) {
|
||||
Box(Modifier.align(Alignment.BottomCenter).offset(y = -L.navUnderline).size(item.width - L.navItemPaddingX * 2, L.navUnderline).background(C.accent))
|
||||
}
|
||||
}
|
||||
|
|
@ -111,10 +114,11 @@ fun NavBar(state: ShellState, scrolled: Boolean, modifier: Modifier = Modifier)
|
|||
}
|
||||
}
|
||||
val avatar = boxes.last()
|
||||
Avatar(state.person, L.navAvatar, WolfskinTokens.Type.avatarLetterNav,
|
||||
Avatar(state.person, L.navAvatar, if (hasSelectedTab) WolfskinTokens.Type.avatarLetterNav else UpdateTypography.avatarLetterNav,
|
||||
Modifier.offset(avatar.left, avatar.top)
|
||||
.focusRing(avatarFocused, L.navAvatar / 2, WolfskinTokens.Focus.ringAvatarRing)
|
||||
.focusProperties { canFocus = false }
|
||||
.clickable { state.focusNav(state.tabs.size); state.key(ShellKey.Ok) })
|
||||
.clickable { state.focusNav(state.tabs.size); state.key(ShellKey.Ok) },
|
||||
backgroundColor = if (hasSelectedTab) personColor(state.person.key) else resolveUpdateAvatarColor(state.person))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,13 +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
|
||||
|
|
@ -36,50 +35,53 @@ import androidx.tv.material3.Text
|
|||
import org.justinvisser.wolfskin.design.WolfskinTokens.Color as C
|
||||
import org.justinvisser.wolfskin.design.WolfskinTokens.Layout as L
|
||||
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, and how far each is"),
|
||||
SettingsGroup("interface", "Interface", "Sign in automatically · Remember selected tabs · Theme music · Subtitle style · Application theme · Screensaver"),
|
||||
SettingsGroup("playback", "Playback", "Skip forward · Skip back · Skip back when resuming"),
|
||||
SettingsGroup("nextup", "Next up", "Show next up timing · Auto play next up · Delay · Passout protection"),
|
||||
SettingsGroup("profile", "User profile settings", "Profile protection · Customize home page (Wolfskin presets: Default, Compact, Series thumbs, Episode thumbnails) · Navigation drawer items · Language", true),
|
||||
SettingsGroup("about", "About", "Installed version · Update (Wolfskin releases on Forgejo) · Wolfskin about", true),
|
||||
SettingsGroup("seerr", "Seerr integration", "Add, change or remove the Seerr server this TV asks for Discover and ratings", true),
|
||||
SettingsGroup("advanced", "Advanced settings", "Ratings display · Backdrop style · Playback backend (ExoPlayer, MPV) · Max resolution and bitrate · Skip segments · Logs and image cache"),
|
||||
SettingsGroup("requests", "Requests", "Coming in: what the household asked for"),
|
||||
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"),
|
||||
SettingsGroup("profile", "User profile settings", "Protection · Home · Language"),
|
||||
SettingsGroup("about", "About", "Installed version · Wolfskin about"),
|
||||
SettingsGroup("seerr", "Seerr integration", "Manage the Seerr server"),
|
||||
SettingsGroup("advanced", "Advanced settings", "Playback backend · Logs · Image cache"),
|
||||
SettingsGroup("classic", "Classic settings (trial)", "Original settings, including About → Update"),
|
||||
)
|
||||
|
||||
/** The count describes returned request history, not an inferred queue of active downloads. */
|
||||
@Composable
|
||||
fun SettingsList(state: ShellState, modifier: Modifier = Modifier, requestCount: Int? = null) {
|
||||
fun SettingsList(state: ShellState, modifier: Modifier = Modifier, requestCount: Int? = null, installedVersion: String = "") {
|
||||
val list = state.view.listState
|
||||
val reduced = reducedMotion()
|
||||
val density = LocalDensity.current
|
||||
LaunchedEffect(state.settingsRow, state.navFocused) {
|
||||
if (state.navFocused) return@LaunchedEffect
|
||||
val y = L.row_lhead + L.row_srow * state.settingsRow
|
||||
val target = if (y >= L.windowAhead) y - L.pinnedLine else S.zero
|
||||
val current = if (list.firstVisibleItemIndex == 0) S.zero else L.row_lhead + L.row_srow * (list.firstVisibleItemIndex - 1)
|
||||
val distance = with(density) { (target - current).toPx() } - list.firstVisibleItemScrollOffset
|
||||
list.animateScrollBy(distance, tween(if (NetworkArtTransition.reduced) 0 else WolfskinTokens.Motion.scroll, easing = WolfskinTokens.Motion.easeOut))
|
||||
val target = (state.settingsRow - WolfskinTokens.Update.settingsVisibleRows.toInt() + 1).coerceAtLeast(0)
|
||||
val distance = with(density) { (L.row_srow * (target - list.firstVisibleItemIndex)).toPx() } - list.firstVisibleItemScrollOffset
|
||||
list.animateScrollBy(distance, tween(if (reduced) 0 else WolfskinTokens.Motion.scroll, easing = WolfskinTokens.Motion.easeOut))
|
||||
}
|
||||
LazyColumn(modifier.fillMaxSize(), state = list,
|
||||
contentPadding = PaddingValues(bottom = S.stageHeight - L.pinnedLine - L.row_srow)) {
|
||||
item(key = "settings-header") {
|
||||
Row(Modifier.height(L.row_lhead).padding(start = L.safeSide, top = S.settingsHeaderTop),
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(S.settingsHeaderGap)) {
|
||||
Avatar(state.person, S.settingsHeaderAvatar, WolfskinTokens.Type.avatarLetterLibHeader, letterColor = C.muted)
|
||||
Text("Settings", Modifier.alignByBaseline(), style = WolfskinTokens.Type.screenTitle, color = C.fg)
|
||||
Text("Requests and Wholphin settings on this TV · ${state.person.name}", Modifier.alignByBaseline(), style = WolfskinTokens.Type.nav.copy(fontWeight = FontWeight.Normal), color = C.muted)
|
||||
}
|
||||
Box(modifier.fillMaxSize()) {
|
||||
Row(Modifier.offset(L.safeSide, WolfskinTokens.Update.headerTop).height(WolfskinTokens.Update.headerHeight),
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(S.settingsHeaderGap)) {
|
||||
Avatar(state.person, S.settingsHeaderAvatar, T.avatarLetterLibHeader, letterColor = C.muted, backgroundColor = resolveUpdateAvatarColor(state.person))
|
||||
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)
|
||||
}
|
||||
itemsIndexed(SETTINGS_GROUPS, key = { _, group -> group.key }) { index, group ->
|
||||
Box(Modifier.height(L.row_srow)) {
|
||||
SettingsRow(group, !state.navFocused && !state.requestsVisible && index == state.settingsRow,
|
||||
if (group.key == "requests") requestCount else null,
|
||||
Modifier.offset(L.safeSide, S.settingsRowTop)) {
|
||||
state.navFocused = false; state.settingsRow = index; state.key(ShellKey.Ok)
|
||||
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)) {
|
||||
val display = if (group.key == "updates") group.copy(description = "$installedVersion · Check and install") else group
|
||||
SettingsRow(display, !state.navFocused && !state.requestsVisible && index == state.settingsRow,
|
||||
if (group.key == "requests") requestCount else null, Modifier.offset(L.safeSide)) {
|
||||
state.navFocused = false; state.settingsRow = index; state.key(ShellKey.Ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -94,15 +96,14 @@ fun SettingsRow(group: SettingsGroup, focused: Boolean, requestCount: Int?, modi
|
|||
.background(if (focused) C.fg else C.listRowRest, RoundedCornerShape(WolfskinTokens.Radius.listRow))
|
||||
.clickable(onClick = onClick).padding(horizontal = S.settingsRowPadding),
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(S.settingsRowGap)) {
|
||||
Glyph(group.key, if (focused) C.onFocus else C.accentText, Modifier.size(S.settingsIcon))
|
||||
Glyph(if (group.key == "updates") "updates" else "settings-list", if (focused) C.onFocus else C.accentText, Modifier.size(S.settingsIcon))
|
||||
Column(Modifier.weight(1f)) {
|
||||
val count = requestCount?.let { " · $it ${if (it == 1) "request" else "requests"}" } ?: ""
|
||||
Text(group.title + count, Modifier.exactLineBox(WolfskinTokens.Type.listTitle), color = foreground, style = WolfskinTokens.Type.listTitle, maxLines = 1)
|
||||
Text(group.description, Modifier.exactLineBox(WolfskinTokens.Type.caption), color = if (focused) C.onFocus else C.muted, style = WolfskinTokens.Type.caption, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(group.title + count, Modifier.exactLineBox(T.listTitle), color = foreground, style = T.listTitle, maxLines = 1)
|
||||
Text(group.description, Modifier.exactLineBox(T.caption), color = if (focused) C.onFocus else C.muted, style = T.caption, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
if (group.wolfskin) SettingsPill("Wolfskin", focused)
|
||||
SettingsPill(if (group.key == "requests") "Wolfskin screen" else "upstream screen", focused)
|
||||
Glyph("right", if (focused) C.onFocus else C.accentText, Modifier.size(S.settingsChevron))
|
||||
SettingsPill(if (group.key == "requests" || group.key == "updates") "Wolfskin screen" else "upstream screen", focused)
|
||||
Glyph("settings-right", if (focused) C.onFocus else C.accentText, Modifier.size(S.settingsIcon))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,6 +123,6 @@ private fun SettingsPill(label: String, focused: Boolean) {
|
|||
drawRoundRect(border, topLeft = inset, size = bounds, cornerRadius = radius, style = outline)
|
||||
}
|
||||
}.padding(horizontal = S.settingsPillPadding + S.settingsPillStroke), contentAlignment = Alignment.Center) {
|
||||
Text(label, color = foreground, style = WolfskinTokens.Type.caption.copy(fontWeight = WolfskinTokens.Type.label.fontWeight))
|
||||
Text(label, color = foreground, style = T.caption.copy(fontWeight = T.label.fontWeight))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,9 +265,13 @@ class ShellState(
|
|||
}
|
||||
} else if (route == "settings") {
|
||||
val group = SETTINGS_GROUPS[settingsRow].key
|
||||
if (group == "requests") openRequests() else {
|
||||
handOff = group
|
||||
onAction(ShellAction.HandOff(group))
|
||||
when (group) {
|
||||
"requests" -> openRequests()
|
||||
"updates" -> onAction(ShellAction.Destination("updates"))
|
||||
else -> {
|
||||
handOff = group
|
||||
onAction(ShellAction.HandOff(group))
|
||||
}
|
||||
}
|
||||
}
|
||||
else contentFocus?.key(ShellKey.Ok)
|
||||
|
|
@ -309,6 +313,7 @@ class ShellState(
|
|||
pickerLaunch = false
|
||||
gearFocused = true
|
||||
} else if (route !in tabs) {
|
||||
if (route == "updates") navFocused = false
|
||||
onAction(ShellAction.Pop)
|
||||
} else if (!navFocused) {
|
||||
navFocused = true
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ fun ShellSurface(
|
|||
modifier: Modifier = Modifier,
|
||||
handlesContentKeys: Boolean = true,
|
||||
settingsRequestCount: Int? = null,
|
||||
settingsInstalledVersion: String = "",
|
||||
overlay: @Composable () -> Unit = {},
|
||||
content: @Composable (Modifier) -> Unit = { ShellPlaceholder(state.route) },
|
||||
) {
|
||||
|
|
@ -95,7 +96,7 @@ fun ShellSurface(
|
|||
if (state.pickerVisible) {
|
||||
WhosWatching(state)
|
||||
} else {
|
||||
if (state.route == "settings") SettingsList(state, requestCount = settingsRequestCount ?: state.requests.snapshot.requestCount) else content(Modifier.fillMaxSize().focusRequester(contentFocus).focusRestorer().focusGroup())
|
||||
if (state.route == "settings") SettingsList(state, requestCount = settingsRequestCount ?: state.requests.snapshot.requestCount, installedVersion = settingsInstalledVersion) else content(Modifier.fillMaxSize().focusRequester(contentFocus).focusRestorer().focusGroup())
|
||||
NavBar(state, view.listState.firstVisibleItemIndex > 0 || view.listState.firstVisibleItemScrollOffset > L.navSolidAfter.value * 2)
|
||||
if (state.requestsVisible) ComingInSheet(state)
|
||||
overlay()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
// 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
|
||||
|
||||
/** 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))
|
||||
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)
|
||||
}
|
||||
|
|
@ -221,6 +221,9 @@ object WolfskinTokens {
|
|||
const val fillInverse = "background color.fg, text color.onFocus"
|
||||
}
|
||||
object Motion {
|
||||
const val updateScan = 1800
|
||||
const val updateSheen = 1600
|
||||
val linear = CubicBezierEasing(0.0f, 0.0f, 1.0f, 1.0f)
|
||||
const val skeletonCycle = 1400
|
||||
const val skeletonLowAlpha = 0.55f
|
||||
const val focusMove = 180
|
||||
|
|
@ -703,4 +706,48 @@ object WolfskinTokens {
|
|||
const val ambientSaturation = 1.2f
|
||||
const val ambientBrightness = 0.55f
|
||||
}
|
||||
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
|
||||
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
|
||||
val headerHeight = 42.0.dp
|
||||
val rowsTop = 139.0.dp
|
||||
val rowsViewport = 362.0.dp
|
||||
val contentTop = 149.0.dp
|
||||
val width = 650.0.dp
|
||||
val padding = 20.0.dp
|
||||
val border = 1.0.dp
|
||||
val radius = 11.0.dp
|
||||
val titleBottom = 8.0.dp
|
||||
val paragraphBottom = 7.0.dp
|
||||
val actionsTop = 18.0.dp
|
||||
val actionGap = 12.0.dp
|
||||
val progressTop = 15.0.dp
|
||||
val progressBottom = 11.0.dp
|
||||
val progressHeight = 6.0.dp
|
||||
val progressRadius = 4.0.dp
|
||||
val autoTop = 431.0.dp
|
||||
val toggleWidth = 40.0.dp
|
||||
val toggleHeight = 21.0.dp
|
||||
val toggleThumb = 15.0.dp
|
||||
val toggleInset = 3.0.dp
|
||||
val notesHeight = 256.0.dp
|
||||
val notesGap = 9.0.dp
|
||||
val notesScrollStep = 40.0.dp
|
||||
val backTop = 92.0.dp
|
||||
const val scanFraction = 0.28f
|
||||
const val scanEnd = 4.6f
|
||||
const val sheenAlpha = 0.35f
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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="M5,8 H27 M5,16 H27 M5,24 H27" android:strokeColor="#FFFFFFFF" android:strokeWidth="2.3" android:fillColor="@android:color/transparent" />
|
||||
</vector>
|
||||
|
|
@ -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="M26,14 A11,11 0,1 0,25 23 M26,5 V14 H17" android:strokeColor="#FFFFFFFF" android:strokeWidth="2.3" android:fillColor="@android:color/transparent" />
|
||||
</vector>
|
||||
BIN
wolfskin-design/src/main/res/font/update_dejavu_sans.ttf
Normal file
BIN
wolfskin-design/src/main/res/font/update_dejavu_sans.ttf
Normal file
Binary file not shown.
BIN
wolfskin-design/src/main/res/font/update_dejavu_sans_bold.ttf
Normal file
BIN
wolfskin-design/src/main/res/font/update_dejavu_sans_bold.ttf
Normal file
Binary file not shown.
|
|
@ -1,66 +0,0 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import java.io.File
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
/** The normal JVM gate rejects untokened styling, undersized typography and internal screen wording. */
|
||||
class OwnedSourceAuditTest {
|
||||
@Test fun ownedScreensUseTokensReadableTextAndViewerFacingNames() {
|
||||
val roots = listOf(File("src/main/java"), File("../app/src/wolfskin"))
|
||||
roots.forEach { assertTrue("Missing owned source root: $it", it.isDirectory) }
|
||||
val violations = roots.flatMap { root ->
|
||||
root.walkTopDown().filter { it.isFile && it.extension == "kt" }.flatMap { source ->
|
||||
audit(source.name, source.readText()).map { "$source:$it" }
|
||||
}.toList()
|
||||
}
|
||||
assertTrue(violations.joinToString("\n"), violations.isEmpty())
|
||||
}
|
||||
|
||||
@Test fun plantedViolationsFailEachDoneBarRule() {
|
||||
val cases = listOf(
|
||||
Triple("Screen.kt", "Box(Modifier.width(12.dp))", "unit"),
|
||||
Triple("Screen.kt", "Box(Modifier.width(12f.dp))", "unit"),
|
||||
Triple("Screen.kt", "Text(fontSize = 14.sp)", "unit"),
|
||||
Triple("Screen.kt", "Box(Modifier.width((width + gap).dp))", "unit"),
|
||||
Triple("Screen.kt", "Text(color = Color(0xFF123456))", "color"),
|
||||
Triple("Screen.kt", "Text(color = ComposeColor(red = 1f, green = 0f, blue = 0f))", "color"),
|
||||
Triple("WolfskinTokens.kt", "val tiny = TextStyle(fontSize = 11.5.sp)", "font-floor"),
|
||||
Triple("Screen.kt", "Text(\"Recs Movies\")", "wording"),
|
||||
Triple("Screen.kt", "Text(\"DGiC\")", "wording"),
|
||||
Triple("Screen.kt", "Text(\"Justin Shows\")", "wording"),
|
||||
Triple("Screen.kt", "Text(\"Next page\")", "wording"),
|
||||
Triple("Screen.kt", "Text(\"Previous page\")", "wording"),
|
||||
Triple("Screen.kt", "Text(\"Page 2\")", "wording"),
|
||||
)
|
||||
cases.forEach { (name, source, rule) ->
|
||||
assertTrue("$rule accepted $source", audit(name, source).any { it.endsWith(":$rule") })
|
||||
}
|
||||
assertTrue(audit("WolfskinTokens.kt", "val body = TextStyle(fontSize = 12.sp); val fill = Color(0xFF123456)").isEmpty())
|
||||
assertTrue(audit("Screen.kt", "import androidx.compose.ui.unit.dp\nText(\"This title can’t be read\", style = WolfskinTokens.Type.body)").isEmpty())
|
||||
}
|
||||
|
||||
private fun audit(name: String, source: String): List<String> {
|
||||
val generated = name == "WolfskinTokens.kt"
|
||||
return source.lineSequence().mapIndexedNotNull { index, line ->
|
||||
val code = line.trimStart()
|
||||
if (code.startsWith("import ") || code.startsWith("//") || code.startsWith("*") || code.startsWith("/*")) return@mapIndexedNotNull null
|
||||
val rules = buildList {
|
||||
if (!generated && units.containsMatchIn(line)) add("unit")
|
||||
if (!generated && colors.containsMatchIn(line)) add("color")
|
||||
if (fonts.findAll(line).any { it.groupValues[1].toDouble() < 12 }) add("font-floor")
|
||||
if (strings.findAll(line).any { forbidden.containsMatchIn(it.value) }) add("wording")
|
||||
}
|
||||
rules.map { "${index + 1}:$it" }
|
||||
}.flatten().toList()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val units = Regex("\\.(?:dp|sp)\\b")
|
||||
private val colors = Regex("\\b(?:Compose)?Color\\s*\\(\\s*(?:0[xX]|[0-9.]|(?:red|green|blue|alpha|color)\\s*=)")
|
||||
private val fonts = Regex("\\bfontSize\\s*=\\s*([0-9]+(?:\\.[0-9]+)?)[fF]?\\.sp\\b")
|
||||
private val strings = Regex("\"(?:\\\\.|[^\"\\\\])*\"")
|
||||
private val forbidden = Regex("Recs (?:Movies|Shows)|DGiC|(?:Justin|Mindel|Shared) (?:Movies|Shows)|(?:Next|Previous) page|Page (?:[0-9]|\\$)|\\bpagination\\b", RegexOption.IGNORE_CASE)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.Escape, Key.Escape), 10)
|
||||
assertEquals("home", state.route)
|
||||
|
|
@ -54,10 +70,10 @@ class ShellKeysTest {
|
|||
assertEquals(state.tabs.size, state.navIndex)
|
||||
}
|
||||
|
||||
@Test fun playbackHandOffInElevenPresses() {
|
||||
replay(settings + listOf(Key.DirectionDown, Key.DirectionDown, Key.DirectionCenter), 11)
|
||||
@Test fun playbackHandOffInTwelvePresses() {
|
||||
replay(settings + listOf(Key.DirectionDown, Key.DirectionDown, Key.DirectionDown, Key.DirectionCenter), 12)
|
||||
assertEquals("playback", state.handOff)
|
||||
assertEquals(2, state.settingsRow)
|
||||
assertEquals(3, state.settingsRow)
|
||||
}
|
||||
|
||||
@Test fun rootBackAndNavDownPreserveViewAndWrap() {
|
||||
|
|
|
|||
|
|
@ -135,6 +135,9 @@
|
|||
--layout-castCircle: 200.0px;
|
||||
--layout-personPortrait: 240.0px;
|
||||
--layout-row_chipsGrid: 136.0px;
|
||||
--motion-updateScan: 1800ms;
|
||||
--motion-updateSheen: 1600ms;
|
||||
--motion-linear: cubic-bezier(0, 0, 1, 1);
|
||||
--motion-skeletonCycle: 1400ms;
|
||||
--motion-focusMove: 180ms;
|
||||
--motion-captionDelay: 60ms;
|
||||
|
|
@ -519,4 +522,42 @@
|
|||
--discover-stageGap: 10.0px;
|
||||
--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;
|
||||
--update-notesMarkerWidth: 29.0px;
|
||||
--update-heading: 36.0px;
|
||||
--update-headerTop: 150.0px;
|
||||
--update-headerHeight: 84.0px;
|
||||
--update-rowsTop: 278.0px;
|
||||
--update-rowsViewport: 724.0px;
|
||||
--update-contentTop: 298.0px;
|
||||
--update-width: 1300.0px;
|
||||
--update-padding: 40.0px;
|
||||
--update-border: 2.0px;
|
||||
--update-radius: 22.0px;
|
||||
--update-titleBottom: 16.0px;
|
||||
--update-paragraphBottom: 14.0px;
|
||||
--update-actionsTop: 36.0px;
|
||||
--update-actionGap: 24.0px;
|
||||
--update-progressTop: 30.0px;
|
||||
--update-progressBottom: 22.0px;
|
||||
--update-progressHeight: 12.0px;
|
||||
--update-progressRadius: 8.0px;
|
||||
--update-autoTop: 862.0px;
|
||||
--update-toggleWidth: 80.0px;
|
||||
--update-toggleHeight: 42.0px;
|
||||
--update-toggleThumb: 30.0px;
|
||||
--update-toggleInset: 6.0px;
|
||||
--update-notesHeight: 512.0px;
|
||||
--update-notesGap: 18.0px;
|
||||
--update-notesScrollStep: 80.0px;
|
||||
--update-backTop: 184.0px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1400,6 +1400,9 @@
|
|||
}
|
||||
},
|
||||
"motion": {
|
||||
"updateScan": { "$type": "duration", "$value": "1800ms", "source": "approved Updates progress busy scan" },
|
||||
"updateSheen": { "$type": "duration", "$value": "1600ms", "source": "approved Updates download highlight" },
|
||||
"linear": { "$type": "cubicBezier", "$value": [0, 0, 1, 1] },
|
||||
"skeletonCycle": { "$type": "duration", "$value": "1400ms" },
|
||||
"skeletonLowAlpha": { "$type": "number", "$value": 0.55 },
|
||||
"focusMove": { "$type": "duration", "$value": "180ms" },
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
b3f096b692c395534177cc82ac68f5b801870bc7e28f0dd5cd7c17e2103920a3 discover-tokens.json
|
||||
c29dc64f3c3ca900c747b3db8b270e25908948bf5f76388d2c627a612fc7c99d generate_tokens.py
|
||||
993d933950e17fb72d0902ffc440e1e4ed4dc088c96a8d12974431c77dc31634 generate_tokens.py
|
||||
72b67ad9ba95e7bbc17cec29cc676b06b7e9d81c546022b5a100d6d4e2a27c64 home-tokens.json
|
||||
192beb887fc34005db99c87ffbcbaf4a057ee2728475fc96224cf89445019b51 shell-tokens.json
|
||||
8c47b082670b4805c9c961addee6ef8b567d874bf93d8f8dc4b160eef6d18b6a src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt
|
||||
42399062a6a22566e26adb71eaa4052b28be93d28d0eb46d9d4b87f3142a2944 src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt
|
||||
98fa6414cc6663624d09329615cdf0e6599baec570a8d878f5214c95cb33cb48 title-tokens.json
|
||||
f188d525547d20d5751688573d8a81f8a2cb6c050dfb0141b8378842a7ff8a9c tokens.css
|
||||
b1cf8f8caea9bfacb7994e0ba39e71ffab33452f6f5227a524ba71874b045b4d tokens.json
|
||||
731ff25d6ec3ba12d0840523a1ebd65a81b3474cf74040f13bbd1f54985a3053 tokens.css
|
||||
12d33b7b5740443e068cbb06528d88c42ad3bb1df0d4d45dd6ee6797d3dfc401 tokens.json
|
||||
19728f99b90bc40c0ebec37c5bcc45b383b89b023211490bb3235c1690237ae5 update-tokens.json
|
||||
|
|
|
|||
167
wolfskin-design/update-tokens.json
Normal file
167
wolfskin-design/update-tokens.json
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
{
|
||||
"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},
|
||||
"notesMarkerWidth": {"$type": "dimension", "px": 29, "dp": 14.5},
|
||||
"heading": {
|
||||
"$type": "typography",
|
||||
"$value": {"fontWeight": 800},
|
||||
"px": 36,
|
||||
"sp": 18,
|
||||
"weight": 800,
|
||||
"lineHeight": 1.12,
|
||||
"letterSpacingEm": -0.02
|
||||
},
|
||||
"settingsVisibleRows": {"$type": "number", "$value": 6},
|
||||
"headerTop": {
|
||||
"$type": "dimension",
|
||||
"px": 150,
|
||||
"dp": 75.0
|
||||
},
|
||||
"headerHeight": {
|
||||
"$type": "dimension",
|
||||
"px": 84,
|
||||
"dp": 42.0
|
||||
},
|
||||
"rowsTop": {
|
||||
"$type": "dimension",
|
||||
"px": 278,
|
||||
"dp": 139.0
|
||||
},
|
||||
"rowsViewport": {
|
||||
"$type": "dimension",
|
||||
"px": 724,
|
||||
"dp": 362.0
|
||||
},
|
||||
"contentTop": {
|
||||
"$type": "dimension",
|
||||
"px": 298,
|
||||
"dp": 149.0
|
||||
},
|
||||
"width": {
|
||||
"$type": "dimension",
|
||||
"px": 1300,
|
||||
"dp": 650.0
|
||||
},
|
||||
"padding": {
|
||||
"$type": "dimension",
|
||||
"px": 40,
|
||||
"dp": 20.0
|
||||
},
|
||||
"border": {
|
||||
"$type": "dimension",
|
||||
"px": 2,
|
||||
"dp": 1.0
|
||||
},
|
||||
"radius": {
|
||||
"$type": "dimension",
|
||||
"px": 22,
|
||||
"dp": 11.0
|
||||
},
|
||||
"titleBottom": {
|
||||
"$type": "dimension",
|
||||
"px": 16,
|
||||
"dp": 8.0
|
||||
},
|
||||
"paragraphBottom": {
|
||||
"$type": "dimension",
|
||||
"px": 14,
|
||||
"dp": 7.0
|
||||
},
|
||||
"actionsTop": {
|
||||
"$type": "dimension",
|
||||
"px": 36,
|
||||
"dp": 18.0
|
||||
},
|
||||
"actionGap": {
|
||||
"$type": "dimension",
|
||||
"px": 24,
|
||||
"dp": 12.0
|
||||
},
|
||||
"progressTop": {
|
||||
"$type": "dimension",
|
||||
"px": 30,
|
||||
"dp": 15.0
|
||||
},
|
||||
"progressBottom": {
|
||||
"$type": "dimension",
|
||||
"px": 22,
|
||||
"dp": 11.0
|
||||
},
|
||||
"progressHeight": {
|
||||
"$type": "dimension",
|
||||
"px": 12,
|
||||
"dp": 6.0
|
||||
},
|
||||
"progressRadius": {
|
||||
"$type": "dimension",
|
||||
"px": 8,
|
||||
"dp": 4.0
|
||||
},
|
||||
"autoTop": {
|
||||
"$type": "dimension",
|
||||
"px": 862,
|
||||
"dp": 431.0
|
||||
},
|
||||
"toggleWidth": {
|
||||
"$type": "dimension",
|
||||
"px": 80,
|
||||
"dp": 40.0
|
||||
},
|
||||
"toggleHeight": {
|
||||
"$type": "dimension",
|
||||
"px": 42,
|
||||
"dp": 21.0
|
||||
},
|
||||
"toggleThumb": {
|
||||
"$type": "dimension",
|
||||
"px": 30,
|
||||
"dp": 15.0
|
||||
},
|
||||
"toggleInset": {
|
||||
"$type": "dimension",
|
||||
"px": 6,
|
||||
"dp": 3.0
|
||||
},
|
||||
"notesHeight": {
|
||||
"$type": "dimension",
|
||||
"px": 512,
|
||||
"dp": 256.0
|
||||
},
|
||||
"notesGap": {
|
||||
"$type": "dimension",
|
||||
"px": 18,
|
||||
"dp": 9.0
|
||||
},
|
||||
"notesScrollStep": {
|
||||
"$type": "dimension",
|
||||
"px": 80,
|
||||
"dp": 40.0
|
||||
},
|
||||
"backTop": {
|
||||
"$type": "dimension",
|
||||
"px": 184,
|
||||
"dp": 92.0
|
||||
},
|
||||
"scanFraction": {
|
||||
"$type": "number",
|
||||
"$value": 0.28
|
||||
},
|
||||
"scanEnd": {
|
||||
"$type": "number",
|
||||
"$value": 4.6
|
||||
},
|
||||
"sheenAlpha": {
|
||||
"$type": "number",
|
||||
"$value": 0.35
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue