diff options
| author | Kalle Lindström <karl.lindstrom@mullvad.net> | 2025-03-10 10:54:53 +0100 |
|---|---|---|
| committer | Kalle Lindström <karl.lindstrom@mullvad.net> | 2025-03-10 10:54:53 +0100 |
| commit | ffbb4f5db993e7494573a3fffd12d53c0cfec369 (patch) | |
| tree | 2d913695d871287951b5768bac9cb6a1274dc34f | |
| parent | fe3e0b38e494353f24a0e7488316fa1feda1bac7 (diff) | |
| download | mullvadvpn-improve-tv-experience-module.tar.xz mullvadvpn-improve-tv-experience-module.zip | |
| -rw-r--r-- | android/lib/tv/build.gradle.kts | 4 | ||||
| -rw-r--r-- | android/lib/tv/src/main/kotlin/net/mullvad/mullvadvpn/lib/tv/NavigationDrawerTv.kt | 171 |
2 files changed, 116 insertions, 59 deletions
diff --git a/android/lib/tv/build.gradle.kts b/android/lib/tv/build.gradle.kts index c6b262d3fa..2434e2ca7e 100644 --- a/android/lib/tv/build.gradle.kts +++ b/android/lib/tv/build.gradle.kts @@ -40,4 +40,8 @@ dependencies { implementation(projects.lib.shared) implementation(projects.lib.sharedCompose) implementation(projects.lib.theme) + + // UI tooling + implementation(libs.compose.ui.tooling.preview) + debugImplementation(libs.compose.ui.tooling) } diff --git a/android/lib/tv/src/main/kotlin/net/mullvad/mullvadvpn/lib/tv/NavigationDrawerTv.kt b/android/lib/tv/src/main/kotlin/net/mullvad/mullvadvpn/lib/tv/NavigationDrawerTv.kt index 4df3524d6c..6b114bf529 100644 --- a/android/lib/tv/src/main/kotlin/net/mullvad/mullvadvpn/lib/tv/NavigationDrawerTv.kt +++ b/android/lib/tv/src/main/kotlin/net/mullvad/mullvadvpn/lib/tv/NavigationDrawerTv.kt @@ -4,13 +4,17 @@ import androidx.activity.compose.BackHandler import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.selection.selectableGroup import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Settings @@ -26,85 +30,72 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.tv.material3.DrawerValue import androidx.tv.material3.ModalNavigationDrawer import androidx.tv.material3.NavigationDrawerItem +import androidx.tv.material3.NavigationDrawerItemDefaults import androidx.tv.material3.rememberDrawerState +class DrawerValueProvider : PreviewParameterProvider<DrawerValue> { + override val values: Sequence<DrawerValue> + get() = sequenceOf(DrawerValue.Closed, DrawerValue.Open) +} + +@Preview("Closed|Open") +@Composable +fun PreviewNavigationDrawerTvClosed( + @PreviewParameter(DrawerValueProvider::class) drawerValue: DrawerValue +) { + NavigationDrawerTv( + daysLeftUntilExpiry = 30, + deviceName = "Cool Cat", + initialDrawerValue = drawerValue, + onSettingsClick = {}, + onAccountClick = {}, + ) {} +} + @Composable @Suppress("LongMethod") fun NavigationDrawerTv( daysLeftUntilExpiry: Long?, deviceName: String?, + initialDrawerValue: DrawerValue = DrawerValue.Closed, onSettingsClick: (() -> Unit), onAccountClick: (() -> Unit), content: @Composable () -> Unit, ) { - val drawerState = rememberDrawerState(DrawerValue.Closed) + val drawerState = rememberDrawerState(initialDrawerValue) if (drawerState.currentValue == DrawerValue.Open) { BackHandler(onBack = { drawerState.setValue(DrawerValue.Closed) }) } + val brush = Brush.horizontalGradient(listOf(Color.Black, Color.Transparent)) + ModalNavigationDrawer( + drawerState = drawerState, + scrimBrush = brush, drawerContent = { - Column( - Modifier.background( - Brush.horizontalGradient(listOf(Color.Black, Color.Transparent)) - ) - .fillMaxHeight() - .padding(12.dp), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.SpaceBetween, + val animatedPadding = animateDpAsState(if (hasFocus) 20.dp else 16.dp) + Box( + Modifier.fillMaxHeight() + .background(brush) + .padding(top = 24.dp, start = 12.dp, end = 12.dp) + .selectableGroup() ) { - val animatedPadding = animateDpAsState(if (hasFocus) 4.dp else 0.dp) - Column(modifier = Modifier.weight(1f)) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - painter = painterResource(id = R.drawable.logo_icon), - contentDescription = null, // No meaningful user info or action. - modifier = - Modifier.padding(start = animatedPadding.value) - // .padding(16.dp) - .size(32.dp), - tint = Color.Unspecified, // Logo should not be tinted - ) - if (hasFocus) { - Icon( - modifier = Modifier.height(16.dp), - painter = painterResource(id = R.drawable.logo_text), - contentDescription = null, // No meaningful user info or action. - tint = Color.Unspecified, // Logo should not be tinted - ) - } - } - - if (hasFocus) { - Text( - text = - stringResource( - id = R.string.top_bar_time_left, - pluralStringResource( - id = R.plurals.days, - daysLeftUntilExpiry?.toInt() ?: 0, - daysLeftUntilExpiry ?: 0, - ), - ), - maxLines = 1, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onPrimary, - ) - Text( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.onPrimary, - text = deviceName ?: "", - maxLines = 1, - overflow = TextOverflow.Clip, - ) - } - } + NavigationDrawerTvHeader( + modifier = + Modifier.align(Alignment.TopStart).padding(start = animatedPadding.value), + isExpanded = hasFocus, + daysLeftUntilExpiry = daysLeftUntilExpiry, + deviceName = deviceName, + ) NavigationDrawerItem( - modifier = Modifier.weight(1f), + modifier = Modifier.align(Alignment.CenterStart), onClick = onAccountClick, selected = false, leadingContent = { @@ -123,8 +114,9 @@ fun NavigationDrawerTv( overflow = TextOverflow.Clip, ) } + NavigationDrawerItem( - modifier = Modifier.weight(1f), + modifier = Modifier.align(Alignment.BottomStart).padding(bottom = 24.dp), onClick = onSettingsClick, selected = false, leadingContent = { @@ -145,8 +137,69 @@ fun NavigationDrawerTv( } } }, - drawerState = drawerState, - scrimBrush = Brush.horizontalGradient(listOf(Color.Black, Color.Transparent)), content = content, ) } + +@Composable +private fun NavigationDrawerTvHeader( + modifier: Modifier = Modifier, + isExpanded: Boolean, + daysLeftUntilExpiry: Long?, + deviceName: String?, +) { + Column( + modifier = + modifier.width( + if (isExpanded) NavigationDrawerItemDefaults.ExpandedDrawerItemWidth + else NavigationDrawerItemDefaults.CollapsedDrawerItemWidth + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Icon( + modifier = Modifier.size(32.dp), + painter = painterResource(id = R.drawable.logo_icon), + contentDescription = null, // No meaningful user info or action. + tint = Color.Unspecified, // Logo should not be tinted + ) + if (isExpanded) { + Icon( + modifier = Modifier.height(13.dp), + painter = painterResource(id = R.drawable.logo_text), + contentDescription = null, // No meaningful user info or action. + tint = Color.Unspecified, // Logo should not be tinted + ) + } + } + Spacer(Modifier.height(6.dp)) + + if (isExpanded) { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.top_bar_device_name, deviceName ?: ""), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onPrimary, + maxLines = 1, + overflow = TextOverflow.Clip, + ) + Text( + text = + stringResource( + id = R.string.top_bar_time_left, + pluralStringResource( + id = R.plurals.days, + daysLeftUntilExpiry?.toInt() ?: 0, + daysLeftUntilExpiry ?: 0, + ), + ), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onPrimary, + maxLines = 1, + overflow = TextOverflow.Clip, + ) + } + } +} |
