@@ -2,10 +2,13 @@ package net.mullvad.mullvadvpn.compose.screen
2
2
3
3
import android.content.Intent
4
4
import android.net.Uri
5
- import androidx.compose.foundation.background
5
+ import androidx.compose.animation.animateContentSize
6
+ import androidx.compose.animation.core.animateFloatAsState
7
+ import androidx.compose.animation.core.tween
6
8
import androidx.compose.foundation.layout.Arrangement
7
9
import androidx.compose.foundation.layout.Column
8
10
import androidx.compose.foundation.layout.Spacer
11
+ import androidx.compose.foundation.layout.defaultMinSize
9
12
import androidx.compose.foundation.layout.fillMaxHeight
10
13
import androidx.compose.foundation.layout.fillMaxWidth
11
14
import androidx.compose.foundation.layout.height
@@ -18,13 +21,18 @@ import androidx.compose.runtime.Composable
18
21
import androidx.compose.runtime.LaunchedEffect
19
22
import androidx.compose.runtime.collectAsState
20
23
import androidx.compose.runtime.getValue
24
+ import androidx.compose.runtime.mutableFloatStateOf
21
25
import androidx.compose.runtime.mutableLongStateOf
22
26
import androidx.compose.runtime.mutableStateOf
23
27
import androidx.compose.runtime.remember
24
28
import androidx.compose.runtime.saveable.rememberSaveable
25
29
import androidx.compose.runtime.setValue
26
30
import androidx.compose.ui.Alignment
27
31
import androidx.compose.ui.Modifier
32
+ import androidx.compose.ui.draw.alpha
33
+ import androidx.compose.ui.graphics.Color
34
+ import androidx.compose.ui.layout.onGloballyPositioned
35
+ import androidx.compose.ui.layout.positionInParent
28
36
import androidx.compose.ui.platform.LocalContext
29
37
import androidx.compose.ui.platform.testTag
30
38
import androidx.compose.ui.res.stringResource
@@ -55,15 +63,28 @@ import net.mullvad.mullvadvpn.compose.test.RECONNECT_BUTTON_TEST_TAG
55
63
import net.mullvad.mullvadvpn.compose.test.SCROLLABLE_COLUMN_TEST_TAG
56
64
import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_BUTTON_TEST_TAG
57
65
import net.mullvad.mullvadvpn.compose.transitions.HomeTransition
66
+ import net.mullvad.mullvadvpn.constant.SECURE_ZOOM
67
+ import net.mullvad.mullvadvpn.constant.SECURE_ZOOM_ANIMATION_MILLIS
68
+ import net.mullvad.mullvadvpn.constant.UNSECURE_ZOOM
69
+ import net.mullvad.mullvadvpn.constant.fallbackLatLong
58
70
import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser
71
+ import net.mullvad.mullvadvpn.lib.map.AnimatedMap
72
+ import net.mullvad.mullvadvpn.lib.map.data.GlobeColors
73
+ import net.mullvad.mullvadvpn.lib.map.data.LocationMarkerColors
74
+ import net.mullvad.mullvadvpn.lib.map.data.Marker
59
75
import net.mullvad.mullvadvpn.lib.theme.AppTheme
60
76
import net.mullvad.mullvadvpn.lib.theme.Dimens
77
+ import net.mullvad.mullvadvpn.lib.theme.color.AlphaInvisible
61
78
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
62
79
import net.mullvad.mullvadvpn.lib.theme.color.AlphaTopBar
80
+ import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
81
+ import net.mullvad.mullvadvpn.model.GeoIpLocation
82
+ import net.mullvad.mullvadvpn.model.LatLong
83
+ import net.mullvad.mullvadvpn.model.Latitude
84
+ import net.mullvad.mullvadvpn.model.Longitude
63
85
import net.mullvad.mullvadvpn.model.TunnelState
64
86
import net.mullvad.mullvadvpn.util.appendHideNavOnPlayBuild
65
87
import net.mullvad.mullvadvpn.viewmodel.ConnectViewModel
66
- import net.mullvad.talpid.tunnel.ActionAfterDisconnect
67
88
import org.koin.androidx.compose.koinViewModel
68
89
69
90
private const val CONNECT_BUTTON_THROTTLE_MILLIS = 1000
@@ -164,66 +185,80 @@ fun ConnectScreen(
164
185
}
165
186
166
187
ScaffoldWithTopBarAndDeviceName (
167
- topBarColor =
168
- if (uiState.tunnelUiState.isSecured()) {
169
- MaterialTheme .colorScheme.inversePrimary
170
- } else {
171
- MaterialTheme .colorScheme.error
172
- },
173
- iconTintColor =
174
- if (uiState.tunnelUiState.isSecured()) {
175
- MaterialTheme .colorScheme.onPrimary
176
- } else {
177
- MaterialTheme .colorScheme.onError
178
- }
179
- .copy(alpha = AlphaTopBar ),
188
+ topBarColor = uiState.tunnelUiState.topBarColor(),
189
+ iconTintColor = uiState.tunnelUiState.iconTintColor(),
180
190
onSettingsClicked = onSettingsClick,
181
191
onAccountClicked = onAccountClick,
182
192
deviceName = uiState.deviceName,
183
193
timeLeft = uiState.daysLeftUntilExpiry
184
194
) {
195
+ var progressIndicatorBias by remember { mutableFloatStateOf(0f ) }
196
+
197
+ // Distance to marker when secure/unsecure
198
+ val baseZoom =
199
+ animateFloatAsState(
200
+ targetValue =
201
+ if (uiState.tunnelRealState is TunnelState .Connected ) SECURE_ZOOM
202
+ else UNSECURE_ZOOM ,
203
+ animationSpec = tween(SECURE_ZOOM_ANIMATION_MILLIS ),
204
+ label = " baseZoom"
205
+ )
206
+
207
+ val markers =
208
+ uiState.tunnelRealState.toMarker(uiState.location)?.let { listOf (it) } ? : emptyList()
209
+
210
+ AnimatedMap (
211
+ modifier = Modifier .padding(top = it.calculateTopPadding()),
212
+ cameraLocation = uiState.location?.toLatLong() ? : fallbackLatLong,
213
+ cameraBaseZoom = baseZoom.value,
214
+ cameraVerticalBias = progressIndicatorBias,
215
+ markers = markers,
216
+ globeColors =
217
+ GlobeColors (
218
+ landColor = MaterialTheme .colorScheme.primary,
219
+ oceanColor = MaterialTheme .colorScheme.secondary,
220
+ )
221
+ )
222
+
185
223
Column (
186
224
verticalArrangement = Arrangement .Bottom ,
187
225
horizontalAlignment = Alignment .Start ,
188
226
modifier =
189
- Modifier .background(color = MaterialTheme .colorScheme.primary )
190
- .padding(it )
227
+ Modifier .animateContentSize( )
228
+ .padding(top = it.calculateTopPadding() )
191
229
.fillMaxHeight()
192
230
.drawVerticalScrollbar(
193
231
scrollState,
194
232
color = MaterialTheme .colorScheme.onPrimary.copy(alpha = AlphaScrollbar )
195
233
)
196
234
.verticalScroll(scrollState)
197
- .padding(bottom = Dimens .screenVerticalMargin)
198
235
.testTag(SCROLLABLE_COLUMN_TEST_TAG )
199
236
) {
200
- NotificationBanner (
201
- notification = uiState.inAppNotification,
202
- isPlayBuild = uiState.isPlayBuild,
203
- onClickUpdateVersion = onUpdateVersionClick,
204
- onClickShowAccount = onManageAccountClick,
205
- onClickDismissNewDevice = onDismissNewDeviceClick,
237
+ Spacer (modifier = Modifier .defaultMinSize(minHeight = Dimens .mediumPadding).weight(1f ))
238
+ MullvadCircularProgressIndicatorLarge (
239
+ color = MaterialTheme .colorScheme.onPrimary,
240
+ modifier =
241
+ Modifier .animateContentSize()
242
+ .padding(
243
+ start = Dimens .sideMargin,
244
+ end = Dimens .sideMargin,
245
+ top = Dimens .mediumPadding
246
+ )
247
+ .alpha(if (uiState.showLoading) AlphaVisible else AlphaInvisible )
248
+ .align(Alignment .CenterHorizontally )
249
+ .testTag(CIRCULAR_PROGRESS_INDICATOR )
250
+ .onGloballyPositioned {
251
+ val offsetY = it.positionInParent().y + it.size.height / 2
252
+ it.parentLayoutCoordinates?.let {
253
+ val parentHeight = it.size.height
254
+ val verticalBias = offsetY / parentHeight
255
+ if (verticalBias.isFinite()) {
256
+ progressIndicatorBias = verticalBias
257
+ }
258
+ }
259
+ }
206
260
)
207
- Spacer (modifier = Modifier .weight(1f ))
208
- if (
209
- uiState.tunnelRealState is TunnelState .Connecting ||
210
- (uiState.tunnelRealState is TunnelState .Disconnecting &&
211
- uiState.tunnelRealState.actionAfterDisconnect ==
212
- ActionAfterDisconnect .Reconnect )
213
- ) {
214
- MullvadCircularProgressIndicatorLarge (
215
- color = MaterialTheme .colorScheme.onPrimary,
216
- modifier =
217
- Modifier .padding(
218
- start = Dimens .sideMargin,
219
- end = Dimens .sideMargin,
220
- top = Dimens .mediumPadding
221
- )
222
- .align(Alignment .CenterHorizontally )
223
- .testTag(CIRCULAR_PROGRESS_INDICATOR )
224
- )
225
- }
226
- Spacer (modifier = Modifier .height(Dimens .mediumPadding))
261
+ Spacer (modifier = Modifier .defaultMinSize(minHeight = Dimens .mediumPadding).weight(1f ))
227
262
ConnectionStatusText (
228
263
state = uiState.tunnelRealState,
229
264
modifier = Modifier .padding(horizontal = Dimens .sideMargin)
@@ -243,9 +278,7 @@ fun ConnectScreen(
243
278
var expanded by rememberSaveable { mutableStateOf(false ) }
244
279
LocationInfo (
245
280
onToggleTunnelInfo = { expanded = ! expanded },
246
- isVisible =
247
- uiState.tunnelRealState !is TunnelState .Disconnected &&
248
- uiState.location?.hostname != null ,
281
+ isVisible = uiState.showLocationInfo,
249
282
isExpanded = expanded,
250
283
location = uiState.location,
251
284
inAddress = uiState.inAddress,
@@ -282,6 +315,55 @@ fun ConnectScreen(
282
315
connectClick = { handleThrottledAction(onConnectClick) },
283
316
reconnectButtonTestTag = RECONNECT_BUTTON_TEST_TAG
284
317
)
318
+ // We need to manually add this padding so we align size with the map
319
+ // component and marker with the progress indicator.
320
+ Spacer (modifier = Modifier .height(it.calculateBottomPadding()))
285
321
}
322
+
323
+ NotificationBanner (
324
+ modifier = Modifier .padding(top = it.calculateTopPadding()),
325
+ notification = uiState.inAppNotification,
326
+ isPlayBuild = uiState.isPlayBuild,
327
+ onClickUpdateVersion = onUpdateVersionClick,
328
+ onClickShowAccount = onManageAccountClick,
329
+ onClickDismissNewDevice = onDismissNewDeviceClick,
330
+ )
331
+ }
332
+ }
333
+
334
+ @Composable
335
+ fun TunnelState.toMarker (location : GeoIpLocation ? ): Marker ? {
336
+ if (location == null ) return null
337
+ return when (this ) {
338
+ is TunnelState .Connected ->
339
+ Marker (
340
+ location.toLatLong(),
341
+ colors =
342
+ LocationMarkerColors (centerColor = MaterialTheme .colorScheme.inversePrimary),
343
+ )
344
+ is TunnelState .Connecting -> null
345
+ is TunnelState .Disconnected ->
346
+ Marker (
347
+ location.toLatLong(),
348
+ colors = LocationMarkerColors (centerColor = MaterialTheme .colorScheme.error)
349
+ )
350
+ is TunnelState .Disconnecting -> null
351
+ is TunnelState .Error -> null
286
352
}
287
353
}
354
+
355
+ @Composable
356
+ fun TunnelState.topBarColor (): Color =
357
+ if (isSecured()) MaterialTheme .colorScheme.inversePrimary else MaterialTheme .colorScheme.error
358
+
359
+ @Composable
360
+ fun TunnelState.iconTintColor (): Color =
361
+ if (isSecured()) {
362
+ MaterialTheme .colorScheme.onPrimary
363
+ } else {
364
+ MaterialTheme .colorScheme.onError
365
+ }
366
+ .copy(alpha = AlphaTopBar )
367
+
368
+ fun GeoIpLocation.toLatLong () =
369
+ LatLong (Latitude (latitude.toFloat()), Longitude (longitude.toFloat()))
0 commit comments