Skip to content

Commit 5b0e46b

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 ba3a288 commit 5b0e46b

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
@@ -23,6 +24,10 @@ import androidx.compose.runtime.remember
2324
import androidx.compose.ui.Modifier
2425
import androidx.compose.ui.graphics.Color
2526
import androidx.compose.ui.input.nestedscroll.nestedScroll
27+
import androidx.compose.ui.res.painterResource
28+
import net.mullvad.mullvadvpn.R
29+
import net.mullvad.mullvadvpn.compose.button.PrimaryButton
30+
import net.mullvad.mullvadvpn.lib.theme.Dimens
2631
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
2732
import net.mullvad.mullvadvpn.lib.theme.color.AlphaTopBar
2833

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

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
@@ -198,6 +199,16 @@ private fun PreviewMediumTopBar() {
198199
}
199200
}
200201

202+
@Preview
203+
@Composable
204+
private fun PreviewLargeTopBar() {
205+
AppTheme {
206+
MullvadLargeTopBar(
207+
title = "Title",
208+
)
209+
}
210+
}
211+
201212
@Preview(widthDp = 260)
202213
@Composable
203214
private fun PreviewSlimMediumTopBar() {
@@ -236,6 +247,27 @@ fun MullvadMediumTopBar(
236247
)
237248
}
238249

250+
@OptIn(ExperimentalMaterial3Api::class)
251+
@Composable
252+
fun MullvadLargeTopBar(
253+
title: String,
254+
navigationIcon: @Composable () -> Unit = {},
255+
actions: @Composable RowScope.() -> Unit = {},
256+
scrollBehavior: TopAppBarScrollBehavior? = null
257+
) {
258+
LargeTopAppBar(
259+
title = { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) },
260+
navigationIcon = navigationIcon,
261+
scrollBehavior = scrollBehavior,
262+
colors =
263+
TopAppBarDefaults.mediumTopAppBarColors(
264+
containerColor = MaterialTheme.colorScheme.background,
265+
actionIconContentColor = MaterialTheme.colorScheme.onPrimary.copy(AlphaTopBar),
266+
),
267+
actions = actions
268+
)
269+
}
270+
239271
@Preview
240272
@Composable
241273
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)