summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2024-03-13 13:09:25 +0100
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2024-03-14 14:53:44 +0100
commitee5d8a5913ed76ef6d3d9d5295e17abb7566028a (patch)
treee4ce217c96517abf83bc582182309aa323a37102 /android
parent866d475e6688ca0fa35ec182b0715a258be467b8 (diff)
downloadmullvadvpn-ee5d8a5913ed76ef6d3d9d5295e17abb7566028a.tar.xz
mullvadvpn-ee5d8a5913ed76ef6d3d9d5295e17abb7566028a.zip
Add custom lists screen
Diffstat (limited to 'android')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt193
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListsUiState.kt10
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModel.kt32
3 files changed, 235 insertions, 0 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt
new file mode 100644
index 0000000000..20a92132f1
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt
@@ -0,0 +1,193 @@
+package net.mullvad.mullvadvpn.compose.screen
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.composedestinations.result.NavResult
+import com.ramcosta.composedestinations.result.ResultRecipient
+import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.cell.NavigationComposeCell
+import net.mullvad.mullvadvpn.compose.communication.CustomListResult
+import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge
+import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton
+import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar
+import net.mullvad.mullvadvpn.compose.constant.ContentType
+import net.mullvad.mullvadvpn.compose.destinations.CreateCustomListDestination
+import net.mullvad.mullvadvpn.compose.destinations.EditCustomListDestination
+import net.mullvad.mullvadvpn.compose.extensions.itemsWithDivider
+import net.mullvad.mullvadvpn.compose.extensions.showSnackbar
+import net.mullvad.mullvadvpn.compose.state.CustomListsUiState
+import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR
+import net.mullvad.mullvadvpn.compose.test.NEW_LIST_BUTTON_TEST_TAG
+import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition
+import net.mullvad.mullvadvpn.lib.theme.AppTheme
+import net.mullvad.mullvadvpn.lib.theme.Dimens
+import net.mullvad.mullvadvpn.lib.theme.color.Alpha60
+import net.mullvad.mullvadvpn.relaylist.RelayItem
+import net.mullvad.mullvadvpn.viewmodel.CustomListsViewModel
+import org.koin.androidx.compose.koinViewModel
+
+@Preview
+@Composable
+private fun PreviewCustomListsScreen() {
+ AppTheme { CustomListsScreen(CustomListsUiState.Content(), SnackbarHostState()) }
+}
+
+@Composable
+@Destination(style = SlideInFromRightTransition::class)
+fun CustomLists(
+ navigator: DestinationsNavigator,
+ editCustomListResultRecipient:
+ ResultRecipient<EditCustomListDestination, CustomListResult.Deleted>
+) {
+ val viewModel = koinViewModel<CustomListsViewModel>()
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+ val scope = rememberCoroutineScope()
+ val context = LocalContext.current
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ editCustomListResultRecipient.onNavResult { result ->
+ when (result) {
+ NavResult.Canceled -> {
+ /* Do nothing */
+ }
+ is NavResult.Value -> {
+ scope.launch {
+ snackbarHostState.currentSnackbarData?.dismiss()
+ snackbarHostState.showSnackbar(
+ message =
+ context.getString(
+ R.string.delete_custom_list_message,
+ result.value.name
+ ),
+ actionLabel = context.getString(R.string.undo),
+ duration = SnackbarDuration.Long,
+ onAction = { viewModel.undoDeleteCustomList(result.value.undo) }
+ )
+ }
+ }
+ }
+ }
+
+ CustomListsScreen(
+ state = state,
+ snackbarHostState = snackbarHostState,
+ addCustomList = {
+ navigator.navigate(
+ CreateCustomListDestination(),
+ ) {
+ launchSingleTop = true
+ }
+ },
+ openCustomList = { customList ->
+ navigator.navigate(EditCustomListDestination(customListId = customList.id)) {
+ launchSingleTop = true
+ }
+ },
+ onBackClick = navigator::navigateUp
+ )
+}
+
+@Composable
+fun CustomListsScreen(
+ state: CustomListsUiState,
+ snackbarHostState: SnackbarHostState,
+ addCustomList: () -> Unit = {},
+ openCustomList: (RelayItem.CustomList) -> Unit = {},
+ onBackClick: () -> Unit = {}
+) {
+ ScaffoldWithMediumTopBar(
+ appBarTitle = stringResource(id = R.string.edit_custom_lists),
+ navigationIcon = { NavigateBackIconButton(onBackClick) },
+ actions = {
+ IconButton(
+ onClick = addCustomList,
+ modifier = Modifier.testTag(NEW_LIST_BUTTON_TEST_TAG)
+ ) {
+ Icon(
+ painterResource(id = R.drawable.ic_icons_add),
+ tint =
+ MaterialTheme.colorScheme.onBackground
+ .copy(alpha = Alpha60)
+ .compositeOver(MaterialTheme.colorScheme.background),
+ contentDescription = stringResource(id = R.string.new_list)
+ )
+ }
+ },
+ snackbarHostState = snackbarHostState
+ ) { modifier: Modifier, lazyListState: LazyListState ->
+ LazyColumn(
+ modifier = modifier,
+ state = lazyListState,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ when (state) {
+ is CustomListsUiState.Content -> {
+ if (state.customLists.isNotEmpty()) {
+ content(customLists = state.customLists, openCustomList = openCustomList)
+ } else {
+ empty()
+ }
+ }
+ is CustomListsUiState.Loading -> {
+ loading()
+ }
+ }
+ }
+ }
+}
+
+private fun LazyListScope.loading() {
+ item(contentType = ContentType.PROGRESS) {
+ MullvadCircularProgressIndicatorLarge(
+ modifier = Modifier.testTag(CIRCULAR_PROGRESS_INDICATOR)
+ )
+ }
+}
+
+private fun LazyListScope.content(
+ customLists: List<RelayItem.CustomList>,
+ openCustomList: (RelayItem.CustomList) -> Unit
+) {
+ itemsWithDivider(
+ items = customLists,
+ key = { item: RelayItem.CustomList -> item.id },
+ contentType = { ContentType.ITEM }
+ ) { customList ->
+ NavigationComposeCell(title = customList.name, onClick = { openCustomList(customList) })
+ }
+}
+
+private fun LazyListScope.empty() {
+ item(contentType = ContentType.EMPTY_TEXT) {
+ Text(
+ text = stringResource(R.string.no_custom_lists_available),
+ modifier = Modifier.padding(Dimens.screenVerticalMargin),
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onSecondary
+ )
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListsUiState.kt
new file mode 100644
index 0000000000..f055bf95d2
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListsUiState.kt
@@ -0,0 +1,10 @@
+package net.mullvad.mullvadvpn.compose.state
+
+import net.mullvad.mullvadvpn.relaylist.RelayItem
+
+interface CustomListsUiState {
+ object Loading : CustomListsUiState
+
+ data class Content(val customLists: List<RelayItem.CustomList> = emptyList()) :
+ CustomListsUiState
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModel.kt
new file mode 100644
index 0000000000..79a2ba61c6
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModel.kt
@@ -0,0 +1,32 @@
+package net.mullvad.mullvadvpn.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.compose.communication.CustomListAction
+import net.mullvad.mullvadvpn.compose.state.CustomListsUiState
+import net.mullvad.mullvadvpn.usecase.RelayListUseCase
+import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
+
+class CustomListsViewModel(
+ relayListUseCase: RelayListUseCase,
+ private val customListActionUseCase: CustomListActionUseCase
+) : ViewModel() {
+
+ val uiState =
+ relayListUseCase
+ .customLists()
+ .map { CustomListsUiState.Content(it) }
+ .stateIn(
+ viewModelScope,
+ started = SharingStarted.WhileSubscribed(),
+ CustomListsUiState.Loading
+ )
+
+ fun undoDeleteCustomList(action: CustomListAction.Create) {
+ viewModelScope.launch { customListActionUseCase.performAction(action) }
+ }
+}