summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--android/lib/tv/build.gradle.kts4
-rw-r--r--android/lib/tv/src/main/kotlin/net/mullvad/mullvadvpn/lib/tv/NavigationDrawerTv.kt171
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,
+ )
+ }
+ }
+}