Align TV layouts and close recovery gaps
This commit is contained in:
parent
bfe3dbbb16
commit
04b1bdd6aa
22 changed files with 522 additions and 53 deletions
|
|
@ -27,7 +27,8 @@ import androidx.compose.ui.graphics.RectangleShape
|
|||
import androidx.compose.ui.graphics.shadow.Shadow
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.layout.FirstBaseline
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.tv.material3.Text
|
||||
import org.justinvisser.wolfskin.design.WolfskinTokens.Color as C
|
||||
import org.justinvisser.wolfskin.design.WolfskinTokens.Discover as D
|
||||
|
|
@ -49,7 +50,7 @@ fun DiscoverOverlay(state: DiscoverState) {
|
|||
is DiscoverOverlayState.Hidden -> {
|
||||
ReceiptPanel("Hidden from your picks", "${overlay.title.title} leaves every row of ${state.shell.person.name}’s picks. It waits under Hidden picks at the end of Discover.")
|
||||
Row(Modifier.padding(top = D.sheetButtonTop), horizontalArrangement = Arrangement.spacedBy(L.buttonGap)) {
|
||||
DiscoverButton("Undo", overlay.focus == 0) { state.restore(overlay.title) }
|
||||
DiscoverButton("Undo", overlay.focus == 0) { state.restore(overlay.title, showToast = false) }
|
||||
DiscoverButton("Done", overlay.focus == 1, primary = false) { overlay.focus = 1; state.key(ShellKey.Ok) }
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +60,7 @@ fun DiscoverOverlay(state: DiscoverState) {
|
|||
Modifier.offset(y = D.sheetSubTop).padding(bottom = D.sheetSubGap + D.sheetSubTop).discoverLineBox(DiscoverType.body),
|
||||
color = C.muted, style = DiscoverType.body)
|
||||
Row(Modifier.padding(top = D.sheetButtonTop), horizontalArrangement = Arrangement.spacedBy(L.buttonGap)) {
|
||||
DiscoverButton("Show again", overlay.focus == 0) { state.restore(overlay.title) }
|
||||
DiscoverButton("Show again", overlay.focus == 0) { state.restore(overlay.title, showToast = true) }
|
||||
DiscoverButton("Back", overlay.focus == 1, primary = false) { state.overlay = null }
|
||||
}
|
||||
}
|
||||
|
|
@ -72,15 +73,10 @@ fun DiscoverOverlay(state: DiscoverState) {
|
|||
|
||||
@Composable
|
||||
private fun RequestSheet(state: DiscoverState, sheet: RequestSheetState) {
|
||||
Text(if (sheet.held) "Add for someone" else "Request", color = C.fg, style = DiscoverType.sheetTitle)
|
||||
Text(if (sheet.held) "Add for someone" else "Request", Modifier.exactLineBox(DiscoverType.sheetTitle), color = C.fg, style = DiscoverType.sheetTitle)
|
||||
Text("${sheet.title.title} · ${sheet.title.typeWord}${sheet.title.year?.let { " · $it" }.orEmpty()}",
|
||||
Modifier.offset(y = D.sheetSubTop).padding(bottom = D.sheetSubGap + D.sheetSubTop), color = C.muted, style = DiscoverType.body)
|
||||
if (state.isSignedIn) DiscoverRatings(sheet.title.ratings, Modifier.layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
layout(placeable.width, placeable.height + (D.sheetRatingTop + D.sheetRatingBottom).roundToPx()) {
|
||||
placeable.placeRelative(0, D.sheetRatingTop.roundToPx())
|
||||
}
|
||||
})
|
||||
Modifier.exactLineBox(DiscoverType.body, top = D.sheetSubTop, bottom = D.sheetSubGap), color = C.muted, style = DiscoverType.body)
|
||||
if (state.isSignedIn) SheetRatings(sheet.title.ratings)
|
||||
when (sheet.phase) {
|
||||
RequestPhase.Choose, RequestPhase.Sending -> RequestChoices(state, sheet)
|
||||
RequestPhase.LoadingRecipients -> {
|
||||
|
|
@ -100,6 +96,28 @@ private fun RequestSheet(state: DiscoverState, sheet: RequestSheetState) {
|
|||
}
|
||||
}
|
||||
|
||||
/** The inline ratings baseline includes the first mark; the inherited text strut keeps its descent. */
|
||||
@Composable
|
||||
private fun SheetRatings(ratings: List<DiscoverRating>) {
|
||||
if (ratings.isEmpty()) return
|
||||
Layout(content = {
|
||||
DiscoverRatings(ratings)
|
||||
Text(" ", color = C.muted, style = DiscoverType.body)
|
||||
}) { children, constraints ->
|
||||
val row = children[0].measure(constraints.copy(minHeight = 0))
|
||||
val strut = children[1].measure(constraints.copy(minWidth = 0, minHeight = 0))
|
||||
val mark = DiscoverType.ratings.fontSize.toPx()
|
||||
val baseline = (row.height + mark) / 2f
|
||||
val descent = maxOf(row.height - baseline, (strut.height - strut[FirstBaseline]).toFloat())
|
||||
val height = maxOf(baseline, strut[FirstBaseline].toFloat()) + descent
|
||||
val top = D.sheetRatingTop.roundToPx()
|
||||
val bottom = D.sheetRatingBottom.roundToPx()
|
||||
layout(row.width, kotlin.math.round(height).toInt() + top + bottom) {
|
||||
row.placeRelative(0, top)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RequestChoices(state: DiscoverState, sheet: RequestSheetState) {
|
||||
Text("For", Modifier.padding(top = D.sheetLabelTop), color = C.dim, style = DiscoverType.label)
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ class DiscoverState(
|
|||
ShellKey.Ok -> when (titleButton) {
|
||||
0 -> if (requests[requireNotNull(title).key]?.any { it.requester == shell.person.key } == true) shell.requestsVisible = true else openRequest(false)
|
||||
1 -> openRequest(true)
|
||||
2 -> title?.let { if (it.key in hidden) restore(it) else hide(it) }
|
||||
2 -> title?.let { if (it.key in hidden) restore(it, showToast = true) else hide(it) }
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
|
@ -328,13 +328,13 @@ class DiscoverState(
|
|||
ShellKey.Left, ShellKey.Up -> sheet.focus = 0
|
||||
ShellKey.Right, ShellKey.Down -> sheet.focus = 1
|
||||
ShellKey.Back -> overlay = null
|
||||
ShellKey.Ok -> if (sheet.focus == 0) restore(sheet.title) else { overlay = null; title = null; onPop() }
|
||||
ShellKey.Ok -> if (sheet.focus == 0) restore(sheet.title, showToast = false) else { overlay = null; title = null; onPop() }
|
||||
}
|
||||
is DiscoverOverlayState.Restore -> when (key) {
|
||||
ShellKey.Left, ShellKey.Up -> sheet.focus = 0
|
||||
ShellKey.Right, ShellKey.Down -> sheet.focus = 1
|
||||
ShellKey.Back -> overlay = null
|
||||
ShellKey.Ok -> if (sheet.focus == 0) restore(sheet.title) else overlay = null
|
||||
ShellKey.Ok -> if (sheet.focus == 0) restore(sheet.title, showToast = true) else overlay = null
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
|
|
@ -380,9 +380,9 @@ class DiscoverState(
|
|||
}
|
||||
}
|
||||
|
||||
fun restore(item: DiscoverTitle) {
|
||||
fun restore(item: DiscoverTitle, showToast: Boolean) {
|
||||
if (!canRequest) { refresh(); return }
|
||||
hidden.remove(item.key); revision++; overlay = null; toast = "Back in your picks."
|
||||
hidden.remove(item.key); revision++; overlay = null; toast = if (showToast) "Back in your picks." else null
|
||||
updateCollectionCards()
|
||||
val profile = shell.person.key
|
||||
val version = (dismissalVersions[item.key] ?: 0) + 1
|
||||
|
|
@ -412,7 +412,7 @@ class DiscoverState(
|
|||
val lanes = service?.let {
|
||||
serviceLanes(it, filter).filter { lane -> lane.titles.isNotEmpty() }
|
||||
} ?: projectDiscoverLanes(snapshot, heroes, filter, hidden)
|
||||
return if (snapshot.savedAt != null) lanes + DiscoverLane("offline-recovery", "Try again", emptyList(), tail = DiscoverTail.Retry) else lanes
|
||||
return if (snapshot.savedAt != null) lanes + DiscoverLane("offline-recovery", "More picks", emptyList(), tail = DiscoverTail.Retry) else lanes
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,23 +4,45 @@ package org.justinvisser.wolfskin.design
|
|||
class HomeRatingCache(private val clock: () -> Long = System::currentTimeMillis) {
|
||||
private data class Entry(val ratings: List<HomeRating>, val readAt: Long)
|
||||
private val entries = linkedMapOf<String, Entry>()
|
||||
private val seasons = linkedMapOf<String, MutableMap<Int, Entry>>()
|
||||
|
||||
fun read(title: HomeTitle): List<HomeRating>? {
|
||||
val key = key(title)
|
||||
val entry = entries[key] ?: return null
|
||||
if (clock() - entry.readAt >= lifetimeMillis) { entries.remove(key); return null }
|
||||
val season = seasonKey(title)
|
||||
val bucket = season?.let { seasons[it] }
|
||||
val entry = if (season != null) bucket?.get(title.episode) else entries[key(title)]
|
||||
if (entry == null) return null
|
||||
if (clock() - entry.readAt >= lifetimeMillis) {
|
||||
if (season == null) entries.remove(key(title)) else {
|
||||
bucket?.remove(title.episode)
|
||||
if (bucket.isNullOrEmpty()) seasons.remove(season)
|
||||
}
|
||||
return null
|
||||
}
|
||||
return entry.ratings
|
||||
}
|
||||
|
||||
fun save(title: HomeTitle, ratings: List<HomeRating>) {
|
||||
val key = key(title)
|
||||
entries.remove(key)
|
||||
entries[key] = Entry(ratings, clock())
|
||||
while (entries.size > capacity) entries.remove(entries.keys.first())
|
||||
val entry = Entry(ratings, clock())
|
||||
val season = seasonKey(title)
|
||||
if (season != null) {
|
||||
val bucket = seasons.remove(season) ?: mutableMapOf()
|
||||
bucket[requireNotNull(title.episode)] = entry
|
||||
seasons[season] = bucket
|
||||
while (seasons.size > capacity) seasons.remove(seasons.keys.first())
|
||||
} else {
|
||||
val key = key(title)
|
||||
entries.remove(key)
|
||||
entries[key] = entry
|
||||
while (entries.size > capacity) entries.remove(entries.keys.first())
|
||||
}
|
||||
}
|
||||
|
||||
private fun key(title: HomeTitle): String = if (title.isEpisode) "episode:${title.id}" else "${title.type}:${title.tmdbId ?: title.id}"
|
||||
|
||||
private fun seasonKey(title: HomeTitle): String? =
|
||||
if (title.isEpisode && title.tmdbId != null && title.season != null && title.episode != null)
|
||||
"${title.tmdbId}:${title.season}" else null
|
||||
|
||||
companion object {
|
||||
private const val capacity = 300
|
||||
private val lifetimeMillis = (WolfskinTokens.Behaviour.cacheRatingsHours * 60 * 60 * 1000).toLong()
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ fun LibraryScreen(
|
|||
}
|
||||
LaunchedEffect(grid.savedAt) { view.savedAt = grid.savedAt }
|
||||
LaunchedEffect(grid.status) {
|
||||
if (grid.status == LibraryStatus.Loading) shell.focusNav(shell.currentTab)
|
||||
if (grid.status == LibraryStatus.Loading && !shell.navFocused) shell.focusNav(shell.currentTab)
|
||||
}
|
||||
val headerCount = if (grid.hasToolbar && grid.status != LibraryStatus.Loading && grid.status != LibraryStatus.Unavailable) 2 else 1
|
||||
val rowHeight = K.posterH + K.posterCaptionBand + L.gridRowGap
|
||||
|
|
@ -128,7 +128,10 @@ fun LibraryScreen(
|
|||
LibraryStatus.Unavailable -> item(key = "unavailable", contentType = "state") { LibraryRecovery(grid, active) }
|
||||
LibraryStatus.Ready -> if (items.isEmpty()) item(key = "empty", contentType = "state") { LibraryRecovery(grid, active && !grid.toolbar) }
|
||||
else {
|
||||
items((items.size + grid.columns - 1) / grid.columns, key = { row -> "grid-${items[row * grid.columns].id}" }, contentType = { "grid" }) { row ->
|
||||
items((items.size + grid.columns - 1) / grid.columns, key = { row ->
|
||||
val title = items[row * grid.columns]
|
||||
"grid-${if (grid.catalogue) title.catalogueKey else title.id}"
|
||||
}, contentType = { "grid" }) { row ->
|
||||
Box(Modifier.fillMaxWidth().height(rowHeight)) {
|
||||
Row(Modifier.padding(start = L.safeSide), horizontalArrangement = Arrangement.spacedBy(K.posterGap)) {
|
||||
for (column in 0 until grid.columns) {
|
||||
|
|
@ -174,7 +177,7 @@ fun LibraryHeader(person: ShellPerson, title: String, subtitle: String = "", sav
|
|||
Box(Modifier.fillMaxWidth().height(L.row_lhead)) {
|
||||
Row(Modifier.offset(L.safeSide, L.navBand), horizontalArrangement = Arrangement.spacedBy(WolfskinTokens.Shell.settingsHeaderGap), verticalAlignment = Alignment.CenterVertically) {
|
||||
Avatar(person, WolfskinTokens.Shell.settingsHeaderAvatar, WolfskinTokens.Type.avatarLetterLibHeader, letterColor = C.muted)
|
||||
Text(title, modifier = Modifier.alignByBaseline(), color = C.fg, style = WolfskinTokens.Type.screenTitle)
|
||||
Text(title, modifier = Modifier.alignByBaseline().exactLineBox(WolfskinTokens.Type.screenTitle), color = C.fg, style = WolfskinTokens.Type.screenTitle)
|
||||
if (subtitle.isNotEmpty()) Text(subtitle, modifier = Modifier.alignByBaseline(), color = C.muted, style = G.headerSubtitle)
|
||||
savedAt?.let { SavedPill(it) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,8 +98,8 @@ fun SettingsRow(group: SettingsGroup, focused: Boolean, openRequests: Int?, modi
|
|||
Glyph(group.key, if (focused) C.onFocus else C.accentText, Modifier.size(S.settingsIcon))
|
||||
Column(Modifier.weight(1f)) {
|
||||
val count = if (openRequests != null && openRequests > 0) " · $openRequests open" else ""
|
||||
Text(group.title + count, color = foreground, style = WolfskinTokens.Type.listTitle, maxLines = 1)
|
||||
Text(group.description, color = if (focused) C.onFocus else C.muted, style = WolfskinTokens.Type.caption, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
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)
|
||||
}
|
||||
if (group.wolfskin) SettingsPill("Wolfskin", focused)
|
||||
SettingsPill(if (group.key == "requests") "Wolfskin screen" else "upstream screen", focused)
|
||||
|
|
|
|||
|
|
@ -91,6 +91,22 @@ class DiscoverKeysTest {
|
|||
assertNull(state.overlay)
|
||||
assertEquals("The Amazing Spider-Man", state.hero?.title)
|
||||
assertEquals(original, state.lanes.map { lane -> lane.id to lane.titles.map { it.key } })
|
||||
assertNull("Undo restores silently; only Show again announces the return", state.toast)
|
||||
}
|
||||
|
||||
@Test fun showAgainRestoresTheHiddenPickAndAnnouncesItsReturn() {
|
||||
replay("Round 3 · Not interested on a pick (Discover hero)")
|
||||
val title = (state.overlay as DiscoverOverlayState.Hidden).title
|
||||
press(Key.DirectionRight, Key.DirectionCenter)
|
||||
rule.runOnIdle {
|
||||
val lane = state.lanes.first { it.hidden }
|
||||
state.focus(state.lanes.indexOf(lane) + 2, lane.titles.indexOfFirst { it.key == title.key })
|
||||
}
|
||||
press(Key.DirectionCenter)
|
||||
assertTrue(state.overlay is DiscoverOverlayState.Restore)
|
||||
press(Key.DirectionCenter)
|
||||
assertFalse(state.isHidden(title))
|
||||
assertEquals("Back in your picks.", state.toast)
|
||||
}
|
||||
|
||||
@Test fun heldOkOpensSheetButNeverSubmits() {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,34 @@ class HomeRatingCacheTest {
|
|||
assertEquals(ratings, cache.read(title.copy(id = "last", tmdbId = 399)))
|
||||
}
|
||||
|
||||
@Test fun episodeScoresShareSeriesSeasonIdentityWithoutCrossingEpisodesOrSeasons() {
|
||||
var now = 0L
|
||||
val cache = HomeRatingCache { now }
|
||||
val episode = HomeTitle("episode-a", "Pilot", "episode", tmdbId = 42, season = 1, episode = 1)
|
||||
val pilot = listOf(HomeRating("tmdb", 8.1, episode = true))
|
||||
val second = listOf(HomeRating("tmdb", 7.2, episode = true))
|
||||
cache.save(episode, pilot)
|
||||
cache.save(episode.copy(id = "episode-b", episode = 2), second)
|
||||
assertEquals(pilot, cache.read(episode.copy(id = "another-jellyfin-copy")))
|
||||
assertEquals(second, cache.read(episode.copy(id = "another-second-copy", episode = 2)))
|
||||
assertNull(cache.read(episode.copy(season = 2)))
|
||||
assertNull(cache.read(episode.copy(tmdbId = 43)))
|
||||
now = 6L * 60 * 60 * 1000
|
||||
assertNull(cache.read(episode))
|
||||
}
|
||||
|
||||
@Test fun seasonCapacityCountsSeasonsNotEpisodesAndIsIndependentOfTitleCapacity() {
|
||||
val cache = HomeRatingCache { 0L }
|
||||
val episode = HomeTitle("episode-a", "Pilot", "episode", tmdbId = 42, season = 1, episode = 1)
|
||||
repeat(301) { index -> cache.save(episode.copy(id = "episode-$index", episode = index + 1), ratings) }
|
||||
repeat(300) { index -> cache.save(title.copy(tmdbId = index + 100), ratings) }
|
||||
assertEquals(ratings, cache.read(episode))
|
||||
repeat(300) { index -> cache.save(episode.copy(season = index + 2), ratings) }
|
||||
assertNull(cache.read(episode))
|
||||
assertEquals(ratings, cache.read(episode.copy(season = 301)))
|
||||
assertEquals(ratings, cache.read(title.copy(tmdbId = 100)))
|
||||
}
|
||||
|
||||
@Test fun decimalScoresFollowLocaleButPercentagesDoNot() {
|
||||
assertEquals("7,8", ratings.single().format(Locale.forLanguageTag("nl-NL")))
|
||||
assertEquals("7.8", ratings.single().format(Locale.US))
|
||||
|
|
|
|||
|
|
@ -172,6 +172,23 @@ class LibraryKeysTest {
|
|||
assertEquals("Title 18", grid.focusedTitle!!.title)
|
||||
}
|
||||
|
||||
@Test fun mixedCatalogueRowsKeepMovieAndSeriesWithTheSameNumericIdentityDistinct() {
|
||||
val titles = List(12) { index ->
|
||||
val type = if (index < 6) "movie" else "tv"
|
||||
LibraryTitle((index % 6).toString(), "$type title ${index % 6}", type, tmdbId = (index % 6).toString())
|
||||
}
|
||||
val grid = LibraryGridState("Trending", titles, catalogue = true, onTitle = { opened = it })
|
||||
shell.pickerVisible = false
|
||||
shell.enterDestination("discover")
|
||||
rule.setContent { WolfskinTheme { ShellSurface(shell) { LibraryScreen(shell, grid, it) } } }
|
||||
press(Key.DirectionDown)
|
||||
assertEquals(6, grid.index)
|
||||
press(Key.DirectionCenter)
|
||||
assertEquals("tv", opened!!.type)
|
||||
assertEquals("0", opened!!.tmdbId)
|
||||
assertEquals("tv title 0", opened!!.title)
|
||||
}
|
||||
|
||||
@Test fun scoresFollowLocaleAndUnratedSortsLast() {
|
||||
assertEquals("7,8", LibraryRating("tmdb", 7.8).formatted(Locale.forLanguageTag("nl-NL")))
|
||||
assertEquals("7.8", LibraryRating("tmdb", 7.8).formatted(Locale.US))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
// 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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue