Skip to content

Commit 6e085e5

Browse files
Create auto connect and lockdown mode screen
Co-Authored-By: Boban Sijuk <49131853+Boki91@users.noreply.github.com> Co-Authored-By: Boban Sijuk <49131853+Boki91@users.noreply.github.com>
1 parent 2c89a29 commit 6e085e5

File tree

6 files changed

+381
-0
lines changed

6 files changed

+381
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package net.mullvad.mullvadvpn.compose.component
2+
3+
import androidx.compose.foundation.ExperimentalFoundationApi
4+
import androidx.compose.foundation.Image
5+
import androidx.compose.foundation.background
6+
import androidx.compose.foundation.layout.Arrangement
7+
import androidx.compose.foundation.layout.Box
8+
import androidx.compose.foundation.layout.Row
9+
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.fillMaxWidth
11+
import androidx.compose.foundation.layout.padding
12+
import androidx.compose.foundation.layout.size
13+
import androidx.compose.foundation.layout.wrapContentHeight
14+
import androidx.compose.foundation.pager.HorizontalPager
15+
import androidx.compose.foundation.pager.rememberPagerState
16+
import androidx.compose.foundation.shape.CircleShape
17+
import androidx.compose.material3.Icon
18+
import androidx.compose.material3.IconButton
19+
import androidx.compose.material3.MaterialTheme
20+
import androidx.compose.material3.Text
21+
import androidx.compose.runtime.Composable
22+
import androidx.compose.runtime.rememberCoroutineScope
23+
import androidx.compose.ui.Alignment
24+
import androidx.compose.ui.Modifier
25+
import androidx.compose.ui.draw.alpha
26+
import androidx.compose.ui.draw.clip
27+
import androidx.compose.ui.draw.rotate
28+
import androidx.compose.ui.graphics.Color
29+
import androidx.compose.ui.res.painterResource
30+
import androidx.compose.ui.res.stringResource
31+
import androidx.compose.ui.text.SpanStyle
32+
import androidx.compose.ui.text.font.FontWeight
33+
import androidx.compose.ui.tooling.preview.Preview
34+
import androidx.constraintlayout.compose.ConstraintLayout
35+
import androidx.core.text.HtmlCompat
36+
import kotlinx.coroutines.launch
37+
import net.mullvad.mullvadvpn.R
38+
import net.mullvad.mullvadvpn.compose.extensions.toAnnotatedString
39+
import net.mullvad.mullvadvpn.lib.theme.AppTheme
40+
import net.mullvad.mullvadvpn.lib.theme.Dimens
41+
import net.mullvad.mullvadvpn.lib.theme.color.AlphaDescription
42+
43+
@Preview
44+
@Composable
45+
fun PreviewAutoConnectCarousel() {
46+
AppTheme { AutoConnectCarousel() }
47+
}
48+
49+
private const val CAROUSEL_PAGE_SIZE = 3
50+
51+
@OptIn(ExperimentalFoundationApi::class)
52+
@Composable
53+
fun AutoConnectCarousel() {
54+
val pagerState = rememberPagerState(pageCount = { CAROUSEL_PAGE_SIZE })
55+
HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page ->
56+
val scope = rememberCoroutineScope()
57+
ConstraintLayout(
58+
modifier = Modifier.fillMaxSize(),
59+
) {
60+
val (
61+
upperTextRef,
62+
backButtonRef,
63+
imageRef,
64+
nextButtonRef,
65+
lowerTextRef,
66+
pageIndicatorRef) =
67+
createRefs()
68+
Text(
69+
modifier =
70+
Modifier.padding(horizontal = Dimens.largePadding).constrainAs(upperTextRef) {
71+
start.linkTo(parent.start)
72+
end.linkTo(parent.end)
73+
bottom.linkTo(imageRef.top)
74+
},
75+
style = MaterialTheme.typography.labelMedium,
76+
color = MaterialTheme.colorScheme.onSecondary,
77+
text =
78+
HtmlCompat.fromHtml(
79+
stringResource(
80+
id =
81+
when (page) {
82+
0 -> R.string.auto_connect_carousel_first_slide_top_text
83+
1 -> R.string.auto_connect_carousel_second_slide_top_text
84+
else -> R.string.auto_connect_carousel_third_slide_top_text
85+
}
86+
),
87+
HtmlCompat.FROM_HTML_MODE_COMPACT
88+
)
89+
.toAnnotatedString(
90+
boldSpanStyle =
91+
SpanStyle(
92+
fontWeight = FontWeight.ExtraBold,
93+
color = MaterialTheme.colorScheme.onPrimary
94+
)
95+
)
96+
)
97+
if (page != 0) {
98+
IconButton(
99+
modifier =
100+
Modifier.constrainAs(backButtonRef) {
101+
top.linkTo(parent.top)
102+
start.linkTo(parent.start)
103+
bottom.linkTo(parent.bottom)
104+
},
105+
onClick = {
106+
scope.launch { pagerState.scrollToPage(pagerState.currentPage - 1) }
107+
},
108+
) {
109+
Icon(
110+
painter = painterResource(id = R.drawable.icon_chevron),
111+
contentDescription = null,
112+
tint = Color.Unspecified,
113+
modifier = Modifier.rotate(180f).alpha(AlphaDescription)
114+
)
115+
}
116+
}
117+
118+
Image(
119+
modifier =
120+
Modifier.padding(top = Dimens.topPadding, bottom = Dimens.bottomPadding)
121+
.constrainAs(imageRef) {
122+
top.linkTo(parent.top)
123+
start.linkTo(parent.start)
124+
bottom.linkTo(parent.bottom)
125+
end.linkTo(parent.end)
126+
},
127+
painter =
128+
when (page) {
129+
0 -> painterResource(id = R.drawable.carousel_slide_1_cogwheel)
130+
1 -> painterResource(id = R.drawable.carousel_slide_2_always_on)
131+
else -> painterResource(id = R.drawable.carousel_slide_3_block_connections)
132+
},
133+
contentDescription = null,
134+
)
135+
136+
if (page < CAROUSEL_PAGE_SIZE) {
137+
IconButton(
138+
modifier =
139+
Modifier.constrainAs(nextButtonRef) {
140+
top.linkTo(parent.top)
141+
end.linkTo(parent.end)
142+
bottom.linkTo(parent.bottom)
143+
},
144+
onClick = {
145+
scope.launch { pagerState.scrollToPage(pagerState.currentPage + 1) }
146+
}
147+
) {
148+
Icon(
149+
painter = painterResource(id = R.drawable.icon_chevron),
150+
contentDescription = null,
151+
tint = Color.Unspecified,
152+
modifier = Modifier.size(Dimens.titleIconSize).alpha(AlphaDescription)
153+
)
154+
}
155+
}
156+
Text(
157+
modifier =
158+
Modifier.padding(horizontal = Dimens.largePadding).constrainAs(lowerTextRef) {
159+
top.linkTo(imageRef.bottom)
160+
end.linkTo(parent.end)
161+
start.linkTo(parent.start)
162+
},
163+
style = MaterialTheme.typography.labelMedium,
164+
color = MaterialTheme.colorScheme.onSecondary,
165+
text =
166+
HtmlCompat.fromHtml(
167+
stringResource(
168+
id =
169+
when (page) {
170+
0 -> R.string.auto_connect_carousel_first_slide_bottom_text
171+
1 -> R.string.auto_connect_carousel_second_slide_bottom_text
172+
else ->
173+
R.string.auto_connect_carousel_third_slide_bottom_text
174+
}
175+
),
176+
HtmlCompat.FROM_HTML_MODE_COMPACT
177+
)
178+
.toAnnotatedString(
179+
boldSpanStyle =
180+
SpanStyle(
181+
fontWeight = FontWeight.ExtraBold,
182+
color = MaterialTheme.colorScheme.onPrimary
183+
)
184+
)
185+
)
186+
187+
Row(
188+
Modifier.wrapContentHeight()
189+
.fillMaxWidth()
190+
.padding(top = Dimens.topPadding)
191+
.constrainAs(pageIndicatorRef) {
192+
top.linkTo(lowerTextRef.bottom)
193+
194+
end.linkTo(parent.end)
195+
start.linkTo(parent.start)
196+
},
197+
horizontalArrangement = Arrangement.Center,
198+
verticalAlignment = Alignment.Bottom
199+
) {
200+
repeat(pagerState.pageCount) { iteration ->
201+
val color =
202+
if (pagerState.currentPage == iteration) MaterialTheme.colorScheme.onPrimary
203+
else MaterialTheme.colorScheme.primary
204+
Box(
205+
modifier =
206+
Modifier.padding(Dimens.indicatorPadding)
207+
.clip(CircleShape)
208+
.background(color)
209+
.size(Dimens.indicatorSize)
210+
)
211+
}
212+
}
213+
}
214+
}
215+
}

android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt

+60
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
1010
import androidx.compose.foundation.rememberScrollState
1111
import androidx.compose.foundation.verticalScroll
1212
import androidx.compose.material3.ExperimentalMaterial3Api
13+
import androidx.compose.material3.Icon
1314
import androidx.compose.material3.MaterialTheme
1415
import androidx.compose.material3.Scaffold
1516
import androidx.compose.material3.Snackbar
@@ -24,7 +25,11 @@ import androidx.compose.runtime.remember
2425
import androidx.compose.ui.Modifier
2526
import androidx.compose.ui.graphics.Color
2627
import androidx.compose.ui.input.nestedscroll.nestedScroll
28+
import androidx.compose.ui.res.painterResource
2729
import com.google.accompanist.systemuicontroller.rememberSystemUiController
30+
import net.mullvad.mullvadvpn.R
31+
import net.mullvad.mullvadvpn.compose.button.PrimaryButton
32+
import net.mullvad.mullvadvpn.lib.theme.Dimens
2833
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
2934
import net.mullvad.mullvadvpn.lib.theme.color.AlphaTopBar
3035

@@ -193,3 +198,58 @@ fun ScaffoldWithMediumTopBar(
193198
}
194199
)
195200
}
201+
202+
@OptIn(ExperimentalMaterial3Api::class)
203+
@Composable
204+
fun ScaffoldWithLargeTopBarAndButton(
205+
appBarTitle: String,
206+
modifier: Modifier = Modifier,
207+
navigationIcon: @Composable () -> Unit = {},
208+
actions: @Composable RowScope.() -> Unit = {},
209+
onButtonClick: () -> Unit = {}, // Add button
210+
buttonTitle: String,
211+
scrollbarColor: Color = MaterialTheme.colorScheme.onBackground.copy(alpha = AlphaScrollbar),
212+
content: @Composable (modifier: Modifier) -> Unit
213+
) {
214+
val appBarState = rememberTopAppBarState()
215+
val scrollState = rememberScrollState()
216+
val canScroll = scrollState.canScrollForward || scrollState.canScrollBackward
217+
val scrollBehavior =
218+
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState, canScroll = { canScroll })
219+
Scaffold(
220+
modifier = modifier.fillMaxSize().nestedScroll(scrollBehavior.nestedScrollConnection),
221+
topBar = {
222+
MullvadLargeTopBar(
223+
title = appBarTitle,
224+
navigationIcon = navigationIcon,
225+
actions,
226+
scrollBehavior = scrollBehavior
227+
)
228+
},
229+
bottomBar = {
230+
PrimaryButton(
231+
text = buttonTitle,
232+
onClick = onButtonClick,
233+
modifier =
234+
Modifier.padding(
235+
horizontal = Dimens.sideMargin,
236+
vertical = Dimens.screenVerticalMargin
237+
),
238+
icon = {
239+
Icon(
240+
painter = painterResource(id = R.drawable.icon_extlink),
241+
contentDescription = null
242+
)
243+
},
244+
)
245+
},
246+
content = {
247+
content(
248+
Modifier.fillMaxSize()
249+
.padding(it)
250+
.drawVerticalScrollbar(state = scrollState, color = scrollbarColor)
251+
.verticalScroll(scrollState)
252+
)
253+
}
254+
)
255+
}

android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt

+32
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.size
2020
import androidx.compose.material3.ExperimentalMaterial3Api
2121
import androidx.compose.material3.Icon
2222
import androidx.compose.material3.IconButton
23+
import androidx.compose.material3.LargeTopAppBar
2324
import androidx.compose.material3.MaterialTheme
2425
import androidx.compose.material3.MediumTopAppBar
2526
import androidx.compose.material3.Surface
@@ -186,6 +187,16 @@ private fun PreviewMediumTopBar() {
186187
}
187188
}
188189

190+
@Preview
191+
@Composable
192+
private fun PreviewLargeTopBar() {
193+
AppTheme {
194+
MullvadLargeTopBar(
195+
title = "Title",
196+
)
197+
}
198+
}
199+
189200
@Preview(widthDp = 260)
190201
@Composable
191202
private fun PreviewSlimMediumTopBar() {
@@ -224,6 +235,27 @@ fun MullvadMediumTopBar(
224235
)
225236
}
226237

238+
@OptIn(ExperimentalMaterial3Api::class)
239+
@Composable
240+
fun MullvadLargeTopBar(
241+
title: String,
242+
navigationIcon: @Composable () -> Unit = {},
243+
actions: @Composable RowScope.() -> Unit = {},
244+
scrollBehavior: TopAppBarScrollBehavior? = null
245+
) {
246+
LargeTopAppBar(
247+
title = { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) },
248+
navigationIcon = navigationIcon,
249+
scrollBehavior = scrollBehavior,
250+
colors =
251+
TopAppBarDefaults.mediumTopAppBarColors(
252+
containerColor = MaterialTheme.colorScheme.background,
253+
actionIconContentColor = MaterialTheme.colorScheme.onPrimary.copy(AlphaTopBar),
254+
),
255+
actions = actions
256+
)
257+
}
258+
227259
@Preview
228260
@Composable
229261
private fun PreviewMullvadTopBarWithLongDeviceName() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package net.mullvad.mullvadvpn.compose.screen
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.ui.platform.LocalContext
5+
import androidx.compose.ui.res.stringResource
6+
import androidx.compose.ui.tooling.preview.Preview
7+
import net.mullvad.mullvadvpn.R
8+
import net.mullvad.mullvadvpn.compose.component.AutoConnectCarousel
9+
import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton
10+
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithLargeTopBarAndButton
11+
import net.mullvad.mullvadvpn.lib.common.util.openVpnSettings
12+
import net.mullvad.mullvadvpn.lib.theme.AppTheme
13+
14+
@Preview
15+
@Composable
16+
private fun PreviewAutoConnectAndLockdownModeScreen() {
17+
18+
AppTheme {
19+
AutoConnectAndLockdownModeScreen(
20+
onBackClick = {},
21+
)
22+
}
23+
}
24+
25+
@Composable
26+
fun AutoConnectAndLockdownModeScreen(
27+
onBackClick: () -> Unit = {},
28+
) {
29+
val context = LocalContext.current
30+
ScaffoldWithLargeTopBarAndButton(
31+
appBarTitle = stringResource(id = R.string.auto_connect_and_lockdown_mode_two_lines),
32+
navigationIcon = { NavigateBackIconButton(onBackClick) },
33+
buttonTitle = stringResource(id = R.string.go_to_vpn_settings),
34+
onButtonClick = { context.openVpnSettings() },
35+
content = { modifier -> AutoConnectCarousel() }
36+
)
37+
}

0 commit comments

Comments
 (0)