diff --git a/app/src/main/java/com/runicgateway/app/ui/RunicApp.kt b/app/src/main/java/com/runicgateway/app/ui/RunicApp.kt index 053ade0..c4f221e 100644 --- a/app/src/main/java/com/runicgateway/app/ui/RunicApp.kt +++ b/app/src/main/java/com/runicgateway/app/ui/RunicApp.kt @@ -162,26 +162,29 @@ fun RunicApp( val capabilities by sessionViewModel.capabilities.collectAsStateWithLifecycle() // Re-validate the cached role each time the app returns to the foreground (ยง4.3), - // and re-read the unread count with it: a tickle that arrived while the app was - // away is exactly what brings someone back to it. - LifecycleResumeEffect(Unit) { + // and re-read the two drawer counts with it: a tickle that arrived while the app + // was away is exactly what brings someone back to it, and a live player count is + // only live if it is re-read when somebody looks. + LifecycleResumeEffect(capabilities) { sessionViewModel.revalidate() inboxBadgeViewModel.refresh() + // The Rust count is a LIVE number, so it is re-read on the same clock the + // unread badge is: coming back to the app is exactly when a stale one + // would be noticed. Keyed on the capability answer as well as on resume, + // because the very first resume happens before this host has said whether + // the module is there โ€” and asking then would either make a request on a + // site that has no Rust, or never make one at all. + capabilities?.let { rustBadgeViewModel.refresh(Capability.RUST in it) } onPauseOrDispose { } } val unread by inboxBadgeViewModel.unread.collectAsStateWithLifecycle() // How many people are on the Rust fleet, for the drawer row's badge โ€” the - // phone's answer to D15's footer count (M14). Refreshed with the capability - // answer rather than on a timer: a badge is a glance, not a feed, and this is - // the only place that knows whether the module is installed at all. A host - // that has not answered yet asks nothing, so a cold start makes no request - // until it knows there is something to ask about. + // phone's answer to D15's footer count (M14). Refreshed on resume, never on a + // timer: a badge is a glance, not a feed. Gated here rather than inside the + // view model because this is the only place that knows whether the module is + // installed at all, and a host that has not answered yet asks nothing. val rustOnline by rustBadgeViewModel.online.collectAsStateWithLifecycle() - LaunchedEffect(capabilities) { - val caps = capabilities - if (caps != null) rustBadgeViewModel.refresh(Capability.RUST in caps) - } // The badge follows the session, so signing out clears it rather than leaving // the previous account's count on the drawer. LaunchedEffect(session) { inboxBadgeViewModel.refresh() } diff --git a/app/src/main/java/com/runicgateway/app/ui/rust/RustServerScreen.kt b/app/src/main/java/com/runicgateway/app/ui/rust/RustServerScreen.kt index db7a543..7b6c2f5 100644 --- a/app/src/main/java/com/runicgateway/app/ui/rust/RustServerScreen.kt +++ b/app/src/main/java/com/runicgateway/app/ui/rust/RustServerScreen.kt @@ -24,8 +24,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -341,6 +343,12 @@ private fun FeedRow(row: RustEventDto) { */ private data class RustColumn(val labelRes: Int, val sort: String?, val value: (RustLeaderboardRowDto) -> String) +/** The name's share of the row against one numeric column's. */ +private const val NAME_WEIGHT = 1.7f + +/** How far a header that is not the current sort is faded. */ +private const val SORTED_AWAY = 0.55f + private val RUST_COLUMNS = listOf( RustColumn(R.string.rust_col_kills, RustSort.KILLS) { it.kills.toString() }, RustColumn(R.string.rust_col_deaths, RustSort.DEATHS) { it.deaths.toString() }, @@ -372,8 +380,15 @@ private fun LeaderboardPanel( ) { item { Row(Modifier.fillMaxWidth()) { - SectionLabel(stringResource(R.string.rust_col_player), Modifier.weight(1f)) + SectionLabel( + text = stringResource(R.string.rust_col_player), + modifier = Modifier.weight(NAME_WEIGHT), + ) RUST_COLUMNS.forEach { column -> + // The ACTIVE sort is marked on the header, not on the + // values: the header is the control, and tinting a + // column of numbers instead says "these are special" + // rather than "this is what the table is ordered by". SectionLabel( text = stringResource(column.labelRes), modifier = Modifier @@ -384,6 +399,13 @@ private fun LeaderboardPanel( } else { Modifier }, + ) + .then( + if (column.sort == sort) { + Modifier.alpha(1f) + } else { + Modifier.alpha(SORTED_AWAY) + }, ), ) } @@ -391,20 +413,23 @@ private fun LeaderboardPanel( } items(state.data, key = { it.steamId }) { row -> Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + // A wider share for the name, and one line with an ellipsis. + // Five numeric columns beside an equal-weight name column + // left "Brannock" touching its own kill count, which the + // walk read as one field. Text( text = playerLabel(row.name, row.steamId), style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(NAME_WEIGHT).padding(end = 8.dp), ) RUST_COLUMNS.forEach { column -> Text( text = column.value(row), style = MaterialTheme.typography.bodySmall, - color = if (column.sort == sort) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, modifier = Modifier.weight(1f), ) } @@ -456,7 +481,10 @@ private fun OnlinePanel( } items(s.data, key = { it.steamId }) { player -> ShardCard(Modifier.fillMaxWidth()) { - Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Row( + Modifier.fillMaxWidth().padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { Text( text = playerLabel(player.name, player.steamId), style = MaterialTheme.typography.bodyMedium, @@ -500,7 +528,10 @@ private fun WipesPanel( ShardCard( Modifier.fillMaxWidth().clickable { onOpenWipe(wipe.wipeId) }, ) { - Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Row( + Modifier.fillMaxWidth().padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { Text( text = wipeDay(wipe.saveCreatedAt ?: wipe.firstSeen) ?: wipe.wipeId, style = MaterialTheme.typography.bodyMedium, diff --git a/app/src/main/java/com/runicgateway/app/ui/rust/RustServersScreen.kt b/app/src/main/java/com/runicgateway/app/ui/rust/RustServersScreen.kt index fdb45bf..353b55e 100644 --- a/app/src/main/java/com/runicgateway/app/ui/rust/RustServersScreen.kt +++ b/app/src/main/java/com/runicgateway/app/ui/rust/RustServersScreen.kt @@ -100,6 +100,11 @@ private fun ServerList( @Composable private fun ServerRow(server: RustServerDto, onOpen: () -> Unit) { ShardCard(modifier = Modifier.fillMaxWidth().clickable(onClick = onOpen)) { + // `ShardCard` is the themed Card and nothing more โ€” it carries no padding + // of its own, so every caller pads its own content. Without this the text + // sits flush against the card's edge and the first glyph of each line + // reads as clipped, which is what the walk saw. + Column(Modifier.padding(16.dp)) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -143,6 +148,7 @@ private fun ServerRow(server: RustServerDto, onOpen: () -> Unit) { color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 2.dp), ) + } } }