diff --git a/core/design-system/src/main/res/drawable/ic_big_plane.xml b/core/design-system/src/main/res/drawable/ic_big_plane.xml new file mode 100644 index 00000000..7b8f6601 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_big_plane.xml @@ -0,0 +1,47 @@ + + + + + + + + + diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index b9a55913..f246b2b7 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -104,6 +104,8 @@ 인증샷 찍기 인증샷 등록에 실패했습니다. 이미지 변환에 실패했습니다. + 인증샷 업로드 중... + 잠시만 기다려 주세요. 코멘트는 5글자로 입력해주세요! diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationScreen.kt b/feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationScreen.kt index 7f4bbfaa..bf990d55 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationScreen.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationScreen.kt @@ -39,6 +39,7 @@ import com.twix.domain.model.enums.AppTextStyle import com.twix.task_certification.certification.component.CameraControlBar import com.twix.task_certification.certification.component.CameraPreviewBox import com.twix.task_certification.certification.component.CommentErrorText +import com.twix.task_certification.certification.component.LoadingContent import com.twix.task_certification.certification.component.TaskCertificationTopBar import com.twix.task_certification.certification.contract.TaskCertificationIntent import com.twix.task_certification.certification.contract.TaskCertificationSideEffect @@ -95,48 +96,53 @@ fun TaskCertificationRoute( ), ) } + TaskCertificationSideEffect.NavigateToBack -> navigateToBack() TaskCertificationSideEffect.NavigateToDetail -> navigateToDetail() } } - TaskCertificationScreen( - uiState = uiState, - cameraPreview = cameraPreview, - onClickClose = navigateToBack, - onCaptureClick = { - coroutineScope.launch { - camera - .takePicture() - .onSuccess { - viewModel.dispatch(TaskCertificationIntent.TakePicture(it)) - }.onFailure { - viewModel.dispatch(TaskCertificationIntent.TakePicture(null)) - } - } - }, - onToggleCameraClick = { - viewModel.dispatch(TaskCertificationIntent.ToggleLens) - }, - onClickFlash = { - viewModel.dispatch(TaskCertificationIntent.ToggleTorch) - }, - onClickGallery = { - pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) - }, - onClickRefresh = { - viewModel.dispatch(TaskCertificationIntent.RetakePicture) - }, - onCommentChanged = { - viewModel.dispatch(TaskCertificationIntent.UpdateComment(it)) - }, - onFocusChanged = { - viewModel.dispatch(TaskCertificationIntent.CommentFocusChanged(it)) - }, - onClickUpload = { - viewModel.dispatch(TaskCertificationIntent.TryUpload) - }, - ) + if (uiState.isLoading) { + LoadingContent() + } else { + TaskCertificationScreen( + uiState = uiState, + cameraPreview = cameraPreview, + onClickClose = navigateToBack, + onCaptureClick = { + coroutineScope.launch { + camera + .takePicture() + .onSuccess { + viewModel.dispatch(TaskCertificationIntent.TakePicture(it)) + }.onFailure { + viewModel.dispatch(TaskCertificationIntent.TakePicture(null)) + } + } + }, + onToggleCameraClick = { + viewModel.dispatch(TaskCertificationIntent.ToggleLens) + }, + onClickFlash = { + viewModel.dispatch(TaskCertificationIntent.ToggleTorch) + }, + onClickGallery = { + pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + }, + onClickRefresh = { + viewModel.dispatch(TaskCertificationIntent.RetakePicture) + }, + onCommentChanged = { + viewModel.dispatch(TaskCertificationIntent.UpdateComment(it)) + }, + onFocusChanged = { + viewModel.dispatch(TaskCertificationIntent.CommentFocusChanged(it)) + }, + onClickUpload = { + viewModel.dispatch(TaskCertificationIntent.TryUpload) + }, + ) + } } @Composable diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationViewModel.kt b/feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationViewModel.kt index bc079ba3..7d895c24 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationViewModel.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationViewModel.kt @@ -125,7 +125,9 @@ class TaskCertificationViewModel( } private fun upload(image: ByteArray) { + reduce { copy(isLoading = true) } launchResult( + onStart = { reduce { copy(isLoading = true) } }, block = { photologRepository.uploadPhotologImage( goalId = navArgs.goalId, @@ -145,6 +147,7 @@ class TaskCertificationViewModel( onError = { showToast(R.string.task_certification_upload_fail, ToastType.ERROR) }, + onFinally = { reduce { copy(isLoading = false) } }, ) } @@ -171,8 +174,10 @@ class TaskCertificationViewModel( when (navArgs.from) { NavRoutes.TaskCertificationRoute.From.HOME -> goalRefreshBus.notifyGoalListChanged() + NavRoutes.TaskCertificationRoute.From.DETAIL -> detailRefreshBus.notifyChanged(TaskCertificationRefreshBus.Publisher.PHOTOLOG) + NavRoutes.TaskCertificationRoute.From.EDITOR -> Unit } tryEmitSideEffect(TaskCertificationSideEffect.NavigateToBack) diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/LoadingContent.kt b/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/LoadingContent.kt new file mode 100644 index 00000000..a1d5e576 --- /dev/null +++ b/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/LoadingContent.kt @@ -0,0 +1,115 @@ +package com.twix.task_certification.certification.component + +import androidx.compose.animation.core.EaseInOut +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +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.graphicsLayer +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.twix.designsystem.R +import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.theme.CommonColor +import com.twix.designsystem.theme.GrayColor +import com.twix.designsystem.theme.TwixTheme +import com.twix.domain.model.enums.AppTextStyle + +@Composable +internal fun LoadingContent() { + Column( + modifier = + Modifier + .fillMaxSize() + .background(CommonColor.White), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + FloatingPlane() + + Spacer(Modifier.height(6.dp)) + + AppText( + text = stringResource(R.string.task_certification_loading), + style = AppTextStyle.H1, + color = GrayColor.C500, + ) + + Spacer(Modifier.height(10.dp)) + + AppText( + text = stringResource(R.string.task_certification_plz_waiting), + style = AppTextStyle.T2, + color = GrayColor.C300, + ) + } +} + +@Composable +private fun FloatingPlane() { + val planeXOffset = 6f + val planeYOffset = 8f + val planeRotation = 6f + val transition = rememberInfiniteTransition(label = "plane") + + val offsetY by transition.animateFloat( + -planeYOffset, + planeYOffset, + infiniteRepeatable( + tween(1500, easing = EaseInOut), + RepeatMode.Reverse, + ), + ) + + val offsetX by transition.animateFloat( + -planeXOffset, + planeXOffset, + infiniteRepeatable( + tween(2200, easing = LinearEasing), + RepeatMode.Reverse, + ), + ) + + val rotation by transition.animateFloat( + -planeRotation, + planeRotation, + infiniteRepeatable( + tween(1300, easing = EaseInOut), + RepeatMode.Reverse, + ), + ) + + Image( + painter = painterResource(R.drawable.ic_big_plane), + contentDescription = null, + modifier = + Modifier.graphicsLayer { + translationX = offsetX + translationY = offsetY + rotationZ = rotation + }, + ) +} + +@Preview(showBackground = true) +@Composable +private fun LoadingContentPreview() { + TwixTheme { + LoadingContent() + } +} diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/certification/contract/TaskCertificationUiState.kt b/feature/task-certification/src/main/java/com/twix/task_certification/certification/contract/TaskCertificationUiState.kt index 2e523cbd..54aa4eed 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/certification/contract/TaskCertificationUiState.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/certification/contract/TaskCertificationUiState.kt @@ -17,6 +17,7 @@ data class TaskCertificationUiState( val preview: CameraPreview? = null, val comment: CommentUiModel = CommentUiModel(), val showCommentError: Boolean = false, + val isLoading: Boolean = false, ) : State { val hasMaxCommentLength: Boolean get() = comment.hasMaxCommentLength