diff --git a/app/src/main/assets/sample_cut.png b/app/src/main/assets/sample_cut.png new file mode 100644 index 0000000..51ff087 Binary files /dev/null and b/app/src/main/assets/sample_cut.png differ diff --git a/app/src/main/assets/sample_cut_portrait.png b/app/src/main/assets/sample_cut_portrait.png new file mode 100644 index 0000000..647ab79 Binary files /dev/null and b/app/src/main/assets/sample_cut_portrait.png differ diff --git a/app/src/main/assets/sample_cut_square.png b/app/src/main/assets/sample_cut_square.png new file mode 100644 index 0000000..d8ce909 Binary files /dev/null and b/app/src/main/assets/sample_cut_square.png differ diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index dbe227b..0485425 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) + implementation(libs.androidx.ui.graphics.android) implementation(libs.camerax.view) implementation(libs.camerax.lifecycle) testImplementation(libs.junit) diff --git a/domain/src/main/java/com/foke/together/domain/interactor/CaptureWithExternalCameraUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/CaptureWithExternalCameraUseCase.kt index a9c1e9a..4cecf95 100644 --- a/domain/src/main/java/com/foke/together/domain/interactor/CaptureWithExternalCameraUseCase.kt +++ b/domain/src/main/java/com/foke/together/domain/interactor/CaptureWithExternalCameraUseCase.kt @@ -12,13 +12,13 @@ class CaptureWithExternalCameraUseCase @Inject constructor( suspend operator fun invoke(fileName: String): Result { externalCameraRepository.capture() .onSuccess { - AppLog.i(TAG, "capture", "success: $it") + AppLog.i(TAG, "invoke", "success: $it") imageRepository.cachingImage(it, fileName) return Result.success(Unit) } .onFailure { // TODO: handle network error - AppLog.i(TAG, "capture", "failure: $it") + AppLog.i(TAG, "invoke", "failure: $it") return Result.failure(it) } return Result.failure(Exception("Unknown error")) diff --git a/domain/src/main/java/com/foke/together/domain/interactor/GenerateImageFromViewUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/GenerateImageFromViewUseCase.kt new file mode 100644 index 0000000..70dc781 --- /dev/null +++ b/domain/src/main/java/com/foke/together/domain/interactor/GenerateImageFromViewUseCase.kt @@ -0,0 +1,30 @@ +package com.foke.together.domain.interactor + +import android.net.Uri +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.layer.GraphicsLayer +import com.foke.together.domain.output.ImageRepositoryInterface +import com.foke.together.util.AppLog +import com.foke.together.util.AppPolicy +import javax.inject.Inject + +class GenerateImageFromViewUseCase @Inject constructor( + private val imageRepositoryInterface: ImageRepositoryInterface +) { + // TODO: UI -> UC -> UI 흐름으로 만들어보기 + suspend operator fun invoke(graphicsLayer: GraphicsLayer, filename: String? = null, isCutFrameForPrint: Boolean = false): Uri { + val bitmap = graphicsLayer.toImageBitmap().asAndroidBitmap() + val finalCachedImageUri = imageRepositoryInterface.cachingImage( + bitmap, + filename ?: run { + if (isCutFrameForPrint) AppPolicy.TWO_ROW_FINAL_IMAGE_NAME else AppPolicy.SINGLE_ROW_FINAL_IMAGE_NAME + } + ) + AppLog.d(TAG, "invoke" ,"finalCachedImageUri: $finalCachedImageUri") + return finalCachedImageUri + } + + companion object { + private val TAG = GenerateImageFromViewUseCase::class.java.simpleName + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/interactor/GeneratePhotoFrameUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/GeneratePhotoFrameUseCaseV1.kt similarity index 68% rename from domain/src/main/java/com/foke/together/domain/interactor/GeneratePhotoFrameUseCase.kt rename to domain/src/main/java/com/foke/together/domain/interactor/GeneratePhotoFrameUseCaseV1.kt index 93cec98..6b3a1c4 100644 --- a/domain/src/main/java/com/foke/together/domain/interactor/GeneratePhotoFrameUseCase.kt +++ b/domain/src/main/java/com/foke/together/domain/interactor/GeneratePhotoFrameUseCaseV1.kt @@ -3,25 +3,27 @@ package com.foke.together.domain.interactor import android.content.Context import android.graphics.Bitmap import android.net.Uri -import com.foke.together.domain.interactor.entity.CutFrameType import com.foke.together.domain.output.ImageRepositoryInterface import com.foke.together.util.AppPolicy import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.Flow import javax.inject.Inject -class GeneratePhotoFrameUseCase @Inject constructor( +@Deprecated("Not in use") +class GeneratePhotoFrameUseCaseV1 @Inject constructor( @ApplicationContext private val context: Context, private val imageRepositoryInterface: ImageRepositoryInterface ){ - fun getCutFrameType(): CutFrameType = imageRepositoryInterface.getCutFrameType() - suspend fun setCutFrameType(type: Int) = imageRepositoryInterface.setCutFrameType(type) - + // 촬영한 이미지 리스트 관리 + // TODO: 추후 세션 관리와 엮어서 처리하기 + @Deprecated("Not in use") fun getCapturedImageListUri(): List = imageRepositoryInterface.getCachedImageUriList() + @Deprecated("Not in use") suspend fun clearCapturedImageList() = imageRepositoryInterface.clearCacheDir() - suspend fun saveGraphicsLayerImage(image: Bitmap, fileName: String) = imageRepositoryInterface.cachingImage(image, fileName) - suspend fun saveFinalImage(image: Bitmap, fileName: String) = imageRepositoryInterface.saveToStorage(image, fileName) + + // 최종 이미지 URL 가져오기 + // TODO: 추후 세션 관리와 엮어서 처리하기 + @Deprecated("Not in use") fun getFinalSingleImageUri(): Uri { var finalSingleImageUri: Uri = Uri.EMPTY context.cacheDir.listFiles().forEach { @@ -29,7 +31,7 @@ class GeneratePhotoFrameUseCase @Inject constructor( } return finalSingleImageUri } - + @Deprecated("Not in use") fun getFinalTwoImageUri(): Uri { var finalTwoImageUri: Uri = Uri.EMPTY context.cacheDir.listFiles().forEach { diff --git a/domain/src/main/java/com/foke/together/domain/interactor/entity/CutFrameType.kt b/domain/src/main/java/com/foke/together/domain/interactor/entity/CutFrameType.kt index c13693b..232294e 100644 --- a/domain/src/main/java/com/foke/together/domain/interactor/entity/CutFrameType.kt +++ b/domain/src/main/java/com/foke/together/domain/interactor/entity/CutFrameType.kt @@ -1,26 +1,168 @@ package com.foke.together.domain.interactor.entity -enum class CutFrameType { - MAKER_FAIRE, - FOURCUT_LIGHT, - FOURCUT_DARK; - - companion object { - fun findBy(name: String): CutFrameType { - return when (name) { - MAKER_FAIRE.name -> MAKER_FAIRE - FOURCUT_LIGHT.name -> FOURCUT_LIGHT - FOURCUT_DARK.name -> FOURCUT_DARK - else -> throw IllegalArgumentException("Unknown value: $name") - } - } - fun findBy(ordinal: Int): CutFrameType { - return when (ordinal) { - MAKER_FAIRE.ordinal -> MAKER_FAIRE - FOURCUT_LIGHT.ordinal -> FOURCUT_LIGHT - FOURCUT_DARK.ordinal -> FOURCUT_DARK - else -> throw IllegalArgumentException("Unknown value: $ordinal") - } - } - } +import androidx.annotation.DrawableRes +import com.foke.together.domain.R + +abstract class CutFrame( + val index: Int, + val frameTitle: String, + val cutCount: Int, + val width: Int, + val height: Int, + @DrawableRes val frameImageSrc: Int, // !!!!! TODO: asset 에 추가 및 src 값을 넣어서 처리 + val photoPosition: List, + val additionalFrameImageSrc: List, // !!!!! TODO: asset 에 추가 및 src 값을 넣어서 처리 +) + +sealed class DefaultCutFrameSet ( + index: Int, + frameTitle: String, + cutCount: Int, + width: Int, + height: Int, + frameImageSrc: Int, // !!!!! TODO: asset 에 추가 및 src 값을 넣어서 처리 + photoPosition: List, + additionalFrameImageSrc: List, // !!!!! TODO: asset 에 추가 및 src 값을 넣어서 처리 + var isDateString: Boolean = false, + val dateStringHeight: Int = 0, +): CutFrame(index, frameTitle, cutCount, width, height, frameImageSrc, photoPosition, additionalFrameImageSrc) { + val cutFrameSetTitle = "기본" + val cutFrameCoverImageSrc = "" + + // TODO: add information of frames + data object FourCutLight: DefaultCutFrameSet( + 7, + "같이네컷 화이트", + 4, 190, 570, + R.drawable.fourcut_frame_medium_light, + listOf( + PhotoPosition(159, 106, 16, 36), + PhotoPosition(159, 106, 16, 147), + PhotoPosition(159, 106, 16, 258), + PhotoPosition(159, 106, 16, 369), + ), + emptyList() + ) + + data object FourCurDark: DefaultCutFrameSet( + 8, + "같이네컷 다크", + 4, 190, 570, + R.drawable.fourcut_frame_medium_dark, + listOf( + PhotoPosition(159, 106, 16, 36), + PhotoPosition(159, 106, 16, 147), + PhotoPosition(159, 106, 16, 258), + PhotoPosition(159, 106, 16, 369), + ), + emptyList() + ) + + data object MakerFaire: DefaultCutFrameSet( + 9, + "Maker Faire Seoul 2024", + 4, 190, 570, + R.drawable.maker_faire_frame, + listOf( + PhotoPosition(159, 106, 16, 36), + PhotoPosition(159, 106, 16, 147), + PhotoPosition(159, 106, 16, 258), + PhotoPosition(159, 106, 16, 369), + ), + emptyList() + ) + + data object Bride1: DefaultCutFrameSet( + 3, + "신부 1", + 4, 190, 570, + R.drawable.bride1, + listOf( + PhotoPosition(172, 115, 9, 9), + PhotoPosition(172, 115, 9, 133), + PhotoPosition(172, 115, 9, 256), + PhotoPosition(172, 115, 9, 383), + ), + emptyList(), + dateStringHeight = 498 + ) + + data object Bride2: DefaultCutFrameSet( + 4, + "신부 2", + 4, 190, 570, + R.drawable.bride2, + listOf( + PhotoPosition(172, 115, 9, 9), + PhotoPosition(172, 115, 9, 133), + PhotoPosition(172, 115, 9, 256), + PhotoPosition(172, 115, 9, 383), + ), + emptyList(), + dateStringHeight = 495 + ) + + data object Groom1: DefaultCutFrameSet( + 5, + "신랑 1", + 4, 190, 570, + R.drawable.groom1, + listOf( + PhotoPosition(172, 115, 9, 9), + PhotoPosition(172, 115, 9, 133), + PhotoPosition(172, 115, 9, 256), + PhotoPosition(172, 115, 9, 383), + ), + emptyList(), + dateStringHeight = 495 + ) + + data object Groom2: DefaultCutFrameSet( + 6, + "신랑 2", + 4, 190, 570, + R.drawable.groom2, + listOf( + PhotoPosition(172, 115, 9, 9), + PhotoPosition(172, 115, 9, 133), + PhotoPosition(172, 115, 9, 256), + PhotoPosition(172, 115, 9, 383), + ), + emptyList(), + dateStringHeight = 495 + ) + + data object Wedding1: DefaultCutFrameSet( + 1, + "웨딩 1", + 4, 190, 570, + R.drawable.wedding, + listOf( + PhotoPosition(172, 115, 9, 9), + PhotoPosition(172, 115, 9, 133), + PhotoPosition(172, 115, 9, 256), + PhotoPosition(172, 115, 9, 383), + ), + listOf( + R.drawable.wedding_overlay1 + ), + dateStringHeight = 495 + ) + + data object Wedding2: DefaultCutFrameSet( + 2, + "웨딩 2", + 4, 190, 570, + R.drawable.wedding, + listOf( + PhotoPosition(172, 115, 9, 9), + PhotoPosition(172, 115, 9, 133), + PhotoPosition(172, 115, 9, 256), + PhotoPosition(172, 115, 9, 383), + ), + listOf( + R.drawable.wedding_overlay2 + ), + dateStringHeight = 495 + ) } \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/interactor/entity/CutFrameTypeV1.kt b/domain/src/main/java/com/foke/together/domain/interactor/entity/CutFrameTypeV1.kt new file mode 100644 index 0000000..91822fc --- /dev/null +++ b/domain/src/main/java/com/foke/together/domain/interactor/entity/CutFrameTypeV1.kt @@ -0,0 +1,26 @@ +package com.foke.together.domain.interactor.entity + +enum class CutFrameTypeV1 { + MAKER_FAIRE, + FOURCUT_LIGHT, + FOURCUT_DARK; + + companion object { + fun findBy(name: String): CutFrameTypeV1 { + return when (name) { + MAKER_FAIRE.name -> MAKER_FAIRE + FOURCUT_LIGHT.name -> FOURCUT_LIGHT + FOURCUT_DARK.name -> FOURCUT_DARK + else -> throw IllegalArgumentException("Unknown value: $name") + } + } + fun findBy(ordinal: Int): CutFrameTypeV1 { + return when (ordinal) { + MAKER_FAIRE.ordinal -> MAKER_FAIRE + FOURCUT_LIGHT.ordinal -> FOURCUT_LIGHT + FOURCUT_DARK.ordinal -> FOURCUT_DARK + else -> throw IllegalArgumentException("Unknown value: $ordinal") + } + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/interactor/entity/PhotoPosition.kt b/domain/src/main/java/com/foke/together/domain/interactor/entity/PhotoPosition.kt new file mode 100644 index 0000000..6399283 --- /dev/null +++ b/domain/src/main/java/com/foke/together/domain/interactor/entity/PhotoPosition.kt @@ -0,0 +1,8 @@ +package com.foke.together.domain.interactor.entity + +data class PhotoPosition ( + val width: Int, + val height: Int, + val x: Int, + val y: Int, +) \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/interactor/entity/SessionData.kt b/domain/src/main/java/com/foke/together/domain/interactor/entity/SessionData.kt new file mode 100644 index 0000000..f7ed37f --- /dev/null +++ b/domain/src/main/java/com/foke/together/domain/interactor/entity/SessionData.kt @@ -0,0 +1,15 @@ +package com.foke.together.domain.interactor.entity + +data class SessionData ( + val sessionId: SessionId, + val cutFrame: CutFrame? = null, + val status: Status +) + +enum class Status { + INIT, + SELECT_FRAME, + CAPTURE, + GENERATE_PHOTO, + SHARE +} \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/interactor/entity/SessionId.kt b/domain/src/main/java/com/foke/together/domain/interactor/entity/SessionId.kt new file mode 100644 index 0000000..dc71da9 --- /dev/null +++ b/domain/src/main/java/com/foke/together/domain/interactor/entity/SessionId.kt @@ -0,0 +1,9 @@ +package com.foke.together.domain.interactor.entity + +data class SessionId ( + val startAt: Long +) { + override fun toString(): String { + return startAt.toString() + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/interactor/session/ClearSessionUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/session/ClearSessionUseCase.kt new file mode 100644 index 0000000..6f6233a --- /dev/null +++ b/domain/src/main/java/com/foke/together/domain/interactor/session/ClearSessionUseCase.kt @@ -0,0 +1,12 @@ +package com.foke.together.domain.interactor.session + +import com.foke.together.domain.output.SessionRepositoryInterface +import javax.inject.Inject + +class ClearSessionUseCase @Inject constructor( + private val sessionRepository: SessionRepositoryInterface +) { + operator fun invoke() { + sessionRepository.clearSession() + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/interactor/session/CreateNewSessionUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/session/CreateNewSessionUseCase.kt new file mode 100644 index 0000000..6cc1ce3 --- /dev/null +++ b/domain/src/main/java/com/foke/together/domain/interactor/session/CreateNewSessionUseCase.kt @@ -0,0 +1,12 @@ +package com.foke.together.domain.interactor.session + +import com.foke.together.domain.output.SessionRepositoryInterface +import javax.inject.Inject + +class CreateNewSessionUseCase @Inject constructor( + private val sessionRepository: SessionRepositoryInterface +) { + operator fun invoke() { + sessionRepository.createSession() + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/interactor/session/GetCurrentSessionUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/session/GetCurrentSessionUseCase.kt new file mode 100644 index 0000000..fa006e2 --- /dev/null +++ b/domain/src/main/java/com/foke/together/domain/interactor/session/GetCurrentSessionUseCase.kt @@ -0,0 +1,13 @@ +package com.foke.together.domain.interactor.session + +import com.foke.together.domain.interactor.entity.SessionData +import com.foke.together.domain.output.SessionRepositoryInterface +import javax.inject.Inject + +class GetCurrentSessionUseCase @Inject constructor( + private val sessionRepository: SessionRepositoryInterface +) { + operator fun invoke(): SessionData? { + return sessionRepository.getSession() + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/interactor/session/UpdateSessionStatusUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/session/UpdateSessionStatusUseCase.kt new file mode 100644 index 0000000..925c909 --- /dev/null +++ b/domain/src/main/java/com/foke/together/domain/interactor/session/UpdateSessionStatusUseCase.kt @@ -0,0 +1,18 @@ +package com.foke.together.domain.interactor.session + +import com.foke.together.domain.interactor.entity.CutFrame +import com.foke.together.domain.interactor.entity.Status +import com.foke.together.domain.output.SessionRepositoryInterface +import javax.inject.Inject + +class UpdateSessionStatusUseCase @Inject constructor( + private val sessionRepository: SessionRepositoryInterface +) { + operator fun invoke(status: Status) { + sessionRepository.updateSession(status) + } + + operator fun invoke(cutFrame: CutFrame) { + sessionRepository.updateSession(cutFrame) + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/interactor/web/SessionKeyUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/web/SessionKeyUseCase.kt deleted file mode 100644 index 7683d1d..0000000 --- a/domain/src/main/java/com/foke/together/domain/interactor/web/SessionKeyUseCase.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.foke.together.domain.interactor.web - -import com.foke.together.domain.output.SessionRepositoryInterface -import javax.inject.Inject - -class SessionKeyUseCase @Inject constructor( - private val sessionRepository: SessionRepositoryInterface -) { - suspend fun setSessionKey() { - sessionRepository.setSessionKey() - } - - fun getSessionKey(): String { - return sessionRepository.getSessionKey() - } -} \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/interactor/web/UploadFileUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/web/UploadFileUseCase.kt index 1528494..700991e 100644 --- a/domain/src/main/java/com/foke/together/domain/interactor/web/UploadFileUseCase.kt +++ b/domain/src/main/java/com/foke/together/domain/interactor/web/UploadFileUseCase.kt @@ -13,10 +13,10 @@ class UploadFileUseCase @Inject constructor( suspend operator fun invoke(key: String, file: File): Result { remoteRepository.getUploadUrl("$key.${file.extension}", file) .onSuccess { preSignedUrl -> - AppLog.e("UploadFileUseCase", "invoke", "preSignedUrl: $preSignedUrl") + AppLog.e(TAG, "invoke", "preSignedUrl: $preSignedUrl") remoteRepository.uploadFile(preSignedUrl, file) .onFailure { - AppLog.e("UploadFileUseCase", "invoke", "upload failed") + AppLog.e(TAG, "invoke", "upload failed") return Result.failure(Exception("cannot upload file: $it")) } } @@ -25,4 +25,8 @@ class UploadFileUseCase @Inject constructor( } return Result.failure(Exception("Unknown error")) } + + companion object { + private val TAG = UploadFileUseCase::class.java.simpleName + } } \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/output/ImageRepositoryInterface.kt b/domain/src/main/java/com/foke/together/domain/output/ImageRepositoryInterface.kt index bad7154..cd037c1 100644 --- a/domain/src/main/java/com/foke/together/domain/output/ImageRepositoryInterface.kt +++ b/domain/src/main/java/com/foke/together/domain/output/ImageRepositoryInterface.kt @@ -2,12 +2,10 @@ package com.foke.together.domain.output import android.graphics.Bitmap import android.net.Uri -import android.os.Environment -import com.foke.together.domain.interactor.entity.CutFrameType -import kotlinx.coroutines.flow.Flow +import com.foke.together.domain.interactor.entity.CutFrameTypeV1 interface ImageRepositoryInterface { - fun getCutFrameType(): CutFrameType + fun getCutFrameType(): CutFrameTypeV1 suspend fun setCutFrameType(type: Int) // 촬영한 사진들 모음 suspend fun cachingImage(image: Bitmap, fileName: String) : Uri diff --git a/domain/src/main/java/com/foke/together/domain/output/SessionRepositoryInterface.kt b/domain/src/main/java/com/foke/together/domain/output/SessionRepositoryInterface.kt index ce27d42..716fe73 100644 --- a/domain/src/main/java/com/foke/together/domain/output/SessionRepositoryInterface.kt +++ b/domain/src/main/java/com/foke/together/domain/output/SessionRepositoryInterface.kt @@ -1,6 +1,17 @@ package com.foke.together.domain.output +import com.foke.together.domain.interactor.entity.CutFrame +import com.foke.together.domain.interactor.entity.SessionData +import com.foke.together.domain.interactor.entity.Status + interface SessionRepositoryInterface { - suspend fun setSessionKey() - fun getSessionKey(): String + fun createSession() + + fun getSession(): SessionData? + + fun updateSession(cutFrame: CutFrame) + fun updateSession(status: Status) + fun updateSession(cutFrame: CutFrame, status: Status) + + fun clearSession() } \ No newline at end of file diff --git a/domain/src/main/res/drawable/bride1.png b/domain/src/main/res/drawable/bride1.png new file mode 100644 index 0000000..87f2675 Binary files /dev/null and b/domain/src/main/res/drawable/bride1.png differ diff --git a/domain/src/main/res/drawable/bride2.png b/domain/src/main/res/drawable/bride2.png new file mode 100644 index 0000000..1e9ebcd Binary files /dev/null and b/domain/src/main/res/drawable/bride2.png differ diff --git a/presenter/src/main/res/drawable/fourcut_frame_medium_dark.png b/domain/src/main/res/drawable/fourcut_frame_medium_dark.png similarity index 100% rename from presenter/src/main/res/drawable/fourcut_frame_medium_dark.png rename to domain/src/main/res/drawable/fourcut_frame_medium_dark.png diff --git a/presenter/src/main/res/drawable/fourcut_frame_medium_light.png b/domain/src/main/res/drawable/fourcut_frame_medium_light.png similarity index 100% rename from presenter/src/main/res/drawable/fourcut_frame_medium_light.png rename to domain/src/main/res/drawable/fourcut_frame_medium_light.png diff --git a/domain/src/main/res/drawable/groom1.png b/domain/src/main/res/drawable/groom1.png new file mode 100644 index 0000000..fe1d747 Binary files /dev/null and b/domain/src/main/res/drawable/groom1.png differ diff --git a/domain/src/main/res/drawable/groom2.png b/domain/src/main/res/drawable/groom2.png new file mode 100644 index 0000000..699bed4 Binary files /dev/null and b/domain/src/main/res/drawable/groom2.png differ diff --git a/presenter/src/main/res/drawable/maker_faire_frame.png b/domain/src/main/res/drawable/maker_faire_frame.png similarity index 100% rename from presenter/src/main/res/drawable/maker_faire_frame.png rename to domain/src/main/res/drawable/maker_faire_frame.png diff --git a/domain/src/main/res/drawable/wedding.png b/domain/src/main/res/drawable/wedding.png new file mode 100644 index 0000000..130c59d Binary files /dev/null and b/domain/src/main/res/drawable/wedding.png differ diff --git a/domain/src/main/res/drawable/wedding_overlay1.png b/domain/src/main/res/drawable/wedding_overlay1.png new file mode 100644 index 0000000..25e5ad3 Binary files /dev/null and b/domain/src/main/res/drawable/wedding_overlay1.png differ diff --git a/domain/src/main/res/drawable/wedding_overlay2.png b/domain/src/main/res/drawable/wedding_overlay2.png new file mode 100644 index 0000000..cbafa78 Binary files /dev/null and b/domain/src/main/res/drawable/wedding_overlay2.png differ 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 5483cdc..7e86701 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 @@ -4,26 +4,21 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.ImageDecoder import android.net.Uri -import android.provider.MediaStore -import com.foke.together.domain.interactor.entity.CutFrameType +import com.foke.together.domain.interactor.entity.CutFrameTypeV1 import com.foke.together.domain.output.ImageRepositoryInterface import com.foke.together.util.AppPolicy import com.foke.together.util.ImageFileUtil import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map import javax.inject.Inject class ImageRepository @Inject constructor( @ApplicationContext private val context: Context ): ImageRepositoryInterface{ - private var cutFrameType: CutFrameType = CutFrameType.MAKER_FAIRE + private var cutFrameType: CutFrameTypeV1 = CutFrameTypeV1.MAKER_FAIRE - override fun getCutFrameType(): CutFrameType = cutFrameType + override fun getCutFrameType(): CutFrameTypeV1 = cutFrameType override suspend fun setCutFrameType(type: Int) { - cutFrameType = CutFrameType.findBy(type) + cutFrameType = CutFrameTypeV1.findBy(type) } override suspend fun cachingImage(image: Bitmap, fileName: String): Uri { diff --git a/external/src/main/java/com/foke/together/external/repository/SessionRepository.kt b/external/src/main/java/com/foke/together/external/repository/SessionRepository.kt index f6a6540..12088ec 100644 --- a/external/src/main/java/com/foke/together/external/repository/SessionRepository.kt +++ b/external/src/main/java/com/foke/together/external/repository/SessionRepository.kt @@ -1,16 +1,71 @@ package com.foke.together.external.repository +import com.foke.together.domain.interactor.entity.CutFrame +import com.foke.together.domain.interactor.entity.SessionData +import com.foke.together.domain.interactor.entity.SessionId +import com.foke.together.domain.interactor.entity.Status import com.foke.together.domain.output.SessionRepositoryInterface +import com.foke.together.util.AppLog import com.foke.together.util.TimeUtil import javax.inject.Inject class SessionRepository @Inject constructor(): SessionRepositoryInterface { - private var sessionKey: String = "" - override suspend fun setSessionKey() { - sessionKey = TimeUtil.getCurrentTimeSec() + + private var sessionData: SessionData? = null + + override fun createSession() { + sessionData = SessionData( + SessionId(startAt = TimeUtil.getCurrentTimestamp()), + cutFrame = null, + status = Status.INIT + ) + AppLog.i(TAG, "createSession", "sessionData: $sessionData") + + // TODO: save session data to Pref. + } + + override fun getSession(): SessionData? { + AppLog.i(TAG, "getSession", "sessionData: $sessionData") + + return sessionData + // TODO: read session data to Pref. + } + + override fun updateSession(cutFrame: CutFrame) { + sessionData = sessionData?.let { + SessionData(it.sessionId, cutFrame, it.status) + } + AppLog.i(TAG, "updateSession", "sessionData: $sessionData") + + // TODO: save session data to Pref. + } + + override fun updateSession(status: Status) { + sessionData = sessionData?.let { + SessionData(it.sessionId, it.cutFrame, status) + } + AppLog.i(TAG, "updateSession", "sessionData: $sessionData") + + // TODO: save session data to Pref. + } + + override fun updateSession(cutFrame: CutFrame, status: Status) { + sessionData = sessionData?.let { + SessionData(it.sessionId, cutFrame, status) + } + AppLog.i(TAG, "updateSession", "sessionData: $sessionData") + + // TODO: save session data to Pref. + } + + override fun clearSession() { + sessionData = null + AppLog.i(TAG, "clearSession", "sessionData: $sessionData") + + // TODO: clear session data to Pref. } - override fun getSessionKey(): String { - return sessionKey + companion object { + private val TAG = SessionRepository::class.java.simpleName } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2f1ef9f..de41aa8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ agp = "8.5.2" # kotlin --------- kotlin = "1.9.20" -coreKtx = "1.13.1" +coreKtx = "1.15.0" # kapt ----------- kotlin-kapt = "1.9.20" @@ -22,11 +22,11 @@ kotlin-kapt = "1.9.20" # android -------- appcompat = "1.7.0" composeCompiler = "1.5.5" -lifecycle-runtime-ktx = "2.8.6" -activity-compose = "1.9.2" -compose-bom = "2024.09.03" +lifecycle-runtime-ktx = "2.8.7" +activity-compose = "1.9.3" +compose-bom = "2024.12.01" material = "1.12.0" -navigation-compose = "2.8.2" +navigation-compose = "2.8.5" # hilt ----------- hilt = "2.49" @@ -38,7 +38,7 @@ androidx-hilt-work = "1.2.0" room = "2.6.1" # work manager --- -work-runtime-ktx = "2.9.1" +work-runtime-ktx = "2.10.0" # retrofit ------- okhttp = "4.12.0" @@ -56,10 +56,10 @@ protobuf-plugin = "0.9.4" protoc = "com.google.protobuf:protoc:4.28.2" # compose extended UI -constraint-layout-compose = "1.0.1" +constraint-layout-compose = "1.1.0" android-mjpeg-view = "1.1.3" -material-icons-extended = "1.7.3" -ui-graphics-android = "1.7.3" +material-icons-extended = "1.7.6" +ui-graphics-android = "1.7.6" # qrcode-kotlin -- qrcode-kotlin = "4.1.1" diff --git a/presenter/build.gradle.kts b/presenter/build.gradle.kts index f47dbbe..d3113c2 100644 --- a/presenter/build.gradle.kts +++ b/presenter/build.gradle.kts @@ -45,6 +45,9 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.coil.compose) + // reflect + implementation(kotlin("reflect")) + // test testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) diff --git a/presenter/src/main/java/com/foke/together/presenter/frame/DefaultCutFrame.kt b/presenter/src/main/java/com/foke/together/presenter/frame/DefaultCutFrame.kt new file mode 100644 index 0000000..117bfe7 --- /dev/null +++ b/presenter/src/main/java/com/foke/together/presenter/frame/DefaultCutFrame.kt @@ -0,0 +1,93 @@ +package com.foke.together.presenter.frame + +import android.net.Uri +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.foke.together.domain.interactor.entity.DefaultCutFrameSet +import com.foke.together.util.TimeUtil + +@Composable +fun DefaultCutFrame( + cutFrame: DefaultCutFrameSet, + imageUrlList : List +) { + Box( + modifier = Modifier + .width(cutFrame.width.dp) + .height(cutFrame.height.dp) + ) { + // Background Image + Image( + painter = painterResource(id = cutFrame.frameImageSrc), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + alignment = Alignment.TopStart, + contentScale = ContentScale.Crop, + ) + + // Cut Image + cutFrame.photoPosition.zip(imageUrlList).forEachIndexed { index, (position, imageUrl) -> + if (index >= cutFrame.cutCount) { return@forEachIndexed } + Box( + modifier = Modifier + .width(position.width.dp) + .height(position.height.dp) + .offset(x = position.x.dp, y = position.y.dp) + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + } + } + + // Additional Images + cutFrame.additionalFrameImageSrc.forEach { layers -> + Image( + painter = painterResource(id = layers), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + alignment = Alignment.TopStart, + contentScale = ContentScale.Crop, + ) + } + + // !!!!! TODO. date string + if (cutFrame.isDateString) { + Text( + modifier = Modifier + .fillMaxSize() + .offset(y = cutFrame.dateStringHeight.dp), + text = TimeUtil.getCurrentDisplayTime(), + color = Color.White, + fontSize = 9.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold + ) + } + + // TODO: QRCode image + } +} \ No newline at end of file diff --git a/presenter/src/main/java/com/foke/together/presenter/navigation/NavGraph.kt b/presenter/src/main/java/com/foke/together/presenter/navigation/NavGraph.kt index 59b2955..5f80650 100644 --- a/presenter/src/main/java/com/foke/together/presenter/navigation/NavGraph.kt +++ b/presenter/src/main/java/com/foke/together/presenter/navigation/NavGraph.kt @@ -6,8 +6,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import com.foke.together.presenter.screen.CameraScreen -import com.foke.together.presenter.screen.GenerateSingleRowImageScreen -import com.foke.together.presenter.screen.GenerateTwoRowImageScreen +import com.foke.together.presenter.screen.GenerateImageScreen import com.foke.together.presenter.screen.HomeScreen import com.foke.together.presenter.screen.SelectFrameScreen import com.foke.together.presenter.screen.SelectMethodScreen @@ -25,8 +24,7 @@ fun NavGraph(navController: NavHostController) { addSelectFrameScreen(navController, this) addSelectMethodScreen(navController, this) addCameraScreen(navController, this) - addGenerateSingleRowImageScreen(navController, this) - addGenerateTwoRowImageScreen(navController, this) + addGenerateImageScreen(navController, this) addShareScreen(navController, this) } } @@ -96,28 +94,12 @@ private fun addCameraScreen( } } -private fun addGenerateSingleRowImageScreen( +private fun addGenerateImageScreen( navController: NavHostController, navGraphBuilder: NavGraphBuilder ) { navGraphBuilder.composable(route = NavRoute.GenerateSingleRowImage.path) { - GenerateSingleRowImageScreen( - navigateToTwoRow = { - navController.navigate(NavRoute.GenerateTwoRowImage.path) - }, - popBackStack = { - navController.popBackStack(NavRoute.Home.path, inclusive = false) - } - ) - } -} - -private fun addGenerateTwoRowImageScreen( - navController: NavHostController, - navGraphBuilder: NavGraphBuilder -) { - navGraphBuilder.composable(route = NavRoute.GenerateTwoRowImage.path) { - GenerateTwoRowImageScreen( + GenerateImageScreen( navigateToShare = { navController.navigate(NavRoute.Share.path) }, diff --git a/presenter/src/main/java/com/foke/together/presenter/screen/CameraScreen.kt b/presenter/src/main/java/com/foke/together/presenter/screen/CameraScreen.kt index 86b1582..a76362d 100644 --- a/presenter/src/main/java/com/foke/together/presenter/screen/CameraScreen.kt +++ b/presenter/src/main/java/com/foke/together/presenter/screen/CameraScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.layer.drawLayer @@ -47,6 +48,12 @@ fun CameraScreen( val externalCameraIP = viewModel.externalCameraIP var frameCount = 0 val graphicsLayer = rememberGraphicsLayer() + + DisposableEffect(Unit) { + viewModel.updateSessionStatus() + onDispose { } + } + ConstraintLayout( modifier = Modifier.fillMaxSize() ) { @@ -159,15 +166,15 @@ fun CameraScreen( } LifecycleEventEffect(Lifecycle.Event.ON_START) { viewModel.setCaptureTimer(graphicsLayer) { navigateToGenerateImage() } - AppLog.d(TAG, "ON_START", mjpegView.toString()) + AppLog.d(TAG, "LifecycleEventEffect. ON_START", mjpegView.toString()) } LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { - AppLog.d(TAG, "ON_RESUME", mjpegView.toString()) + AppLog.d(TAG, "LifecycleEventEffect. ON_RESUME", mjpegView.toString()) mjpegView?.startStream() } LifecycleEventEffect(Lifecycle.Event.ON_STOP) { viewModel.stopCaptureTimer() - AppLog.d(TAG, "ON_STOP", mjpegView.toString()) + AppLog.d(TAG, "LifecycleEventEffect. ON_STOP", mjpegView.toString()) frameCount = 0 mjpegView?.stopStream() } diff --git a/presenter/src/main/java/com/foke/together/presenter/screen/GenerateImageScreen.kt b/presenter/src/main/java/com/foke/together/presenter/screen/GenerateImageScreen.kt new file mode 100644 index 0000000..e0182a4 --- /dev/null +++ b/presenter/src/main/java/com/foke/together/presenter/screen/GenerateImageScreen.kt @@ -0,0 +1,167 @@ +package com.foke.together.presenter.screen + +import android.net.Uri +import androidx.compose.foundation.background +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.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.rememberGraphicsLayer +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect +import com.foke.together.domain.interactor.entity.DefaultCutFrameSet +import com.foke.together.presenter.R +import com.foke.together.presenter.frame.DefaultCutFrame +import com.foke.together.presenter.theme.FourCutTogetherTheme +import com.foke.together.presenter.viewmodel.GenerateImageViewModel +import com.foke.together.util.AppLog +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun GenerateImageScreen( + navigateToShare: () -> Unit, + popBackStack: () -> Unit, + viewModel: GenerateImageViewModel = hiltViewModel() +) { + val TAG = "GenerateImageScreen" + val graphicsLayer1 = rememberGraphicsLayer() + val graphicsLayer2 = rememberGraphicsLayer() + val coroutineScope = rememberCoroutineScope() + val isFirstState = remember { mutableStateOf(true) } + + LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { + AppLog.d(TAG, "LifecycleEventEffect: ON_RESUME", "${viewModel.imageUri}") + coroutineScope.launch { + isFirstState.value = true + delay(200) // !!!!! TODO. timing issue + viewModel.generateImage(graphicsLayer1) + + isFirstState.value = false + delay(200) // !!!!! TODO. timing issue + viewModel.generateImageForPrint(graphicsLayer2) + + delay(1000) + navigateToShare() + } + } + + Column ( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "이미지를 생성중입니다", + fontWeight = FontWeight.Bold, + fontSize = 48.sp, + modifier = Modifier + .padding(bottom = 48.dp) + ) + + Row { + if (isFirstState.value) { + // TODO: 나중에 다른 CutFrameSet 어떻게 처리해야 할지? + GetDefaultFrame( + cutFrame = viewModel.cutFrame as DefaultCutFrameSet, + imageUri = viewModel.imageUri, + graphicsLayer = graphicsLayer1, + ) + } else { + GetDefaultFrame( + cutFrame = viewModel.cutFrame as DefaultCutFrameSet, + imageUri = viewModel.imageUri, + graphicsLayer = graphicsLayer2, + isForPrint = true, + isPaddingHorizontal = 16.dp + ) + } + } + + CircularProgressIndicator( + modifier = Modifier + .width(128.dp) + .height(128.dp) + .padding(top = 48.dp), + color = colorResource(R.color.app_primary_color), + strokeWidth = 24.dp + ) + } +} + +// TODO: !!!!! left / right 20dp 마진 있었음 +@Composable +fun GetDefaultFrame( + cutFrame: DefaultCutFrameSet, + imageUri: List, + graphicsLayer: GraphicsLayer, + isForPrint: Boolean = false, + isPaddingHorizontal: Dp = 0.dp +) { + Column ( + modifier = Modifier + .drawWithContent { + graphicsLayer.record { + this@drawWithContent.drawContent() + } + drawLayer(graphicsLayer) + } + ) { + if (isForPrint) { WhiteBox(isPaddingHorizontal) } + Row { + if (isForPrint) { WhiteBox(isPaddingHorizontal) } + DefaultCutFrame(cutFrame, imageUri) + if (isForPrint) { + DefaultCutFrame(cutFrame, imageUri) + WhiteBox(isPaddingHorizontal) + } + } + if (isForPrint) { WhiteBox(isPaddingHorizontal) } + } +} + +@Composable +private fun WhiteBox( + size: Dp +) { + Box( + modifier = Modifier + .width(size) + .height(size) + .background(colorResource(R.color.md_theme_background)) + ) +} + +@Preview(showBackground = true) +@Composable +private fun DefaultFrame() { + FourCutTogetherTheme { + GenerateImageScreen( + navigateToShare = {}, + popBackStack = {} + ) + } +} \ No newline at end of file diff --git a/presenter/src/main/java/com/foke/together/presenter/screen/GenerateSingleRowImageScreen.kt b/presenter/src/main/java/com/foke/together/presenter/screen/GenerateSingleRowImageScreen.kt deleted file mode 100644 index 1274ac3..0000000 --- a/presenter/src/main/java/com/foke/together/presenter/screen/GenerateSingleRowImageScreen.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.foke.together.presenter.screen - -import android.net.Uri -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.graphics.layer.GraphicsLayer -import androidx.compose.ui.graphics.layer.drawLayer -import androidx.compose.ui.graphics.rememberGraphicsLayer -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.sp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.compose.LifecycleEventEffect -import com.foke.together.domain.interactor.entity.CutFrameType -import com.foke.together.presenter.frame.FourCutFrame -import com.foke.together.presenter.frame.MakerFaireFrame -import com.foke.together.presenter.theme.FourCutTogetherTheme -import com.foke.together.presenter.theme.mediumContrastLightColorScheme -import com.foke.together.presenter.viewmodel.GenerateSingleRowImageViewModel -import com.foke.together.util.AppLog -import kotlinx.coroutines.launch - -@Composable -fun GenerateSingleRowImageScreen( - navigateToTwoRow: () -> Unit, - popBackStack: () -> Unit, - viewModel: GenerateSingleRowImageViewModel = hiltViewModel() -) { - - val graphicsLayer = rememberGraphicsLayer() - val coroutineScope = rememberCoroutineScope() - ConstraintLayout( - modifier = Modifier.fillMaxSize() - ) { - val (editFrame, description) = createRefs() - Text( - text = "이미지를 생성중입니다", - modifier = Modifier.constrainAs(description) { - top.linkTo(parent.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - bottom.linkTo(editFrame.top) - }, - fontWeight = FontWeight.Bold, - fontSize = 24.sp, - ) - Row( - modifier = Modifier - .constrainAs(editFrame) { - top.linkTo(description.bottom) - start.linkTo(parent.start) - end.linkTo(parent.end) - bottom.linkTo(parent.bottom) - } - .aspectRatio(0.3333f) - .drawWithContent { - graphicsLayer.record { - this@drawWithContent.drawContent() - } - drawLayer(graphicsLayer) - } - ) { - GetFrame( - cutFrameType = viewModel.cutFrameType.ordinal, - imageUri = viewModel.imageUri - ) - } - } - - LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { - AppLog.d("GenerateSingleRowImageScreen", "LifecycleEventEffect: ON_RESUME", "${viewModel.imageUri}") - coroutineScope.launch { - viewModel.generateImage(graphicsLayer) - } - navigateToTwoRow() - } -} - -@Preview(showBackground = true) -@Composable -private fun DefaultFrame() { - FourCutTogetherTheme { - GenerateSingleRowImageScreen( - navigateToTwoRow = {}, - popBackStack = {} - ) - } -} \ No newline at end of file diff --git a/presenter/src/main/java/com/foke/together/presenter/screen/GenerateTwoRowImageScreen.kt b/presenter/src/main/java/com/foke/together/presenter/screen/GenerateTwoRowImageScreen.kt deleted file mode 100644 index b743f02..0000000 --- a/presenter/src/main/java/com/foke/together/presenter/screen/GenerateTwoRowImageScreen.kt +++ /dev/null @@ -1,131 +0,0 @@ -package com.foke.together.presenter.screen - -import android.net.Uri -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.graphics.layer.drawLayer -import androidx.compose.ui.graphics.rememberGraphicsLayer -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.sp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.compose.LifecycleEventEffect -import com.foke.together.domain.interactor.entity.CutFrameType -import com.foke.together.domain.interactor.entity.FramePosition -import com.foke.together.presenter.frame.FourCutFrame -import com.foke.together.presenter.frame.MakerFaireFrame -import com.foke.together.presenter.theme.FourCutTogetherTheme -import com.foke.together.presenter.theme.mediumContrastDarkColorScheme -import com.foke.together.presenter.theme.mediumContrastLightColorScheme -import com.foke.together.presenter.viewmodel.GenerateTwoRowImageViewModel -import com.foke.together.util.AppLog -import kotlinx.coroutines.launch - -@Composable -fun GenerateTwoRowImageScreen( - navigateToShare: () -> Unit, - popBackStack: () -> Unit, - viewModel: GenerateTwoRowImageViewModel = hiltViewModel() -) { - - val graphicsLayer = rememberGraphicsLayer() - val coroutineScope = rememberCoroutineScope() - ConstraintLayout( - modifier = Modifier.fillMaxSize() - ) { - val (editFrame, description) = createRefs() - Text( - text = "이미지를 생성중입니다", - modifier = Modifier.constrainAs(description) { - top.linkTo(parent.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - bottom.linkTo(editFrame.top) - }, - fontWeight = FontWeight.Bold, - fontSize = 24.sp, - ) - Row( - modifier = Modifier - .constrainAs(editFrame) { - top.linkTo(description.bottom) - start.linkTo(parent.start) - end.linkTo(parent.end) - bottom.linkTo(parent.bottom) - } - .aspectRatio(0.6666f) - .drawWithContent { - graphicsLayer.record { - this@drawWithContent.drawContent() - } - drawLayer(graphicsLayer) - } - ) { - GetFrame( - cutFrameType = viewModel.cutFrameType.ordinal, - imageUri = viewModel.imageUri, - position = FramePosition.LEFT - ) - GetFrame( - cutFrameType = viewModel.cutFrameType.ordinal, - imageUri = viewModel.imageUri, - position = FramePosition.RIGHT - ) - } - } - - LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { - AppLog.d("GenerateTwoRowImageScreen", "LifecycleEventEffect: ON_RESUME", "${viewModel.imageUri}") - coroutineScope.launch { - viewModel.generateImage(graphicsLayer) - } - navigateToShare() - } -} - - -@Composable -fun GetFrame( - cutFrameType : Int, - imageUri: List, - position: FramePosition? = null -) { - when(cutFrameType) { - CutFrameType.MAKER_FAIRE.ordinal -> MakerFaireFrame( - cameraImageUrlList = imageUri, - position = position - ) - - CutFrameType.FOURCUT_LIGHT.ordinal -> FourCutFrame( - designColorScheme = mediumContrastLightColorScheme, - cameraImageUrlList = imageUri, - position = position - ) - - CutFrameType.FOURCUT_DARK.ordinal -> FourCutFrame( - designColorScheme = mediumContrastDarkColorScheme, - cameraImageUrlList = imageUri, - position = position - ) - else -> TODO() - } -} - -@Preview(showBackground = true) -@Composable -private fun DefaultFrame() { - FourCutTogetherTheme { - GenerateTwoRowImageScreen( - navigateToShare = {}, - popBackStack = {} - ) - } -} \ No newline at end of file diff --git a/presenter/src/main/java/com/foke/together/presenter/screen/HomeScreen.kt b/presenter/src/main/java/com/foke/together/presenter/screen/HomeScreen.kt index e9218b3..3320e4e 100644 --- a/presenter/src/main/java/com/foke/together/presenter/screen/HomeScreen.kt +++ b/presenter/src/main/java/com/foke/together/presenter/screen/HomeScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -28,6 +29,11 @@ fun HomeScreen( popBackStack: () -> Unit, viewModel: HomeViewModel = hiltViewModel() ) { + DisposableEffect(Unit) { + viewModel.createSession() + onDispose { } + } + FourCutTogetherTheme() { ConstraintLayout( modifier = Modifier.fillMaxSize() 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 401236e..042191d 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,11 +1,14 @@ package com.foke.together.presenter.screen -import androidx.compose.foundation.Image +import android.net.Uri import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager @@ -14,15 +17,19 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -31,8 +38,9 @@ import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import androidx.hilt.navigation.compose.hiltViewModel -import com.foke.together.domain.interactor.entity.CutFrameType +import com.foke.together.domain.interactor.entity.DefaultCutFrameSet import com.foke.together.presenter.R +import com.foke.together.presenter.frame.DefaultCutFrame import com.foke.together.presenter.theme.FourCutTogetherTheme import com.foke.together.presenter.viewmodel.SelectFrameViewModel @@ -42,16 +50,32 @@ fun SelectFrameScreen( popBackStack: () -> Unit, viewModel: SelectFrameViewModel = hiltViewModel() ) { + val cutFrames = remember { mutableStateOf>(emptyList()) } + val isDateDisplay = remember { mutableStateOf(false) } + + DisposableEffect(Unit) { + viewModel.updateSessionStatus() + + // get frames + cutFrames.value = DefaultCutFrameSet::class.sealedSubclasses.mapNotNull { classes -> + val cutFrame = classes.objectInstance + cutFrame?.isDateString = isDateDisplay.value + cutFrame + }.sortedBy { it.index } + + onDispose { } + } + FourCutTogetherTheme { val pagerState = rememberPagerState( - initialPage = CutFrameType.MAKER_FAIRE.ordinal + initialPage = 0 ) { - CutFrameType.entries.size // 총 페이지 수 설정 + cutFrames.value.size // 총 페이지 수 설정 } ConstraintLayout( modifier = Modifier.fillMaxSize() ) { - val (backKey, title, pager, frameSelectButton) = createRefs() + val (backKey, title, pager, isDateDisplayCheckBox, frameSelectButton) = createRefs() val topGuideLine = createGuidelineFromTop(0.1f) val bottomGuideLine = createGuidelineFromBottom(0.1f) val startGuideLine = createGuidelineFromStart(0.1f) @@ -90,71 +114,104 @@ fun SelectFrameScreen( HorizontalPager( modifier = Modifier + .padding(top = 30.dp, bottom = 30.dp) .constrainAs(pager) { top.linkTo(title.bottom) start.linkTo(startGuideLine) end.linkTo(endGuideLine) - bottom.linkTo(frameSelectButton.top) + bottom.linkTo(isDateDisplayCheckBox.top) width = Dimension.wrapContent - height = Dimension.fillToConstraints - } - .aspectRatio(0.5f), + height = Dimension.wrapContent + }, verticalAlignment = Alignment.Top, state = pagerState, - pageSize = PageSize.Fill, + pageSize = PageSize.Fixed(250.dp), contentPadding = PaddingValues( - start = 40.dp, - end = 40.dp + start = 120.dp, + end = 120.dp, ) ) { page -> - when(page){ - CutFrameType.MAKER_FAIRE.ordinal -> { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .clickable { - viewModel.setCutFrameType(pagerState.currentPage) - navigateToMethod() - } - ) { Image(painter = painterResource(id = R.drawable.maker_faire_frame), contentDescription = "maker_faire_frame") } + Column ( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .clickable { + viewModel.setCutFrameType(cutFrames.value[page]) + navigateToMethod() + } + ) { + DefaultCutFrame( + cutFrames.value[page], + listOf( + // !!!!! TODO: empty 혹은 다른 이미지로 교체 + Uri.parse("file:///android_asset/sample_cut.png"), + Uri.parse("file:///android_asset/sample_cut.png"), + Uri.parse("file:///android_asset/sample_cut.png"), + Uri.parse("file:///android_asset/sample_cut.png"), + ), + + ) } - CutFrameType.FOURCUT_LIGHT.ordinal -> { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .clickable { - viewModel.setCutFrameType(pagerState.currentPage) - navigateToMethod() - } + Text( + text = cutFrames.value[page].frameTitle, + fontWeight = FontWeight.Bold, + color = Color(200,200,200), + fontSize = 24.sp, + modifier = Modifier.padding(top = 16.dp) + ) + } + } - ) { Image(painter = painterResource(id = R.drawable.fourcut_frame_medium_light), contentDescription = "fourcut_frame_medium_light") } + Row ( + modifier = Modifier + .constrainAs(isDateDisplayCheckBox) { + top.linkTo(pager.bottom) + start.linkTo(startGuideLine) + end.linkTo(endGuideLine) + bottom.linkTo(frameSelectButton.top) + width = Dimension.wrapContent + height = Dimension.wrapContent } - CutFrameType.FOURCUT_DARK.ordinal -> { - Box( - modifier = Modifier - .clickable { - viewModel.setCutFrameType(pagerState.currentPage) - navigateToMethod() - }, - contentAlignment = Alignment.Center - ) { Image(painter = painterResource(id = R.drawable.fourcut_frame_medium_dark), contentDescription = "fourcut_frame_medium_dark") } + .padding(bottom = 30.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = isDateDisplay.value, + onCheckedChange = { isChecked -> + isDateDisplay.value = isChecked + cutFrames.value = cutFrames.value.map { + it.isDateString = isChecked + it + } } - } + ) + Text( + text = "날짜 표시하기", + fontWeight = FontWeight.Bold, + color = Color.Gray, + fontSize = 16.sp, + modifier = Modifier.padding(start = 10.dp) + ) } Button( onClick = { - viewModel.setCutFrameType(pagerState.currentPage) + viewModel.setCutFrameType(cutFrames.value[pagerState.currentPage]) navigateToMethod() }, - modifier = Modifier.constrainAs(frameSelectButton) { - top.linkTo(pager.bottom) - start.linkTo(startGuideLine) - end.linkTo(endGuideLine) - bottom.linkTo(bottomGuideLine) - height = Dimension.wrapContent - width = Dimension.wrapContent - }.width(200.dp), + modifier = Modifier + .constrainAs(frameSelectButton) { + top.linkTo(isDateDisplayCheckBox.bottom) + start.linkTo(startGuideLine) + end.linkTo(endGuideLine) + bottom.linkTo(bottomGuideLine) + height = Dimension.wrapContent + width = Dimension.wrapContent + } + .width(200.dp), ) { Text( text = stringResource(id = R.string.select_frame_button_next), @@ -170,7 +227,7 @@ fun SelectFrameScreen( @Preview(showBackground = true) @Composable private fun DefaultPreview() { - FourCutTogetherTheme() { + FourCutTogetherTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background diff --git a/presenter/src/main/java/com/foke/together/presenter/screen/SelectMethodScreen.kt b/presenter/src/main/java/com/foke/together/presenter/screen/SelectMethodScreen.kt index fe58645..05b803c 100644 --- a/presenter/src/main/java/com/foke/together/presenter/screen/SelectMethodScreen.kt +++ b/presenter/src/main/java/com/foke/together/presenter/screen/SelectMethodScreen.kt @@ -42,9 +42,7 @@ fun SelectMethodScreen( modifier = Modifier.fillMaxSize() ) { val context = LocalContext.current - val (backKey, title, timerButton, gestureButton) = createRefs() - val topGuideLine = createGuidelineFromTop(0.1f) val bottomGuideLine = createGuidelineFromBottom(0.1f) val startGuideLine = createGuidelineFromStart(0.2f) 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 0cea2ac..1d80abd 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 @@ -1,10 +1,14 @@ package com.foke.together.presenter.screen +import android.content.Context +import android.widget.Toast import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Print import androidx.compose.material.icons.filled.Share @@ -13,10 +17,12 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import androidx.core.content.FileProvider @@ -26,6 +32,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.AsyncImage import coil.request.ImageRequest import com.foke.together.presenter.viewmodel.ShareViewModel +import com.foke.together.util.AppLog import com.foke.together.util.ImageFileUtil @Composable @@ -34,6 +41,12 @@ fun ShareScreen( viewModel: ShareViewModel = hiltViewModel() ) { val context = LocalContext.current + + DisposableEffect(Unit) { + viewModel.updateSessionStatus() + onDispose { } + } + ConstraintLayout( modifier = Modifier.fillMaxSize() ) { @@ -44,6 +57,11 @@ fun ShareScreen( val startGuideLine = createGuidelineFromStart(0.1f) val endGuideLine = createGuidelineFromEnd(0.1f) + DisposableEffect(Unit) { + saveToLocal(context, viewModel) + onDispose { } + } + // TODO: need check to change single ImageView AsyncImage( model = ImageRequest.Builder(context) @@ -73,10 +91,14 @@ fun ShareScreen( }, horizontalAlignment = Alignment.CenterHorizontally ) { - IconButton( - onClick = { popBackStack() }, - modifier = Modifier.weight(1f) + onClick = { + viewModel.closeSession() + popBackStack() + }, + modifier = Modifier + .width(70.dp) + .weight(1f) ) { Icon( modifier = Modifier.fillMaxSize(), @@ -88,9 +110,12 @@ fun ShareScreen( IconButton( onClick = { - ImageFileUtil.printFromUri(context,viewModel.twoImageUri) + AppLog.e("", "", "asdf: ${viewModel.twoImageUri}") + ImageFileUtil.printFromUri(context, viewModel.twoImageUri) }, - modifier = Modifier.weight(1f) + modifier = Modifier + .width(70.dp) + .weight(1f) ) { Icon( modifier = Modifier.fillMaxSize(), @@ -109,7 +134,9 @@ fun ShareScreen( ) ImageFileUtil.shareUri(context, contentUri) }, - modifier = Modifier.weight(1f) + modifier = Modifier + .width(70.dp) + .weight(1f) ) { Icon( modifier = Modifier.fillMaxSize(), @@ -119,6 +146,23 @@ fun ShareScreen( ) } + // TODO: add android native share button + IconButton( + onClick = { + saveToLocal(context, viewModel) + }, + modifier = Modifier + .width(70.dp) + .weight(1f) + ) { + Icon( + modifier = Modifier.fillMaxSize(), + imageVector = Icons.Filled.Download, + contentDescription = "Download", + tint = MaterialTheme.colorScheme.primary + ) + } + Box( modifier = Modifier.weight(1f) ){ @@ -130,25 +174,20 @@ fun ShareScreen( modifier = Modifier.fillMaxSize() ) } - -// TODO: add android native share button -// IconButton( -// onClick = { -// viewModel.downloadImage() -// }, -// modifier = Modifier.weight(1f) -// ) { -// Icon( -// modifier = Modifier.fillMaxSize(), -// imageVector = Icons.Filled.Download, -// contentDescription = "Download", -// tint = MaterialTheme.colorScheme.primary -// ) -// } } } } +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) @Composable private fun DefaultPreview() { 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 b67edda..ad177bd 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 @@ -5,14 +5,15 @@ import android.os.CountDownTimer import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.foke.together.domain.interactor.CaptureWithExternalCameraUseCase -import com.foke.together.domain.interactor.GeneratePhotoFrameUseCase +import com.foke.together.domain.interactor.GenerateImageFromViewUseCase +import com.foke.together.domain.interactor.GeneratePhotoFrameUseCaseV1 import com.foke.together.domain.interactor.GetExternalCameraPreviewUrlUseCase -import com.foke.together.domain.interactor.web.SessionKeyUseCase +import com.foke.together.domain.interactor.entity.Status +import com.foke.together.domain.interactor.session.UpdateSessionStatusUseCase import com.foke.together.util.AppPolicy import com.foke.together.util.AppPolicy.CAPTURE_INTERVAL import com.foke.together.util.AppPolicy.COUNTDOWN_INTERVAL @@ -27,8 +28,9 @@ class CameraViewModel @Inject constructor( @ApplicationContext private val context: Context, getExternalCameraPreviewUrlUseCase: GetExternalCameraPreviewUrlUseCase, private val captureWithExternalCameraUseCase: CaptureWithExternalCameraUseCase, - private val generatePhotoFrameUseCase: GeneratePhotoFrameUseCase, - private val sessionKeyUseCase: SessionKeyUseCase + private val generateImageFromViewUseCase: GenerateImageFromViewUseCase, + private val generatePhotoFrameUseCaseV1: GeneratePhotoFrameUseCaseV1, + private val updateSessionStatusUseCase: UpdateSessionStatusUseCase ): ViewModel() { val externalCameraIP = getExternalCameraPreviewUrlUseCase() @@ -39,12 +41,18 @@ class CameraViewModel @Inject constructor( val captureCount: Int by _captureCount private var captureTimer: CountDownTimer? = null private var mTimerState = false + fun setCaptureTimer( graphicsLayer: GraphicsLayer, nextNavigate: () -> Unit ) { + if (AppPolicy.isNoCameraDebugMode) { + nextNavigate() + return + } + viewModelScope.launch { - generatePhotoFrameUseCase.clearCapturedImageList() + generatePhotoFrameUseCaseV1.clearCapturedImageList() } captureTimer = object : CountDownTimer(CAPTURE_INTERVAL, COUNTDOWN_INTERVAL) { override fun onTick(millisUntilFinished: Long) { @@ -53,13 +61,12 @@ class CameraViewModel @Inject constructor( override fun onFinish() { viewModelScope.launch { SoundUtil.getCameraSound(context = context ) - val bitmap = graphicsLayer.toImageBitmap().asAndroidBitmap() // TODO: 현재 External 실패 시, 스크린 캡쳐 화면을 사용하도록 구성함 val fileName = "${AppPolicy.CAPTURED_FOUR_CUT_IMAGE_NAME}_${_captureCount.value}" captureWithExternalCameraUseCase(fileName) .onFailure { - generatePhotoFrameUseCase.saveGraphicsLayerImage(bitmap, fileName) + generateImageFromViewUseCase(graphicsLayer, filename = fileName) } _progressState.floatValue = 1f @@ -67,7 +74,6 @@ class CameraViewModel @Inject constructor( _captureCount.intValue += 1 mTimerState = false } else { - sessionKeyUseCase.setSessionKey() stopCaptureTimer() _captureCount.intValue = 1 nextNavigate() @@ -92,4 +98,8 @@ class CameraViewModel @Inject constructor( captureTimer!!.cancel() } } + + fun updateSessionStatus() { + updateSessionStatusUseCase(Status.CAPTURE) + } } \ No newline at end of file diff --git a/presenter/src/main/java/com/foke/together/presenter/viewmodel/GenerateImageViewModel.kt b/presenter/src/main/java/com/foke/together/presenter/viewmodel/GenerateImageViewModel.kt new file mode 100644 index 0000000..ba69db4 --- /dev/null +++ b/presenter/src/main/java/com/foke/together/presenter/viewmodel/GenerateImageViewModel.kt @@ -0,0 +1,35 @@ +package com.foke.together.presenter.viewmodel + +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.lifecycle.ViewModel +import com.foke.together.domain.interactor.GenerateImageFromViewUseCase +import com.foke.together.domain.interactor.GeneratePhotoFrameUseCaseV1 +import com.foke.together.domain.interactor.entity.CutFrame +import com.foke.together.domain.interactor.session.GetCurrentSessionUseCase +import com.foke.together.util.AppLog +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class GenerateImageViewModel @Inject constructor( + getCurrentSessionUseCase: GetCurrentSessionUseCase, + private val generateImageFromViewUseCase: GenerateImageFromViewUseCase, + generatePhotoFrameUseCaseV1: GeneratePhotoFrameUseCaseV1, +): ViewModel() { + val cutFrame: CutFrame = getCurrentSessionUseCase()?.cutFrame ?: run { throw Exception("invalid cut frame") } + val imageUri = generatePhotoFrameUseCaseV1.getCapturedImageListUri() + + suspend fun generateImage(graphicsLayer: GraphicsLayer) { + val finalCachedImageUri = generateImageFromViewUseCase(graphicsLayer, isCutFrameForPrint = false) + AppLog.d(TAG, "generateImage" ,"finalCachedImageUri: $finalCachedImageUri") + } + + suspend fun generateImageForPrint(graphicsLayer: GraphicsLayer) { + val finalExternalImageUri = generateImageFromViewUseCase(graphicsLayer, isCutFrameForPrint = true) + AppLog.d(TAG, "generateImageForPrint" ,"finalExternalImageUri: $finalExternalImageUri") + } + + companion object { + private val TAG = GenerateImageViewModel::class.java.simpleName + } +} \ No newline at end of file diff --git a/presenter/src/main/java/com/foke/together/presenter/viewmodel/GenerateSingleRowImageViewModel.kt b/presenter/src/main/java/com/foke/together/presenter/viewmodel/GenerateSingleRowImageViewModel.kt deleted file mode 100644 index beaf3e9..0000000 --- a/presenter/src/main/java/com/foke/together/presenter/viewmodel/GenerateSingleRowImageViewModel.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.foke.together.presenter.viewmodel - -import androidx.compose.ui.graphics.asAndroidBitmap -import androidx.compose.ui.graphics.layer.GraphicsLayer -import androidx.lifecycle.ViewModel -import com.foke.together.domain.interactor.GeneratePhotoFrameUseCase -import com.foke.together.domain.interactor.entity.CutFrameType -import com.foke.together.util.AppLog -import com.foke.together.util.AppPolicy -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject - -@HiltViewModel -class GenerateSingleRowImageViewModel @Inject constructor( - private val generatePhotoFrameUseCase: GeneratePhotoFrameUseCase, -): ViewModel() { - val cutFrameType: CutFrameType = generatePhotoFrameUseCase.getCutFrameType() - val imageUri = generatePhotoFrameUseCase.getCapturedImageListUri() - - suspend fun generateImage(graphicsLayer: GraphicsLayer) { - val bitmap = graphicsLayer.toImageBitmap().asAndroidBitmap() - val finalCachedImageUri = generatePhotoFrameUseCase.saveGraphicsLayerImage(bitmap, AppPolicy.SINGLE_ROW_FINAL_IMAGE_NAME) - AppLog.d("GenerateImageViewModel", "generateTwoRowImage" ,"twoRow: $finalCachedImageUri") - } -} \ No newline at end of file diff --git a/presenter/src/main/java/com/foke/together/presenter/viewmodel/GenerateTwoRowImageViewModel.kt b/presenter/src/main/java/com/foke/together/presenter/viewmodel/GenerateTwoRowImageViewModel.kt deleted file mode 100644 index 47850f1..0000000 --- a/presenter/src/main/java/com/foke/together/presenter/viewmodel/GenerateTwoRowImageViewModel.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.foke.together.presenter.viewmodel - -import android.content.Context -import androidx.compose.ui.graphics.asAndroidBitmap -import androidx.compose.ui.graphics.layer.GraphicsLayer -import androidx.lifecycle.ViewModel -import com.foke.together.domain.interactor.GeneratePhotoFrameUseCase -import com.foke.together.domain.interactor.entity.CutFrameType -import com.foke.together.util.AppLog -import com.foke.together.util.AppPolicy -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject - -@HiltViewModel -class GenerateTwoRowImageViewModel @Inject constructor( - @ApplicationContext private val context: Context, - private val generatePhotoFrameUseCase: GeneratePhotoFrameUseCase -): ViewModel() { - val cutFrameType: CutFrameType = generatePhotoFrameUseCase.getCutFrameType() - val imageUri = generatePhotoFrameUseCase.getCapturedImageListUri() - - suspend fun generateImage(graphicsLayer: GraphicsLayer) { - val bitmap = graphicsLayer.toImageBitmap().asAndroidBitmap() - val finalExternalImageUri = generatePhotoFrameUseCase.saveGraphicsLayerImage(bitmap, AppPolicy.TWO_ROW_FINAL_IMAGE_NAME) - AppLog.d("GenerateImageViewModel", "generateTwoRowImage" ,"twoRow: $finalExternalImageUri") - } -} \ No newline at end of file diff --git a/presenter/src/main/java/com/foke/together/presenter/viewmodel/HomeViewModel.kt b/presenter/src/main/java/com/foke/together/presenter/viewmodel/HomeViewModel.kt index d561952..5bb70bc 100644 --- a/presenter/src/main/java/com/foke/together/presenter/viewmodel/HomeViewModel.kt +++ b/presenter/src/main/java/com/foke/together/presenter/viewmodel/HomeViewModel.kt @@ -3,6 +3,7 @@ package com.foke.together.presenter.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.foke.together.domain.interactor.AppInitUseCase +import com.foke.together.domain.interactor.session.CreateNewSessionUseCase import com.foke.together.domain.interactor.web.GetCurrentUserInformationUseCase import com.foke.together.domain.interactor.web.SignInUseCase import com.foke.together.util.AppLog @@ -17,7 +18,8 @@ class HomeViewModel @Inject constructor( @IODispatcher private val ioDispatcher: CoroutineDispatcher, private val signInUseCase: SignInUseCase, private val getCurrentUserInformationUseCase: GetCurrentUserInformationUseCase, - private val appInitUseCase: AppInitUseCase + private val appInitUseCase: AppInitUseCase, + private val createNewSessionUseCase: CreateNewSessionUseCase ): ViewModel() { init { viewModelScope.launch(ioDispatcher) { @@ -41,6 +43,10 @@ class HomeViewModel @Inject constructor( } } + fun createSession() { + createNewSessionUseCase() + } + companion object { private val TAG = HomeViewModel::class.java.simpleName } 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 f3bca66..a585926 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 @@ -2,16 +2,23 @@ package com.foke.together.presenter.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.foke.together.domain.interactor.GeneratePhotoFrameUseCase +import com.foke.together.domain.interactor.entity.CutFrame +import com.foke.together.domain.interactor.entity.Status +import com.foke.together.domain.interactor.session.UpdateSessionStatusUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class SelectFrameViewModel @Inject constructor( - private val generatePhotoFrameUseCase: GeneratePhotoFrameUseCase + private val updateSessionStatusUseCase: UpdateSessionStatusUseCase ): ViewModel() { - fun setCutFrameType(type:Int) = viewModelScope.launch { - generatePhotoFrameUseCase.setCutFrameType(type) + + fun updateSessionStatus() { + updateSessionStatusUseCase(Status.SELECT_FRAME) + } + + fun setCutFrameType(cutFrame: CutFrame) = viewModelScope.launch { + updateSessionStatusUseCase(cutFrame) } } \ No newline at end of file diff --git a/presenter/src/main/java/com/foke/together/presenter/viewmodel/ShareViewModel.kt b/presenter/src/main/java/com/foke/together/presenter/viewmodel/ShareViewModel.kt index 833bb46..9b0029f 100644 --- a/presenter/src/main/java/com/foke/together/presenter/viewmodel/ShareViewModel.kt +++ b/presenter/src/main/java/com/foke/together/presenter/viewmodel/ShareViewModel.kt @@ -10,10 +10,13 @@ import androidx.core.content.FileProvider import androidx.core.net.toFile import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.foke.together.domain.interactor.GeneratePhotoFrameUseCase +import com.foke.together.domain.interactor.GeneratePhotoFrameUseCaseV1 import com.foke.together.domain.interactor.GetQRCodeUseCase +import com.foke.together.domain.interactor.entity.Status +import com.foke.together.domain.interactor.session.ClearSessionUseCase +import com.foke.together.domain.interactor.session.GetCurrentSessionUseCase +import com.foke.together.domain.interactor.session.UpdateSessionStatusUseCase import com.foke.together.domain.interactor.web.GetDownloadUrlUseCase -import com.foke.together.domain.interactor.web.SessionKeyUseCase import com.foke.together.domain.interactor.web.UploadFileUseCase import com.foke.together.util.AppLog import com.foke.together.util.AppPolicy @@ -28,13 +31,15 @@ class ShareViewModel @Inject constructor( @ApplicationContext private val context: Context, private val getQRCodeUseCase: GetQRCodeUseCase, private val getDownloadUrlUseCase: GetDownloadUrlUseCase, - private val sessionKeyUseCase: SessionKeyUseCase, private val uploadFileUseCase: UploadFileUseCase, - private val generatePhotoFrameUseCase: GeneratePhotoFrameUseCase + private val generatePhotoFrameUseCaseV1: GeneratePhotoFrameUseCaseV1, + private val getCurrentSessionUseCase: GetCurrentSessionUseCase, + private val updateSessionStatusUseCase: UpdateSessionStatusUseCase, + private val clearSessionUseCase: ClearSessionUseCase ): ViewModel() { var qrCodeBitmap by mutableStateOf(null) - val singleImageUri: Uri = generatePhotoFrameUseCase.getFinalSingleImageUri() - val twoImageUri: Uri = generatePhotoFrameUseCase.getFinalTwoImageUri() + val singleImageUri: Uri = generatePhotoFrameUseCaseV1.getFinalSingleImageUri() + val twoImageUri: Uri = generatePhotoFrameUseCaseV1.getFinalTwoImageUri() init { viewModelScope.launch { @@ -42,28 +47,36 @@ class ShareViewModel @Inject constructor( } } - fun downloadImage() { - val imageBitmap = ImageFileUtil.getBitmapFromUri(context, singleImageUri) - viewModelScope.launch { - ImageFileUtil.saveBitmapToStorage( - context, - imageBitmap, - AppPolicy.SINGLE_ROW_FINAL_IMAGE_NAME - ) + fun downloadImage(): Result { + return getCurrentSessionUseCase()?.let { session -> + val imageBitmap = ImageFileUtil.getBitmapFromUri(context, singleImageUri) + viewModelScope.launch { + ImageFileUtil.saveBitmapToStorage( + context, + imageBitmap, + session.sessionId.toString() + ) + } + Result.success(Unit) + } ?: run { + Result.failure(Exception("invalid session id")) } } private suspend fun generateQRcode() { - val result = uploadFileUseCase(sessionKeyUseCase.getSessionKey(), singleImageUri.toFile()) - AppLog.d("GenerateImageViewModel", "UploadFile" ,"result: $result") - val sessionKey = sessionKeyUseCase.getSessionKey() - val downloadUrl: String = getDownloadUrlUseCase(sessionKey).getOrElse { "https://4cuts.store" } + getCurrentSessionUseCase()?.let { session -> + val sessionKey = session.sessionId.toString() + + val result = uploadFileUseCase(sessionKey, singleImageUri.toFile()) + AppLog.d(TAG, "generateQRcode" ,"result: $result") - if (AppPolicy.isDebugMode) { - AppLog.e(TAG, "generateQRcode", "sessionKey: $sessionKey") - AppLog.e(TAG, "generateQRcode", "downloadUrl: $downloadUrl") + val downloadUrl: String = getDownloadUrlUseCase(sessionKey).getOrElse { "https://4cuts.store" } + if (AppPolicy.isDebugMode) { + AppLog.e(TAG, "generateQRcode", "sessionKey: $sessionKey") + AppLog.e(TAG, "generateQRcode", "downloadUrl: $downloadUrl") + } + qrCodeBitmap = getQRCodeUseCase(sessionKey, downloadUrl).getOrNull() } - qrCodeBitmap = getQRCodeUseCase(sessionKey, downloadUrl).getOrNull() } fun shareImage() { @@ -79,6 +92,14 @@ class ShareViewModel @Inject constructor( ImageFileUtil.printFromUri(activityContext,twoImageUri) } + fun updateSessionStatus() { + updateSessionStatusUseCase(Status.SHARE) + } + + fun closeSession() { + clearSessionUseCase() + } + companion object { private val TAG = ShareViewModel::class.java.simpleName } diff --git a/presenter/src/main/res/drawable/ic_launcher_background.xml b/presenter/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/presenter/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/presenter/src/main/res/drawable/ic_launcher_foreground.xml b/presenter/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/presenter/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file 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 3607233..60c061a 100644 --- a/util/src/main/java/com/foke/together/util/AppPolicy.kt +++ b/util/src/main/java/com/foke/together/util/AppPolicy.kt @@ -1,7 +1,8 @@ package com.foke.together.util object AppPolicy { - const val isDebugMode = false + const val isDebugMode = true // TODO: change to false + const val isNoCameraDebugMode = false // TODO: change to false // network const val WEB_SERVER_URL = "https://4cuts.store/" diff --git a/util/src/main/java/com/foke/together/util/TimeUtil.kt b/util/src/main/java/com/foke/together/util/TimeUtil.kt index 3d579bc..4f272e3 100644 --- a/util/src/main/java/com/foke/together/util/TimeUtil.kt +++ b/util/src/main/java/com/foke/together/util/TimeUtil.kt @@ -4,6 +4,11 @@ import java.text.SimpleDateFormat import java.util.Date object TimeUtil { + fun getCurrentTimestamp(): Long { + // TODO: LocalDate로 변경 + return System.currentTimeMillis() + } + fun getCurrentDisplayTime(): String { val dateFormat = "yyyy.MM.dd" // TODO: LocalDate로 변경