diff --git a/domain/src/main/java/com/foke/together/domain/GetCaptureDurationUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/GetCaptureDurationUseCase.kt similarity index 89% rename from domain/src/main/java/com/foke/together/domain/GetCaptureDurationUseCase.kt rename to domain/src/main/java/com/foke/together/domain/interactor/GetCaptureDurationUseCase.kt index a8802d6..17e7b32 100644 --- a/domain/src/main/java/com/foke/together/domain/GetCaptureDurationUseCase.kt +++ b/domain/src/main/java/com/foke/together/domain/interactor/GetCaptureDurationUseCase.kt @@ -1,4 +1,4 @@ -package com.foke.together.domain +package com.foke.together.domain.interactor import com.foke.together.domain.output.AppPreferenceInterface import kotlinx.coroutines.flow.Flow diff --git a/domain/src/main/java/com/foke/together/domain/interactor/GetCapturedImageUriUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/GetCapturedImageUriUseCase.kt new file mode 100644 index 0000000..06af1be --- /dev/null +++ b/domain/src/main/java/com/foke/together/domain/interactor/GetCapturedImageUriUseCase.kt @@ -0,0 +1,12 @@ +package com.foke.together.domain.interactor + +import android.net.Uri +import com.foke.together.domain.output.InternalCameraRepositoryInterface +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class GetCapturedImageUriUseCase @Inject constructor( + private val internalCameraRepository: InternalCameraRepositoryInterface +) { + operator fun invoke() : Flow = internalCameraRepository.getCapturedImageUri() +} \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/SetCaptureDurationUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/SetCaptureDurationUseCase.kt similarity index 88% rename from domain/src/main/java/com/foke/together/domain/SetCaptureDurationUseCase.kt rename to domain/src/main/java/com/foke/together/domain/interactor/SetCaptureDurationUseCase.kt index 11f7ae7..4e458d6 100644 --- a/domain/src/main/java/com/foke/together/domain/SetCaptureDurationUseCase.kt +++ b/domain/src/main/java/com/foke/together/domain/interactor/SetCaptureDurationUseCase.kt @@ -1,4 +1,4 @@ -package com.foke.together.domain +package com.foke.together.domain.interactor import com.foke.together.domain.output.AppPreferenceInterface import javax.inject.Inject diff --git a/domain/src/main/java/com/foke/together/domain/output/InternalCameraRepositoryInterface.kt b/domain/src/main/java/com/foke/together/domain/output/InternalCameraRepositoryInterface.kt index 11053ce..2abace4 100644 --- a/domain/src/main/java/com/foke/together/domain/output/InternalCameraRepositoryInterface.kt +++ b/domain/src/main/java/com/foke/together/domain/output/InternalCameraRepositoryInterface.kt @@ -2,13 +2,17 @@ package com.foke.together.domain.output import android.content.Context import android.graphics.Bitmap +import android.net.Uri import androidx.annotation.IntRange import androidx.camera.core.CameraSelector import androidx.camera.core.ImageAnalysis import androidx.camera.view.PreviewView import androidx.lifecycle.LifecycleOwner +import kotlinx.coroutines.flow.Flow interface InternalCameraRepositoryInterface { + + fun getCapturedImageUri() : Flow suspend fun capture( context: Context, fileName : String, diff --git a/external/src/main/java/com/foke/together/external/repository/ImageRepository.kt b/external/src/main/java/com/foke/together/external/repository/ImageRepository.kt index fb62d23..d10a354 100644 --- a/external/src/main/java/com/foke/together/external/repository/ImageRepository.kt +++ b/external/src/main/java/com/foke/together/external/repository/ImageRepository.kt @@ -47,7 +47,7 @@ class ImageRepository @Inject constructor( MediaStore.Images.Media.DATE_ADDED ) val selection = "${MediaStore.Images.Media.RELATIVE_PATH} LIKE ?" - val selectionArgs = arrayOf("%Pictures/4cuts/backup%") + val selectionArgs = arrayOf("%${AppPolicy.MEDIA_STORE_RELATIVE_PATH}%") val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC" val cursor = context.contentResolver.query( diff --git a/external/src/main/java/com/foke/together/external/repository/InternalCameraRepository.kt b/external/src/main/java/com/foke/together/external/repository/InternalCameraRepository.kt index 79c3a1c..1442b05 100644 --- a/external/src/main/java/com/foke/together/external/repository/InternalCameraRepository.kt +++ b/external/src/main/java/com/foke/together/external/repository/InternalCameraRepository.kt @@ -3,6 +3,7 @@ package com.foke.together.external.repository import android.content.ContentValues import android.content.Context import android.graphics.Bitmap +import android.net.Uri import android.os.Build import android.provider.MediaStore import androidx.annotation.IntRange @@ -20,6 +21,9 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.LifecycleOwner import com.foke.together.domain.output.InternalCameraRepositoryInterface import com.foke.together.util.AppLog +import com.foke.together.util.AppPolicy +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject import javax.inject.Singleton @@ -29,7 +33,9 @@ class InternalCameraRepository @Inject constructor( private lateinit var cameraController: LifecycleCameraController private lateinit var imageCapture: ImageCapture - private var imageBitmap : Bitmap? = null + private val capturedImageUri = MutableStateFlow(null) + override fun getCapturedImageUri(): Flow = capturedImageUri + override suspend fun capture( context: Context, fileName : String, @@ -38,7 +44,7 @@ class InternalCameraRepository @Inject constructor( val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, "${fileName}.jpg") put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") - put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/4cuts/backup") + put(MediaStore.Images.Media.RELATIVE_PATH, AppPolicy.MEDIA_STORE_RELATIVE_PATH) } val outputOptions = ImageCapture.OutputFileOptions.Builder( context.contentResolver, @@ -50,10 +56,8 @@ class InternalCameraRepository @Inject constructor( object : ImageCapture.OnImageSavedCallback { override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { if (outputFileResults.savedUri != null) { - imageBitmap = MediaStore.Images.Media.getBitmap( - context.contentResolver, - outputFileResults.savedUri - ) + AppLog.d(TAG, "onImageSaved", "Photo capture succeeded: ${outputFileResults.savedUri}") + capturedImageUri.value = outputFileResults.savedUri } } override fun onError(exception: ImageCaptureException) { diff --git a/presenter/src/main/java/com/foke/together/presenter/component/Basic.kt b/presenter/src/main/java/com/foke/together/presenter/component/Basic.kt index 0fd32af..ad0315f 100644 --- a/presenter/src/main/java/com/foke/together/presenter/component/Basic.kt +++ b/presenter/src/main/java/com/foke/together/presenter/component/Basic.kt @@ -21,6 +21,8 @@ import androidx.compose.material3.ButtonColors import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxColors import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold @@ -462,4 +464,31 @@ fun AppBottomBar( content = content ) } +} + +@Composable +fun AppCheckBox( + modifier: Modifier = Modifier, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, +){ + Checkbox( + modifier = modifier, + checked = checked, + onCheckedChange = onCheckedChange, + colors = CheckboxColors( + checkedCheckmarkColor = AppTheme.colorScheme.tint, + uncheckedCheckmarkColor = AppTheme.colorScheme.bottom, + checkedBoxColor = AppTheme.colorScheme.top, + uncheckedBoxColor = AppTheme.colorScheme.bottom, + disabledCheckedBoxColor = AppTheme.colorScheme.bottom, + disabledUncheckedBoxColor = AppTheme.colorScheme.bottom, + disabledIndeterminateBoxColor= AppTheme.colorScheme.bottom, + checkedBorderColor = AppTheme.colorScheme.border, + uncheckedBorderColor = AppTheme.colorScheme.top, + disabledBorderColor = AppTheme.colorScheme.border, + disabledUncheckedBorderColor = AppTheme.colorScheme.border, + disabledIndeterminateBorderColor= AppTheme.colorScheme.border, + ) + ) } \ No newline at end of file diff --git a/presenter/src/main/java/com/foke/together/presenter/screen/InternalCameraScreen.kt b/presenter/src/main/java/com/foke/together/presenter/screen/InternalCameraScreen.kt index f544e81..a68bca0 100644 --- a/presenter/src/main/java/com/foke/together/presenter/screen/InternalCameraScreen.kt +++ b/presenter/src/main/java/com/foke/together/presenter/screen/InternalCameraScreen.kt @@ -1,6 +1,7 @@ package com.foke.together.presenter.screen import android.content.Context +import android.net.Uri import androidx.camera.view.PreviewView import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween @@ -22,6 +23,7 @@ import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -44,6 +46,8 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage +import coil.request.ImageRequest import com.foke.together.presenter.R import com.foke.together.presenter.component.AppBottomBar import com.foke.together.presenter.component.AppTopBar @@ -63,13 +67,14 @@ fun InternalCameraScreenRoot( popBackStack: () -> Unit, viewModel: InternelCameraViewModel = hiltViewModel() ){ - val state = viewModel.state val lifecycleOwner = LocalLifecycleOwner.current val context = LocalContext.current val captureCount by viewModel.captureCount.collectAsStateWithLifecycle() val progress by viewModel.progressState.collectAsStateWithLifecycle() + val isFlashAnimationVisible by viewModel.flashAnimationState.collectAsStateWithLifecycle() val countdownSeconds by viewModel.countdownSeconds.collectAsStateWithLifecycle() + val capturedImageUri by viewModel.capturedImageUri.collectAsState() LifecycleEventEffect(Lifecycle.Event.ON_START) { viewModel.setCaptureTimer(context) { navigateToGenerateImage() } AppLog.d(TAG, "LifecycleEventEffect. ON_START", "") @@ -83,11 +88,10 @@ fun InternalCameraScreenRoot( AppLog.d(TAG, "LifecycleEventEffect. ON_STOP", "") } InternalCameraScreen( - state = state, captureCount = captureCount, - progress = progress, - isFlashAnimationVisible = isFlashAnimationVisible, countdownSeconds = countdownSeconds, + isFlashAnimationVisible = isFlashAnimationVisible, + capturedImageUri = capturedImageUri, initialPreview = { context, previewView -> viewModel.initial(context, lifecycleOwner, previewView) }, @@ -100,11 +104,10 @@ fun InternalCameraScreenRoot( @OptIn(ExperimentalMaterial3Api::class) @Composable fun InternalCameraScreen( - state : InternalCameraState, captureCount : Int, - progress: Float, isFlashAnimationVisible: Boolean, countdownSeconds: Int, + capturedImageUri : Uri?, initialPreview : (Context, PreviewView) -> Unit, releasePreview : (Context) -> Unit, ){ @@ -185,7 +188,15 @@ fun InternalCameraScreen( .padding(24.dp) ) } - + this@Column.AnimatedVisibility( + visible = capturedImageUri != null && countdownSeconds == 0, + ) { + AsyncImage( + model = capturedImageUri, + contentDescription = "Captured Image", + modifier = Modifier.fillMaxSize() + ) + } } } } @@ -197,18 +208,16 @@ fun InternalCameraScreen( ) @Composable fun InternalScreenPreview(){ - val state by remember { mutableStateOf(InternalCameraState()) } val captureCount by remember{ mutableStateOf(1) } - val progress by remember{ mutableStateOf(1f)} val isFlashAnimationVisible by remember{ mutableStateOf(false)} val countdownSeconds by remember{ mutableStateOf(3)} + val capturedImageUri by remember { mutableStateOf(Uri.EMPTY) } FourCutTogetherTheme() { InternalCameraScreen( - state = state, captureCount = captureCount, - progress = progress, isFlashAnimationVisible = isFlashAnimationVisible, countdownSeconds = countdownSeconds, + capturedImageUri = capturedImageUri, initialPreview = { context, previewView -> }, diff --git a/presenter/src/main/java/com/foke/together/presenter/screen/SelectFrameScreen.kt b/presenter/src/main/java/com/foke/together/presenter/screen/SelectFrameScreen.kt index b3b0dcd..8a82854 100644 --- a/presenter/src/main/java/com/foke/together/presenter/screen/SelectFrameScreen.kt +++ b/presenter/src/main/java/com/foke/together/presenter/screen/SelectFrameScreen.kt @@ -1,9 +1,11 @@ package com.foke.together.presenter.screen import android.content.res.Configuration +import android.net.Uri import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio @@ -15,6 +17,7 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxColors import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -27,6 +30,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel @@ -35,9 +39,13 @@ import com.foke.together.domain.interactor.entity.DefaultCutFrameSet import com.foke.together.presenter.component.AppBottomBar import com.foke.together.presenter.component.AppTopBar import com.foke.together.presenter.component.BasicScaffold +import com.foke.together.presenter.frame.DefaultCutFrame import com.foke.together.presenter.theme.AppTheme import com.foke.together.presenter.theme.FourCutTogetherTheme import com.foke.together.presenter.viewmodel.SelectFrameViewModel +import androidx.core.net.toUri +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.foke.together.presenter.component.AppCheckBox @Composable fun SelectFrameScreen( @@ -45,13 +53,12 @@ fun SelectFrameScreen( popBackStack: () -> Unit, viewModel: SelectFrameViewModel = hiltViewModel() ) { - val cutFrames by viewModel.cutFrames.collectAsState() - val isDateDisplay by viewModel.isDateDisplay.collectAsState() - val isQRDisplay by viewModel.isQRDisplay.collectAsState() + val cutFrames by viewModel.cutFrames.collectAsStateWithLifecycle() + val isDateDisplay by viewModel.isDateDisplay.collectAsStateWithLifecycle() + val isQRDisplay by viewModel.isQRDisplay.collectAsStateWithLifecycle() DisposableEffect(Unit) { viewModel.updateSessionStatus() - onDispose { } } SelectFrameContent( @@ -77,9 +84,9 @@ fun SelectFrameContent( cutFrames: List, isDateDisplay: Boolean, isQRDisplay : Boolean, - selectFrame: (cutFrame: CutFrame) -> Unit, - onDisplayDate : (Boolean ) -> Unit, - onDisplayQR : (Boolean) -> Unit, + selectFrame: ( cutFrame: CutFrame ) -> Unit, + onDisplayDate : ( Boolean ) -> Unit, + onDisplayQR : ( Boolean ) -> Unit, ){ BasicScaffold( modifier = Modifier.fillMaxSize(), @@ -95,11 +102,11 @@ fun SelectFrameContent( modifier = Modifier.wrapContentSize(), horizontalAlignment = Alignment.CenterHorizontally ){ - Checkbox( + AppCheckBox( checked = isDateDisplay, onCheckedChange = { isChecked -> onDisplayDate(isChecked) - } + }, ) Text( text = "날짜 표시하기", @@ -111,7 +118,7 @@ fun SelectFrameContent( modifier = Modifier.wrapContentSize(), horizontalAlignment = Alignment.CenterHorizontally ){ - Checkbox( + AppCheckBox( checked = isQRDisplay, onCheckedChange = { isChecked -> onDisplayQR(isChecked) @@ -140,17 +147,29 @@ fun SelectFrameContent( columns = GridCells.Fixed(6) ){ items(cutFrames.size){ index -> - Image( - modifier = Modifier.aspectRatio(0.3333f) - .clickable( - true, - onClick = { - selectFrame(cutFrames[index]) - } + Box( + modifier = Modifier.fillMaxSize() + .clickable{ + selectFrame(cutFrames[index]) + }, + contentAlignment = Alignment.Center, + ){ + DefaultCutFrame( + cutFrame = cutFrames[index], + imageUrlList = listOf( + "file:///android_asset/sample_cut.png".toUri(), + "file:///android_asset/sample_cut.png".toUri(), + "file:///android_asset/sample_cut.png".toUri(), + "file:///android_asset/sample_cut.png".toUri(), ), - painter = painterResource(id = cutFrames[index].frameImageSrc), - contentDescription = cutFrames[index].frameTitle - ) + ) + Text( + modifier = Modifier.align(Alignment.TopCenter), + text = cutFrames[index].frameTitle, + style = AppTheme.typography.body.copy(fontWeight = FontWeight.Bold), + color = AppTheme.colorScheme.top + ) + } } } } diff --git a/presenter/src/main/java/com/foke/together/presenter/screen/SettingScreen.kt b/presenter/src/main/java/com/foke/together/presenter/screen/SettingScreen.kt index 3384690..0a5ab49 100644 --- a/presenter/src/main/java/com/foke/together/presenter/screen/SettingScreen.kt +++ b/presenter/src/main/java/com/foke/together/presenter/screen/SettingScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.CameraFront import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.material3.MaterialTheme @@ -143,6 +144,7 @@ fun SettingContent( SliderPreference( modifier = Modifier.fillMaxWidth().wrapContentHeight(), title = "Capture Duration", + iconVector = Icons.Filled.CameraFront, sliderText = "${captureDuration / 1000}s", sliderValue = captureDuration.toFloat(), sliderRange = 1f..10f, diff --git a/presenter/src/main/java/com/foke/together/presenter/screen/ShareScreen.kt b/presenter/src/main/java/com/foke/together/presenter/screen/ShareScreen.kt index 2f068c7..eeb50f6 100644 --- a/presenter/src/main/java/com/foke/together/presenter/screen/ShareScreen.kt +++ b/presenter/src/main/java/com/foke/together/presenter/screen/ShareScreen.kt @@ -74,6 +74,21 @@ fun ShareScreen( captureImageUri = captureImageUri, qrImageBitmap = qrImageBitmap, popBackStack = popBackStack, + printImage = { + viewModel.printImage(context) + }, + downloadImage = { + viewModel.downloadImage() + .onSuccess { + Toast.makeText(context, "사진이 저장되었습니다.", Toast.LENGTH_SHORT).show() + } + .onFailure { + Toast.makeText(context, "사진 저장에 실패했습니다.", Toast.LENGTH_SHORT).show() + } + }, + shareImage = { + viewModel.shareImage() + } ) } @@ -83,6 +98,9 @@ fun ShareContent( captureImageUri : Uri, qrImageBitmap : Bitmap?, popBackStack: () -> Unit, + downloadImage: () -> Unit, + printImage: () -> Unit, + shareImage: () -> Unit ){ BasicScaffold( modifier = Modifier.fillMaxSize(), @@ -104,7 +122,10 @@ fun ShareContent( bottomBar = { AppBottomBar{ Column( - modifier = Modifier.wrapContentSize(), + modifier = Modifier.wrapContentSize() + .clickable{ + printImage() + }, verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ){ @@ -121,7 +142,10 @@ fun ShareContent( } Column( - modifier = Modifier.wrapContentSize(), + modifier = Modifier.wrapContentSize() + .clickable{ + downloadImage() + }, verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ){ @@ -138,7 +162,10 @@ fun ShareContent( } Column( - modifier = Modifier.wrapContentSize(), + modifier = Modifier.wrapContentSize() + .clickable{ + shareImage() + }, verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ){ @@ -190,16 +217,6 @@ fun ShareContent( } } -private fun saveToLocal(context: Context, viewModel: ShareViewModel) { - viewModel.downloadImage() - .onSuccess { - Toast.makeText(context, "사진이 저장되었습니다.", Toast.LENGTH_SHORT).show() - } - .onFailure { - Toast.makeText(context, "사진 저장에 실패했습니다.", Toast.LENGTH_SHORT).show() - } -} - @Preview( showBackground = true, device = Devices.TABLET @@ -212,7 +229,10 @@ private fun ShareScreenPreview() { context = LocalContext.current, captureImageUri = Uri.EMPTY, qrImageBitmap = qrBitmap, - popBackStack = {} + popBackStack = {}, + downloadImage = {}, + printImage = {}, + shareImage = {} ) } } \ No newline at end of file diff --git a/presenter/src/main/java/com/foke/together/presenter/viewmodel/CameraViewModel.kt b/presenter/src/main/java/com/foke/together/presenter/viewmodel/CameraViewModel.kt index ad177bd..bc451e1 100644 --- a/presenter/src/main/java/com/foke/together/presenter/viewmodel/CameraViewModel.kt +++ b/presenter/src/main/java/com/foke/together/presenter/viewmodel/CameraViewModel.kt @@ -22,6 +22,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.time.DurationUnit @HiltViewModel class CameraViewModel @Inject constructor( @@ -54,9 +55,9 @@ class CameraViewModel @Inject constructor( viewModelScope.launch { generatePhotoFrameUseCaseV1.clearCapturedImageList() } - captureTimer = object : CountDownTimer(CAPTURE_INTERVAL, COUNTDOWN_INTERVAL) { + captureTimer = object : CountDownTimer(CAPTURE_INTERVAL.toLong(DurationUnit.MILLISECONDS), COUNTDOWN_INTERVAL) { override fun onTick(millisUntilFinished: Long) { - _progressState.floatValue = 1f - (millisUntilFinished.toFloat() / CAPTURE_INTERVAL) + _progressState.floatValue = 1f - (millisUntilFinished.toFloat() / CAPTURE_INTERVAL.toLong(DurationUnit.MILLISECONDS)) } override fun onFinish() { viewModelScope.launch { diff --git a/presenter/src/main/java/com/foke/together/presenter/viewmodel/InternelCameraViewModel.kt b/presenter/src/main/java/com/foke/together/presenter/viewmodel/InternelCameraViewModel.kt index 78cdefa..f130b66 100644 --- a/presenter/src/main/java/com/foke/together/presenter/viewmodel/InternelCameraViewModel.kt +++ b/presenter/src/main/java/com/foke/together/presenter/viewmodel/InternelCameraViewModel.kt @@ -1,6 +1,7 @@ package com.foke.together.presenter.viewmodel import android.content.Context +import android.net.Uri import android.os.CountDownTimer import androidx.camera.core.CameraSelector import androidx.camera.core.ImageCapture @@ -12,8 +13,9 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.foke.together.domain.GetCaptureDurationUseCase +import com.foke.together.domain.interactor.GetCaptureDurationUseCase import com.foke.together.domain.interactor.GeneratePhotoFrameUseCaseV1 +import com.foke.together.domain.interactor.GetCapturedImageUriUseCase import com.foke.together.domain.interactor.GetInternalCameraCaptureModeUseCase import com.foke.together.domain.interactor.GetInternalCameraFlashModeUseCase import com.foke.together.domain.interactor.GetInternalCameraLensFacingUseCase @@ -44,6 +46,7 @@ class InternelCameraViewModel @Inject constructor( private val getInternalCameraLensFacingUseCase: GetInternalCameraLensFacingUseCase, private val getInternalCameraFlashModeUseCase: GetInternalCameraFlashModeUseCase, private val getInternalCameraCaptureModeUseCase: GetInternalCameraCaptureModeUseCase, + private val getCapturedImageUriUseCase: GetCapturedImageUriUseCase, private val getSessionUseCase : GetCurrentSessionUseCase, private val generatePhotoFrameUseCaseV1: GeneratePhotoFrameUseCaseV1, ): ViewModel() { @@ -53,10 +56,15 @@ class InternelCameraViewModel @Inject constructor( val progressState = MutableStateFlow(1f) val flashAnimationState = MutableStateFlow(false) val countdownSeconds = MutableStateFlow(0) + val capturedImageUri = getCapturedImageUriUseCase().stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = null + ) private val captureDuration = getCaptureDurationUseCase().stateIn( scope = viewModelScope, started = SharingStarted.Lazily, - initialValue = AppPolicy.CAPTURE_INTERVAL.toDuration(DurationUnit.MILLISECONDS) + initialValue = AppPolicy.CAPTURE_INTERVAL ) private var captureTimer: CountDownTimer? = null private var mTimerState = false @@ -141,7 +149,6 @@ class InternelCameraViewModel @Inject constructor( captureTimer = object : CountDownTimer(captureDuration.value.toLong(DurationUnit.MILLISECONDS), COUNTDOWN_INTERVAL) { override fun onTick(millisUntilFinished: Long) { - progressState.value = 1f - (millisUntilFinished.toFloat() / CAPTURE_INTERVAL) countdownSeconds.value = ((millisUntilFinished / 1000) + 1).toInt() } override fun onFinish() { @@ -151,18 +158,17 @@ class InternelCameraViewModel @Inject constructor( viewModelScope.launch { capture(context) - progressState.value = 1f if (captureCount.value < AppPolicy.CAPTURE_COUNT) { AppLog.d(TAG, "captureCount", "${captureCount.value}") - captureCount.value += 1 // 촬영 후 2초 대기 - delay(2.seconds.toLong(DurationUnit.MILLISECONDS)) + delay(5.seconds.toLong(DurationUnit.MILLISECONDS)) + captureCount.value += 1 start() } else { AppLog.d(TAG, "captureCount", "${captureCount.value}") stopCaptureTimer() - delay(2.seconds.toLong(DurationUnit.MILLISECONDS)) + delay(5.seconds.toLong(DurationUnit.MILLISECONDS)) captureCount.value = 1 nextNavigate() } diff --git a/presenter/src/main/java/com/foke/together/presenter/viewmodel/SelectFrameViewModel.kt b/presenter/src/main/java/com/foke/together/presenter/viewmodel/SelectFrameViewModel.kt index 9703595..2d494ff 100644 --- a/presenter/src/main/java/com/foke/together/presenter/viewmodel/SelectFrameViewModel.kt +++ b/presenter/src/main/java/com/foke/together/presenter/viewmodel/SelectFrameViewModel.kt @@ -6,9 +6,9 @@ import com.foke.together.domain.interactor.entity.CutFrame import com.foke.together.domain.interactor.entity.DefaultCutFrameSet import com.foke.together.domain.interactor.entity.Status import com.foke.together.domain.interactor.session.UpdateSessionStatusUseCase +import com.foke.together.util.AppLog import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject @@ -17,32 +17,38 @@ class SelectFrameViewModel @Inject constructor( private val updateSessionStatusUseCase: UpdateSessionStatusUseCase ): ViewModel() { - val cutFrames = MutableStateFlow(DefaultCutFrameSet.entries) val isDateDisplay = MutableStateFlow(false) val isQRDisplay = MutableStateFlow(false) + + val cutFrames = MutableStateFlow>(emptyList()) + fun updateSessionStatus() { updateSessionStatusUseCase(Status.SELECT_FRAME) + cutFrames.value = DefaultCutFrameSet.entries } fun updateDateDisplay(state: Boolean) = viewModelScope.launch{ isDateDisplay.value = state - cutFrames.value = cutFrames.value.map { - it.isDateString = isDateDisplay.value - it + cutFrames.value = DefaultCutFrameSet.entries.map { cutFrame -> + cutFrame.isDateString = state + AppLog.d(TAG, "updateDateDisplay", "isDateString : ${cutFrame.isDateString}") + cutFrame } } - // TODO(QR 표시 기능 구현) fun updateQRDisplay( state: Boolean ) = viewModelScope.launch{ - isQRDisplay.value = state - cutFrames.value = cutFrames.value.map { - it -// it.isQRString = isQRDisplay.value -// it + isQRDisplay.emit(state) + cutFrames.value = DefaultCutFrameSet.entries.map { cutFrame -> + cutFrame.isDateString = state + cutFrame } } fun setCutFrameType(cutFrame: CutFrame) = viewModelScope.launch { updateSessionStatusUseCase(cutFrame) } + + companion object { + private val TAG = SelectFrameViewModel::class.java.simpleName + } } \ No newline at end of file diff --git a/presenter/src/main/java/com/foke/together/presenter/viewmodel/SettingViewModel.kt b/presenter/src/main/java/com/foke/together/presenter/viewmodel/SettingViewModel.kt index 243ed98..d808536 100644 --- a/presenter/src/main/java/com/foke/together/presenter/viewmodel/SettingViewModel.kt +++ b/presenter/src/main/java/com/foke/together/presenter/viewmodel/SettingViewModel.kt @@ -6,8 +6,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.foke.together.domain.GetCaptureDurationUseCase -import com.foke.together.domain.SetCaptureDurationUseCase +import com.foke.together.domain.interactor.GetCaptureDurationUseCase +import com.foke.together.domain.interactor.SetCaptureDurationUseCase import com.foke.together.domain.interactor.GetCameraSourceTypeUseCase import com.foke.together.domain.interactor.GetExternalCameraIPUseCase import com.foke.together.domain.interactor.GetInternalCameraLensFacingUseCase @@ -17,6 +17,7 @@ import com.foke.together.domain.interactor.SetInternalCameraLensFacingUseCase import com.foke.together.domain.interactor.entity.CameraSourceType import com.foke.together.domain.interactor.entity.ExternalCameraIP import com.foke.together.util.AppLog +import com.foke.together.util.AppPolicy import com.foke.together.util.di.IODispatcher import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher @@ -35,7 +36,6 @@ import kotlin.time.toDuration @HiltViewModel class SettingViewModel @Inject constructor( - @IODispatcher private val ioDispatcher: CoroutineDispatcher, private val getCameraSourceTypeUseCase: GetCameraSourceTypeUseCase, private val setCameraSourceTypeUseCase: SetCameraSourceTypeUseCase, private val getExternalCameraIPUseCase: GetExternalCameraIPUseCase, @@ -80,7 +80,7 @@ class SettingViewModel @Inject constructor( }.stateIn( scope = viewModelScope, started = SharingStarted.Lazily, - initialValue = 5.seconds.toLong(DurationUnit.MILLISECONDS) + initialValue = AppPolicy.CAPTURE_INTERVAL.toLong(DurationUnit.MILLISECONDS) ) fun setCameraSourceType(index: Int) = viewModelScope.launch { diff --git a/util/src/main/java/com/foke/together/util/AppPolicy.kt b/util/src/main/java/com/foke/together/util/AppPolicy.kt index 60c061a..fbf9f03 100644 --- a/util/src/main/java/com/foke/together/util/AppPolicy.kt +++ b/util/src/main/java/com/foke/together/util/AppPolicy.kt @@ -1,5 +1,7 @@ package com.foke.together.util +import kotlin.time.Duration.Companion.seconds + object AppPolicy { const val isDebugMode = true // TODO: change to false const val isNoCameraDebugMode = false // TODO: change to false @@ -17,7 +19,7 @@ object AppPolicy { const val EXTERNAL_CAMERA_WRITE_TIMEOUT = 10L // capture settings - const val CAPTURE_INTERVAL = 3000L + val CAPTURE_INTERVAL = 10.seconds const val CAPTURE_COUNT = 4 const val COUNTDOWN_INTERVAL = 10L @@ -26,4 +28,6 @@ object AppPolicy { const val SINGLE_ROW_FINAL_IMAGE_NAME = "final_single_row" const val TWO_ROW_FINAL_IMAGE_NAME = "final_two_row" const val DEFAULT_QR_CODE_IMAGE_NAME = "qrcode" + + const val MEDIA_STORE_RELATIVE_PATH = "Pictures/4cuts" } \ No newline at end of file