diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2024-03-13 13:09:25 +0100 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2024-03-14 14:53:44 +0100 |
| commit | ee5d8a5913ed76ef6d3d9d5295e17abb7566028a (patch) | |
| tree | e4ce217c96517abf83bc582182309aa323a37102 /android | |
| parent | 866d475e6688ca0fa35ec182b0715a258be467b8 (diff) | |
| download | mullvadvpn-ee5d8a5913ed76ef6d3d9d5295e17abb7566028a.tar.xz mullvadvpn-ee5d8a5913ed76ef6d3d9d5295e17abb7566028a.zip | |
Add custom lists screen
Diffstat (limited to 'android')
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) } + } +} |
