diff options
Diffstat (limited to 'android/app')
3 files changed, 188 insertions, 0 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialog.kt new file mode 100644 index 0000000000..9f46ee1d5a --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialog.kt @@ -0,0 +1,107 @@ +package net.mullvad.mullvadvpn.compose.dialog + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +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.result.ResultBackNavigator +import com.ramcosta.composedestinations.spec.DestinationStyle +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.button.PrimaryButton +import net.mullvad.mullvadvpn.compose.communication.CustomListResult +import net.mullvad.mullvadvpn.compose.component.CustomListNameTextField +import net.mullvad.mullvadvpn.compose.state.UpdateCustomListUiState +import net.mullvad.mullvadvpn.compose.test.EDIT_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG +import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect +import net.mullvad.mullvadvpn.lib.theme.AppTheme +import net.mullvad.mullvadvpn.viewmodel.EditCustomListNameDialogSideEffect +import net.mullvad.mullvadvpn.viewmodel.EditCustomListNameDialogViewModel +import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf + +@Preview +@Composable +private fun PreviewEditCustomListNameDialog() { + AppTheme { EditCustomListNameDialog(UpdateCustomListUiState()) } +} + +@Composable +@Destination(style = DestinationStyle.Dialog::class) +fun EditCustomListName( + backNavigator: ResultBackNavigator<CustomListResult.Renamed>, + customListId: String, + initialName: String +) { + val vm: EditCustomListNameDialogViewModel = + koinViewModel(parameters = { parametersOf(customListId, initialName) }) + LaunchedEffectCollect(vm.uiSideEffect) { sideEffect -> + when (sideEffect) { + is EditCustomListNameDialogSideEffect.ReturnWithResult -> { + backNavigator.navigateBack(result = sideEffect.result) + } + } + } + + val state by vm.uiState.collectAsStateWithLifecycle() + EditCustomListNameDialog( + state = state, + updateName = vm::updateCustomListName, + onInputChanged = vm::clearError, + onDismiss = backNavigator::navigateBack + ) +} + +@Composable +fun EditCustomListNameDialog( + state: UpdateCustomListUiState, + updateName: (String) -> Unit = {}, + onInputChanged: () -> Unit = {}, + onDismiss: () -> Unit = {} +) { + val name = remember { mutableStateOf(state.name) } + val isValidName by remember { derivedStateOf { name.value.isNotBlank() } } + + AlertDialog( + title = { + Text( + text = stringResource(id = R.string.update_list_name), + ) + }, + text = { + CustomListNameTextField( + name = name.value, + isValidName = isValidName, + error = state.error, + onSubmit = updateName, + onValueChanged = { + name.value = it + onInputChanged() + }, + modifier = Modifier.testTag(EDIT_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG) + ) + }, + containerColor = MaterialTheme.colorScheme.background, + titleContentColor = MaterialTheme.colorScheme.onBackground, + onDismissRequest = onDismiss, + confirmButton = { + PrimaryButton( + text = stringResource(id = R.string.save), + onClick = { updateName(name.value) }, + isEnabled = isValidName + ) + }, + dismissButton = { + PrimaryButton(text = stringResource(id = R.string.cancel), onClick = onDismiss) + } + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/UpdateCustomListUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/UpdateCustomListUiState.kt new file mode 100644 index 0000000000..7eac74a40a --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/UpdateCustomListUiState.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.compose.state + +import net.mullvad.mullvadvpn.model.CustomListsError + +data class UpdateCustomListUiState(val name: String = "", val error: CustomListsError? = null) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt new file mode 100644 index 0000000000..c2625e6d56 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt @@ -0,0 +1,76 @@ +package net.mullvad.mullvadvpn.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.compose.communication.CustomListAction +import net.mullvad.mullvadvpn.compose.communication.CustomListResult +import net.mullvad.mullvadvpn.compose.state.UpdateCustomListUiState +import net.mullvad.mullvadvpn.model.CustomListsError +import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase +import net.mullvad.mullvadvpn.usecase.customlists.CustomListsException + +class EditCustomListNameDialogViewModel( + private val customListId: String, + private val initialName: String, + private val customListActionUseCase: CustomListActionUseCase +) : ViewModel() { + + private val _uiSideEffect = + Channel<EditCustomListNameDialogSideEffect>(1, BufferOverflow.DROP_OLDEST) + val uiSideEffect = _uiSideEffect.receiveAsFlow() + + private val _error = MutableStateFlow<CustomListsError?>(null) + + val uiState = + _error + .map { UpdateCustomListUiState(name = initialName, error = it) } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(), + UpdateCustomListUiState(name = initialName) + ) + + fun updateCustomListName(name: String) { + viewModelScope.launch { + customListActionUseCase + .performAction( + CustomListAction.Rename( + customListId = customListId, + name = initialName, + newName = name + ) + ) + .fold( + onSuccess = { result -> + _uiSideEffect.send( + EditCustomListNameDialogSideEffect.ReturnWithResult(result) + ) + }, + onFailure = { exception -> + if (exception is CustomListsException) { + _error.emit(exception.error) + } else { + _error.emit(CustomListsError.OtherError) + } + } + ) + } + } + + fun clearError() { + viewModelScope.launch { _error.emit(null) } + } +} + +sealed interface EditCustomListNameDialogSideEffect { + data class ReturnWithResult(val result: CustomListResult.Renamed) : + EditCustomListNameDialogSideEffect +} |
