summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
Diffstat (limited to 'android')
-rw-r--r--android/app/build.gradle.kts1
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt8
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreenTest.kt4
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreenTest.kt18
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ExpandableComposeCell.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/RelayLocationCell.kt273
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadCheckbox.kt43
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/CustomListLocationUiStatePreviewParameterProvider.kt7
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemStatusCellPreviewParameterProvider.kt34
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsUiStatePreviewParameterProvider.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt44
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/LocationBottomSheet.kt75
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/RelayListContent.kt225
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreen.kt18
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationList.kt15
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt22
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt11
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SearchLocationUiState.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationListUiState.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt84
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt66
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt30
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt39
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt50
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModelTest.kt2
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt2
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt2
-rw-r--r--android/gradle/verification-metadata.keys.xml10
-rw-r--r--android/gradle/verification-metadata.xml37
-rw-r--r--android/lib/resource/src/main/res/values-da/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-de/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-es/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-fi/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-fr/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-it/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-ja/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-ko/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-my/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-nb/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-nl/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-pl/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-pt/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-ru/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-sv/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-th/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-tr/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-zh-rCN/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-zh-rTW/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values/strings.xml5
-rw-r--r--android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt2
-rw-r--r--android/lib/ui/component/build.gradle.kts11
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/ExpandChevron.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ExpandChevron.kt)25
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/CheckableRelayListItem.kt89
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemCheckableCellPreviewParameterProvider.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemCheckableCellPreviewParameterProvider.kt)2
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt)2
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayListItem.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListItem.kt)45
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayListItemPreviewData.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayListItemPreviewData.kt)11
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItem.kt206
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItemPreviewParameterProvider.kt66
-rw-r--r--android/lib/ui/designsystem/build.gradle.kts43
-rw-r--r--android/lib/ui/designsystem/src/main/AndroidManifest.xml2
-rw-r--r--android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/Checkbox.kt56
-rw-r--r--android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/RelayListHeader.kt78
-rw-r--r--android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/RelayListItem.kt313
-rw-r--r--android/settings.gradle.kts3
67 files changed, 1338 insertions, 798 deletions
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 32ed53060b..24172279fc 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -348,6 +348,7 @@ dependencies {
implementation(projects.lib.shared)
implementation(projects.lib.talpid)
implementation(projects.lib.tv)
+ implementation(projects.lib.ui.designsystem)
implementation(projects.lib.ui.component)
implementation(projects.lib.ui.tag)
implementation(projects.tile)
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt
index d556fe5d10..a7bed534ba 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt
@@ -14,8 +14,8 @@ import net.mullvad.mullvadvpn.compose.data.DUMMY_RELAY_COUNTRIES
import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsData
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
-import net.mullvad.mullvadvpn.compose.state.RelayLocationListItem
import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.CheckableRelayListItem
import net.mullvad.mullvadvpn.lib.ui.tag.CIRCULAR_PROGRESS_INDICATOR_TEST_TAG
import net.mullvad.mullvadvpn.lib.ui.tag.SAVE_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.util.Lce
@@ -104,11 +104,11 @@ class CustomListLocationsScreenTest {
CustomListLocationsData(
locations =
listOf(
- RelayLocationListItem(
+ CheckableRelayListItem(
DUMMY_RELAY_COUNTRIES[0],
checked = true,
),
- RelayLocationListItem(
+ CheckableRelayListItem(
DUMMY_RELAY_COUNTRIES[1],
checked = false,
),
@@ -141,7 +141,7 @@ class CustomListLocationsScreenTest {
CustomListLocationsData(
locations =
listOf(
- RelayLocationListItem(selectedCountry, checked = true)
+ CheckableRelayListItem(selectedCountry, checked = true)
),
searchTerm = "",
saveEnabled = false,
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreenTest.kt
index 32a91fa61b..7e03afb18c 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreenTest.kt
@@ -12,11 +12,11 @@ import io.mockk.verify
import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
import net.mullvad.mullvadvpn.compose.data.DUMMY_RELAY_ITEM_CUSTOM_LISTS
import net.mullvad.mullvadvpn.compose.setContentWithTheme
-import net.mullvad.mullvadvpn.compose.state.RelayListItem
import net.mullvad.mullvadvpn.compose.state.SearchLocationUiState
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemId
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_CUSTOM_LIST_HEADER_TEST_TAG
import net.mullvad.mullvadvpn.util.Lce
import org.junit.jupiter.api.AfterEach
@@ -44,7 +44,6 @@ class SearchLocationScreenTest {
onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit = { _, _, _ -> },
onSearchInputChanged: (String) -> Unit = {},
onCreateCustomList: (location: RelayItem.Location?) -> Unit = {},
- onEditCustomLists: () -> Unit = {},
onAddLocationToList:
(location: RelayItem.Location, customList: RelayItem.CustomList) -> Unit =
{ _, _ ->
@@ -67,7 +66,6 @@ class SearchLocationScreenTest {
onToggleExpand = onToggleExpand,
onSearchInputChanged = onSearchInputChanged,
onCreateCustomList = onCreateCustomList,
- onEditCustomLists = onEditCustomLists,
onAddLocationToList = onAddLocationToList,
onRemoveLocationFromList = onRemoveLocationFromList,
onEditCustomListName = onEditCustomListName,
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreenTest.kt
index e03786aef3..0767fc35ad 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreenTest.kt
@@ -15,12 +15,13 @@ import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
import net.mullvad.mullvadvpn.compose.data.DUMMY_RELAY_COUNTRIES
import net.mullvad.mullvadvpn.compose.data.DUMMY_RELAY_ITEM_CUSTOM_LISTS
import net.mullvad.mullvadvpn.compose.setContentWithTheme
-import net.mullvad.mullvadvpn.compose.state.RelayListItem
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationListUiState
import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.ItemPosition
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_CUSTOM_LIST_BOTTOM_SHEET_TEST_TAG
import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_LOCATION_BOTTOM_SHEET_TEST_TAG
import net.mullvad.mullvadvpn.performLongClick
@@ -110,7 +111,10 @@ class SelectLocationScreenTest {
SelectLocationListUiState(
relayListItems =
DUMMY_RELAY_COUNTRIES.map {
- RelayListItem.GeoLocationItem(item = it)
+ RelayListItem.GeoLocationItem(
+ item = it,
+ itemPosition = ItemPosition.Single,
+ )
},
customLists = emptyList(),
)
@@ -250,7 +254,13 @@ class SelectLocationScreenTest {
MutableStateFlow(
Lce.Content(
SelectLocationListUiState(
- relayListItems = listOf(RelayListItem.GeoLocationItem(relayItem)),
+ relayListItems =
+ listOf(
+ RelayListItem.GeoLocationItem(
+ relayItem,
+ itemPosition = ItemPosition.Single,
+ )
+ ),
customLists = emptyList(),
)
)
@@ -278,6 +288,6 @@ class SelectLocationScreenTest {
}
companion object {
- private const val CUSTOM_LISTS_EMPTY_TEXT = "To create a custom list press the \"︙\""
+ private const val CUSTOM_LISTS_EMPTY_TEXT = "To create a custom list press the \"+\""
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt
index a3fbd4d94d..633b32e420 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt
@@ -14,9 +14,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
-import net.mullvad.mullvadvpn.compose.component.MullvadCheckbox
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
+import net.mullvad.mullvadvpn.lib.ui.designsystem.Checkbox
@Preview
@Composable
@@ -46,7 +46,7 @@ internal fun CheckboxCell(
.background(background)
.padding(start = startPadding, end = endPadding),
) {
- MullvadCheckbox(checked = checked, onCheckedChange = onCheckedChange)
+ Checkbox(checked = checked, onCheckedChange = onCheckedChange)
Text(
text = title,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ExpandableComposeCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ExpandableComposeCell.kt
index 536a7ef8b8..a811682767 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ExpandableComposeCell.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ExpandableComposeCell.kt
@@ -19,11 +19,11 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.compose.component.ExpandChevronIconButton
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaInactive
import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
+import net.mullvad.mullvadvpn.lib.ui.component.ExpandChevronIconButton
@Preview
@Composable
@@ -103,11 +103,7 @@ private fun ExpandableComposeCellBody(
}
}
- ExpandChevronIconButton(
- isExpanded = isExpanded,
- onExpand = onExpand,
- color = MaterialTheme.colorScheme.onPrimary,
- )
+ ExpandChevronIconButton(isExpanded = isExpanded, onExpand = onExpand)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/RelayLocationCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/RelayLocationCell.kt
deleted file mode 100644
index 45fd0d3bd0..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/RelayLocationCell.kt
+++ /dev/null
@@ -1,273 +0,0 @@
-package net.mullvad.mullvadvpn.compose.cell
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.material3.VerticalDivider
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.unit.dp
-import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.compose.component.ExpandChevron
-import net.mullvad.mullvadvpn.compose.component.MullvadCheckbox
-import net.mullvad.mullvadvpn.compose.preview.RelayItemCheckableCellPreviewParameterProvider
-import net.mullvad.mullvadvpn.compose.state.RelayListItemState
-import net.mullvad.mullvadvpn.lib.model.RelayItem
-import net.mullvad.mullvadvpn.lib.theme.AppTheme
-import net.mullvad.mullvadvpn.lib.theme.Dimens
-import net.mullvad.mullvadvpn.lib.theme.color.AlphaInactive
-import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
-import net.mullvad.mullvadvpn.lib.theme.color.selected
-import net.mullvad.mullvadvpn.lib.ui.tag.EXPAND_BUTTON_TEST_TAG
-import net.mullvad.mullvadvpn.lib.ui.tag.LOCATION_CELL_TEST_TAG
-
-@Composable
-@Preview
-private fun PreviewCheckableRelayLocationCell(
- @PreviewParameter(RelayItemCheckableCellPreviewParameterProvider::class)
- relayItems: List<RelayItem.Location.Country>
-) {
- AppTheme {
- Column(Modifier.background(color = MaterialTheme.colorScheme.surface)) {
- relayItems.map {
- CheckableRelayLocationCell(
- item = it,
- checked = false,
- expanded = false,
- depth = 0,
- onExpand = {},
- modifier = Modifier.testTag(LOCATION_CELL_TEST_TAG),
- )
- }
- }
- }
-}
-
-@Composable
-fun StatusRelayItemCell(
- item: RelayItem,
- isSelected: Boolean,
- state: RelayListItemState?,
- modifier: Modifier = Modifier,
- onClick: () -> Unit,
- onLongClick: (() -> Unit)? = null,
- onToggleExpand: ((Boolean) -> Unit),
- isExpanded: Boolean = false,
- depth: Int = 0,
- activeColor: Color = MaterialTheme.colorScheme.selected,
- inactiveColor: Color = MaterialTheme.colorScheme.error,
- disabledColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
-) {
- RelayItemCell(
- modifier = modifier,
- item = item,
- isSelected = isSelected,
- state = state,
- onClick = onClick,
- onLongClick = onLongClick,
- onToggleExpand = onToggleExpand,
- isExpanded = isExpanded,
- depth = depth,
- content = {
- if (isSelected) {
- Icon(imageVector = Icons.Default.Check, contentDescription = null)
- } else {
- Box(
- modifier =
- Modifier.padding(4.dp)
- .size(Dimens.relayCircleSize)
- .background(
- color =
- when {
- item is RelayItem.CustomList && item.locations.isEmpty() ->
- disabledColor
- state != null -> disabledColor
- item.active -> activeColor
- else -> inactiveColor
- },
- shape = CircleShape,
- )
- )
- }
- },
- )
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun RelayItemCell(
- modifier: Modifier = Modifier,
- item: RelayItem,
- isSelected: Boolean,
- state: RelayListItemState?,
- onClick: () -> Unit,
- onLongClick: (() -> Unit)? = null,
- onToggleExpand: (Boolean) -> Unit,
- isExpanded: Boolean,
- depth: Int,
- content: @Composable (RowScope.() -> Unit)? = null,
-) {
-
- val leadingContentStartPadding = Dimens.cellStartPadding
- val leadingContentStarPaddingModifier = Dimens.mediumPadding
- val startPadding = leadingContentStartPadding + leadingContentStarPaddingModifier * depth
- Row(
- modifier =
- modifier
- .fillMaxWidth()
- .height(IntrinsicSize.Min)
- .background(
- when {
- isSelected -> MaterialTheme.colorScheme.selected
- else -> depth.toBackgroundColor()
- }
- ),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- // Duplicate row is needed for selection of the item on TV.
- Row(
- modifier =
- Modifier.combinedClickable(
- enabled = state == null && item.active,
- onClick = onClick,
- onLongClick = onLongClick,
- )
- .padding(start = startPadding)
- .weight(1f),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- if (content != null) {
- content()
- }
- Name(name = item.name, state = state, active = item.active)
- }
-
- if (item.hasChildren) {
- ExpandButton(
- color = MaterialTheme.colorScheme.onSurface,
- isExpanded = isExpanded,
- onClick = { onToggleExpand(!isExpanded) },
- modifier = Modifier.testTag(EXPAND_BUTTON_TEST_TAG),
- )
- }
- }
-}
-
-@Composable
-fun CheckableRelayLocationCell(
- item: RelayItem,
- modifier: Modifier = Modifier,
- checked: Boolean,
- onRelayCheckedChange: (isChecked: Boolean) -> Unit = { _ -> },
- expanded: Boolean,
- onExpand: (Boolean) -> Unit,
- depth: Int,
-) {
- RelayItemCell(
- modifier = modifier,
- item = item,
- isSelected = false,
- state = null,
- onClick = { onRelayCheckedChange(!checked) },
- onToggleExpand = onExpand,
- isExpanded = expanded,
- depth = depth,
- content = {
- MullvadCheckbox(
- checked = checked,
- onCheckedChange = { isChecked -> onRelayCheckedChange(isChecked) },
- )
- },
- )
-}
-
-@Composable
-private fun Name(
- modifier: Modifier = Modifier,
- name: String,
- state: RelayListItemState?,
- active: Boolean,
-) {
- Text(
- text = state?.let { name.withSuffix(state) } ?: name,
- color = MaterialTheme.colorScheme.onSurface,
- modifier =
- modifier
- .alpha(
- if (state == null && active) {
- AlphaVisible
- } else {
- AlphaInactive
- }
- )
- .padding(horizontal = Dimens.smallPadding, vertical = Dimens.mediumPadding),
- maxLines = 1,
- style = MaterialTheme.typography.bodyLarge,
- overflow = TextOverflow.Ellipsis,
- )
-}
-
-@Composable
-private fun RowScope.ExpandButton(
- modifier: Modifier,
- color: Color,
- isExpanded: Boolean,
- onClick: (expand: Boolean) -> Unit,
-) {
- VerticalDivider(
- color = MaterialTheme.colorScheme.surface,
- modifier = Modifier.padding(vertical = Dimens.verticalDividerPadding),
- )
- ExpandChevron(
- color = color,
- isExpanded = isExpanded,
- modifier =
- modifier
- .fillMaxHeight()
- .clickable { onClick(!isExpanded) }
- .padding(horizontal = Dimens.largePadding)
- .align(Alignment.CenterVertically),
- )
-}
-
-@Suppress("MagicNumber")
-@Composable
-private fun Int.toBackgroundColor(): Color =
- when (this) {
- 0 -> MaterialTheme.colorScheme.surfaceContainerHighest
- 1 -> MaterialTheme.colorScheme.surfaceContainerHigh
- 2 -> MaterialTheme.colorScheme.surfaceContainerLow
- else -> MaterialTheme.colorScheme.surfaceContainerLowest
- }
-
-@Composable
-private fun String.withSuffix(state: RelayListItemState) =
- when (state) {
- RelayListItemState.USED_AS_EXIT -> stringResource(R.string.x_exit, this)
- RelayListItemState.USED_AS_ENTRY -> stringResource(R.string.x_entry, this)
- }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadCheckbox.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadCheckbox.kt
deleted file mode 100644
index 8ff5ae1df1..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadCheckbox.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package net.mullvad.mullvadvpn.compose.component
-
-import androidx.compose.foundation.background
-import androidx.compose.material3.Checkbox
-import androidx.compose.material3.CheckboxDefaults
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.tooling.preview.Preview
-import net.mullvad.mullvadvpn.lib.theme.AppTheme
-import net.mullvad.mullvadvpn.lib.theme.color.selected
-
-@Preview
-@Composable
-private fun PreviewMullvadCheckbox() {
- AppTheme {
- SpacedColumn(Modifier.background(color = MaterialTheme.colorScheme.primary)) {
- MullvadCheckbox(checked = false) {}
- MullvadCheckbox(checked = true) {}
- }
- }
-}
-
-@Composable
-fun MullvadCheckbox(
- checkedColor: Color = MaterialTheme.colorScheme.onPrimary,
- uncheckedColor: Color = MaterialTheme.colorScheme.onPrimary,
- checkmarkColor: Color = MaterialTheme.colorScheme.selected,
- checked: Boolean,
- onCheckedChange: (Boolean) -> Unit,
-) {
- Checkbox(
- checked = checked,
- onCheckedChange = onCheckedChange,
- colors =
- CheckboxDefaults.colors(
- checkedColor = checkedColor,
- uncheckedColor = uncheckedColor,
- checkmarkColor = checkmarkColor,
- ),
- )
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/CustomListLocationUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/CustomListLocationUiStatePreviewParameterProvider.kt
index 514005095b..d17a42d76f 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/CustomListLocationUiStatePreviewParameterProvider.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/CustomListLocationUiStatePreviewParameterProvider.kt
@@ -3,8 +3,9 @@ package net.mullvad.mullvadvpn.compose.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsData
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
-import net.mullvad.mullvadvpn.compose.state.RelayLocationListItem
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.CheckableRelayListItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.generateRelayItemCountry
import net.mullvad.mullvadvpn.util.Lce
class CustomListLocationUiStatePreviewParameterProvider :
@@ -18,7 +19,7 @@ class CustomListLocationUiStatePreviewParameterProvider :
CustomListLocationsData(
locations =
listOf(
- RelayLocationListItem(
+ CheckableRelayListItem(
item =
generateRelayItemCountry(
name = "A relay",
@@ -27,7 +28,7 @@ class CustomListLocationUiStatePreviewParameterProvider :
active = true,
)
),
- RelayLocationListItem(
+ CheckableRelayListItem(
item =
generateRelayItemCountry(
name = "Another relay",
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemStatusCellPreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemStatusCellPreviewParameterProvider.kt
deleted file mode 100644
index fcb1fdd194..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemStatusCellPreviewParameterProvider.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package net.mullvad.mullvadvpn.compose.preview
-
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import net.mullvad.mullvadvpn.lib.model.RelayItem
-
-class RelayItemStatusCellPreviewParameterProvider :
- PreviewParameterProvider<List<RelayItem.Location.Country>> {
- override val values =
- sequenceOf(
- listOf(
- generateRelayItemCountry(
- name = "Relay country Active",
- cityNames = listOf("Relay city 1", "Relay city 2"),
- relaysPerCity = 2,
- ),
- generateRelayItemCountry(
- name = "Not Enabled Relay country",
- cityNames = listOf("Not Enabled city"),
- relaysPerCity = 1,
- active = false,
- ),
- generateRelayItemCountry(
- name = "Relay country Expanded",
- cityNames = listOf("Normal city"),
- relaysPerCity = 2,
- ),
- generateRelayItemCountry(
- name = "Country and city Expanded",
- cityNames = listOf("Expanded city A", "Expanded city B"),
- relaysPerCity = 2,
- ),
- )
- )
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsUiStatePreviewParameterProvider.kt
index 50cf464bb5..c868360c55 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsUiStatePreviewParameterProvider.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsUiStatePreviewParameterProvider.kt
@@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.compose.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import net.mullvad.mullvadvpn.compose.state.SearchLocationUiState
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItemPreviewData
import net.mullvad.mullvadvpn.usecase.FilterChip
import net.mullvad.mullvadvpn.util.Lce
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
index 878ab70d9d..82d6736c99 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
@@ -83,7 +83,6 @@ import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.button.ConnectionButton
import net.mullvad.mullvadvpn.compose.button.SwitchLocationButton
import net.mullvad.mullvadvpn.compose.component.ConnectionStatusText
-import net.mullvad.mullvadvpn.compose.component.ExpandChevron
import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge
import net.mullvad.mullvadvpn.compose.component.MullvadSnackbar
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBarAndDeviceName
@@ -128,6 +127,7 @@ import net.mullvad.mullvadvpn.lib.theme.color.AlphaInvisible
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
import net.mullvad.mullvadvpn.lib.tv.NavigationDrawerTv
+import net.mullvad.mullvadvpn.lib.ui.component.ExpandChevron
import net.mullvad.mullvadvpn.lib.ui.tag.CONNECT_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.lib.ui.tag.CONNECT_CARD_HEADER_TEST_TAG
import net.mullvad.mullvadvpn.lib.ui.tag.RECONNECT_BUTTON_TEST_TAG
@@ -581,7 +581,7 @@ private fun ConnectionCardHeader(
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
ConnectionStatusText(state = state.tunnelState)
if (state.tunnelState is TunnelState.Connected) {
- ExpandChevron(isExpanded = !expanded, color = MaterialTheme.colorScheme.onSurface)
+ ExpandChevron(isExpanded = !expanded)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt
index 1e81231e7d..6aea279110 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt
@@ -8,10 +8,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
-import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -33,7 +32,6 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.compose.cell.CheckableRelayLocationCell
import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
import net.mullvad.mullvadvpn.compose.component.EmptyRelayListText
import net.mullvad.mullvadvpn.compose.component.LocationsEmptyText
@@ -46,6 +44,7 @@ import net.mullvad.mullvadvpn.compose.constant.ContentType
import net.mullvad.mullvadvpn.compose.dialog.info.Confirmed
import net.mullvad.mullvadvpn.compose.extensions.animateScrollAndCentralizeItem
import net.mullvad.mullvadvpn.compose.preview.CustomListLocationUiStatePreviewParameterProvider
+import net.mullvad.mullvadvpn.compose.screen.location.positionalPadding
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsData
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
import net.mullvad.mullvadvpn.compose.textfield.SearchTextField
@@ -57,6 +56,7 @@ import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.CheckableRelayLocationCell
import net.mullvad.mullvadvpn.lib.ui.tag.SAVE_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.util.Lce
import net.mullvad.mullvadvpn.viewmodel.CustomListLocationsSideEffect
@@ -162,6 +162,7 @@ fun CustomListLocationsScreen(
state = lazyListState,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = AlphaScrollbar),
)
+ .padding(horizontal = Dimens.mediumPadding)
.fillMaxWidth(),
state = lazyListState,
) {
@@ -169,9 +170,11 @@ fun CustomListLocationsScreen(
is Lce.Loading -> {
loading()
}
+
is Lce.Error -> {
empty()
}
+
is Lce.Content -> {
content(
uiState = state.content.value,
@@ -231,32 +234,15 @@ private fun LazyListScope.content(
LocationsEmptyText(searchTerm = uiState.searchTerm)
}
} else {
- itemsIndexed(uiState.locations, key = { index, listItem -> listItem.item.id }) {
- index,
- listItem ->
- Column(modifier = Modifier.animateItem()) {
- if (index != 0) {
- HorizontalDivider()
- }
- CheckableRelayLocationCell(
- item = listItem.item,
- onRelayCheckedChange = { isChecked ->
- onRelaySelectedChanged(listItem.item, isChecked)
- },
- checked = listItem.checked,
- depth = listItem.depth,
- onExpand = { expand -> onExpand(listItem.item, expand) },
- expanded = listItem.expanded,
- )
- }
+ items(uiState.locations, key = { listItem -> listItem.item.id }) { listItem ->
+ CheckableRelayLocationCell(
+ modifier = Modifier.animateItem().positionalPadding(listItem.itemPosition),
+ item = listItem,
+ onRelayCheckedChange = { isChecked ->
+ onRelaySelectedChanged(listItem.item, isChecked)
+ },
+ onExpand = { expand -> onExpand(listItem.item, expand) },
+ )
}
}
}
-
-private fun Lce<Boolean, CustomListLocationsUiState, Boolean>.newList(): Boolean {
- return when (this) {
- is Lce.Content -> this.value.newList
- is Lce.Loading -> this.value
- is Lce.Error -> this.error
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/LocationBottomSheet.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/LocationBottomSheet.kt
index 46013a4f1a..29870b710b 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/LocationBottomSheet.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/LocationBottomSheet.kt
@@ -30,7 +30,6 @@ import net.mullvad.mullvadvpn.compose.cell.IconCell
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
import net.mullvad.mullvadvpn.compose.component.MullvadModalBottomSheet
-import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowCustomListsBottomSheet
import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowCustomListsEntryBottomSheet
import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowEditCustomListBottomSheet
import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowLocationBottomSheet
@@ -38,9 +37,6 @@ import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.CustomListName
import net.mullvad.mullvadvpn.lib.model.RelayItem
-import net.mullvad.mullvadvpn.lib.theme.color.AlphaInactive
-import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
-import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_CUSTOM_LIST_BOTTOM_SHEET_TEST_TAG
import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_LOCATION_BOTTOM_SHEET_TEST_TAG
import net.mullvad.mullvadvpn.relaylist.canAddLocation
@@ -49,7 +45,6 @@ import net.mullvad.mullvadvpn.relaylist.canAddLocation
internal fun LocationBottomSheets(
locationBottomSheetState: LocationBottomSheetState?,
onCreateCustomList: (RelayItem.Location?) -> Unit,
- onEditCustomLists: () -> Unit,
onAddLocationToList: (RelayItem.Location, RelayItem.CustomList) -> Unit,
onRemoveLocationFromList: (location: RelayItem.Location, parent: CustomListId) -> Unit,
onEditCustomListName: (RelayItem.CustomList) -> Unit,
@@ -70,17 +65,6 @@ internal fun LocationBottomSheets(
val onBackgroundColor: Color = MaterialTheme.colorScheme.onSurface
when (locationBottomSheetState) {
- is ShowCustomListsBottomSheet -> {
- CustomListsBottomSheet(
- backgroundColor = backgroundColor,
- onBackgroundColor = onBackgroundColor,
- sheetState = sheetState,
- bottomSheetState = locationBottomSheetState,
- onCreateCustomList = { onCreateCustomList(null) },
- onEditCustomLists = onEditCustomLists,
- closeBottomSheet = onCloseBottomSheet,
- )
- }
is ShowLocationBottomSheet -> {
LocationBottomSheet(
backgroundColor = backgroundColor,
@@ -125,62 +109,6 @@ internal fun LocationBottomSheets(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-private fun CustomListsBottomSheet(
- backgroundColor: Color,
- onBackgroundColor: Color,
- sheetState: SheetState,
- bottomSheetState: ShowCustomListsBottomSheet,
- onCreateCustomList: () -> Unit,
- onEditCustomLists: () -> Unit,
- closeBottomSheet: (animate: Boolean) -> Unit,
-) {
-
- MullvadModalBottomSheet(
- sheetState = sheetState,
- backgroundColor = backgroundColor,
- onBackgroundColor = onBackgroundColor,
- onDismissRequest = { closeBottomSheet(false) },
- modifier = Modifier.testTag(SELECT_LOCATION_CUSTOM_LIST_BOTTOM_SHEET_TEST_TAG),
- ) {
- HeaderCell(
- text = stringResource(id = R.string.edit_custom_lists),
- background = backgroundColor,
- )
- HorizontalDivider(color = onBackgroundColor)
- IconCell(
- imageVector = Icons.Default.Add,
- title = stringResource(id = R.string.new_list),
- titleColor = onBackgroundColor,
- onClick = {
- onCreateCustomList()
- closeBottomSheet(true)
- },
- background = backgroundColor,
- )
- IconCell(
- imageVector = Icons.Default.Edit,
- title = stringResource(id = R.string.edit_lists),
- titleColor =
- onBackgroundColor.copy(
- alpha =
- if (bottomSheetState.editListEnabled) {
- AlphaVisible
- } else {
- AlphaInactive
- }
- ),
- onClick = {
- onEditCustomLists()
- closeBottomSheet(true)
- },
- background = backgroundColor,
- enabled = bottomSheetState.editListEnabled,
- )
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
private fun LocationBottomSheet(
backgroundColor: Color,
onBackgroundColor: Color,
@@ -407,9 +335,6 @@ internal fun <D : DestinationSpec, R : CustomListActionResultData> ResultRecipie
}
sealed interface LocationBottomSheetState {
-
- data class ShowCustomListsBottomSheet(val editListEnabled: Boolean) : LocationBottomSheetState
-
data class ShowCustomListsEntryBottomSheet(
val customListId: CustomListId,
val customListName: CustomListName,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/RelayListContent.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/RelayListContent.kt
index 0b5617fc7e..a39270921e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/RelayListContent.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/RelayListContent.kt
@@ -2,105 +2,81 @@ package net.mullvad.mullvadvpn.compose.screen.location
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
-import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.material3.HorizontalDivider
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.compose.cell.HeaderCell
-import net.mullvad.mullvadvpn.compose.cell.StatusRelayItemCell
import net.mullvad.mullvadvpn.compose.cell.SwitchComposeSubtitleCell
-import net.mullvad.mullvadvpn.compose.cell.ThreeDotCell
import net.mullvad.mullvadvpn.compose.component.EmptyRelayListText
import net.mullvad.mullvadvpn.compose.component.LocationsEmptyText
-import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowCustomListsBottomSheet
import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowCustomListsEntryBottomSheet
import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowEditCustomListBottomSheet
import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowLocationBottomSheet
-import net.mullvad.mullvadvpn.compose.state.RelayListItem
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemId
+import net.mullvad.mullvadvpn.lib.theme.Dimens
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.ItemPosition
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.SelectableRelayListItem
+import net.mullvad.mullvadvpn.lib.ui.designsystem.RelayListHeader
import net.mullvad.mullvadvpn.lib.ui.tag.LOCATION_CELL_TEST_TAG
import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_CUSTOM_LIST_HEADER_TEST_TAG
/** Used by both the select location screen and search select location screen */
fun LazyListScope.relayListContent(
- backgroundColor: Color,
relayListItems: List<RelayListItem>,
customLists: List<RelayItem.CustomList>,
onSelectRelay: (RelayItem) -> Unit,
onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit,
onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
- customListHeader: @Composable LazyItemScope.() -> Unit = {
- CustomListHeader(
- onShowCustomListBottomSheet = {
- onUpdateBottomSheetState(
- ShowCustomListsBottomSheet(editListEnabled = customLists.isNotEmpty())
- )
- }
- )
- },
- locationHeader: @Composable LazyItemScope.() -> Unit = { RelayLocationHeader() },
+ customListHeader: @Composable (LazyItemScope.() -> Unit) = {},
+ locationHeader: @Composable (LazyItemScope.() -> Unit) = { RelayLocationHeader() },
) {
- itemsIndexed(
+ items(
items = relayListItems,
- key = { _: Int, item: RelayListItem -> item.key },
- contentType = { _, item -> item.contentType },
- itemContent = { index: Int, listItem: RelayListItem ->
+ key = { item: RelayListItem -> item.key },
+ contentType = { item: RelayListItem -> item.contentType },
+ itemContent = { listItem: RelayListItem ->
Column(modifier = Modifier.animateItem()) {
- if (index != 0) {
- HorizontalDivider(color = backgroundColor)
- }
when (listItem) {
RelayListItem.CustomListHeader -> customListHeader()
is RelayListItem.CustomListItem ->
CustomListItem(
listItem,
- onSelectRelay,
- { onUpdateBottomSheetState(ShowEditCustomListBottomSheet(it)) },
- { customListId, expand -> onToggleExpand(customListId, null, expand) },
+ onSelectRelay = onSelectRelay,
+ onToggleExpand = onToggleExpand,
+ onUpdateBottomSheetState = onUpdateBottomSheetState,
)
is RelayListItem.CustomListEntryItem ->
CustomListEntryItem(
listItem,
- { onSelectRelay(listItem.item) },
- // Only direct children can be removed
- if (listItem.depth == 1) {
- {
- onUpdateBottomSheetState(
- ShowCustomListsEntryBottomSheet(
- listItem.parentId,
- listItem.parentName,
- listItem.item,
- )
- )
- }
- } else {
- null
- },
- { expand: Boolean ->
- onToggleExpand(listItem.item.id, listItem.parentId, expand)
- },
+ onSelectRelay = onSelectRelay,
+ onToggleExpand = onToggleExpand,
+ onUpdateBottomSheetState = onUpdateBottomSheetState,
)
is RelayListItem.CustomListFooter -> CustomListFooter(listItem)
RelayListItem.LocationHeader -> locationHeader()
is RelayListItem.GeoLocationItem ->
- RelayLocationItem(
+ GeoLocationItem(
listItem,
- { onSelectRelay(listItem.item) },
- {
- onUpdateBottomSheetState(
- ShowLocationBottomSheet(customLists, listItem.item)
- )
- },
- { expand -> onToggleExpand(listItem.item.id, null, expand) },
+ onSelectRelay = onSelectRelay,
+ onToggleExpand = onToggleExpand,
+ onUpdateBottomSheetState = onUpdateBottomSheetState,
+ customLists = customLists,
)
is RelayListItem.LocationsEmptyText -> LocationsEmptyText(listItem.searchTerm)
is RelayListItem.EmptyRelayList -> EmptyRelayListText()
@@ -111,76 +87,107 @@ fun LazyListScope.relayListContent(
}
@Composable
-private fun LazyItemScope.RelayLocationItem(
- relayItem: RelayListItem.GeoLocationItem,
- onSelectRelay: () -> Unit,
- onLongClick: () -> Unit,
- onExpand: (Boolean) -> Unit,
+fun Modifier.positionalPadding(itemPosition: ItemPosition): Modifier =
+ when (itemPosition) {
+ ItemPosition.Top,
+ ItemPosition.Single -> padding(top = Dimens.miniPadding)
+ ItemPosition.Middle -> padding(top = Dimens.listItemDivider)
+ ItemPosition.Bottom -> padding(top = Dimens.listItemDivider, bottom = Dimens.miniPadding)
+ }
+
+@Composable
+private fun GeoLocationItem(
+ listItem: RelayListItem.GeoLocationItem,
+ onSelectRelay: (RelayItem) -> Unit,
+ onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit,
+ onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
+ customLists: List<RelayItem.CustomList>,
) {
- val location = relayItem.item
- StatusRelayItemCell(
- item = location,
- state = relayItem.state,
- isSelected = relayItem.isSelected,
- onClick = { onSelectRelay() },
- onLongClick = { onLongClick() },
- onToggleExpand = { onExpand(it) },
- isExpanded = relayItem.expanded,
- depth = relayItem.depth,
- modifier = Modifier.testTag(LOCATION_CELL_TEST_TAG),
+ SelectableRelayListItem(
+ relayListItem = listItem,
+ onClick = { onSelectRelay(listItem.item) },
+ onLongClick = {
+ onUpdateBottomSheetState(ShowLocationBottomSheet(customLists, listItem.item))
+ },
+ onToggleExpand = { onToggleExpand(listItem.item.id, null, it) },
+ modifier = Modifier.positionalPadding(listItem.itemPosition).testTag(LOCATION_CELL_TEST_TAG),
)
}
@Composable
-private fun LazyItemScope.CustomListEntryItem(
- itemState: RelayListItem.CustomListEntryItem,
- onSelectRelay: () -> Unit,
- onShowEditCustomListEntryBottomSheet: (() -> Unit)?,
- onToggleExpand: (Boolean) -> Unit,
+private fun CustomListItem(
+ listItem: RelayListItem.CustomListItem,
+ onSelectRelay: (RelayItem) -> Unit,
+ onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit,
+ onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
) {
- val customListEntryItem = itemState.item
- StatusRelayItemCell(
- item = customListEntryItem,
- state = itemState.state,
- isSelected = false,
- onClick = onSelectRelay,
- onLongClick = onShowEditCustomListEntryBottomSheet,
- onToggleExpand = onToggleExpand,
- isExpanded = itemState.expanded,
- depth = itemState.depth,
+ SelectableRelayListItem(
+ relayListItem = listItem,
+ onClick = { onSelectRelay(listItem.item) },
+ onLongClick = { onUpdateBottomSheetState(ShowEditCustomListBottomSheet(listItem.item)) },
+ onToggleExpand = { onToggleExpand(listItem.item.id, null, it) },
+ modifier = Modifier.positionalPadding(listItem.itemPosition),
)
}
@Composable
-private fun LazyItemScope.CustomListItem(
- itemState: RelayListItem.CustomListItem,
- onSelectRelay: (item: RelayItem) -> Unit,
- onShowEditBottomSheet: (RelayItem.CustomList) -> Unit,
- onExpand: ((CustomListId, Boolean) -> Unit),
+private fun CustomListEntryItem(
+ listItem: RelayListItem.CustomListEntryItem,
+ onSelectRelay: (RelayItem) -> Unit,
+ onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit,
+ onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
) {
- val customListItem = itemState.item
- StatusRelayItemCell(
- item = customListItem,
- state = itemState.state,
- isSelected = itemState.isSelected,
- onClick = { onSelectRelay(customListItem) },
- onLongClick = { onShowEditBottomSheet(customListItem) },
- onToggleExpand = { onExpand(customListItem.id, it) },
- isExpanded = itemState.expanded,
+ SelectableRelayListItem(
+ relayListItem = listItem,
+ onClick = { onSelectRelay(listItem.item) },
+ // Only direct children can be removed
+ onLongClick =
+ if (listItem.depth == 1) {
+ {
+ onUpdateBottomSheetState(
+ ShowCustomListsEntryBottomSheet(
+ listItem.parentId,
+ listItem.parentName,
+ listItem.item,
+ )
+ )
+ }
+ } else {
+ null
+ },
+ onToggleExpand = { expand: Boolean ->
+ onToggleExpand(listItem.item.id, listItem.parentId, expand)
+ },
+ modifier = Modifier.positionalPadding(listItem.itemPosition),
)
}
@Composable
-private fun LazyItemScope.CustomListHeader(onShowCustomListBottomSheet: () -> Unit) {
- ThreeDotCell(
- text = stringResource(R.string.custom_lists),
- onClickDots = onShowCustomListBottomSheet,
+fun CustomListHeader(addCustomList: () -> Unit, editCustomLists: (() -> Unit)?) {
+ RelayListHeader(
+ { Text(stringResource(R.string.custom_lists), overflow = TextOverflow.Ellipsis) },
+ actions = {
+ IconButton(onClick = addCustomList) {
+ Icon(
+ imageVector = Icons.Default.Add,
+ contentDescription = stringResource(id = R.string.new_list),
+ )
+ }
+ editCustomLists?.run {
+ IconButton(onClick = editCustomLists) {
+ Icon(
+ imageVector = Icons.Default.Edit,
+ contentDescription = stringResource(id = R.string.edit_lists),
+ )
+ }
+ }
+ },
modifier = Modifier.testTag(SELECT_LOCATION_CUSTOM_LIST_HEADER_TEST_TAG),
)
}
@Composable
-private fun LazyItemScope.CustomListFooter(item: RelayListItem.CustomListFooter) {
+private fun CustomListFooter(item: RelayListItem.CustomListFooter) {
SwitchComposeSubtitleCell(
text =
if (item.hasCustomList) {
@@ -193,6 +200,10 @@ private fun LazyItemScope.CustomListFooter(item: RelayListItem.CustomListFooter)
}
@Composable
-private fun LazyItemScope.RelayLocationHeader() {
- HeaderCell(text = stringResource(R.string.all_locations))
+private fun RelayLocationHeader() {
+ RelayListHeader(
+ content = {
+ Text(text = stringResource(R.string.all_locations), overflow = TextOverflow.Ellipsis)
+ }
+ )
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreen.kt
index f095160107..ea88ada52b 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreen.kt
@@ -45,7 +45,6 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.CreateCustomListDestination
import com.ramcosta.composedestinations.generated.destinations.CustomListLocationsDestination
-import com.ramcosta.composedestinations.generated.destinations.CustomListsDestination
import com.ramcosta.composedestinations.generated.destinations.DeleteCustomListDestination
import com.ramcosta.composedestinations.generated.destinations.EditCustomListNameDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -93,7 +92,6 @@ private fun PreviewSearchLocationScreen(
{ _, _, _ -> },
{},
{},
- {},
{ _, _ -> },
{ _, _ -> },
{},
@@ -150,6 +148,15 @@ fun SearchLocation(
message = context.getString(R.string.error_occurred)
)
}
+
+ is SearchLocationSideEffect.RelayItemInactive -> {
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message =
+ context.getString(R.string.relayitem_is_inactive, it.relayItem.name)
+ )
+ }
+ }
}
}
@@ -183,7 +190,6 @@ fun SearchLocation(
dropUnlessResumed { relayItem ->
navigator.navigate(CreateCustomListDestination(locationCode = relayItem?.id))
},
- onEditCustomLists = dropUnlessResumed { navigator.navigate(CustomListsDestination()) },
onAddLocationToList = viewModel::addLocationToList,
onRemoveLocationFromList = viewModel::removeLocationFromList,
onEditCustomListName =
@@ -226,7 +232,6 @@ fun SearchLocationScreen(
onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit,
onSearchInputChanged: (String) -> Unit,
onCreateCustomList: (location: RelayItem.Location?) -> Unit,
- onEditCustomLists: () -> Unit,
onAddLocationToList: (location: RelayItem.Location, customList: RelayItem.CustomList) -> Unit,
onRemoveLocationFromList: (location: RelayItem.Location, customListId: CustomListId) -> Unit,
onEditCustomListName: (RelayItem.CustomList) -> Unit,
@@ -251,7 +256,6 @@ fun SearchLocationScreen(
LocationBottomSheets(
locationBottomSheetState = locationBottomSheetState,
onCreateCustomList = onCreateCustomList,
- onEditCustomLists = onEditCustomLists,
onAddLocationToList = onAddLocationToList,
onRemoveLocationFromList = onRemoveLocationFromList,
onEditCustomListName = onEditCustomListName,
@@ -277,6 +281,7 @@ fun SearchLocationScreen(
LazyColumn(
modifier =
Modifier.fillMaxSize()
+ .padding(horizontal = Dimens.mediumPadding)
.background(color = backgroundColor)
.drawVerticalScrollbar(
lazyListState,
@@ -301,9 +306,8 @@ fun SearchLocationScreen(
}
is Lce.Content -> {
relayListContent(
- backgroundColor = backgroundColor,
- customLists = state.value.customLists,
relayListItems = state.value.relayListItems,
+ customLists = state.value.customLists,
onSelectRelay = onSelectRelay,
onToggleExpand = onToggleExpand,
onUpdateBottomSheetState = { newSheetState ->
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationList.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationList.kt
index bf3db9a534..4ef79723c0 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationList.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationList.kt
@@ -14,7 +14,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextAlign
@@ -26,13 +25,13 @@ import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicator
import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar
import net.mullvad.mullvadvpn.compose.constant.ContentType
import net.mullvad.mullvadvpn.compose.extensions.animateScrollAndCentralizeItem
-import net.mullvad.mullvadvpn.compose.state.RelayListItem
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationListUiState
import net.mullvad.mullvadvpn.compose.util.RunOnKeyChange
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
import net.mullvad.mullvadvpn.util.Lce
import net.mullvad.mullvadvpn.viewmodel.location.SelectLocationListViewModel
import org.koin.androidx.compose.koinViewModel
@@ -44,10 +43,11 @@ private typealias Content = Lce.Content<SelectLocationListUiState>
@Composable
fun SelectLocationList(
- backgroundColor: Color,
relayListType: RelayListType,
onSelectRelay: (RelayItem) -> Unit,
openDaitaSettings: () -> Unit,
+ onAddCustomList: () -> Unit,
+ onEditCustomLists: (() -> Unit)?,
onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
) {
val viewModel =
@@ -67,6 +67,7 @@ fun SelectLocationList(
LazyColumn(
modifier =
Modifier.fillMaxSize()
+ .padding(horizontal = Dimens.mediumPadding)
.drawVerticalScrollbar(
lazyListState,
MaterialTheme.colorScheme.onSurface.copy(alpha = AlphaScrollbar),
@@ -89,12 +90,18 @@ fun SelectLocationList(
}
is Content -> {
relayListContent(
- backgroundColor = backgroundColor,
relayListItems = stateActual.value.relayListItems,
customLists = stateActual.value.customLists,
onSelectRelay = onSelectRelay,
onToggleExpand = viewModel::onToggleExpand,
onUpdateBottomSheetState = onUpdateBottomSheetState,
+ customListHeader = {
+ CustomListHeader(
+ onAddCustomList,
+ if (stateActual.value.customLists.isNotEmpty()) onEditCustomLists
+ else null,
+ )
+ },
)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt
index ec8c25207b..7429b39324 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt
@@ -31,7 +31,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
@@ -152,6 +151,14 @@ fun SelectLocation(
message = context.getString(R.string.error_occurred)
)
}
+
+ is SelectLocationSideEffect.RelayItemInactive ->
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message =
+ context.getString(R.string.relayitem_is_inactive, it.relayItem.name)
+ )
+ }
}
}
@@ -295,7 +302,6 @@ fun SelectLocationScreen(
LocationBottomSheets(
locationBottomSheetState = locationBottomSheetState,
onCreateCustomList = onCreateCustomList,
- onEditCustomLists = onEditCustomLists,
onAddLocationToList = onAddLocationToList,
onRemoveLocationFromList = onRemoveLocationFromList,
onEditCustomListName = onEditCustomListName,
@@ -341,9 +347,10 @@ fun SelectLocationScreen(
RelayLists(
state = state.value,
- backgroundColor = backgroundColor,
onSelectRelay = onSelectRelay,
openDaitaSettings = openDaitaSettings,
+ onAddCustomList = { onCreateCustomList(null) },
+ onEditCustomLists = onEditCustomLists,
onUpdateBottomSheetState = { newState ->
locationBottomSheetState = newState
},
@@ -381,9 +388,10 @@ private fun MultihopBar(relayListType: RelayListType, onSelectRelayList: (RelayL
@Composable
private fun RelayLists(
state: SelectLocationUiState,
- backgroundColor: Color,
onSelectRelay: (RelayItem) -> Unit,
openDaitaSettings: () -> Unit,
+ onAddCustomList: () -> Unit,
+ onEditCustomLists: (() -> Unit)?,
onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
) {
// This is a workaround for the HorizontalPager being broken on Android TV when it contains
@@ -392,10 +400,11 @@ private fun RelayLists(
if (configuration.navigation == Configuration.NAVIGATION_DPAD) {
SelectLocationList(
- backgroundColor = backgroundColor,
relayListType = state.relayListType,
onSelectRelay = onSelectRelay,
openDaitaSettings = openDaitaSettings,
+ onAddCustomList = onAddCustomList,
+ onEditCustomLists = onEditCustomLists,
onUpdateBottomSheetState = onUpdateBottomSheetState,
)
} else {
@@ -420,10 +429,11 @@ private fun RelayLists(
},
) { pageIndex ->
SelectLocationList(
- backgroundColor = backgroundColor,
relayListType = RelayListType.entries[pageIndex],
onSelectRelay = onSelectRelay,
openDaitaSettings = openDaitaSettings,
+ onAddCustomList = onAddCustomList,
+ onEditCustomLists = onEditCustomLists,
onUpdateBottomSheetState = onUpdateBottomSheetState,
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt
index 9f8eae8c53..527bad85fa 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt
@@ -1,6 +1,6 @@
package net.mullvad.mullvadvpn.compose.state
-import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.CheckableRelayListItem
import net.mullvad.mullvadvpn.util.Lce
data class CustomListLocationsUiState(
@@ -12,12 +12,5 @@ data class CustomListLocationsData(
val saveEnabled: Boolean,
val hasUnsavedChanges: Boolean,
val searchTerm: String,
- val locations: List<RelayLocationListItem>,
-)
-
-data class RelayLocationListItem(
- val item: RelayItem.Location,
- val depth: Int = 0,
- val checked: Boolean = false,
- val expanded: Boolean = false,
+ val locations: List<CheckableRelayListItem>,
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SearchLocationUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SearchLocationUiState.kt
index c377be8814..909e4ea8eb 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SearchLocationUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SearchLocationUiState.kt
@@ -1,6 +1,7 @@
package net.mullvad.mullvadvpn.compose.state
import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
import net.mullvad.mullvadvpn.usecase.FilterChip
data class SearchLocationUiState(
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationListUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationListUiState.kt
index 393286a35e..39199b9d04 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationListUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationListUiState.kt
@@ -1,6 +1,7 @@
package net.mullvad.mullvadvpn.compose.state
import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
data class SelectLocationListUiState(
val relayListItems: List<RelayListItem>,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt
index baadf379cb..8cee6c7423 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt
@@ -21,9 +21,10 @@ import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
import net.mullvad.mullvadvpn.compose.communication.LocationsChanged
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsData
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
-import net.mullvad.mullvadvpn.compose.state.RelayLocationListItem
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemId
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.CheckableRelayListItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.ItemPosition
import net.mullvad.mullvadvpn.relaylist.ancestors
import net.mullvad.mullvadvpn.relaylist.descendants
import net.mullvad.mullvadvpn.relaylist.newFilterOnSearch
@@ -62,11 +63,13 @@ class CustomListLocationsViewModel(
newList = navArgs.newList,
content = Lce.Loading(Unit),
)
+
relayCountries.isEmpty() ->
CustomListLocationsUiState(
newList = navArgs.newList,
content = Lce.Error(Unit),
)
+
else -> {
val (expandSet, filteredRelayCountries) =
searchRelayListLocations(searchTerm, relayCountries)
@@ -78,10 +81,13 @@ class CustomListLocationsViewModel(
CustomListLocationsData(
searchTerm = searchTerm,
locations =
- filteredRelayCountries.toRelayItems(
- isSelected = { it in selectedLocations },
- isExpanded = { it in expandedLocations },
- ),
+ filteredRelayCountries.flatMap {
+ it.toRelayItems(
+ isSelected = { it in selectedLocations },
+ isExpanded = { it in expandedLocations },
+ isLastChild = true,
+ )
+ },
saveEnabled =
selectedLocations.isNotEmpty() &&
selectedLocations != _initialLocations.value,
@@ -190,6 +196,7 @@ class CustomListLocationsViewModel(
.find { it.id == relayItem.id.country }
?.let { updateSelectionList.remove(it) }
}
+
is RelayItem.Location.Relay -> {
availableLocations
.flatMap { country -> country.cities }
@@ -199,6 +206,7 @@ class CustomListLocationsViewModel(
.find { it.id == relayItem.id.country }
?.let { updateSelectionList.remove(it) }
}
+
is RelayItem.Location.Country -> {
/* Do nothing */
}
@@ -216,9 +224,11 @@ class CustomListLocationsViewModel(
saveSelectionList.removeAll(relayItem.cities)
saveSelectionList.removeAll(relayItem.relays)
}
+
is RelayItem.Location.City -> {
saveSelectionList.removeAll(relayItem.relays)
}
+
is RelayItem.Location.Relay -> {
/* Do nothing */
}
@@ -240,42 +250,56 @@ class CustomListLocationsViewModel(
private fun initialExpands(locations: List<RelayItem.Location>): Set<RelayItemId> =
locations.flatMap { it.id.ancestors() }.toSet()
- private fun List<RelayItem.Location>.toRelayItems(
+ private fun RelayItem.Location.toRelayItems(
isSelected: (RelayItem) -> Boolean,
isExpanded: (RelayItemId) -> Boolean,
depth: Int = 0,
- ): List<RelayLocationListItem> = flatMap { relayItem ->
- buildList {
- val expanded = isExpanded(relayItem.id)
- add(
- RelayLocationListItem(
- item = relayItem,
- depth = depth,
- checked = isSelected(relayItem),
- expanded = expanded,
- )
+ isLastChild: Boolean,
+ ): List<CheckableRelayListItem> = buildList {
+ val expanded = isExpanded(id)
+ add(
+ CheckableRelayListItem(
+ item = this@toRelayItems,
+ depth = depth,
+ checked = isSelected(this@toRelayItems),
+ expanded = expanded,
+ itemPosition =
+ when {
+ this@toRelayItems is RelayItem.Location.Country ->
+ if (!expanded) ItemPosition.Single else ItemPosition.Top
+ isLastChild && !expanded -> ItemPosition.Bottom
+ else -> ItemPosition.Middle
+ },
)
- if (expanded) {
- when (relayItem) {
- is RelayItem.Location.City ->
- addAll(
- relayItem.relays.toRelayItems(
+ )
+ if (expanded) {
+ when (this@toRelayItems) {
+ is RelayItem.Location.City ->
+ addAll(
+ relays.flatMapIndexed { index, relay ->
+ relay.toRelayItems(
isSelected = isSelected,
isExpanded = isExpanded,
depth = depth + 1,
+ isLastChild = isLastChild && index == relays.lastIndex,
)
- )
- is RelayItem.Location.Country ->
- addAll(
- relayItem.cities.toRelayItems(
+ }
+ )
+
+ is RelayItem.Location.Country ->
+ addAll(
+ cities.flatMapIndexed { index, item ->
+ item.toRelayItems(
isSelected = isSelected,
isExpanded = isExpanded,
depth = depth + 1,
+ isLastChild = isLastChild && index == cities.lastIndex,
)
- )
- is RelayItem.Location.Relay -> {
- /* Do nothing */
- }
+ }
+ )
+
+ is RelayItem.Location.Relay -> {
+ /* Do nothing */
}
}
}
@@ -299,6 +323,7 @@ class CustomListLocationsViewModel(
relayListRepository.find(success.addedLocations.first())!!.name,
undo = success.undo,
)
+
success.removedLocations.size == 1 && success.addedLocations.isEmpty() ->
CustomListActionResultData.Success.LocationRemoved(
customListName = success.name,
@@ -306,6 +331,7 @@ class CustomListLocationsViewModel(
relayListRepository.find(success.removedLocations.first())!!.name,
undo = success.undo,
)
+
else ->
CustomListActionResultData.Success.LocationChanged(
customListName = success.name,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt
index d80b8fc548..85bd7b282f 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt
@@ -1,13 +1,14 @@
package net.mullvad.mullvadvpn.viewmodel.location
-import net.mullvad.mullvadvpn.compose.state.RelayListItem
-import net.mullvad.mullvadvpn.compose.state.RelayListItemState
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemId
import net.mullvad.mullvadvpn.lib.model.RelayItemSelection
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.ItemPosition
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItemState
import net.mullvad.mullvadvpn.relaylist.filterOnSearchTerm
// Creates a relay list to be displayed by RelayListContent
@@ -176,19 +177,26 @@ private fun createCustomListRelayItems(
selectedByOtherId = selectedByOtherEntryExitList,
),
expanded = expanded,
+ itemPosition =
+ if (expanded) {
+ ItemPosition.Top
+ } else {
+ ItemPosition.Single
+ },
)
)
if (expanded) {
addAll(
- customList.locations.flatMap {
+ customList.locations.flatMapIndexed { index, item ->
createCustomListEntry(
parent = customList,
- item = it,
+ item = item,
relayListType = relayListType,
selectedByOtherEntryExitList = selectedByOtherEntryExitList,
depth = 1,
isExpanded = isExpanded,
+ isLast = index == customList.locations.lastIndex,
)
}
)
@@ -205,13 +213,14 @@ private fun createLocationSection(
): List<RelayListItem> = buildList {
add(RelayListItem.LocationHeader)
addAll(
- countries.flatMap { country ->
+ countries.flatMapIndexed { index, country ->
createGeoLocationEntry(
item = country,
selectedByThisEntryExitList = selectedByThisEntryExitList,
relayListType = relayListType,
selectedByOtherEntryExitList = selectedByOtherEntryExitList,
isExpanded = isExpanded,
+ isLast = true,
)
}
)
@@ -234,6 +243,7 @@ private fun createLocationSectionSearching(
relayListType = relayListType,
selectedByOtherEntryExitList = selectedByOtherEntryExitList,
isExpanded = isExpanded,
+ isLast = true,
)
}
)
@@ -247,6 +257,7 @@ private fun createCustomListEntry(
selectedByOtherEntryExitList: RelayItemId?,
depth: Int = 1,
isExpanded: (String) -> Boolean,
+ isLast: Boolean,
): List<RelayListItem.CustomListEntryItem> = buildList {
val expanded = isExpanded(item.id.expandKey(parent.id))
add(
@@ -261,6 +272,12 @@ private fun createCustomListEntry(
),
expanded = expanded,
depth = depth,
+ itemPosition =
+ if (!expanded && isLast) {
+ ItemPosition.Bottom
+ } else {
+ ItemPosition.Middle
+ },
)
)
@@ -268,27 +285,29 @@ private fun createCustomListEntry(
when (item) {
is RelayItem.Location.City ->
addAll(
- item.relays.flatMap {
+ item.relays.flatMapIndexed { index, relay ->
createCustomListEntry(
parent = parent,
- item = it,
+ item = relay,
relayListType = relayListType,
selectedByOtherEntryExitList = selectedByOtherEntryExitList,
depth = depth + 1,
isExpanded = isExpanded,
+ isLast = isLast && index == item.relays.lastIndex,
)
}
)
is RelayItem.Location.Country ->
addAll(
- item.cities.flatMap {
+ item.cities.flatMapIndexed { index, city ->
createCustomListEntry(
parent = parent,
- item = it,
+ item = city,
relayListType = relayListType,
selectedByOtherEntryExitList = selectedByOtherEntryExitList,
depth = depth + 1,
isExpanded = isExpanded,
+ isLast = isLast && index == item.cities.lastIndex,
)
}
)
@@ -304,6 +323,7 @@ private fun createGeoLocationEntry(
selectedByOtherEntryExitList: RelayItemId?,
depth: Int = 0,
isExpanded: (String) -> Boolean,
+ isLast: Boolean,
): List<RelayListItem.GeoLocationItem> = buildList {
val expanded = isExpanded(item.id.expandKey())
@@ -318,6 +338,24 @@ private fun createGeoLocationEntry(
),
depth = depth,
expanded = expanded,
+ itemPosition =
+ when (item) {
+ is RelayItem.Location.Country -> {
+ if (expanded) {
+ ItemPosition.Top
+ } else {
+ ItemPosition.Single
+ }
+ }
+
+ else -> {
+ if (isLast && !expanded) {
+ ItemPosition.Bottom
+ } else {
+ ItemPosition.Middle
+ }
+ }
+ },
)
)
@@ -325,27 +363,29 @@ private fun createGeoLocationEntry(
when (item) {
is RelayItem.Location.City ->
addAll(
- item.relays.flatMap {
+ item.relays.flatMapIndexed { index, relay ->
createGeoLocationEntry(
- item = it,
+ item = relay,
relayListType = relayListType,
selectedByThisEntryExitList = selectedByThisEntryExitList,
selectedByOtherEntryExitList = selectedByOtherEntryExitList,
depth = depth + 1,
isExpanded = isExpanded,
+ isLast = isLast && index == item.relays.lastIndex,
)
}
)
is RelayItem.Location.Country ->
addAll(
- item.cities.flatMap {
+ item.cities.flatMapIndexed { index, city ->
createGeoLocationEntry(
- item = it,
+ item = city,
relayListType = relayListType,
selectedByThisEntryExitList = selectedByThisEntryExitList,
selectedByOtherEntryExitList = selectedByOtherEntryExitList,
depth = depth + 1,
isExpanded = isExpanded,
+ isLast = isLast && index == item.cities.lastIndex,
)
}
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt
index 5310fe5ca8..9ac657423f 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt
@@ -119,16 +119,24 @@ class SearchLocationViewModel(
fun selectRelay(relayItem: RelayItem) {
viewModelScope.launch {
- selectRelayItem(
- relayItem = relayItem,
- relayListType = relayListType,
- selectEntryLocation = wireguardConstraintsRepository::setEntryLocation,
- selectExitLocation = relayListRepository::updateSelectedRelayLocation,
- )
- .fold(
- { _uiSideEffect.send(SearchLocationSideEffect.GenericError) },
- { _uiSideEffect.send(SearchLocationSideEffect.LocationSelected(relayListType)) },
- )
+ if (relayItem.active) {
+ selectRelayItem(
+ relayItem = relayItem,
+ relayListType = relayListType,
+ selectEntryLocation = wireguardConstraintsRepository::setEntryLocation,
+ selectExitLocation = relayListRepository::updateSelectedRelayLocation,
+ )
+ .fold(
+ { _uiSideEffect.send(SearchLocationSideEffect.GenericError) },
+ {
+ _uiSideEffect.send(
+ SearchLocationSideEffect.LocationSelected(relayListType)
+ )
+ },
+ )
+ } else {
+ _uiSideEffect.send(SearchLocationSideEffect.RelayItemInactive(relayItem))
+ }
}
}
@@ -217,5 +225,7 @@ sealed interface SearchLocationSideEffect {
data class CustomListActionToast(val resultData: CustomListActionResultData) :
SearchLocationSideEffect
+ data class RelayItemInactive(val relayItem: RelayItem) : SearchLocationSideEffect
+
data object GenericError : SearchLocationSideEffect
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt
index c9cca74602..0420559245 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt
@@ -75,22 +75,27 @@ class SelectLocationViewModel(
fun selectRelay(relayItem: RelayItem) {
viewModelScope.launch {
- selectRelayItem(
- relayItem = relayItem,
- relayListType = _relayListType.value,
- selectEntryLocation = wireguardConstraintsRepository::setEntryLocation,
- selectExitLocation = relayListRepository::updateSelectedRelayLocation,
- )
- .fold(
- { _uiSideEffect.send(SelectLocationSideEffect.GenericError) },
- {
- when (_relayListType.value) {
- RelayListType.ENTRY -> _relayListType.emit(RelayListType.EXIT)
- RelayListType.EXIT ->
- _uiSideEffect.send(SelectLocationSideEffect.CloseScreen)
- }
- },
- )
+ if (relayItem.active) {
+
+ selectRelayItem(
+ relayItem = relayItem,
+ relayListType = _relayListType.value,
+ selectEntryLocation = wireguardConstraintsRepository::setEntryLocation,
+ selectExitLocation = relayListRepository::updateSelectedRelayLocation,
+ )
+ .fold(
+ { _uiSideEffect.send(SelectLocationSideEffect.GenericError) },
+ {
+ when (_relayListType.value) {
+ RelayListType.ENTRY -> _relayListType.emit(RelayListType.EXIT)
+ RelayListType.EXIT ->
+ _uiSideEffect.send(SelectLocationSideEffect.CloseScreen)
+ }
+ },
+ )
+ } else {
+ _uiSideEffect.send(SelectLocationSideEffect.RelayItemInactive(relayItem))
+ }
}
}
@@ -139,4 +144,6 @@ sealed interface SelectLocationSideEffect {
SelectLocationSideEffect
data object GenericError : SelectLocationSideEffect
+
+ data class RelayItemInactive(val relayItem: RelayItem) : SelectLocationSideEffect
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt
index edb4a9adfe..9b31f8bf24 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt
@@ -15,7 +15,6 @@ import net.mullvad.mullvadvpn.compose.communication.LocationsChanged
import net.mullvad.mullvadvpn.compose.screen.CustomListLocationsNavArgs
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsData
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
-import net.mullvad.mullvadvpn.compose.state.RelayLocationListItem
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
import net.mullvad.mullvadvpn.lib.common.test.assertLists
import net.mullvad.mullvadvpn.lib.model.CustomList
@@ -25,6 +24,7 @@ import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.Ownership
import net.mullvad.mullvadvpn.lib.model.ProviderId
import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.CheckableRelayListItem
import net.mullvad.mullvadvpn.relaylist.descendants
import net.mullvad.mullvadvpn.relaylist.withDescendants
import net.mullvad.mullvadvpn.repository.RelayListRepository
@@ -77,7 +77,7 @@ class CustomListLocationsViewModelTest {
// Arrange
val expectedList =
DUMMY_COUNTRIES.map {
- RelayLocationListItem(
+ CheckableRelayListItem(
item = it,
depth = it.toDepth(),
checked = false,
@@ -118,8 +118,9 @@ class CustomListLocationsViewModelTest {
viewModel.uiState.test {
// Check no selected
val firstState = awaitItem()
- assertIs<Lce.Content<CustomListLocationsData>>(firstState.content)
- assertEquals(emptyList<RelayItem>(), firstState.content.selectedLocations())
+ val firstStateContent = firstState.content
+ assertIs<Lce.Content<CustomListLocationsData>>(firstStateContent)
+ assertEquals(emptyList<RelayItem>(), firstStateContent.selectedLocations())
// Expand country
viewModel.onExpand(DUMMY_COUNTRIES[0], true)
awaitItem()
@@ -130,8 +131,9 @@ class CustomListLocationsViewModelTest {
viewModel.onRelaySelectionClick(DUMMY_COUNTRIES[0], true)
// Check all items selected
val secondState = awaitItem()
- assertIs<Lce.Content<CustomListLocationsData>>(secondState.content)
- assertLists(expectedSelection, secondState.content.selectedLocations())
+ val content = secondState.content
+ assertIs<Lce.Content<CustomListLocationsData>>(content)
+ assertLists(expectedSelection, content.selectedLocations())
}
}
@@ -150,14 +152,14 @@ class CustomListLocationsViewModelTest {
// Act, Assert
viewModel.uiState.test {
// Check initial selected
- val firstState = awaitItem()
- assertIs<Lce.Content<CustomListLocationsData>>(firstState.content)
- assertEquals(initialSelectionIds, firstState.content.selectedLocations())
+ val firstStateContent = awaitItem().content
+ assertIs<Lce.Content<CustomListLocationsData>>(firstStateContent)
+ assertEquals(initialSelectionIds, firstStateContent.selectedLocations())
viewModel.onRelaySelectionClick(DUMMY_COUNTRIES[0].cities[0].relays[0], false)
// Check all items selected
- val secondState = awaitItem()
- assertIs<Lce.Content<CustomListLocationsData>>(secondState.content)
- assertEquals(expectedSelection, secondState.content.selectedLocations())
+ val secondStateContent = awaitItem().content
+ assertIs<Lce.Content<CustomListLocationsData>>(secondStateContent)
+ assertEquals(expectedSelection, secondStateContent.selectedLocations())
}
}
@@ -176,14 +178,14 @@ class CustomListLocationsViewModelTest {
// Act, Assert
viewModel.uiState.test {
- val firstState = awaitItem()
- assertIs<Lce.Content<CustomListLocationsData>>(firstState.content)
- assertEquals(initialSelectionIds, firstState.content.selectedLocations())
+ val firstStateContent = awaitItem().content
+ assertIs<Lce.Content<CustomListLocationsData>>(firstStateContent)
+ assertEquals(initialSelectionIds, firstStateContent.selectedLocations())
viewModel.onRelaySelectionClick(DUMMY_COUNTRIES[0], false)
// Check all items selected
- val secondState = awaitItem()
- assertIs<Lce.Content<CustomListLocationsData>>(secondState.content)
- assertEquals(expectedSelection, secondState.content.selectedLocations())
+ val secondStateContent = awaitItem().content
+ assertIs<Lce.Content<CustomListLocationsData>>(secondStateContent)
+ assertEquals(expectedSelection, secondStateContent.selectedLocations())
}
}
@@ -205,14 +207,14 @@ class CustomListLocationsViewModelTest {
// Expand city
viewModel.onExpand(DUMMY_COUNTRIES[0].cities[0], true)
// Check no selected
- val firstState = awaitItem()
- assertIs<Lce.Content<CustomListLocationsData>>(firstState.content)
- assertEquals(emptyList<RelayItem>(), firstState.content.selectedLocations())
+ val firstStateContent = awaitItem().content
+ assertIs<Lce.Content<CustomListLocationsData>>(firstStateContent)
+ assertEquals(emptyList<RelayItem>(), firstStateContent.selectedLocations())
viewModel.onRelaySelectionClick(DUMMY_COUNTRIES[0].cities[0].relays[0], true)
// Check all items selected
- val secondState = awaitItem()
- assertIs<Lce.Content<CustomListLocationsData>>(secondState.content)
- assertEquals(expectedSelection, secondState.content.selectedLocations())
+ val secondStateContent = awaitItem().content
+ assertIs<Lce.Content<CustomListLocationsData>>(secondStateContent)
+ assertEquals(expectedSelection, secondStateContent.selectedLocations())
}
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModelTest.kt
index 0166bafa98..ad0f87638f 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModelTest.kt
@@ -8,7 +8,6 @@ import kotlin.test.assertIs
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.compose.screen.location.SearchLocationNavArgs
-import net.mullvad.mullvadvpn.compose.state.RelayListItem
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SearchLocationUiState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
@@ -18,6 +17,7 @@ import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemSelection
import net.mullvad.mullvadvpn.lib.model.WireguardConstraints
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
import net.mullvad.mullvadvpn.repository.CustomListsRepository
import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
import net.mullvad.mullvadvpn.repository.RelayListRepository
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt
index 46994ead49..fb974e52fb 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt
@@ -6,7 +6,6 @@ import io.mockk.mockk
import kotlin.test.assertIs
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
-import net.mullvad.mullvadvpn.compose.state.RelayListItem
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationListUiState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
@@ -16,6 +15,7 @@ import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemSelection
import net.mullvad.mullvadvpn.lib.model.Settings
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt
index 304b664dc6..7115cd58c0 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt
@@ -108,6 +108,7 @@ class SelectLocationViewModelTest {
val mockRelayItem: RelayItem.Location.Country = mockk()
val relayItemId: GeoLocationId.Country = mockk(relaxed = true)
every { mockRelayItem.id } returns relayItemId
+ every { mockRelayItem.active } returns true
coEvery { mockRelayListRepository.updateSelectedRelayLocation(relayItemId) } returns
Unit.right()
@@ -126,6 +127,7 @@ class SelectLocationViewModelTest {
// Arrange
val mockRelayItem: RelayItem.Location.Country = mockk()
val relayItemId: GeoLocationId.Country = mockk(relaxed = true)
+ every { mockRelayItem.active } returns true
every { mockRelayItem.id } returns relayItemId
coEvery { mockWireguardConstraintsRepository.setEntryLocation(relayItemId) } returns
Unit.right()
diff --git a/android/gradle/verification-metadata.keys.xml b/android/gradle/verification-metadata.keys.xml
index 5e8e8ea39d..4782bca7a1 100644
--- a/android/gradle/verification-metadata.keys.xml
+++ b/android/gradle/verification-metadata.keys.xml
@@ -260,6 +260,11 @@
<sha256 value="d3a676709dea04f2a8506e2ae85052fff763db526ac7f16b04de50fdd05b0720" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="androidx.activity" name="activity-compose" version="1.7.0">
+ <artifact name="activity-compose-1.7.0.module">
+ <sha256 value="f7a29bcba338575dcf89a553cff9cfad3f140340eaf2b56fd0193244da602c0a" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ </component>
<component group="androidx.activity" name="activity-ktx" version="1.7.0">
<artifact name="activity-ktx-1.7.0.aar">
<sha256 value="fce317d61a22f12967b475bfcb80c89dda66e418975e890ea703cb74e12b5b11" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -625,6 +630,11 @@
<sha256 value="8083710b758ac096891e91f51d91ee56a445b265d7becf230355377327c0418b" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
+ <component group="androidx.lifecycle" name="lifecycle-viewmodel-savedstate" version="2.5.1">
+ <artifact name="lifecycle-viewmodel-savedstate-2.5.1.module">
+ <sha256 value="29acd5fe614b3f89123eb838f688d625eaa8b422c8d1905b48ad8e760cd7ad8b" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ </component>
<component group="androidx.lifecycle" name="lifecycle-viewmodel-savedstate" version="2.6.1">
<artifact name="lifecycle-viewmodel-savedstate-2.6.1.module">
<sha256 value="dafb8649763d29c29cda27bc22fcdab9a9efc53c0fff9ae3de90882eabaa8944" origin="Generated by Gradle" reason="Artifact is not signed"/>
diff --git a/android/gradle/verification-metadata.xml b/android/gradle/verification-metadata.xml
index 8512f7c0db..7f3a3cc60d 100644
--- a/android/gradle/verification-metadata.xml
+++ b/android/gradle/verification-metadata.xml
@@ -91,6 +91,11 @@
<sha256 value="1ed13a50edbb885962751e1bcb5b8a4207a20cb780ea248ffa653aab3fb10fe9" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="androidx.activity" name="activity-compose" version="1.7.0">
+ <artifact name="activity-compose-1.7.0.module">
+ <sha256 value="f7a29bcba338575dcf89a553cff9cfad3f140340eaf2b56fd0193244da602c0a" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="androidx.activity" name="activity-compose" version="1.8.0">
<artifact name="activity-compose-1.8.0.aar">
<sha256 value="3b462bc760eba180956543232ca467c7b6200a091c5bfa146f906cfabc856314" origin="Generated by Gradle"/>
@@ -364,11 +369,6 @@
<sha256 value="4228894e25eec7e17329b206ba72e8aad255bc535018dc62acc5eff3b5c7aaa5" origin="Generated by Gradle"/>
</artifact>
</component>
- <component group="androidx.compose.animation" name="animation" version="1.6.0">
- <artifact name="animation-1.6.0.module">
- <sha256 value="e98defdf92ca1fcbeaf16e78a60c18052b01340da3849b93e25e944f06a4e527" origin="Generated by Gradle"/>
- </artifact>
- </component>
<component group="androidx.compose.animation" name="animation" version="1.6.8">
<artifact name="animation-1.6.8.module">
<sha256 value="31e6783f9a1de6e021942c5be1f1d777e330bfe017f5429032a24f4c3a940726" origin="Generated by Gradle"/>
@@ -415,9 +415,6 @@
<artifact name="animation-android-1.7.2.module">
<sha256 value="7f4773c5800c09adb41f37a266effaac1618737eeadae80310a771a37cd8b547" origin="Generated by Gradle"/>
</artifact>
- <artifact name="animation-release.aar">
- <sha256 value="cbd5746f38201f19511c4afc99311c646647b2bed1f902ec677d3e17d83576da" origin="Generated by Gradle"/>
- </artifact>
</component>
<component group="androidx.compose.animation" name="animation-android" version="1.8.3">
<artifact name="animation-android-1.8.3.module">
@@ -471,11 +468,6 @@
<sha256 value="c571f43b5780fb80f42e85644e0ba5696af088d7228f3251c3e1d64c3bd64376" origin="Generated by Gradle"/>
</artifact>
</component>
- <component group="androidx.compose.animation" name="animation-core-android" version="1.6.8">
- <artifact name="animation-core-android-1.6.8.module">
- <sha256 value="2814fcf1645cb1d5782b216236b99a4e2dde5bdcbb8e815f4514c044c28b2bef" origin="Generated by Gradle"/>
- </artifact>
- </component>
<component group="androidx.compose.animation" name="animation-core-android" version="1.7.0">
<artifact name="animation-core-android-1.7.0.module">
<sha256 value="4c6bed497979faa58f3252b7922da7cab80da247ef40aca8eb513243e9d40cdc" origin="Generated by Gradle"/>
@@ -484,14 +476,6 @@
<sha256 value="2dc22359de83ed0259a8b7834f16d987d82a90c7197eabb44d72cbdac67bf82c" origin="Generated by Gradle"/>
</artifact>
</component>
- <component group="androidx.compose.animation" name="animation-core-android" version="1.7.2">
- <artifact name="animation-core-android-1.7.2.module">
- <sha256 value="81c5eb129bdc1d09567726b7a88297c6d43def70a1065734bc64fba648403c5b" origin="Generated by Gradle"/>
- </artifact>
- <artifact name="animation-core-release.aar">
- <sha256 value="a44134a34b934b75e93a06b4605a581073902f7d232a167fd1446c95200b5c4d" origin="Generated by Gradle"/>
- </artifact>
- </component>
<component group="androidx.compose.animation" name="animation-core-android" version="1.8.3">
<artifact name="animation-core-android-1.8.3.module">
<sha256 value="ccb58ff68ab6188b143f9b45d5b7e67838e3b7127a0ed76cd05ac80dd773c136" origin="Generated by Gradle"/>
@@ -569,9 +553,6 @@
<artifact name="foundation-android-1.7.2.module">
<sha256 value="d1d33ea24a8dde06b182bf34e2509bd3f6a1217a9cb411c82645d705fe377b24" origin="Generated by Gradle"/>
</artifact>
- <artifact name="foundation-release.aar">
- <sha256 value="93c53326154ea17b4d8c3e04b54361da1e1715bc9b40461a44ffc23b37227a87" origin="Generated by Gradle"/>
- </artifact>
</component>
<component group="androidx.compose.foundation" name="foundation-android" version="1.8.3">
<artifact name="foundation-android-1.8.3.module">
@@ -635,9 +616,6 @@
<artifact name="foundation-layout-android-1.7.2.module">
<sha256 value="34978d5115d736c20a6ae0aa8805a5799c8f4bd902059171a8e644b9484702f5" origin="Generated by Gradle"/>
</artifact>
- <artifact name="foundation-layout-release.aar">
- <sha256 value="1cbdce4b394da76b0f98ff8d31a3613c4ec355c9198cfe4d8c06bb02f7b76b38" origin="Generated by Gradle"/>
- </artifact>
</component>
<component group="androidx.compose.foundation" name="foundation-layout-android" version="1.8.3">
<artifact name="foundation-layout-android-1.8.3.module">
@@ -2155,6 +2133,11 @@
<sha256 value="8083710b758ac096891e91f51d91ee56a445b265d7becf230355377327c0418b" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="androidx.lifecycle" name="lifecycle-viewmodel-savedstate" version="2.5.1">
+ <artifact name="lifecycle-viewmodel-savedstate-2.5.1.module">
+ <sha256 value="29acd5fe614b3f89123eb838f688d625eaa8b422c8d1905b48ad8e760cd7ad8b" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="androidx.lifecycle" name="lifecycle-viewmodel-savedstate" version="2.6.1">
<artifact name="lifecycle-viewmodel-savedstate-2.6.1.module">
<sha256 value="dafb8649763d29c29cda27bc22fcdab9a9efc53c0fff9ae3de90882eabaa8944" origin="Generated by Gradle"/>
diff --git a/android/lib/resource/src/main/res/values-da/strings.xml b/android/lib/resource/src/main/res/values-da/strings.xml
index abb07da226..48ed96072a 100644
--- a/android/lib/resource/src/main/res/values-da/strings.xml
+++ b/android/lib/resource/src/main/res/values-da/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">Dette felt er påkrævet</string>
<string name="this_is_already_set_as_current">Den er allerede indstillet som aktuel</string>
<string name="time_added">Tid tilføjet</string>
- <string name="to_add_locations_to_a_list">Tryk på \" ︙ \" eller tryk langvarigt på et land, en by eller en server for at tilføje placeringer til en liste.</string>
- <string name="to_create_a_custom_list">Tryk på \" ︙ \" for at oprette en brugerdefineret liste</string>
<string name="toggle_vpn">Slå VPN til/fra</string>
<string name="top_bar_device_name">Enhedsnavn: %1$s</string>
<string name="top_bar_time_left">Resterende tid: %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-de/strings.xml b/android/lib/resource/src/main/res/values-de/strings.xml
index 37277ab1cf..3dcfcae8d8 100644
--- a/android/lib/resource/src/main/res/values-de/strings.xml
+++ b/android/lib/resource/src/main/res/values-de/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">Dieses Feld ist erforderlich</string>
<string name="this_is_already_set_as_current">Diese ist bereits die aktuelle</string>
<string name="time_added">Zeit hinzugefügt</string>
- <string name="to_add_locations_to_a_list">Um Standorte zu einer Liste hinzuzufügen, drücken Sie auf „︙“ oder drücken Sie lange auf ein Land, eine Stadt oder einen Server.</string>
- <string name="to_create_a_custom_list">Um eine eigene Liste zu erstellen, drücken Sie auf „︙“</string>
<string name="toggle_vpn">VPN umschalten</string>
<string name="top_bar_device_name">Gerätename: %1$s</string>
<string name="top_bar_time_left">Verbleibende Zeit: %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-es/strings.xml b/android/lib/resource/src/main/res/values-es/strings.xml
index 6644043d86..b5b55248a8 100644
--- a/android/lib/resource/src/main/res/values-es/strings.xml
+++ b/android/lib/resource/src/main/res/values-es/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">Este campo es obligatorio</string>
<string name="this_is_already_set_as_current">Este ya está configurado como actual</string>
<string name="time_added">Tiempo añadido</string>
- <string name="to_add_locations_to_a_list">Para añadir ubicaciones a una lista, pulse «︙» o mantenga pulsado unos segundos un país, ciudad o servidor.</string>
- <string name="to_create_a_custom_list">Para crear una lista personalizada, pulse «︙»</string>
<string name="toggle_vpn">Alternar VPN</string>
<string name="top_bar_device_name">Nombre del dispositivo: %1$s</string>
<string name="top_bar_time_left">Tiempo restante: %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-fi/strings.xml b/android/lib/resource/src/main/res/values-fi/strings.xml
index b81c2b8d6b..71b5abaa50 100644
--- a/android/lib/resource/src/main/res/values-fi/strings.xml
+++ b/android/lib/resource/src/main/res/values-fi/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">Tämä kenttä on pakollinen</string>
<string name="this_is_already_set_as_current">Tämä on jo asetettu nykyiseksi</string>
<string name="time_added">Käyttöaikaa lisätty</string>
- <string name="to_add_locations_to_a_list">Jos haluat lisätä luetteloon sijainteja, paina \"︙\" tai paina pitkään maata, kaupunkia tai palvelinta.</string>
- <string name="to_create_a_custom_list">Voit luoda mukautetun luettelon painamalla \"︙\"</string>
<string name="toggle_vpn">Vaihda VPN:ää</string>
<string name="top_bar_device_name">Laitteen nimi: %1$s</string>
<string name="top_bar_time_left">Aikaa jäljellä: %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-fr/strings.xml b/android/lib/resource/src/main/res/values-fr/strings.xml
index a9299b359c..5eb05d1775 100644
--- a/android/lib/resource/src/main/res/values-fr/strings.xml
+++ b/android/lib/resource/src/main/res/values-fr/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">Ce champ est requis</string>
<string name="this_is_already_set_as_current">Déjà définie comme méthode actuelle</string>
<string name="time_added">Temps ajouté</string>
- <string name="to_add_locations_to_a_list">Pour ajouter des localisations à une liste, appuyez sur la touche « ︙ » ou appuyez longuement sur un pays, une ville ou un serveur.</string>
- <string name="to_create_a_custom_list">Appuyez sur « ︙ » pour créer une liste personnalisée</string>
<string name="toggle_vpn">Activer/désactiver le VPN</string>
<string name="top_bar_device_name">Nom de l\'appareil : %1$s</string>
<string name="top_bar_time_left">Temps restant : %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-it/strings.xml b/android/lib/resource/src/main/res/values-it/strings.xml
index b17d8d64f8..84a4bd347a 100644
--- a/android/lib/resource/src/main/res/values-it/strings.xml
+++ b/android/lib/resource/src/main/res/values-it/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">Questo campo è obbligatorio</string>
<string name="this_is_already_set_as_current">È già impostato come attuale</string>
<string name="time_added">Tempo aggiunto</string>
- <string name="to_add_locations_to_a_list">Per aggiungere posizioni a un elenco, premi \"︙\" o tieni premuto su un Paese, una città o un server.</string>
- <string name="to_create_a_custom_list">Per creare un elenco personalizzato, premi \"︙\"</string>
<string name="toggle_vpn">Attiva/disattiva VPN</string>
<string name="top_bar_device_name">Nome del dispositivo: %1$s</string>
<string name="top_bar_time_left">Tempo rimasto: %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-ja/strings.xml b/android/lib/resource/src/main/res/values-ja/strings.xml
index d90c551c36..9c623768af 100644
--- a/android/lib/resource/src/main/res/values-ja/strings.xml
+++ b/android/lib/resource/src/main/res/values-ja/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">このフィールドは必須です</string>
<string name="this_is_already_set_as_current">これはすでに現在の設定として設定されています</string>
<string name="time_added">時間が追加されました</string>
- <string name="to_add_locations_to_a_list">リストに場所を追加するには、\"︙\" を押すか、または、国、都市、サーバーを長押ししてください。</string>
- <string name="to_create_a_custom_list">カスタムリストを作成するには、\"︙\" を押してください</string>
<string name="toggle_vpn">VPNの切り替え</string>
<string name="top_bar_device_name">デバイス名: %1$s</string>
<string name="top_bar_time_left">残り時間: %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-ko/strings.xml b/android/lib/resource/src/main/res/values-ko/strings.xml
index e47b9d5ed7..1e4e0950e5 100644
--- a/android/lib/resource/src/main/res/values-ko/strings.xml
+++ b/android/lib/resource/src/main/res/values-ko/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">이 필드는 필수입니다</string>
<string name="this_is_already_set_as_current">이미 현재로 설정되어 있습니다</string>
<string name="time_added">시간 추가됨</string>
- <string name="to_add_locations_to_a_list">목록에 위치를 추가하려면 \"︙\"를 누르거나 국가, 도시 또는 서버를 길게 누릅니다.</string>
- <string name="to_create_a_custom_list">사용자 지정 목록을 생성하려면 \"︙\"를 누릅니다</string>
<string name="toggle_vpn">VPN 전환</string>
<string name="top_bar_device_name">장치 이름: %1$s</string>
<string name="top_bar_time_left">남은 시간: %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-my/strings.xml b/android/lib/resource/src/main/res/values-my/strings.xml
index 252426d735..deeb595a88 100644
--- a/android/lib/resource/src/main/res/values-my/strings.xml
+++ b/android/lib/resource/src/main/res/values-my/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">ဤအကွက်ကို မဖြစ်မနေဖြည့်ရမည်</string>
<string name="this_is_already_set_as_current">၎င်းကို လက်ရှိအဖြစ် သတ်မှတ်ထားပြီးပါပြီ</string>
<string name="time_added">အချိန်တိုးထားသည်</string>
- <string name="to_add_locations_to_a_list">စာရင်းထဲသို့ တည်နေရာများကို ပေါင်းထည့်ရန် \"︙\" ကို နှိပ်ပါ သို့မဟုတ် နိုင်ငံ၊ မြို့၊ ဆာဗာကို နှိပ်ပါ။</string>
- <string name="to_create_a_custom_list">စိတ်ကြိုက် စာရင်းများကို ဖန်တီးရန် \"︙\" ကို နှိပ်ပါ</string>
<string name="toggle_vpn">VPN ရွေးသုံးရန်</string>
<string name="top_bar_device_name">စက်အမည်- %1$s</string>
<string name="top_bar_time_left">ကျန်သည့် အချိန်- %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-nb/strings.xml b/android/lib/resource/src/main/res/values-nb/strings.xml
index 310f0bfabf..5d9fe91901 100644
--- a/android/lib/resource/src/main/res/values-nb/strings.xml
+++ b/android/lib/resource/src/main/res/values-nb/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">Feltet er påkrevd</string>
<string name="this_is_already_set_as_current">Denne er allerede angitt som gjeldende</string>
<string name="time_added">Tid lagt til</string>
- <string name="to_add_locations_to_a_list">Hvis du vil legge til plasseringer i en liste, trykker du på ︙ eller trykker på og holder inne et land, en by eller en server.</string>
- <string name="to_create_a_custom_list">For å opprette en egendefinert liste trykker du på ︙</string>
<string name="toggle_vpn">Velg VPN</string>
<string name="top_bar_device_name">Enhetsnavn: %1$s</string>
<string name="top_bar_time_left">Tid igjen: %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-nl/strings.xml b/android/lib/resource/src/main/res/values-nl/strings.xml
index 472ea9f1ab..4687d40208 100644
--- a/android/lib/resource/src/main/res/values-nl/strings.xml
+++ b/android/lib/resource/src/main/res/values-nl/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">Dit veld is verplicht</string>
<string name="this_is_already_set_as_current">Deze is al ingesteld als huidige</string>
<string name="time_added">Tijd van toevoegen</string>
- <string name="to_add_locations_to_a_list">Druk op de \"︙\" of druk lang op een land, plaats of server om locaties toe te voegen.</string>
- <string name="to_create_a_custom_list">Druk op de \"︙\" om een aangepaste lijst te maken</string>
<string name="toggle_vpn">VPN in-/uitschakelen</string>
<string name="top_bar_device_name">Apparaatnaam: %1$s</string>
<string name="top_bar_time_left">Resterende tijd: %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-pl/strings.xml b/android/lib/resource/src/main/res/values-pl/strings.xml
index 76a26b0ec9..fb344a0998 100644
--- a/android/lib/resource/src/main/res/values-pl/strings.xml
+++ b/android/lib/resource/src/main/res/values-pl/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">To pole jest wymagane</string>
<string name="this_is_already_set_as_current">Już ustawiona jako bieżąca</string>
<string name="time_added">Dodano czas</string>
- <string name="to_add_locations_to_a_list">Aby dodać lokalizacje do listy, naciśnij przycisk „︙” lub naciśnij i przytrzymaj kraj, miasto albo serwer.</string>
- <string name="to_create_a_custom_list">Aby utworzyć listę niestandardową, naciśnij przycisk „︙”</string>
<string name="toggle_vpn">Przełącz VPN</string>
<string name="top_bar_device_name">Nazwa urządzenia: %1$s</string>
<string name="top_bar_time_left">Pozostało: %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-pt/strings.xml b/android/lib/resource/src/main/res/values-pt/strings.xml
index 39727d780c..dd3b3be340 100644
--- a/android/lib/resource/src/main/res/values-pt/strings.xml
+++ b/android/lib/resource/src/main/res/values-pt/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">Este campo é obrigatório</string>
<string name="this_is_already_set_as_current">Este já está definido como o atual</string>
<string name="time_added">Tempo adicionado</string>
- <string name="to_add_locations_to_a_list">Para adicionar localizações a uma lista, prima o botão \"︙\" ou mantenha premido um país, uma cidade ou um servidor.</string>
- <string name="to_create_a_custom_list">Para criar uma lista personalizada prima o botão \"︙\"</string>
<string name="toggle_vpn">Alternar VPN</string>
<string name="top_bar_device_name">Nome do dispositivo: %1$s</string>
<string name="top_bar_time_left">Tempo restante: %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-ru/strings.xml b/android/lib/resource/src/main/res/values-ru/strings.xml
index aa7785dc7b..4c57b0817a 100644
--- a/android/lib/resource/src/main/res/values-ru/strings.xml
+++ b/android/lib/resource/src/main/res/values-ru/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">Это обязательное поле</string>
<string name="this_is_already_set_as_current">Этот метод уже установлен как текущий</string>
<string name="time_added">Время добавлено</string>
- <string name="to_add_locations_to_a_list">Чтобы добавить местоположения в список, нажмите «︙» или нажмите и удерживайте страну, город или сервер.</string>
- <string name="to_create_a_custom_list">Чтобы создать свой список, нажмите «︙»</string>
<string name="toggle_vpn">Включение VPN</string>
<string name="top_bar_device_name">Имя устройства: %1$s</string>
<string name="top_bar_time_left">Осталось времени: %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-sv/strings.xml b/android/lib/resource/src/main/res/values-sv/strings.xml
index 732e8a11ed..93a98f6874 100644
--- a/android/lib/resource/src/main/res/values-sv/strings.xml
+++ b/android/lib/resource/src/main/res/values-sv/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">Fältet är obligatoriskt</string>
<string name="this_is_already_set_as_current">Den har redan ställts in som aktuell</string>
<string name="time_added">Tid har lagts till</string>
- <string name="to_add_locations_to_a_list">Om du vill lägga till platser i en lista kan du trycka på \"︙\" eller trycka länge på ett land, en stad eller en server.</string>
- <string name="to_create_a_custom_list">Om du vill skapa en anpassad lista trycker du på \"︙\"</string>
<string name="toggle_vpn">Växla VPN</string>
<string name="top_bar_device_name">Enhetsnamn: %1$s</string>
<string name="top_bar_time_left">Tid kvar: %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-th/strings.xml b/android/lib/resource/src/main/res/values-th/strings.xml
index 598334218a..2bbca2a79b 100644
--- a/android/lib/resource/src/main/res/values-th/strings.xml
+++ b/android/lib/resource/src/main/res/values-th/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">จำเป็นต้องกรอกช่องนี้</string>
<string name="this_is_already_set_as_current">นี่ได้รับการตั้งค่าเป็นปัจจุบันแล้ว</string>
<string name="time_added">เพิ่มเวลาแล้ว</string>
- <string name="to_add_locations_to_a_list">กด \"︙\" หรือกดประเทศ เมือง หรือเซิร์ฟเวอร์ค้างไว้ เพื่อเพิ่มตำแหน่งที่ตั้งลงในรายการ</string>
- <string name="to_create_a_custom_list">กด \"︙\" เพื่อสร้างรายการแบบกำหนดเอง</string>
<string name="toggle_vpn">เปิด/ปิด VPN</string>
<string name="top_bar_device_name">ชื่ออุปกรณ์: %1$s</string>
<string name="top_bar_time_left">เหลือเวลา: %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-tr/strings.xml b/android/lib/resource/src/main/res/values-tr/strings.xml
index 0c7cbace17..038f87f4d8 100644
--- a/android/lib/resource/src/main/res/values-tr/strings.xml
+++ b/android/lib/resource/src/main/res/values-tr/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">Bu alan gereklidir</string>
<string name="this_is_already_set_as_current">Bu, zaten geçerli olarak ayarlı yöntemdir</string>
<string name="time_added">Süre eklendi</string>
- <string name="to_add_locations_to_a_list">Listeye konum eklemek için \"︙\" düğmesine basın veya bir ülke, şehir veya sunucunun üzerine uzun basın.</string>
- <string name="to_create_a_custom_list">Özel bir liste oluşturmak için \"︙\" düğmesine basın</string>
<string name="toggle_vpn">VPN\'i aç/kapat</string>
<string name="top_bar_device_name">Cihaz adı: %1$s</string>
<string name="top_bar_time_left">Kalan süre: %1$s</string>
diff --git a/android/lib/resource/src/main/res/values-zh-rCN/strings.xml b/android/lib/resource/src/main/res/values-zh-rCN/strings.xml
index 55e749d49c..fbf050b7b8 100644
--- a/android/lib/resource/src/main/res/values-zh-rCN/strings.xml
+++ b/android/lib/resource/src/main/res/values-zh-rCN/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">此字段为必填项</string>
<string name="this_is_already_set_as_current">它已被设置为当前方法</string>
<string name="time_added">时间已添加</string>
- <string name="to_add_locations_to_a_list">要将位置添加到列表中,请按“︙”或长按国家/地区、城市或服务器。</string>
- <string name="to_create_a_custom_list">要创建自定义列表,请按“︙”</string>
<string name="toggle_vpn">切换 VPN</string>
<string name="top_bar_device_name">设备名称:%1$s</string>
<string name="top_bar_time_left">剩余时间:%1$s</string>
diff --git a/android/lib/resource/src/main/res/values-zh-rTW/strings.xml b/android/lib/resource/src/main/res/values-zh-rTW/strings.xml
index edfd7b2d02..be4e67c35c 100644
--- a/android/lib/resource/src/main/res/values-zh-rTW/strings.xml
+++ b/android/lib/resource/src/main/res/values-zh-rTW/strings.xml
@@ -370,8 +370,6 @@
<string name="this_field_is_required">此欄位為必填項</string>
<string name="this_is_already_set_as_current">已將其設定為目前方式</string>
<string name="time_added">已增加時間</string>
- <string name="to_add_locations_to_a_list">若要在清單中新增位置,請按下「︙」,或是長按下國家/地區、城市或伺服器。</string>
- <string name="to_create_a_custom_list">若要建立自訂清單,請按下「︙」</string>
<string name="toggle_vpn">切換 VPN</string>
<string name="top_bar_device_name">裝置名稱:%1$s</string>
<string name="top_bar_time_left">剩餘時間:%1$s</string>
diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml
index 09cd1ebd99..fd01cfbcd6 100644
--- a/android/lib/resource/src/main/res/values/strings.xml
+++ b/android/lib/resource/src/main/res/values/strings.xml
@@ -259,9 +259,9 @@
<string name="custom_list_error_list_exists">Name is already taken.</string>
<string name="update_list_name">Update list name</string>
<string name="no_custom_lists_available">No custom lists available</string>
- <string name="to_create_a_custom_list">To create a custom list press the \"︙\"</string>
+ <string name="to_create_a_custom_list">To create a custom list press the \"+\"</string>
<string name="new_list">New list</string>
- <string name="to_add_locations_to_a_list">To add locations to a list, press the \"︙\" or long press on a country, city, or server.</string>
+ <string name="to_add_locations_to_a_list">To add locations to a list, press the pen or long press on a country, city, or server.</string>
<string name="edit_list">Edit list</string>
<string name="delete">Delete</string>
<string name="delete_custom_list_message">\"%s\" was deleted</string>
@@ -426,4 +426,5 @@
<string name="time_added">Time added</string>
<string name="app_is_blocking_internet">The app is blocking internet, please disconnect first</string>
<string name="in_app_products_unavailable">In-app products unavailable, please make sure you have the latest version of Google Play.</string>
+ <string name="relayitem_is_inactive">%s is unavailable</string>
</resources>
diff --git a/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt b/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt
index 0d40e7c805..5179b5c306 100644
--- a/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt
+++ b/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt
@@ -59,6 +59,8 @@ data class Dimensions(
val problemReportIconToTitlePadding: Dp = 60.dp,
val reconnectButtonMinInteractiveComponentSize: Dp = 40.dp,
val relayCircleSize: Dp = 16.dp,
+ val relayCirclePadding: Dp = 6.dp,
+ val relayItemCornerRadius: Dp = 16.dp,
val screenBottomMargin: Dp = 16.dp,
val screenTopMargin: Dp = 24.dp,
val searchFieldHeight: Dp = 42.dp,
diff --git a/android/lib/ui/component/build.gradle.kts b/android/lib/ui/component/build.gradle.kts
index 8ca10e29c5..56ec9a63ef 100644
--- a/android/lib/ui/component/build.gradle.kts
+++ b/android/lib/ui/component/build.gradle.kts
@@ -35,15 +35,18 @@ android {
}
dependencies {
+ implementation(projects.lib.model)
+ implementation(projects.lib.resource)
+ implementation(projects.lib.theme)
implementation(projects.lib.ui.tag)
+ implementation(projects.lib.ui.designsystem)
+
implementation(libs.compose.material3)
implementation(libs.compose.ui)
+ implementation(libs.compose.ui.tooling)
+ implementation(libs.compose.ui.tooling.preview)
implementation(libs.compose.constrainlayout)
implementation(libs.kotlin.stdlib)
implementation(libs.compose.icons.extended)
implementation(libs.androidx.ktx)
- implementation(projects.lib.resource)
- implementation(projects.lib.shared)
- implementation(projects.lib.theme)
- implementation(projects.lib.model)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ExpandChevron.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/ExpandChevron.kt
index d1ec86fde7..5c5eed486e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ExpandChevron.kt
+++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/ExpandChevron.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.compose.component
+package net.mullvad.mullvadvpn.lib.ui.component
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.TweenSpec
@@ -8,28 +8,30 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.lib.theme.AppTheme
@Composable
@Preview
private fun PreviewChevron() {
- Column {
- ExpandChevron(color = MaterialTheme.colorScheme.onPrimary, isExpanded = false)
- ExpandChevron(color = MaterialTheme.colorScheme.onPrimary, isExpanded = true)
+ AppTheme {
+ Surface {
+ Column {
+ ExpandChevron(isExpanded = false)
+ ExpandChevron(isExpanded = true)
+ }
+ }
}
}
@Composable
-fun ExpandChevron(modifier: Modifier = Modifier, color: Color, isExpanded: Boolean) {
-
+fun ExpandChevron(modifier: Modifier = Modifier, isExpanded: Boolean) {
val degree = remember(isExpanded) { if (isExpanded) UP_ROTATION else DOWN_ROTATION }
val stateLabel =
if (isExpanded) {
@@ -47,7 +49,7 @@ fun ExpandChevron(modifier: Modifier = Modifier, color: Color, isExpanded: Boole
Icon(
imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = stateLabel,
- tint = color,
+ // tint = color,
modifier = modifier.rotate(animatedRotation.value),
)
}
@@ -55,12 +57,11 @@ fun ExpandChevron(modifier: Modifier = Modifier, color: Color, isExpanded: Boole
@Composable
fun ExpandChevronIconButton(
modifier: Modifier = Modifier,
- color: Color,
onExpand: (Boolean) -> Unit,
isExpanded: Boolean,
) {
IconButton(modifier = modifier, onClick = { onExpand(!isExpanded) }) {
- ExpandChevron(isExpanded = isExpanded, color = color)
+ ExpandChevron(isExpanded = isExpanded)
}
}
diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/CheckableRelayListItem.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/CheckableRelayListItem.kt
new file mode 100644
index 0000000000..d92e978d5c
--- /dev/null
+++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/CheckableRelayListItem.kt
@@ -0,0 +1,89 @@
+package net.mullvad.mullvadvpn.lib.ui.component.relaylist
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.times
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.theme.AppTheme
+import net.mullvad.mullvadvpn.lib.theme.Dimens
+import net.mullvad.mullvadvpn.lib.ui.component.ExpandChevron
+import net.mullvad.mullvadvpn.lib.ui.designsystem.Checkbox
+import net.mullvad.mullvadvpn.lib.ui.designsystem.RelayListItem
+import net.mullvad.mullvadvpn.lib.ui.designsystem.RelayListItemDefaults
+import net.mullvad.mullvadvpn.lib.ui.tag.EXPAND_BUTTON_TEST_TAG
+import net.mullvad.mullvadvpn.lib.ui.tag.LOCATION_CELL_TEST_TAG
+
+@Composable
+@Preview
+private fun PreviewCheckableRelayLocationCell(
+ @PreviewParameter(RelayItemCheckableCellPreviewParameterProvider::class)
+ relayItems: List<RelayItem.Location.Country>
+) {
+ AppTheme {
+ Column(Modifier.background(color = MaterialTheme.colorScheme.surface)) {
+ relayItems.map {
+ Spacer(Modifier.size(1.dp))
+ CheckableRelayLocationCell(
+ item = CheckableRelayListItem(item = it, itemPosition = ItemPosition.Single),
+ onExpand = {},
+ modifier = Modifier.testTag(LOCATION_CELL_TEST_TAG),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun CheckableRelayLocationCell(
+ item: CheckableRelayListItem,
+ modifier: Modifier = Modifier,
+ onRelayCheckedChange: (isChecked: Boolean) -> Unit = { _ -> },
+ onExpand: (Boolean) -> Unit,
+) {
+ RelayListItem(
+ modifier = modifier.clip(itemPosition = item.itemPosition),
+ selected = false,
+ content = {
+ Row(
+ modifier =
+ Modifier.padding(start = item.depth * Dimens.mediumPadding)
+ .padding(Dimens.mediumPadding),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Name(name = item.item.name, state = null, active = true)
+ }
+ },
+ leadingContent = {
+ Checkbox(checked = item.checked, onCheckedChange = onRelayCheckedChange)
+ },
+ onClick = { onRelayCheckedChange(!item.checked) },
+ onLongClick = null,
+ trailingContent = {
+ if (item.item.hasChildren) {
+ ExpandChevron(
+ isExpanded = item.expanded,
+ modifier =
+ Modifier.clickable { onExpand(!item.expanded) }
+ .fillMaxSize()
+ .padding(Dimens.mediumPadding)
+ .testTag(EXPAND_BUTTON_TEST_TAG),
+ )
+ }
+ },
+ colors = RelayListItemDefaults.colors(containerColor = item.depth.toBackgroundColor()),
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemCheckableCellPreviewParameterProvider.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemCheckableCellPreviewParameterProvider.kt
index c0d4ddc063..918203db53 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemCheckableCellPreviewParameterProvider.kt
+++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemCheckableCellPreviewParameterProvider.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.compose.preview
+package net.mullvad.mullvadvpn.lib.ui.component.relaylist
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import net.mullvad.mullvadvpn.lib.model.RelayItem
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt
index 3d706443d2..35397a6a27 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt
+++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.compose.preview
+package net.mullvad.mullvadvpn.lib.ui.component.relaylist
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.Ownership
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListItem.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayListItem.kt
index 028912f15d..8132a9ece7 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListItem.kt
+++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayListItem.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.compose.state
+package net.mullvad.mullvadvpn.lib.ui.component.relaylist
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.CustomListName
@@ -30,17 +30,20 @@ sealed interface RelayListItem {
}
sealed interface SelectableItem : RelayListItem {
+ val item: RelayItem
val depth: Int
val isSelected: Boolean
val expanded: Boolean
val state: RelayListItemState?
+ val itemPosition: ItemPosition
}
data class CustomListItem(
- val item: RelayItem.CustomList,
+ override val item: RelayItem.CustomList,
override val isSelected: Boolean = false,
override val expanded: Boolean = false,
override val state: RelayListItemState? = null,
+ override val itemPosition: ItemPosition = ItemPosition.Single,
) : SelectableItem {
override val key = item.id
override val depth: Int = 0
@@ -50,10 +53,11 @@ sealed interface RelayListItem {
data class CustomListEntryItem(
val parentId: CustomListId,
val parentName: CustomListName,
- val item: RelayItem.Location,
+ override val item: RelayItem.Location,
override val expanded: Boolean,
override val depth: Int = 0,
override val state: RelayListItemState? = null,
+ override val itemPosition: ItemPosition,
) : SelectableItem {
override val key = parentId to item.id
@@ -73,11 +77,12 @@ sealed interface RelayListItem {
}
data class GeoLocationItem(
- val item: RelayItem.Location,
+ override val item: RelayItem.Location,
override val isSelected: Boolean = false,
override val depth: Int = 0,
override val expanded: Boolean = false,
override val state: RelayListItemState? = null,
+ override val itemPosition: ItemPosition,
) : SelectableItem {
override val key = item.id
override val contentType = RelayListItemContentType.LOCATION_ITEM
@@ -93,3 +98,35 @@ sealed interface RelayListItem {
override val contentType = RelayListItemContentType.EMPTY_RELAY_LIST
}
}
+
+data class CheckableRelayListItem(
+ val item: RelayItem.Location,
+ val depth: Int = 0,
+ val checked: Boolean = false,
+ val expanded: Boolean = false,
+ val itemPosition: ItemPosition = ItemPosition.Single,
+)
+
+sealed interface ItemPosition {
+ data object Top : ItemPosition
+
+ data object Middle : ItemPosition
+
+ data object Bottom : ItemPosition
+
+ data object Single : ItemPosition
+
+ fun roundTop(): Boolean =
+ when (this) {
+ is Single,
+ Top -> true
+ else -> false
+ }
+
+ fun roundBottom(): Boolean =
+ when (this) {
+ is Single,
+ Bottom -> true
+ else -> false
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayListItemPreviewData.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayListItemPreviewData.kt
index aaf5fd5a6e..5776601168 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayListItemPreviewData.kt
+++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayListItemPreviewData.kt
@@ -1,7 +1,5 @@
-package net.mullvad.mullvadvpn.compose.preview
+package net.mullvad.mullvadvpn.lib.ui.component.relaylist
-import net.mullvad.mullvadvpn.compose.state.RelayListItem
-import net.mullvad.mullvadvpn.compose.state.RelayListItemState
import net.mullvad.mullvadvpn.lib.model.CustomList
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.CustomListName
@@ -39,6 +37,7 @@ object RelayListItemPreviewData {
isSelected = false,
state = null,
expanded = false,
+ itemPosition = ItemPosition.Single,
)
}
if (!isSearching) {
@@ -69,6 +68,7 @@ object RelayListItemPreviewData {
depth = 0,
expanded = true,
state = null,
+ itemPosition = ItemPosition.Middle,
),
RelayListItem.GeoLocationItem(
item = locations[0].cities[0],
@@ -76,6 +76,7 @@ object RelayListItemPreviewData {
depth = 1,
expanded = false,
state = null,
+ itemPosition = ItemPosition.Middle,
),
RelayListItem.GeoLocationItem(
item = locations[0].cities[1],
@@ -83,6 +84,7 @@ object RelayListItemPreviewData {
depth = 1,
expanded = true,
state = null,
+ itemPosition = ItemPosition.Middle,
),
RelayListItem.GeoLocationItem(
item = locations[0].cities[1].relays[0],
@@ -90,6 +92,7 @@ object RelayListItemPreviewData {
depth = 2,
expanded = false,
state = RelayListItemState.USED_AS_EXIT,
+ itemPosition = ItemPosition.Middle,
),
RelayListItem.GeoLocationItem(
item = locations[0].cities[1].relays[1],
@@ -97,6 +100,7 @@ object RelayListItemPreviewData {
depth = 2,
expanded = false,
state = null,
+ itemPosition = ItemPosition.Middle,
),
RelayListItem.GeoLocationItem(
item = locations[1],
@@ -104,6 +108,7 @@ object RelayListItemPreviewData {
depth = 0,
expanded = false,
state = null,
+ itemPosition = ItemPosition.Bottom,
),
)
)
diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItem.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItem.kt
new file mode 100644
index 0000000000..e66bfbd359
--- /dev/null
+++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItem.kt
@@ -0,0 +1,206 @@
+package net.mullvad.mullvadvpn.lib.ui.component.relaylist
+
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.times
+import net.mullvad.mullvadvpn.lib.resource.R
+import net.mullvad.mullvadvpn.lib.theme.AppTheme
+import net.mullvad.mullvadvpn.lib.theme.Dimens
+import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
+import net.mullvad.mullvadvpn.lib.theme.color.selected
+import net.mullvad.mullvadvpn.lib.ui.component.ExpandChevron
+import net.mullvad.mullvadvpn.lib.ui.designsystem.RelayListItem
+import net.mullvad.mullvadvpn.lib.ui.designsystem.RelayListItemDefaults
+import net.mullvad.mullvadvpn.lib.ui.designsystem.RelayListTokens
+import net.mullvad.mullvadvpn.lib.ui.tag.EXPAND_BUTTON_TEST_TAG
+
+@Composable
+@Preview
+private fun PreviewSelectableRelayLocationItem(
+ @PreviewParameter(SelectableRelayListItemPreviewParameterProvider::class)
+ relayItems: List<RelayListItem.SelectableItem>
+) {
+ AppTheme {
+ Column(Modifier.background(color = MaterialTheme.colorScheme.surface)) {
+ relayItems.map {
+ Spacer(Modifier.size(1.dp))
+ SelectableRelayListItem(relayListItem = it, onClick = {}, onToggleExpand = {})
+ }
+ }
+ }
+}
+
+@Composable
+fun SelectableRelayListItem(
+ relayListItem: RelayListItem.SelectableItem,
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit,
+ onLongClick: (() -> Unit)? = null,
+ onToggleExpand: ((Boolean) -> Unit),
+) {
+ RelayListItem(
+ modifier = modifier,
+ shape = relayListItem.itemPosition.toShape(),
+ selected = relayListItem.isSelected,
+ enabled = relayListItem.item.active,
+ content = {
+ Row(
+ modifier =
+ Modifier.fillMaxSize()
+ .padding(start = relayListItem.depth * Dimens.mediumPadding)
+ .padding(Dimens.mediumPadding),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(Dimens.smallPadding),
+ ) {
+ val iconTint =
+ when {
+ !relayListItem.item.active -> MaterialTheme.colorScheme.error
+ relayListItem.isSelected -> MaterialTheme.colorScheme.tertiary
+ else -> Color.Transparent
+ }
+ if (relayListItem.isSelected) {
+ Icon(
+ imageVector = Icons.Default.Check,
+ contentDescription = null,
+ tint = iconTint,
+ )
+ } else if (!relayListItem.item.active) {
+ InactiveRelayIndicator(iconTint)
+ }
+
+ Name(
+ name = relayListItem.item.name,
+ state = relayListItem.state,
+ active = relayListItem.item.active,
+ )
+ }
+ },
+ onClick = onClick,
+ onLongClick = onLongClick,
+ trailingContent =
+ if (relayListItem.item.hasChildren) {
+ {
+ ExpandChevron(
+ isExpanded = relayListItem.expanded,
+ modifier =
+ Modifier.clickable { onToggleExpand(!relayListItem.expanded) }
+ .fillMaxSize()
+ .padding(Dimens.mediumPadding)
+ .testTag(EXPAND_BUTTON_TEST_TAG),
+ )
+ }
+ } else {
+ null
+ },
+ colors =
+ RelayListItemDefaults.colors(containerColor = relayListItem.depth.toBackgroundColor()),
+ )
+}
+
+@Composable
+internal fun Name(
+ modifier: Modifier = Modifier,
+ name: String,
+ state: RelayListItemState?,
+ active: Boolean,
+) {
+ Text(
+ text = state?.let { name.withSuffix(state) } ?: name,
+ style = MaterialTheme.typography.bodyLarge,
+ modifier =
+ modifier.alpha(
+ if (state == null && active) {
+ AlphaVisible
+ } else {
+ RelayListTokens.RelayListItemDisabledLabelTextOpacity
+ }
+ ),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+}
+
+@Suppress("MagicNumber")
+@Composable
+internal fun Int.toBackgroundColor(): Color =
+ when (this) {
+ 0 -> MaterialTheme.colorScheme.surfaceContainerHighest
+ 1 -> MaterialTheme.colorScheme.surfaceContainerHigh
+ 2 -> MaterialTheme.colorScheme.surfaceContainerLow
+ else -> MaterialTheme.colorScheme.surfaceContainerLowest
+ }
+
+@Composable
+private fun String.withSuffix(state: RelayListItemState) =
+ when (state) {
+ RelayListItemState.USED_AS_EXIT -> stringResource(R.string.x_exit, this)
+ RelayListItemState.USED_AS_ENTRY -> stringResource(R.string.x_entry, this)
+ }
+
+@Composable
+fun InactiveRelayIndicator(tint: Color) {
+ Box(
+ modifier =
+ Modifier.size(Dimens.listIconSize)
+ .padding(Dimens.relayCirclePadding)
+ .background(color = tint, shape = CircleShape)
+ )
+}
+
+@Composable
+internal fun Modifier.clip(itemPosition: ItemPosition): Modifier {
+ val topCornerSize =
+ animateDpAsState(if (itemPosition.roundTop()) Dimens.relayItemCornerRadius else 0.dp)
+ val bottomCornerSize =
+ animateDpAsState(if (itemPosition.roundBottom()) Dimens.relayItemCornerRadius else 0.dp)
+ return clip(
+ RoundedCornerShape(
+ topStart = CornerSize(topCornerSize.value),
+ topEnd = CornerSize(topCornerSize.value),
+ bottomStart = CornerSize(bottomCornerSize.value),
+ bottomEnd = CornerSize(bottomCornerSize.value),
+ )
+ )
+}
+
+@Composable
+private fun ItemPosition.toShape(): Shape {
+ val topCornerSize = if (roundTop()) Dimens.relayItemCornerRadius else 0.dp
+ val bottomCornerSize = if (roundBottom()) Dimens.relayItemCornerRadius else 0.dp
+ return RoundedCornerShape(
+ topStart = CornerSize(topCornerSize),
+ topEnd = CornerSize(topCornerSize),
+ bottomStart = CornerSize(bottomCornerSize),
+ bottomEnd = CornerSize(bottomCornerSize),
+ )
+}
diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItemPreviewParameterProvider.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItemPreviewParameterProvider.kt
new file mode 100644
index 0000000000..732c03bbc4
--- /dev/null
+++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItemPreviewParameterProvider.kt
@@ -0,0 +1,66 @@
+package net.mullvad.mullvadvpn.lib.ui.component.relaylist
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+
+class SelectableRelayListItemPreviewParameterProvider :
+ PreviewParameterProvider<List<RelayListItem.SelectableItem>> {
+ override val values =
+ sequenceOf(
+ listOf(
+ RelayListItem.GeoLocationItem(
+ item =
+ generateRelayItemCountry(
+ name = "Relay country Active",
+ cityNames = listOf("Relay city 1", "Relay city 2"),
+ relaysPerCity = 2,
+ ),
+ isSelected = true,
+ expanded = false,
+ itemPosition = ItemPosition.Single,
+ ),
+ RelayListItem.GeoLocationItem(
+ item =
+ generateRelayItemCountry(
+ name = "Not Enabled Relay country",
+ cityNames = listOf("Not Enabled city"),
+ relaysPerCity = 1,
+ active = false,
+ ),
+ isSelected = false,
+ itemPosition = ItemPosition.Single,
+ ),
+ RelayListItem.GeoLocationItem(
+ item =
+ generateRelayItemCountry(
+ name = "Relay country Expanded",
+ cityNames = listOf("Normal city"),
+ relaysPerCity = 2,
+ ),
+ isSelected = true,
+ expanded = true,
+ itemPosition = ItemPosition.Single,
+ ),
+ RelayListItem.GeoLocationItem(
+ item =
+ generateRelayItemCountry(
+ name = "Country and city Expanded",
+ cityNames = listOf("Expanded city A", "Expanded city B"),
+ relaysPerCity = 2,
+ ),
+ isSelected = false,
+ itemPosition = ItemPosition.Single,
+ ),
+ RelayListItem.GeoLocationItem(
+ item =
+ generateRelayItemCountry(
+ name = "Country selected but inactive",
+ cityNames = listOf("Expanded city A", "Expanded city B"),
+ relaysPerCity = 2,
+ active = false,
+ ),
+ isSelected = true,
+ itemPosition = ItemPosition.Single,
+ ),
+ )
+ )
+}
diff --git a/android/lib/ui/designsystem/build.gradle.kts b/android/lib/ui/designsystem/build.gradle.kts
new file mode 100644
index 0000000000..efc9d0108b
--- /dev/null
+++ b/android/lib/ui/designsystem/build.gradle.kts
@@ -0,0 +1,43 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.compose)
+}
+
+android {
+ namespace = "net.mullvad.mullvadvpn.lib.ui.designsystem"
+ compileSdk = libs.versions.compile.sdk.get().toInt()
+ buildToolsVersion = libs.versions.build.tools.get()
+
+ defaultConfig { minSdk = libs.versions.min.sdk.get().toInt() }
+
+ buildFeatures { compose = true }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = libs.versions.jvm.target.get()
+ allWarningsAsErrors = true
+ }
+
+ lint {
+ lintConfig = file("${rootProject.projectDir}/config/lint.xml")
+ abortOnError = true
+ warningsAsErrors = true
+ }
+}
+
+dependencies {
+ implementation(projects.lib.theme)
+ implementation(projects.lib.model)
+ implementation(projects.lib.ui.tag)
+
+ implementation(libs.compose.ui)
+ implementation(libs.compose.ui.tooling)
+ implementation(libs.compose.ui.tooling.preview)
+ implementation(libs.compose.material3)
+ implementation(libs.compose.icons.extended)
+}
diff --git a/android/lib/ui/designsystem/src/main/AndroidManifest.xml b/android/lib/ui/designsystem/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..b2d3ea1235
--- /dev/null
+++ b/android/lib/ui/designsystem/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
diff --git a/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/Checkbox.kt b/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/Checkbox.kt
new file mode 100644
index 0000000000..15e9556e47
--- /dev/null
+++ b/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/Checkbox.kt
@@ -0,0 +1,56 @@
+package net.mullvad.mullvadvpn.lib.ui.designsystem
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Checkbox as Material3Checkbox
+import androidx.compose.material3.CheckboxColors
+import androidx.compose.material3.CheckboxDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import net.mullvad.mullvadvpn.lib.theme.AppTheme
+import net.mullvad.mullvadvpn.lib.theme.Dimens
+import net.mullvad.mullvadvpn.lib.theme.color.selected
+
+@Composable
+fun Checkbox(
+ checked: Boolean,
+ onCheckedChange: ((Boolean) -> Unit)?,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ colors: CheckboxColors =
+ CheckboxDefaults.colors(
+ checkedColor = MaterialTheme.colorScheme.onPrimary,
+ uncheckedColor = MaterialTheme.colorScheme.onPrimary,
+ checkmarkColor = MaterialTheme.colorScheme.selected,
+ ),
+ interactionSource: MutableInteractionSource? = null,
+) {
+ Material3Checkbox(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = modifier,
+ enabled = enabled,
+ colors = colors,
+ interactionSource = interactionSource,
+ )
+}
+
+@Preview
+@Composable
+private fun PreviewCheckbox() {
+ AppTheme {
+ Column(
+ Modifier.background(color = MaterialTheme.colorScheme.background),
+ verticalArrangement = Arrangement.spacedBy(Dimens.smallSpacer),
+ ) {
+ Checkbox(checked = false, null)
+ Checkbox(checked = true, null)
+ Checkbox(checked = false, null, enabled = false)
+ Checkbox(checked = true, null, enabled = false)
+ }
+ }
+}
diff --git a/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/RelayListHeader.kt b/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/RelayListHeader.kt
new file mode 100644
index 0000000000..3dc20ff0e7
--- /dev/null
+++ b/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/RelayListHeader.kt
@@ -0,0 +1,78 @@
+package net.mullvad.mullvadvpn.lib.ui.designsystem
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.mullvad.mullvadvpn.lib.theme.AppTheme
+import net.mullvad.mullvadvpn.lib.theme.Dimens
+
+private val LIST_HEADER_MIN_HEIGHT = 48.dp
+
+@Composable
+fun RelayListHeader(
+ content: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ actions: @Composable (RowScope.() -> Unit)? = null,
+) {
+ ProvideContentColorTextStyle(
+ MaterialTheme.colorScheme.onBackground,
+ MaterialTheme.typography.bodyLarge,
+ ) {
+ Row(
+ modifier =
+ Modifier.padding(horizontal = Dimens.tinyPadding)
+ .defaultMinSize(minHeight = LIST_HEADER_MIN_HEIGHT)
+ .height(IntrinsicSize.Min)
+ .then(modifier),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ content()
+ HorizontalDivider(
+ Modifier.weight(1f, true).padding(start = Dimens.smallPadding),
+ color =
+ MaterialTheme.colorScheme.onBackground.copy(
+ alpha = RelayListHeaderTokens.RelayListHeaderDividerAlpha
+ ),
+ )
+ actions?.invoke(this)
+ }
+ }
+}
+
+object RelayListHeaderTokens {
+ const val RelayListHeaderDividerAlpha = 0.2f
+}
+
+@Preview(backgroundColor = 0xFF192E45, showBackground = true)
+@Composable
+fun PreviewRelayListHeader() {
+ AppTheme {
+ Column {
+ RelayListHeader(content = { Text("Header") })
+ RelayListHeader(
+ content = { Text("Header") },
+ actions = {
+ IconButton(onClick = {}) {
+ Icon(imageVector = Icons.Default.Edit, contentDescription = null)
+ }
+ },
+ )
+ }
+ }
+}
diff --git a/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/RelayListItem.kt b/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/RelayListItem.kt
new file mode 100644
index 0000000000..c2e9664a18
--- /dev/null
+++ b/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/RelayListItem.kt
@@ -0,0 +1,313 @@
+package net.mullvad.mullvadvpn.lib.ui.designsystem
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewFontScale
+import androidx.compose.ui.unit.dp
+import net.mullvad.mullvadvpn.lib.theme.AppTheme
+import net.mullvad.mullvadvpn.lib.theme.color.AlphaInactive
+
+@Composable
+fun RelayListItem(
+ modifier: Modifier = Modifier,
+ selected: Boolean = false,
+ enabled: Boolean = true,
+ onClick: (() -> Unit) = {},
+ onLongClick: (() -> Unit)? = {},
+ leadingContent: @Composable (() -> Unit)? = null,
+ content: @Composable () -> Unit,
+ trailingContent: @Composable (() -> Unit)? = null,
+ colors: RelayListItemColors = RelayListItemDefaults.colors(),
+ shape: Shape = RectangleShape,
+) {
+ Surface(
+ modifier =
+ modifier
+ .defaultMinSize(minHeight = RelayListTokens.listItemMinHeight)
+ .height(IntrinsicSize.Min),
+ shape = shape,
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(RelayListTokens.listItemSpacer),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ if (leadingContent != null) {
+ Box(
+ Modifier.background(colors.containerColor)
+ .width(RelayListTokens.listItemButtonWidth)
+ .fillMaxHeight(),
+ contentAlignment = Alignment.Center,
+ ) {
+ ProvideContentColorTextStyle(
+ colors.leadingIconColor,
+ MaterialTheme.typography.titleMedium,
+ ) {
+ leadingContent()
+ }
+ }
+ }
+
+ Row(
+ Modifier.weight(1f, fill = true)
+ .background(colors.containerColor)
+ .fillMaxHeight()
+ .combinedClickable(
+ enabled = true,
+ onClick = onClick,
+ onLongClick = onLongClick,
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ ProvideContentColorTextStyle(
+ colors.headlineColor(enabled, selected),
+ MaterialTheme.typography.titleMedium,
+ ) {
+ content()
+ }
+ }
+
+ if (trailingContent != null) {
+ Box(
+ Modifier.background(color = colors.containerColor)
+ .width(RelayListTokens.listItemButtonWidth)
+ .fillMaxHeight()
+ ) {
+ ProvideContentColorTextStyle(
+ colors.trailingIconColor,
+ MaterialTheme.typography.titleMedium,
+ ) {
+ trailingContent()
+ }
+ }
+ }
+ }
+ }
+}
+
+// Based of ListItem
+@Immutable
+class RelayListItemColors(
+ val containerColor: Color,
+ val headlineColor: Color,
+ val leadingIconColor: Color,
+ val trailingIconColor: Color,
+ val selectedHeadlineColor: Color,
+ val disabledHeadlineColor: Color,
+) {
+ internal fun containerColor(): Color = containerColor
+
+ @Stable
+ internal fun headlineColor(enabled: Boolean, selected: Boolean): Color =
+ when {
+ !enabled -> disabledHeadlineColor
+ selected -> selectedHeadlineColor
+ else -> headlineColor
+ }
+}
+
+@Composable
+internal fun ProvideContentColorTextStyle(
+ contentColor: Color,
+ textStyle: TextStyle,
+ content: @Composable () -> Unit,
+) {
+ val mergedStyle = LocalTextStyle.current.merge(textStyle)
+ CompositionLocalProvider(
+ LocalContentColor provides contentColor,
+ LocalTextStyle provides mergedStyle,
+ content = content,
+ )
+}
+
+object RelayListItemDefaults {
+ @Composable
+ fun colors(
+ containerColor: Color = MaterialTheme.colorScheme.surface,
+ headlineColor: Color = MaterialTheme.colorScheme.onSurface,
+ leadingIconColor: Color = MaterialTheme.colorScheme.onSurface,
+ trailingIconColor: Color = MaterialTheme.colorScheme.onSurface,
+ selectedHeadlineColor: Color = MaterialTheme.colorScheme.tertiary,
+ disabledHeadlineColor: Color =
+ headlineColor.copy(alpha = RelayListTokens.RelayListItemDisabledLabelTextOpacity),
+ ): RelayListItemColors =
+ RelayListItemColors(
+ containerColor = containerColor,
+ headlineColor = headlineColor,
+ leadingIconColor = leadingIconColor,
+ trailingIconColor = trailingIconColor,
+ selectedHeadlineColor = selectedHeadlineColor,
+ disabledHeadlineColor = disabledHeadlineColor,
+ )
+}
+
+object RelayListTokens {
+ const val RelayListItemDisabledLabelTextOpacity = AlphaInactive
+
+ val listItemMinHeight = 56.dp
+ val listItemSpacer = 2.dp
+ val listItemButtonWidth = 56.dp
+}
+
+@Preview
+@PreviewFontScale
+@Composable
+private fun PreviewSimpleRelayListItem() {
+ AppTheme {
+ RelayListItem(
+ modifier = Modifier.fillMaxWidth(),
+ content = { Text("Hello world", modifier = Modifier.padding(16.dp).fillMaxSize()) },
+ )
+ }
+}
+
+@Preview
+@PreviewFontScale
+@Composable
+private fun PreviewLeadingRelayListItem() {
+ AppTheme {
+ RelayListItem(
+ modifier = Modifier.fillMaxWidth(),
+ content = {
+ Text(
+ "Hello world fsadhkuhfiuskahf iuhsadhuf sa",
+ modifier =
+ Modifier.padding(16.dp)
+ .fillMaxSize()
+ .wrapContentHeight(align = Alignment.CenterVertically),
+ style = MaterialTheme.typography.titleMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ },
+ leadingContent = {
+ Box(
+ modifier = Modifier.fillMaxSize().clickable(onClick = { /* Handle click */ }),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ modifier = Modifier.padding(16.dp),
+ imageVector = Icons.Default.KeyboardArrowDown,
+ contentDescription = null,
+ )
+ }
+ },
+ )
+ }
+}
+
+@Preview
+@PreviewFontScale
+@Composable
+private fun PreviewTrailingRelayListItem() {
+ AppTheme {
+ RelayListItem(
+ modifier = Modifier.fillMaxWidth(),
+ selected = true,
+ content = {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(imageVector = Icons.Default.Check, contentDescription = null)
+ Spacer(Modifier.width(8.dp))
+ Text(
+ "Hello world fsadhkuhfiuskahf iuhsadhuf sa",
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ },
+ trailingContent = {
+ Box(
+ modifier = Modifier.fillMaxSize().clickable(onClick = {}),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ modifier = Modifier.padding(16.dp),
+ imageVector = Icons.Default.Add,
+ contentDescription = null,
+ )
+ }
+ },
+ )
+ }
+}
+
+@Preview
+@PreviewFontScale
+@Composable
+private fun PreviewLeadingAndTrailingRelayListItem() {
+ AppTheme {
+ RelayListItem(
+ modifier = Modifier.fillMaxWidth(),
+ content = {
+ Text(
+ "Hello world iuhsadhuf sa",
+ modifier = Modifier.clickable {}.padding(16.dp).fillMaxSize(),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ },
+ leadingContent = {
+ Box(
+ modifier = Modifier.fillMaxSize().clickable(onClick = {}),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ modifier = Modifier.padding(16.dp),
+ imageVector = Icons.Default.KeyboardArrowDown,
+ contentDescription = null,
+ )
+ }
+ },
+ trailingContent = {
+ Box(
+ modifier = Modifier.fillMaxSize().clickable(onClick = {}),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ modifier = Modifier.padding(16.dp),
+ imageVector = Icons.Default.Add,
+ contentDescription = null,
+ )
+ }
+ },
+ )
+ }
+}
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
index 686df96851..0d3df97602 100644
--- a/android/settings.gradle.kts
+++ b/android/settings.gradle.kts
@@ -29,7 +29,9 @@ include(
":lib:talpid",
":lib:theme",
":lib:tv",
+ ":lib:ui:designsystem",
":lib:ui:component",
+ ":lib:ui:tag"
)
include(
":test",
@@ -38,4 +40,3 @@ include(
":test:e2e",
":test:mockapi"
)
-include(":lib:ui:tag")