diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/BoardNavigation.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/BoardNavigation.kt index 96c4ca93..c2866b7f 100644 --- a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/BoardNavigation.kt +++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/BoardNavigation.kt @@ -12,6 +12,7 @@ import com.goalpanzi.mission_mate.feature.board.screen.BoardFinishRoute import com.goalpanzi.mission_mate.feature.board.screen.BoardMissionDetailRoute import com.goalpanzi.mission_mate.feature.board.screen.BoardRoute import com.goalpanzi.mission_mate.feature.board.screen.UserStoryScreen +import com.goalpanzi.mission_mate.feature.board.screen.VerificationPreviewRoute import java.net.URLEncoder import java.nio.charset.StandardCharsets @@ -37,7 +38,8 @@ fun NavGraphBuilder.boardNavGraph( onNavigateDetail : (Long) -> Unit, onNavigateFinish : (Long) -> Unit, onNavigateStory: (UserStory) -> Unit, - onClickSetting: () -> Unit + onClickSetting: () -> Unit, + onNavigateToPreview: (Long, String) -> Unit ) { composable( "RouteModel.Board/{$missionIdArg}", @@ -53,7 +55,8 @@ fun NavGraphBuilder.boardNavGraph( }, onNavigateFinish = onNavigateFinish, onClickSetting = onClickSetting, - onClickStory = onNavigateStory + onClickStory = onNavigateStory, + onPreviewImage = onNavigateToPreview ) } } @@ -148,3 +151,31 @@ fun NavGraphBuilder.userStoryNavGraph( } } } + +fun NavController.navigateToVerificationPreview( + missionId: Long, + imageUrl: String +) { + val encodedUrl = URLEncoder.encode(imageUrl, StandardCharsets.UTF_8.toString()) + this.navigate("RouteModel.VerificationPreview" + "/${missionId}" +"/${encodedUrl}") +} + +fun NavGraphBuilder.verificationPreviewNavGraph( + onClickClose: () -> Unit +) { + composable( + route = "RouteModel.VerificationPreview/{$missionIdArg}/{$imageUrlArg}", + arguments = listOf( + navArgument(missionIdArg) { + type = NavType.LongType + }, + navArgument(imageUrlArg) { + type = NavType.StringType + } + ) + ) { + VerificationPreviewRoute( + onClickClose = onClickClose + ) + } +} diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardScreen.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardScreen.kt index 8ea16cf5..fc112993 100644 --- a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardScreen.kt +++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardScreen.kt @@ -58,6 +58,7 @@ fun BoardRoute( onNavigateFinish : (Long) -> Unit, onClickSetting: () -> Unit, onClickStory: (UserStory) -> Unit, + onPreviewImage: (Long, String) -> Unit, modifier: Modifier = Modifier, viewModel: BoardViewModel = hiltViewModel() ) { @@ -79,9 +80,10 @@ fun BoardRoute( contract = ActivityResultContracts.PickVisualMedia(), onResult = { imageUri -> imageUri?.let { original -> - ImageCompressor.getCompressedImage(context, original).let { compressed -> - viewModel.verify(compressed) - } + onPreviewImage(viewModel.missionId, original.toString()) +// ImageCompressor.getCompressedImage(context, original).let { compressed -> +// viewModel.verify(compressed) +// } } } ) diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/VerificationPreviewScreen.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/VerificationPreviewScreen.kt new file mode 100644 index 00000000..ff065f83 --- /dev/null +++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/VerificationPreviewScreen.kt @@ -0,0 +1,170 @@ +package com.goalpanzi.mission_mate.feature.board.screen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.paint +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorBlack_FF000000 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF +import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography +import java.net.URLDecoder +import java.nio.charset.StandardCharsets +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +@Composable +fun VerificationPreviewRoute( + onClickClose: () -> Unit, + viewModel: VerificationPreviewViewModel = hiltViewModel() +) { + + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + VerificationPreviewScreen( + onClickClose = onClickClose, + uiState = uiState + ) +} + +@Composable +fun VerificationPreviewScreen( + onClickClose: () -> Unit, + uiState: VerificationPreviewUiState +) { + val dateTime = LocalDateTime.now() + val formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd") + + Box( + modifier = Modifier + .fillMaxSize() + .background(ColorWhite_FFFFFFFF) + .navigationBarsPadding() + ) { + Box( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding() + ) { + when (uiState) { + VerificationPreviewUiState.Loading -> VerificationPreviewLoading() + is VerificationPreviewUiState.Success -> { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(URLDecoder.decode(uiState.imageUrl, StandardCharsets.UTF_8.toString())) + .build(), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.FillBounds, + filterQuality = FilterQuality.None + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .background( + brush = Brush.verticalGradient( + colors = listOf( + ColorBlack_FF000000.copy(alpha = 0.4f), + Color.Transparent + ) + ) + ) + .height(93.dp) + .padding(horizontal = 24.dp, vertical = 14.dp) + ) { + Image( + modifier = Modifier + .padding(top = 6.dp) + .size(28.dp) + .border(1.dp, ColorWhite_FFFFFFFF, CircleShape) + .paint( + painter = painterResource(uiState.character.backgroundId), + contentScale = ContentScale.FillWidth + ) + .padding(5.dp), + painter = painterResource(uiState.character.imageId), + contentDescription = "" + ) + Text( + text = uiState.nickname, + style = MissionMateTypography.body_xl_bold, + color = ColorWhite_FFFFFFFF, + modifier = Modifier + .padding(start = 8.dp) + .wrapContentWidth() + .padding(top = 6.dp) + ) + + Text( + text = dateTime.format(formatter), + style = MissionMateTypography.body_xl_regular, + color = ColorWhite_FFFFFFFF, + modifier = Modifier + .padding(start = 8.dp) + .weight(1f) + .padding(top = 6.dp) + ) + + IconButton( + onClick = onClickClose, + modifier = Modifier.wrapContentSize() + ) { + Icon( + imageVector = ImageVector.vectorResource(id = com.goalpanzi.mission_mate.core.designsystem.R.drawable.ic_close), + contentDescription = "", + tint = ColorWhite_FFFFFFFF + ) + } + } + } + } + } + } +} + +@Composable +fun VerificationPreviewLoading() { + Box( + modifier = Modifier + .background(ColorWhite_FFFFFFFF) + .statusBarsPadding() + .navigationBarsPadding() + ) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } +} \ No newline at end of file diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/VerificationPreviewViewModel.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/VerificationPreviewViewModel.kt new file mode 100644 index 00000000..026e6119 --- /dev/null +++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/VerificationPreviewViewModel.kt @@ -0,0 +1,58 @@ +package com.goalpanzi.mission_mate.feature.board.screen + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.goalpanzi.mission_mate.core.domain.usecase.ProfileUseCase +import com.goalpanzi.mission_mate.core.domain.usecase.VerifyMissionUseCase +import com.goalpanzi.mission_mate.feature.board.imageUrlArg +import com.goalpanzi.mission_mate.feature.board.model.Character +import com.luckyoct.core.model.UserProfile +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@HiltViewModel +class VerificationPreviewViewModel @Inject constructor( + private val verifyMissionUseCase: VerifyMissionUseCase, + private val profileUseCase: ProfileUseCase, + savedStateHandle: SavedStateHandle +): ViewModel() { + + private val _errorFlow = MutableSharedFlow() + val errorFlow = _errorFlow.asSharedFlow() + + val uiState: StateFlow = flow { + val profile = profileUseCase.getProfile() as UserProfile + emit(profile) + }.catch { + _errorFlow.emit(it) + }.map { + VerificationPreviewUiState.Success( + nickname = it.nickname, + character = Character.valueOf(it.characterType.name), + imageUrl = savedStateHandle.get(imageUrlArg) ?: "" + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = VerificationPreviewUiState.Loading + ) + +} + +sealed interface VerificationPreviewUiState { + data object Loading: VerificationPreviewUiState + data class Success( + val nickname: String, + val character: Character, + val imageUrl: String + ): VerificationPreviewUiState +} \ No newline at end of file diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt index 0cf17aee..93d29494 100644 --- a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt +++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt @@ -13,6 +13,7 @@ import com.goalpanzi.mission_mate.feature.board.boardDetailNavGraph import com.goalpanzi.mission_mate.feature.board.boardFinishNavGraph import com.goalpanzi.mission_mate.feature.board.boardNavGraph import com.goalpanzi.mission_mate.feature.board.userStoryNavGraph +import com.goalpanzi.mission_mate.feature.board.verificationPreviewNavGraph import com.goalpanzi.mission_mate.feature.login.loginNavGraph import com.goalpanzi.mission_mate.feature.onboarding.boardSetupNavGraph import com.goalpanzi.mission_mate.feature.onboarding.boardSetupSuccessNavGraph @@ -103,6 +104,9 @@ internal fun MainNavHost( }, onNavigateStory = { userStory -> navigator.navigationToUserStory(userStory) + }, + onNavigateToPreview = { missionId, imageUrl -> + navigator.navigationToVerificationPreview(missionId, imageUrl) } ) boardDetailNavGraph( @@ -126,6 +130,11 @@ internal fun MainNavHost( navigator.popBackStack() } ) + verificationPreviewNavGraph( + onClickClose = { + navigator.popBackStack() + } + ) } } } \ No newline at end of file diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt index 65f7953e..7cb245bf 100644 --- a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt +++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt @@ -11,6 +11,7 @@ import com.goalpanzi.mission_mate.feature.board.navigateToBoard import com.goalpanzi.mission_mate.feature.board.navigateToBoardDetail import com.goalpanzi.mission_mate.feature.board.navigateToBoardFinish import com.goalpanzi.mission_mate.feature.board.navigateToUserStory +import com.goalpanzi.mission_mate.feature.board.navigateToVerificationPreview import com.goalpanzi.mission_mate.feature.login.navigateToLogin import com.goalpanzi.mission_mate.feature.onboarding.navigateToBoardSetup import com.goalpanzi.mission_mate.feature.onboarding.navigateToBoardSetupSuccess @@ -86,6 +87,10 @@ class MainNavigator( fun navigationToUserStory(userStory: UserStory) { navController.navigateToUserStory(userStory) } + + fun navigationToVerificationPreview(missionId: Long, imageUrl : String) { + navController.navigateToVerificationPreview(missionId, imageUrl) + } } @Composable