@@ -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,26 @@ 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
61
77
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
62
78
import net.mullvad.mullvadvpn.lib.theme.color.AlphaTopBar
79
+ import net.mullvad.mullvadvpn.model.GeoIpLocation
80
+ import net.mullvad.mullvadvpn.model.LatLong
81
+ import net.mullvad.mullvadvpn.model.Latitude
82
+ import net.mullvad.mullvadvpn.model.Longitude
63
83
import net.mullvad.mullvadvpn.model.TunnelState
64
84
import net.mullvad.mullvadvpn.util.appendHideNavOnPlayBuild
65
85
import net.mullvad.mullvadvpn.viewmodel.ConnectViewModel
66
- import net.mullvad.talpid.tunnel.ActionAfterDisconnect
67
86
import org.koin.androidx.compose.koinViewModel
68
87
69
88
private const val CONNECT_BUTTON_THROTTLE_MILLIS = 1000
@@ -164,66 +183,88 @@ fun ConnectScreen(
164
183
}
165
184
166
185
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 ),
186
+ topBarColor = uiState.tunnelUiState.topBarColor(),
187
+ iconTintColor = uiState.tunnelUiState.iconTintColor(),
180
188
onSettingsClicked = onSettingsClick,
181
189
onAccountClicked = onAccountClick,
182
190
deviceName = uiState.deviceName,
183
191
timeLeft = uiState.daysLeftUntilExpiry
184
192
) {
193
+ var progressIndicatorBias by remember { mutableFloatStateOf(0f ) }
194
+
195
+ // Distance to marker when secure/unsecure
196
+ val baseZoom =
197
+ animateFloatAsState(
198
+ targetValue =
199
+ if (uiState.tunnelRealState is TunnelState .Connected ) SECURE_ZOOM
200
+ else UNSECURE_ZOOM ,
201
+ animationSpec = tween(SECURE_ZOOM_ANIMATION_MILLIS ),
202
+ label = " baseZoom"
203
+ )
204
+
205
+ val markers =
206
+ uiState.tunnelRealState.toMarker(uiState.location)?.let { listOf (it) } ? : emptyList()
207
+
208
+ AnimatedMap (
209
+ modifier = Modifier .padding(top = it.calculateTopPadding()),
210
+ cameraLocation = uiState.location?.toLatLong() ? : fallbackLatLong,
211
+ cameraBaseZoom = baseZoom.value,
212
+ cameraVerticalBias = progressIndicatorBias,
213
+ markers = markers,
214
+ globeColors =
215
+ GlobeColors (
216
+ landColor = MaterialTheme .colorScheme.primary,
217
+ oceanColor = MaterialTheme .colorScheme.secondary,
218
+ )
219
+ )
220
+
221
+ NotificationBanner (
222
+ modifier = Modifier .padding(top = it.calculateTopPadding()),
223
+ notification = uiState.inAppNotification,
224
+ isPlayBuild = uiState.isPlayBuild,
225
+ onClickUpdateVersion = onUpdateVersionClick,
226
+ onClickShowAccount = onManageAccountClick,
227
+ onClickDismissNewDevice = onDismissNewDeviceClick,
228
+ )
229
+
185
230
Column (
186
231
verticalArrangement = Arrangement .Bottom ,
187
232
horizontalAlignment = Alignment .Start ,
188
233
modifier =
189
- Modifier .background(color = MaterialTheme .colorScheme.primary )
190
- .padding(it )
234
+ Modifier .animateContentSize( )
235
+ .padding(top = it.calculateTopPadding() )
191
236
.fillMaxHeight()
192
237
.drawVerticalScrollbar(
193
238
scrollState,
194
239
color = MaterialTheme .colorScheme.onPrimary.copy(alpha = AlphaScrollbar )
195
240
)
196
241
.verticalScroll(scrollState)
197
- .padding(bottom = Dimens .screenVerticalMargin)
198
242
.testTag(SCROLLABLE_COLUMN_TEST_TAG )
199
243
) {
200
- NotificationBanner (
201
- notification = uiState.inAppNotification,
202
- isPlayBuild = uiState.isPlayBuild,
203
- onClickUpdateVersion = onUpdateVersionClick,
204
- onClickShowAccount = onManageAccountClick,
205
- onClickDismissNewDevice = onDismissNewDeviceClick,
244
+ Spacer (modifier = Modifier .defaultMinSize(minHeight = Dimens .mediumPadding).weight(1f ))
245
+ MullvadCircularProgressIndicatorLarge (
246
+ color = MaterialTheme .colorScheme.onPrimary,
247
+ modifier =
248
+ Modifier .animateContentSize()
249
+ .padding(
250
+ start = Dimens .sideMargin,
251
+ end = Dimens .sideMargin,
252
+ top = Dimens .mediumPadding
253
+ )
254
+ .alpha(if (uiState.showLoading) 1f else 0f )
255
+ .align(Alignment .CenterHorizontally )
256
+ .testTag(CIRCULAR_PROGRESS_INDICATOR )
257
+ .onGloballyPositioned {
258
+ val offsetY = it.positionInParent().y + it.size.height / 2
259
+ it.parentLayoutCoordinates?.let {
260
+ val parentHeight = it.size.height
261
+ if (parentHeight != 0 ) {
262
+ progressIndicatorBias = offsetY / parentHeight
263
+ }
264
+ }
265
+ }
206
266
)
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))
267
+ Spacer (modifier = Modifier .defaultMinSize(minHeight = Dimens .mediumPadding).weight(1f ))
227
268
ConnectionStatusText (
228
269
state = uiState.tunnelRealState,
229
270
modifier = Modifier .padding(horizontal = Dimens .sideMargin)
@@ -243,9 +284,7 @@ fun ConnectScreen(
243
284
var expanded by rememberSaveable { mutableStateOf(false ) }
244
285
LocationInfo (
245
286
onToggleTunnelInfo = { expanded = ! expanded },
246
- isVisible =
247
- uiState.tunnelRealState !is TunnelState .Disconnected &&
248
- uiState.location?.hostname != null ,
287
+ isVisible = uiState.showLocationInfo,
249
288
isExpanded = expanded,
250
289
location = uiState.location,
251
290
inAddress = uiState.inAddress,
@@ -282,6 +321,46 @@ fun ConnectScreen(
282
321
connectClick = { handleThrottledAction(onConnectClick) },
283
322
reconnectButtonTestTag = RECONNECT_BUTTON_TEST_TAG
284
323
)
324
+ // We need to manually add this padding so we align size with the map
325
+ // component and marker with the progress indicator.
326
+ Spacer (modifier = Modifier .height(it.calculateBottomPadding()))
285
327
}
286
328
}
287
329
}
330
+
331
+ @Composable
332
+ fun TunnelState.toMarker (location : GeoIpLocation ? ): Marker ? {
333
+ if (location == null ) return null
334
+ return when (this ) {
335
+ is TunnelState .Connected ->
336
+ Marker (
337
+ location.toLatLong(),
338
+ colors =
339
+ LocationMarkerColors (centerColor = MaterialTheme .colorScheme.inversePrimary),
340
+ )
341
+ is TunnelState .Connecting -> null
342
+ is TunnelState .Disconnected ->
343
+ Marker (
344
+ location.toLatLong(),
345
+ colors = LocationMarkerColors (centerColor = MaterialTheme .colorScheme.error)
346
+ )
347
+ is TunnelState .Disconnecting -> null
348
+ is TunnelState .Error -> null
349
+ }
350
+ }
351
+
352
+ @Composable
353
+ fun TunnelState.topBarColor (): Color =
354
+ if (isSecured()) MaterialTheme .colorScheme.inversePrimary else MaterialTheme .colorScheme.error
355
+
356
+ @Composable
357
+ fun TunnelState.iconTintColor (): Color =
358
+ if (isSecured()) {
359
+ MaterialTheme .colorScheme.onPrimary
360
+ } else {
361
+ MaterialTheme .colorScheme.onError
362
+ }
363
+ .copy(alpha = AlphaTopBar )
364
+
365
+ fun GeoIpLocation.toLatLong () =
366
+ LatLong (Latitude (latitude.toFloat()), Longitude (longitude.toFloat()))
0 commit comments