Skip to content

Commit 5677ff9

Browse files
MaryamShaghaghiBoki91
authored andcommitted
Create auto connect and lockdown mode screen
Co-Authored-By: Boban Sijuk <49131853+Boki91@users.noreply.github.com>
1 parent 9386505 commit 5677ff9

File tree

5 files changed

+351
-0
lines changed

5 files changed

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

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

+67
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package net.mullvad.mullvadvpn.compose.component
22

3+
import androidx.compose.foundation.background
34
import androidx.compose.foundation.layout.Column
45
import androidx.compose.foundation.layout.PaddingValues
56
import androidx.compose.foundation.layout.RowScope
67
import androidx.compose.foundation.layout.fillMaxSize
78
import androidx.compose.foundation.layout.padding
9+
import androidx.compose.foundation.layout.systemBarsPadding
810
import androidx.compose.foundation.lazy.LazyListState
911
import androidx.compose.foundation.lazy.rememberLazyListState
1012
import androidx.compose.foundation.rememberScrollState
1113
import androidx.compose.foundation.verticalScroll
1214
import androidx.compose.material3.ExperimentalMaterial3Api
15+
import androidx.compose.material3.Icon
1316
import androidx.compose.material3.MaterialTheme
1417
import androidx.compose.material3.Scaffold
1518
import androidx.compose.material3.Snackbar
@@ -23,6 +26,10 @@ import androidx.compose.runtime.remember
2326
import androidx.compose.ui.Modifier
2427
import androidx.compose.ui.graphics.Color
2528
import androidx.compose.ui.input.nestedscroll.nestedScroll
29+
import androidx.compose.ui.res.painterResource
30+
import net.mullvad.mullvadvpn.R
31+
import net.mullvad.mullvadvpn.compose.button.PrimaryButton
32+
import net.mullvad.mullvadvpn.lib.theme.Dimens
2633
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
2734
import net.mullvad.mullvadvpn.lib.theme.color.AlphaTopBar
2835

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

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 com.ramcosta.composedestinations.annotation.Destination
8+
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
9+
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
10+
import net.mullvad.mullvadvpn.R
11+
import net.mullvad.mullvadvpn.compose.component.AutoConnectCarousel
12+
import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton
13+
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithLargeTopBarAndButton
14+
import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition
15+
import net.mullvad.mullvadvpn.lib.common.util.openVpnSettings
16+
import net.mullvad.mullvadvpn.lib.theme.AppTheme
17+
18+
@Preview
19+
@Composable
20+
private fun PreviewAutoConnectAndLockdownModeScreen() {
21+
AppTheme { AutoConnectAndLockdownModeScreen(EmptyDestinationsNavigator) }
22+
}
23+
24+
@Destination(style = SlideInFromRightTransition::class)
25+
@Composable
26+
fun AutoConnectAndLockdownModeScreen(
27+
navigator: DestinationsNavigator,
28+
) {
29+
val context = LocalContext.current
30+
ScaffoldWithLargeTopBarAndButton(
31+
appBarTitle = stringResource(id = R.string.auto_connect_and_lockdown_mode_two_lines),
32+
navigationIcon = { NavigateBackIconButton(navigator::navigateUp) },
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)