@@ -2,14 +2,21 @@ package com.ninecraft.booket.feature.record.ocr
22
33import android.net.Uri
44import androidx.compose.runtime.Composable
5+ import androidx.compose.runtime.LaunchedEffect
56import androidx.compose.runtime.getValue
7+ import androidx.compose.runtime.mutableIntStateOf
68import androidx.compose.runtime.mutableStateOf
79import androidx.compose.runtime.rememberCoroutineScope
810import androidx.compose.runtime.setValue
11+ import androidx.core.net.toUri
912import com.ninecraft.booket.core.common.analytics.AnalyticsHelper
13+ import com.ninecraft.booket.core.common.utils.UiText
1014import com.ninecraft.booket.core.common.utils.handleException
1115import com.ninecraft.booket.core.ocr.recognizer.CloudOcrRecognizer
16+ import com.ninecraft.booket.feature.record.R
17+ import com.ninecraft.booket.feature.record.ocr.OcrSideEffect.ShowToast
1218import com.ninecraft.booket.feature.screens.OcrScreen
19+ import com.ninecraft.booket.feature.screens.OcrScreen.OcrResult
1320import com.orhanobut.logger.Logger
1421import com.slack.circuit.codegen.annotations.CircuitInject
1522import com.slack.circuit.retained.rememberRetained
@@ -24,6 +31,7 @@ import kotlinx.collections.immutable.persistentListOf
2431import kotlinx.collections.immutable.persistentSetOf
2532import kotlinx.collections.immutable.toPersistentList
2633import kotlinx.collections.immutable.toPersistentSet
34+ import kotlinx.coroutines.delay
2735import kotlinx.coroutines.launch
2836
2937@AssistedInject
@@ -41,22 +49,65 @@ class OcrPresenter(
4149
4250 companion object {
4351 private const val RECORD_OCR_SENTENCE = " record_OCR_sentence"
52+ private const val CAMERA_MAX_FAILURES = 2
4453 }
4554
4655 @Composable
4756 override fun present (): OcrUiState {
4857 val scope = rememberCoroutineScope()
58+ var isLoading by rememberRetained { mutableStateOf(false ) }
4959 var currentUi by rememberRetained { mutableStateOf(OcrUi .CAMERA ) }
5060 var isPermissionDialogVisible by rememberRetained { mutableStateOf(false ) }
61+ var selectedImage by rememberRetained { mutableStateOf(" " ) }
5162 var sentenceList by rememberRetained { mutableStateOf(persistentListOf<String >()) }
5263 var selectedIndices by rememberRetained { mutableStateOf(persistentSetOf<Int >()) }
5364 var mergedSentence by rememberRetained { mutableStateOf(" " ) }
5465 var isTextDetectionFailed by rememberRetained { mutableStateOf(false ) }
66+ var isCameraRecognitionFailedDialogVisible by rememberRetained { mutableStateOf(false ) }
67+ var isGalleryRecognitionFailedDialogVisible by rememberRetained { mutableStateOf(false ) }
5568 var isRecaptureDialogVisible by rememberRetained { mutableStateOf(false ) }
56- var isLoading by rememberRetained { mutableStateOf( false ) }
69+ var cameraFailureCount by rememberRetained { mutableIntStateOf( 0 ) }
5770 var sideEffect by rememberRetained { mutableStateOf<OcrSideEffect ?>(null ) }
5871
59- fun recognizeText (imageUri : Uri ) {
72+ LaunchedEffect (isTextDetectionFailed) {
73+ if (isTextDetectionFailed) {
74+ delay(2000 )
75+ isTextDetectionFailed = false
76+ }
77+ }
78+
79+ fun handleRecognitionSuccess (text : String ) {
80+ isTextDetectionFailed = false
81+ cameraFailureCount = 0
82+
83+ val sentences = text
84+ .split(" \n " )
85+ .map { it.trim() }
86+ .filter { it.isNotEmpty() }
87+
88+ sentenceList = sentences.toPersistentList()
89+ currentUi = OcrUi .RESULT
90+ analyticsHelper.logScreenView(RECORD_OCR_SENTENCE )
91+ }
92+
93+ fun handleRecognitionFailure (source : RecognizeSource ) {
94+ when (source) {
95+ RecognizeSource .CAMERA -> {
96+ isTextDetectionFailed = true
97+ cameraFailureCount + = 1
98+
99+ if (cameraFailureCount > CAMERA_MAX_FAILURES ) {
100+ isCameraRecognitionFailedDialogVisible = true
101+ }
102+ }
103+
104+ RecognizeSource .GALLERY -> {
105+ isGalleryRecognitionFailedDialogVisible = true
106+ }
107+ }
108+ }
109+
110+ fun recognizeText (imageUri : Uri , source : RecognizeSource ) {
60111 scope.launch {
61112 try {
62113 isLoading = true
@@ -65,25 +116,17 @@ class OcrPresenter(
65116 val text = it.responses.firstOrNull()?.fullTextAnnotation?.text.orEmpty()
66117
67118 if (text.isNotBlank()) {
68- isTextDetectionFailed = false
69- val sentences = text
70- .split(" \n " )
71- .map { it.trim() }
72- .filter { it.isNotEmpty() }
73-
74- sentenceList = sentences.toPersistentList()
75- currentUi = OcrUi .RESULT
76- analyticsHelper.logScreenView(RECORD_OCR_SENTENCE )
119+ handleRecognitionSuccess(text)
77120 } else {
78- isTextDetectionFailed = true
121+ handleRecognitionFailure(source)
79122 }
80123 }
81124 .onFailure { exception ->
82- isTextDetectionFailed = true
125+ handleRecognitionFailure(source)
83126
84127 val handleErrorMessage = { message: String ->
85128 Logger .e(" Cloud Vision API Error: ${exception.message} " )
86- sideEffect = OcrSideEffect . ShowToast (message)
129+ sideEffect = ShowToast (UiText . DirectString ( message) )
87130 }
88131
89132 handleException(
@@ -118,14 +161,24 @@ class OcrPresenter(
118161
119162 is OcrUiEvent .OnCaptureFailed -> {
120163 isLoading = false
121- sideEffect = OcrSideEffect . ShowToast (" 이미지 캡처에 실패했어요 " )
164+ sideEffect = ShowToast (UiText . StringResource ( R .string.ocr_capture_failed) )
122165 Logger .e(" ImageCaptureException: ${event.exception.message} " )
123166 }
124167
125168 is OcrUiEvent .OnImageCaptured -> {
126169 isTextDetectionFailed = false
127170
128- recognizeText(event.imageUri)
171+ recognizeText(event.imageUri, RecognizeSource .CAMERA )
172+ }
173+
174+ is OcrUiEvent .OnImageSelected -> {
175+ currentUi = OcrUi .IMAGE
176+ selectedImage = event.imageUri
177+ isTextDetectionFailed = false
178+ cameraFailureCount = 0
179+
180+ val pareUri = selectedImage.toUri()
181+ recognizeText(pareUri, RecognizeSource .GALLERY )
129182 }
130183
131184 is OcrUiEvent .OnReCaptureButtonClick -> {
@@ -135,7 +188,7 @@ class OcrPresenter(
135188 is OcrUiEvent .OnSelectionConfirmed -> {
136189 mergedSentence = selectedIndices
137190 .sorted().joinToString(" " ) { sentenceList[it] }
138- navigator.pop(result = OcrScreen . OcrResult (mergedSentence))
191+ navigator.pop(result = OcrResult (mergedSentence))
139192 }
140193
141194 is OcrUiEvent .OnSentenceSelected -> {
@@ -155,6 +208,19 @@ class OcrPresenter(
155208 is OcrUiEvent .OnRecaptureDialogDismissed -> {
156209 isRecaptureDialogVisible = false
157210 }
211+
212+ OcrUiEvent .OnImageContentClosed -> {
213+ currentUi = OcrUi .CAMERA
214+ }
215+
216+ OcrUiEvent .OnCameraRecognitionFailedDialogDismissed -> {
217+ isCameraRecognitionFailedDialogVisible = false
218+ cameraFailureCount = 0
219+ }
220+
221+ OcrUiEvent .OnImageRecognitionFailedDialogDismissed -> {
222+ isGalleryRecognitionFailedDialogVisible = false
223+ }
158224 }
159225 }
160226
@@ -165,9 +231,12 @@ class OcrPresenter(
165231 return OcrUiState (
166232 currentUi = currentUi,
167233 isPermissionDialogVisible = isPermissionDialogVisible,
234+ selectedImage = selectedImage,
168235 sentenceList = sentenceList,
169236 selectedIndices = selectedIndices,
170237 isTextDetectionFailed = isTextDetectionFailed,
238+ isCameraRecognitionFailedDialogVisible = isCameraRecognitionFailedDialogVisible,
239+ isGalleryRecognitionFailedDialogVisible = isGalleryRecognitionFailedDialogVisible,
171240 isRecaptureDialogVisible = isRecaptureDialogVisible,
172241 isLoading = isLoading,
173242 sideEffect = sideEffect,
0 commit comments