Skip to content

Commit 95751dc

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

File tree

6 files changed

+377
-0
lines changed

6 files changed

+377
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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+
@OptIn(ExperimentalFoundationApi::class)
50+
@Composable
51+
fun AutoConnectCarousel() {
52+
val pagerState = rememberPagerState(pageCount = { 3 })
53+
HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page ->
54+
val scope = rememberCoroutineScope()
55+
ConstraintLayout(
56+
modifier = Modifier.fillMaxSize(),
57+
) {
58+
val (
59+
upperTextRef,
60+
backButtonRef,
61+
imageRef,
62+
nextButtonRef,
63+
lowerTextRef,
64+
pageIndicatorRef) =
65+
createRefs()
66+
Text(
67+
modifier =
68+
Modifier.padding(horizontal = Dimens.largePadding).constrainAs(upperTextRef) {
69+
start.linkTo(parent.start)
70+
end.linkTo(parent.end)
71+
bottom.linkTo(imageRef.top)
72+
},
73+
text =
74+
HtmlCompat.fromHtml(
75+
stringResource(
76+
id =
77+
when (page) {
78+
0 -> R.string.carousel_slide_1_text_1
79+
1 -> R.string.carousel_slide_2_text_1
80+
else -> R.string.carousel_slide_3_text_1
81+
}
82+
),
83+
HtmlCompat.FROM_HTML_MODE_COMPACT
84+
)
85+
.toAnnotatedString(
86+
boldSpanStyle =
87+
SpanStyle(
88+
fontWeight = FontWeight.ExtraBold,
89+
color = MaterialTheme.colorScheme.onSecondary
90+
)
91+
)
92+
)
93+
if (page != 0) {
94+
IconButton(
95+
modifier =
96+
Modifier.constrainAs(backButtonRef) {
97+
top.linkTo(parent.top)
98+
start.linkTo(parent.start)
99+
bottom.linkTo(parent.bottom)
100+
},
101+
onClick = {
102+
scope.launch { pagerState.scrollToPage(pagerState.currentPage - 1) }
103+
},
104+
) {
105+
Icon(
106+
painter = painterResource(id = R.drawable.icon_chevron),
107+
contentDescription = null,
108+
tint = Color.Unspecified,
109+
modifier = Modifier.rotate(180f).alpha(AlphaDescription)
110+
)
111+
}
112+
}
113+
114+
Image(
115+
modifier =
116+
Modifier.padding(top = Dimens.topPadding, bottom = Dimens.bottomPadding)
117+
.constrainAs(imageRef) {
118+
top.linkTo(parent.top)
119+
start.linkTo(parent.start)
120+
bottom.linkTo(parent.bottom)
121+
end.linkTo(parent.end)
122+
},
123+
painter =
124+
when (page) {
125+
0 -> painterResource(id = R.drawable.carousel_slide_1_cogwheel)
126+
1 -> painterResource(id = R.drawable.carousel_slide_2_always_on)
127+
else -> painterResource(id = R.drawable.carousel_slide_3_block_connections)
128+
},
129+
contentDescription = null,
130+
)
131+
132+
if (page != 2) {
133+
IconButton(
134+
modifier =
135+
Modifier.constrainAs(nextButtonRef) {
136+
top.linkTo(parent.top)
137+
end.linkTo(parent.end)
138+
bottom.linkTo(parent.bottom)
139+
},
140+
onClick = {
141+
scope.launch { pagerState.scrollToPage(pagerState.currentPage + 1) }
142+
}
143+
) {
144+
Icon(
145+
painter = painterResource(id = R.drawable.icon_chevron),
146+
contentDescription = null,
147+
tint = Color.Unspecified,
148+
modifier = Modifier.size(Dimens.titleIconSize).alpha(AlphaDescription)
149+
)
150+
}
151+
}
152+
Text(
153+
modifier =
154+
Modifier.padding(horizontal = Dimens.largePadding).constrainAs(lowerTextRef) {
155+
top.linkTo(imageRef.bottom)
156+
end.linkTo(parent.end)
157+
start.linkTo(parent.start)
158+
},
159+
text =
160+
HtmlCompat.fromHtml(
161+
stringResource(
162+
id =
163+
when (page) {
164+
0 -> R.string.carousel_slide_1_text_2
165+
1 -> R.string.carousel_slide_2_text_2
166+
else -> R.string.carousel_slide_3_text_2
167+
}
168+
),
169+
HtmlCompat.FROM_HTML_MODE_COMPACT
170+
)
171+
.toAnnotatedString(
172+
boldSpanStyle =
173+
SpanStyle(
174+
fontWeight = FontWeight.ExtraBold,
175+
color = MaterialTheme.colorScheme.onSecondary
176+
)
177+
)
178+
)
179+
180+
Row(
181+
Modifier.wrapContentHeight()
182+
.fillMaxWidth()
183+
.padding(top = Dimens.topPadding)
184+
.constrainAs(pageIndicatorRef) {
185+
top.linkTo(lowerTextRef.bottom)
186+
end.linkTo(parent.end)
187+
start.linkTo(parent.start)
188+
},
189+
horizontalArrangement = Arrangement.Center,
190+
verticalAlignment = Alignment.Bottom
191+
) {
192+
repeat(pagerState.pageCount) { iteration ->
193+
val color =
194+
if (pagerState.currentPage == iteration) Color.LightGray else Color.DarkGray
195+
Box(
196+
modifier =
197+
Modifier.padding(Dimens.indicatorPadding)
198+
.clip(CircleShape)
199+
.background(color)
200+
.size(Dimens.indicatorSize)
201+
)
202+
}
203+
}
204+
}
205+
}
206+
}

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,45 @@
1+
package net.mullvad.mullvadvpn.compose.screen
2+
3+
import android.content.ActivityNotFoundException
4+
import android.widget.Toast
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.ui.platform.LocalContext
7+
import androidx.compose.ui.res.stringResource
8+
import androidx.compose.ui.tooling.preview.Preview
9+
import net.mullvad.mullvadvpn.R
10+
import net.mullvad.mullvadvpn.compose.component.AutoConnectCarousel
11+
import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton
12+
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithLargeTopBarAndButton
13+
import net.mullvad.mullvadvpn.lib.common.util.openVpnSettings
14+
import net.mullvad.mullvadvpn.lib.theme.AppTheme
15+
16+
@Preview
17+
@Composable
18+
private fun PreviewAutoConnectAndLockdownModeScreen() {
19+
20+
AppTheme {
21+
AutoConnectAndLockdownModeScreen(
22+
onBackClick = {},
23+
)
24+
}
25+
}
26+
27+
@Composable
28+
fun AutoConnectAndLockdownModeScreen(
29+
onBackClick: () -> Unit = {},
30+
) {
31+
val context = LocalContext.current
32+
ScaffoldWithLargeTopBarAndButton(
33+
appBarTitle = stringResource(id = R.string.auto_connect_and_lockdown_mode_two_lines),
34+
navigationIcon = { NavigateBackIconButton(onBackClick) },
35+
buttonTitle = stringResource(id = R.string.go_to_vpn_settings),
36+
onButtonClick = {
37+
try {
38+
context.openVpnSettings()
39+
} catch (e: ActivityNotFoundException) {
40+
Toast.makeText(context, R.string.settings_vpn, Toast.LENGTH_SHORT).show()
41+
}
42+
},
43+
content = { modifier -> AutoConnectCarousel() }
44+
)
45+
}

0 commit comments

Comments
 (0)