From 6634b8d7bda89024507d240aacc712bab12c7cc7 Mon Sep 17 00:00:00 2001 From: MyungHyun Ryu Date: Sat, 28 Oct 2023 20:41:56 +0900 Subject: [PATCH 1/6] :sparkles: Implement TTS feature --- .../speechbuddy/compose/SpeechBuddyApp.kt | 7 + .../compose/landing/LandingScreen.kt | 6 +- .../texttospeach/TextToSpeachScreen.kt | 131 ++++++++++++++++++ .../ui/models/TextToSpeechUiState.kt | 6 + .../viewmodel/TextToSpeechViewModel.kt | 99 +++++++++++++ .../app/src/main/res/drawable/stop_icon.png | Bin 0 -> 3051 bytes frontend/app/src/main/res/values/strings.xml | 5 + 7 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeach/TextToSpeachScreen.kt create mode 100644 frontend/app/src/main/java/com/example/speechbuddy/ui/models/TextToSpeechUiState.kt create mode 100644 frontend/app/src/main/java/com/example/speechbuddy/viewmodel/TextToSpeechViewModel.kt create mode 100644 frontend/app/src/main/res/drawable/stop_icon.png diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyApp.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyApp.kt index 867da5c8..f406e95e 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyApp.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyApp.kt @@ -10,6 +10,7 @@ import com.example.speechbuddy.compose.login.LoginScreen import com.example.speechbuddy.compose.emailverification.EmailVerificationScreen import com.example.speechbuddy.compose.resetpassword.ResetPasswordScreen import com.example.speechbuddy.compose.signup.SignupScreen +import com.example.speechbuddy.compose.texttospeach.TextToSpeechScreen @Composable fun SpeechBuddyApp() { @@ -29,6 +30,9 @@ fun SpeechBuddyNavHost( LandingScreen( onLoginClick = { navController.navigate("login") + }, + onGuestClick = { + navController.navigate("texttospeech") } ) } @@ -72,5 +76,8 @@ fun SpeechBuddyNavHost( onNextClick = {} ) } + composable("texttospeech"){ + TextToSpeechScreen() + } } } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/landing/LandingScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/landing/LandingScreen.kt index 900b676b..715c63a0 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/landing/LandingScreen.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/landing/LandingScreen.kt @@ -22,6 +22,7 @@ import com.example.speechbuddy.ui.SpeechBuddyTheme @Composable fun LandingScreen( modifier: Modifier = Modifier, + onGuestClick: () -> Unit, onLoginClick: () -> Unit, ) { Surface(modifier = modifier.fillMaxSize(), color = MaterialTheme.colorScheme.primaryContainer) { @@ -38,7 +39,8 @@ fun LandingScreen( ) { ButtonUi( text = stringResource(id = R.string.guess_mode_action), - onClick = { /*TODO*/ }) + onClick = onGuestClick + ) ButtonUi(text = stringResource(id = R.string.login_action), onClick = onLoginClick) } } @@ -49,6 +51,6 @@ fun LandingScreen( @Composable private fun LandingScreenPreview() { SpeechBuddyTheme { - LandingScreen(onLoginClick = {}) + LandingScreen(onLoginClick = {}, onGuestClick = {}) } } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeach/TextToSpeachScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeach/TextToSpeachScreen.kt new file mode 100644 index 00000000..a046cf48 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeach/TextToSpeachScreen.kt @@ -0,0 +1,131 @@ +package com.example.speechbuddy.compose.texttospeach + +import android.text.method.ScrollingMovementMethod +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.example.speechbuddy.R +import com.example.speechbuddy.compose.utils.TitleUi +import com.example.speechbuddy.viewmodel.TextToSpeechViewModel + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TextToSpeechScreen( + viewModel: TextToSpeechViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsState() + val context = LocalContext.current + + Surface( + modifier = Modifier + .fillMaxSize() + ) { + Column( + modifier = Modifier + .padding(25.dp) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + TitleUi( + title = stringResource(id = R.string.tts_text), + description = stringResource(id = R.string.tts_explain) + ) + + Spacer(modifier = Modifier.height(20.dp)) + + OutlinedTextField( + value = viewModel.textInput, + onValueChange = { + viewModel.setText(it) + }, + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 300.dp) + .verticalScroll(rememberScrollState()) + .height(300.dp), + textStyle = MaterialTheme.typography.bodyMedium, + shape = RoundedCornerShape(10.dp) + ) + + Spacer(modifier = Modifier.height(20.dp)) + + // Play button + Button( + onClick = { + viewModel.ttsStart(context) + }, + enabled = uiState.isPlayEnabled, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onBackground + ) + ) { + Text( + style = MaterialTheme.typography.headlineMedium, + text = stringResource(id = R.string.play_text) + ) + Icon( + Icons.Filled.PlayArrow, + contentDescription = stringResource(id = R.string.play_text), + modifier = Modifier.size(36.dp) + ) + } + + // Stop button + Button( + onClick = { + viewModel.ttsStop() + }, + enabled = uiState.isStopEnabled, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onBackground + ) + ) { + Text( + style = MaterialTheme.typography.headlineMedium, + text = stringResource(id = R.string.stop_text) + ) + Icon( + painterResource(R.drawable.stop_icon), + contentDescription = stringResource(id = R.string.pause_text), + modifier = Modifier.size(36.dp) + ) + } + } + + } + +} diff --git a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/TextToSpeechUiState.kt b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/TextToSpeechUiState.kt new file mode 100644 index 00000000..4e184a51 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/TextToSpeechUiState.kt @@ -0,0 +1,6 @@ +package com.example.speechbuddy.ui.models + +data class TextToSpeechUiState( + val isPlayEnabled:Boolean = true, + val isStopEnabled:Boolean = false, +) \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/TextToSpeechViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/TextToSpeechViewModel.kt new file mode 100644 index 00000000..3b2e1719 --- /dev/null +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/TextToSpeechViewModel.kt @@ -0,0 +1,99 @@ +package com.example.speechbuddy.viewmodel + +import android.content.Context +import android.os.Bundle +import android.speech.tts.TextToSpeech +import android.speech.tts.UtteranceProgressListener +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import com.example.speechbuddy.ui.models.TextToSpeechUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.util.Locale +import javax.inject.Inject + +@HiltViewModel +class TextToSpeechViewModel @Inject internal constructor( +) : ViewModel() { + + private val _state = MutableStateFlow(TextToSpeechUiState()) + val uiState: StateFlow = _state.asStateFlow() + + private var textToSpeech: TextToSpeech? = null + + var textInput by mutableStateOf("") + private set + + fun setText(input: String) { + textInput = input + } + + fun ttsStop(){ + textToSpeech?.stop() + } + + fun ttsStart(context: Context) { + // disable button + _state.value = uiState.value.copy( + isPlayEnabled = false, + isStopEnabled = true + ) + textToSpeech = TextToSpeech(context) { + if (it == TextToSpeech.SUCCESS) { + textToSpeech?.let { txtToSpeech -> + txtToSpeech.language = Locale.KOREAN + txtToSpeech.setSpeechRate(1.0f) + + val params = Bundle() + params.putString(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "") + txtToSpeech.setOnUtteranceProgressListener(object : UtteranceProgressListener() { + override fun onStart(utteranceId: String?) { + // Speech has started, button is already disabled + } + + override fun onStop(utteranceId: String?, interrupted: Boolean) { + _state.value = uiState.value.copy( + isPlayEnabled = true, + isStopEnabled = false + ) + } + + override fun onDone(utteranceId: String?) { + // Speech has finished, re-enable the button + _state.value = uiState.value.copy( + isPlayEnabled = true, + isStopEnabled = false + ) + } + + @Deprecated("Deprecated in Java") + override fun onError(utteranceId: String?) { + // There was an error, re-enable the button + _state.value = uiState.value.copy( + isPlayEnabled = true, + isStopEnabled = false + ) + } + }) + + txtToSpeech.speak( + textInput, + TextToSpeech.QUEUE_ADD, + params, + "UniqueID" + ) + } + } else { + // Initialization failed, re-enable the button + _state.value = uiState.value.copy( + isPlayEnabled = true + ) + } + } + } + +} \ No newline at end of file diff --git a/frontend/app/src/main/res/drawable/stop_icon.png b/frontend/app/src/main/res/drawable/stop_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2800abf6c5741bdabd5c3c0a7f2b36cf0c2afb7f GIT binary patch literal 3051 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn2Hfk$L90|U1(2s1Lwnj-;Z z%=L6}45^s&_HJa?+*Fa)hqfH1=2>Y8IqRls2>AxgX?o@+qT4YM2G zDqIn~@z-9_ob{<=#PWSnCqEysZuz@$(kiQTkX^$JyeLh4-MaR5^!wbqla>g-%|HCl z?M?pCR>myb&0k+$TOa-3gw0)BmZl7R2BTHe*KK$^d6vUCQtr6 zb2f|O-}S$LPv5^}>f8Ja+RSric0W%4H|d=F@AoV}jyhN<$DjMPc>0p+TEhbuW9NQ7 zuIH(~+vq@Gt@UgE*hzld>KP;pRDK`-z0R)4(`v0Zv&_+dJMXOKkD286dNE^P(*L5q ztL%E7%fCKmDC|G;>+!TD|FizL9B60nsH(drzg(2i=JZ*{dUOR^?|Bgzp z=8vAl_w+ZT!kcev>~lP=uKWvQbbJp{@-|;oAcesr>jXoC0kcDpGaEyTB*P>rBL;;t z3=^(QU}Q*SYgp38!@yz8pkkWB;4p(xVWlE7!yz69PhLp|fwWPrqro(qBt~=2XelvT zB92xZq*Ri~m9csH-$d_A3-fpcN^7slZv$51Ul|waL277QYlkb!(e>v+RsH2#_gA#D zb^NZpCci^v?lpd3xX닉네임을 입력해주세요 로그인 오류 회원가입 오류 + 소리로 말해보아요 + 키보드로 텍스트를 입력해주세요 + 재생하기 + 일시정지 + 정지 \ No newline at end of file From 12539f1b72f4fc30593aca148384e5cee725dec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A5=98=EB=AA=85=ED=98=84?= Date: Sat, 28 Oct 2023 21:32:42 +0900 Subject: [PATCH 2/6] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 이석찬 / Lee Sukchan --- .../java/com/example/speechbuddy/compose/SpeechBuddyApp.kt | 4 ++-- .../com/example/speechbuddy/compose/landing/LandingScreen.kt | 2 +- .../speechbuddy/compose/texttospeach/TextToSpeachScreen.kt | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyApp.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyApp.kt index f406e95e..385f4e42 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyApp.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyApp.kt @@ -32,7 +32,7 @@ fun SpeechBuddyNavHost( navController.navigate("login") }, onGuestClick = { - navController.navigate("texttospeech") + navController.navigate("text_to_speech") } ) } @@ -76,7 +76,7 @@ fun SpeechBuddyNavHost( onNextClick = {} ) } - composable("texttospeech"){ + composable("text_to_speech"){ TextToSpeechScreen() } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/landing/LandingScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/landing/LandingScreen.kt index 715c63a0..961ec29b 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/landing/LandingScreen.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/landing/LandingScreen.kt @@ -51,6 +51,6 @@ fun LandingScreen( @Composable private fun LandingScreenPreview() { SpeechBuddyTheme { - LandingScreen(onLoginClick = {}, onGuestClick = {}) + LandingScreen(onGuestClick = {}, onLoginClick = {}) } } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeach/TextToSpeachScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeach/TextToSpeachScreen.kt index a046cf48..7ce4e828 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeach/TextToSpeachScreen.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeach/TextToSpeachScreen.kt @@ -1,6 +1,5 @@ package com.example.speechbuddy.compose.texttospeach -import android.text.method.ScrollingMovementMethod import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -53,7 +52,7 @@ fun TextToSpeechScreen( ) { Column( modifier = Modifier - .padding(25.dp) + .padding(24.dp) .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, @@ -72,7 +71,6 @@ fun TextToSpeechScreen( }, modifier = Modifier .fillMaxWidth() - .defaultMinSize(minHeight = 300.dp) .verticalScroll(rememberScrollState()) .height(300.dp), textStyle = MaterialTheme.typography.bodyMedium, From 9bf07f348e7f52e1525c8c26b78ff4c394a281e4 Mon Sep 17 00:00:00 2001 From: MyungHyun Ryu Date: Sat, 28 Oct 2023 22:02:59 +0900 Subject: [PATCH 3/6] :art: Apply suggestions from code review --- .../speechbuddy/compose/SpeechBuddyApp.kt | 2 +- .../TextToSpeechScreen.kt} | 96 ++++++++++--------- .../ui/models/TextToSpeechUiState.kt | 10 +- .../viewmodel/TextToSpeechViewModel.kt | 56 ++++++----- 4 files changed, 90 insertions(+), 74 deletions(-) rename frontend/app/src/main/java/com/example/speechbuddy/compose/{texttospeach/TextToSpeachScreen.kt => texttospeech/TextToSpeechScreen.kt} (57%) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyApp.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyApp.kt index 385f4e42..e3d42bbe 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyApp.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyApp.kt @@ -10,7 +10,7 @@ import com.example.speechbuddy.compose.login.LoginScreen import com.example.speechbuddy.compose.emailverification.EmailVerificationScreen import com.example.speechbuddy.compose.resetpassword.ResetPasswordScreen import com.example.speechbuddy.compose.signup.SignupScreen -import com.example.speechbuddy.compose.texttospeach.TextToSpeechScreen +import com.example.speechbuddy.compose.texttospeech.TextToSpeechScreen @Composable fun SpeechBuddyApp() { diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeach/TextToSpeachScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeech/TextToSpeechScreen.kt similarity index 57% rename from frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeach/TextToSpeachScreen.kt rename to frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeech/TextToSpeechScreen.kt index 7ce4e828..adcc1d4a 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeach/TextToSpeachScreen.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeech/TextToSpeechScreen.kt @@ -1,9 +1,8 @@ -package com.example.speechbuddy.compose.texttospeach +package com.example.speechbuddy.compose.texttospeech import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -22,6 +21,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -35,6 +35,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.example.speechbuddy.R import com.example.speechbuddy.compose.utils.TitleUi +import com.example.speechbuddy.ui.models.ButtonStatusType import com.example.speechbuddy.viewmodel.TextToSpeechViewModel @@ -74,56 +75,57 @@ fun TextToSpeechScreen( .verticalScroll(rememberScrollState()) .height(300.dp), textStyle = MaterialTheme.typography.bodyMedium, - shape = RoundedCornerShape(10.dp) + shape = RoundedCornerShape(10.dp), + colors = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = MaterialTheme.colorScheme.tertiary, + unfocusedBorderColor = MaterialTheme.colorScheme.outline + ) ) Spacer(modifier = Modifier.height(20.dp)) - // Play button - Button( - onClick = { - viewModel.ttsStart(context) - }, - enabled = uiState.isPlayEnabled, - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.onBackground - ) - ) { - Text( - style = MaterialTheme.typography.headlineMedium, - text = stringResource(id = R.string.play_text) - ) - Icon( - Icons.Filled.PlayArrow, - contentDescription = stringResource(id = R.string.play_text), - modifier = Modifier.size(36.dp) - ) - } - - // Stop button - Button( - onClick = { - viewModel.ttsStop() - }, - enabled = uiState.isStopEnabled, - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.onBackground - ) - ) { - Text( - style = MaterialTheme.typography.headlineMedium, - text = stringResource(id = R.string.stop_text) - ) - Icon( - painterResource(R.drawable.stop_icon), - contentDescription = stringResource(id = R.string.pause_text), - modifier = Modifier.size(36.dp) - ) + // show PLAY/STOP depending on tts speaking + if (uiState.isButtonEnabled == ButtonStatusType.PLAY) { + Button( + onClick = { + viewModel.ttsStart(context) + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onBackground + ) + ) { + Text( + style = MaterialTheme.typography.headlineMedium, + text = stringResource(id = R.string.play_text) + ) + Icon( + Icons.Filled.PlayArrow, + contentDescription = stringResource(id = R.string.play_text), + modifier = Modifier.size(36.dp) + ) + } + } else { + Button( + onClick = { + viewModel.ttsStop() + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onBackground + ) + ) { + Text( + style = MaterialTheme.typography.headlineMedium, + text = stringResource(id = R.string.stop_text) + ) + Icon( + painterResource(R.drawable.stop_icon), + contentDescription = stringResource(id = R.string.pause_text), + modifier = Modifier.size(36.dp) + ) + } } } - } - } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/TextToSpeechUiState.kt b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/TextToSpeechUiState.kt index 4e184a51..d78ce989 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/TextToSpeechUiState.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/TextToSpeechUiState.kt @@ -1,6 +1,10 @@ package com.example.speechbuddy.ui.models data class TextToSpeechUiState( - val isPlayEnabled:Boolean = true, - val isStopEnabled:Boolean = false, -) \ No newline at end of file + val isButtonEnabled: ButtonStatusType = ButtonStatusType.PLAY +) + +enum class ButtonStatusType { + PLAY, + STOP +} \ No newline at end of file diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/TextToSpeechViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/TextToSpeechViewModel.kt index 3b2e1719..eff306b3 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/TextToSpeechViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/TextToSpeechViewModel.kt @@ -8,11 +8,13 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import com.example.speechbuddy.ui.models.ButtonStatusType import com.example.speechbuddy.ui.models.TextToSpeechUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import java.util.Locale import javax.inject.Inject @@ -20,8 +22,8 @@ import javax.inject.Inject class TextToSpeechViewModel @Inject internal constructor( ) : ViewModel() { - private val _state = MutableStateFlow(TextToSpeechUiState()) - val uiState: StateFlow = _state.asStateFlow() + private val _uiState = MutableStateFlow(TextToSpeechUiState()) + val uiState: StateFlow = _uiState.asStateFlow() private var textToSpeech: TextToSpeech? = null @@ -32,16 +34,18 @@ class TextToSpeechViewModel @Inject internal constructor( textInput = input } - fun ttsStop(){ + fun ttsStop() { textToSpeech?.stop() } fun ttsStart(context: Context) { // disable button - _state.value = uiState.value.copy( - isPlayEnabled = false, - isStopEnabled = true - ) + _uiState.update { currentState -> + currentState.copy( + isButtonEnabled = ButtonStatusType.STOP + ) + } + textToSpeech = TextToSpeech(context) { if (it == TextToSpeech.SUCCESS) { textToSpeech?.let { txtToSpeech -> @@ -50,33 +54,37 @@ class TextToSpeechViewModel @Inject internal constructor( val params = Bundle() params.putString(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "") - txtToSpeech.setOnUtteranceProgressListener(object : UtteranceProgressListener() { + txtToSpeech.setOnUtteranceProgressListener(object : + UtteranceProgressListener() { override fun onStart(utteranceId: String?) { // Speech has started, button is already disabled } override fun onStop(utteranceId: String?, interrupted: Boolean) { - _state.value = uiState.value.copy( - isPlayEnabled = true, - isStopEnabled = false - ) + _uiState.update { currentState -> + currentState.copy( + isButtonEnabled = ButtonStatusType.PLAY + ) + } } override fun onDone(utteranceId: String?) { // Speech has finished, re-enable the button - _state.value = uiState.value.copy( - isPlayEnabled = true, - isStopEnabled = false - ) + _uiState.update { currentState -> + currentState.copy( + isButtonEnabled = ButtonStatusType.PLAY + ) + } } @Deprecated("Deprecated in Java") override fun onError(utteranceId: String?) { // There was an error, re-enable the button - _state.value = uiState.value.copy( - isPlayEnabled = true, - isStopEnabled = false - ) + _uiState.update { currentState -> + currentState.copy( + isButtonEnabled = ButtonStatusType.PLAY + ) + } } }) @@ -89,9 +97,11 @@ class TextToSpeechViewModel @Inject internal constructor( } } else { // Initialization failed, re-enable the button - _state.value = uiState.value.copy( - isPlayEnabled = true - ) + _uiState.update { currentState -> + currentState.copy( + isButtonEnabled = ButtonStatusType.PLAY + ) + } } } } From c90c6e61221bd21bd5016bece8d3ce2dc4ad5084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A5=98=EB=AA=85=ED=98=84?= Date: Sun, 29 Oct 2023 17:21:35 +0900 Subject: [PATCH 4/6] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 이석찬 / Lee Sukchan --- .../texttospeech/TextToSpeechScreen.kt | 91 ++++++++++--------- .../ui/models/TextToSpeechUiState.kt | 2 +- .../viewmodel/TextToSpeechViewModel.kt | 15 ++- frontend/app/src/main/res/values/strings.xml | 1 - 4 files changed, 60 insertions(+), 49 deletions(-) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeech/TextToSpeechScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeech/TextToSpeechScreen.kt index adcc1d4a..6b741c8b 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeech/TextToSpeechScreen.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeech/TextToSpeechScreen.kt @@ -84,48 +84,55 @@ fun TextToSpeechScreen( Spacer(modifier = Modifier.height(20.dp)) - // show PLAY/STOP depending on tts speaking - if (uiState.isButtonEnabled == ButtonStatusType.PLAY) { - Button( - onClick = { - viewModel.ttsStart(context) - }, - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.onBackground - ) - ) { - Text( - style = MaterialTheme.typography.headlineMedium, - text = stringResource(id = R.string.play_text) - ) - Icon( - Icons.Filled.PlayArrow, - contentDescription = stringResource(id = R.string.play_text), - modifier = Modifier.size(36.dp) - ) - } - } else { - Button( - onClick = { - viewModel.ttsStop() - }, - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.onBackground - ) - ) { - Text( - style = MaterialTheme.typography.headlineMedium, - text = stringResource(id = R.string.stop_text) - ) - Icon( - painterResource(R.drawable.stop_icon), - contentDescription = stringResource(id = R.string.pause_text), - modifier = Modifier.size(36.dp) - ) - } - } + TextToSpeechButton( + buttonStatus = uiState.buttonStatus, + onPlay = { viewModel.ttsStart(context) }, + onStop = { viewModel.ttsStop() } + ) + } + } +} + +@Composable +private fun TextToSpeechButton( + buttonStatus: ButtonStatusType, + onPlay: () -> Unit, + onStop: () -> Unit +) { + val textToSpeechButtonColors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onBackground + ) + + when (buttonStatus) { + ButtonStatusType.PLAY -> Button( + onClick = onPlay, + colors = textToSpeechButtonColors + ) { + Text( + style = MaterialTheme.typography.headlineMedium, + text = stringResource(id = R.string.play_text) + ) + Icon( + Icons.Filled.PlayArrow, + contentDescription = stringResource(id = R.string.play_text), + modifier = Modifier.size(36.dp) + ) + } + + ButtonStatusType.STOP -> Button( + onClick = onStop, + colors = textToSpeechButtonColors + ) { + Text( + style = MaterialTheme.typography.headlineMedium, + text = stringResource(id = R.string.stop_text) + ) + Icon( + painterResource(R.drawable.stop_icon), + contentDescription = stringResource(id = R.string.stop_text), + modifier = Modifier.size(36.dp) + ) } } } diff --git a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/TextToSpeechUiState.kt b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/TextToSpeechUiState.kt index d78ce989..ec920cf2 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/ui/models/TextToSpeechUiState.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/ui/models/TextToSpeechUiState.kt @@ -1,7 +1,7 @@ package com.example.speechbuddy.ui.models data class TextToSpeechUiState( - val isButtonEnabled: ButtonStatusType = ButtonStatusType.PLAY + val buttonStatus: ButtonStatusType = ButtonStatusType.PLAY ) enum class ButtonStatusType { diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/TextToSpeechViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/TextToSpeechViewModel.kt index eff306b3..2807cd97 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/TextToSpeechViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/TextToSpeechViewModel.kt @@ -34,6 +34,10 @@ class TextToSpeechViewModel @Inject internal constructor( textInput = input } +private fun clearText() { + textInput = "" +} + fun ttsStop() { textToSpeech?.stop() } @@ -42,7 +46,7 @@ class TextToSpeechViewModel @Inject internal constructor( // disable button _uiState.update { currentState -> currentState.copy( - isButtonEnabled = ButtonStatusType.STOP + buttonStatus = ButtonStatusType.STOP ) } @@ -63,7 +67,7 @@ class TextToSpeechViewModel @Inject internal constructor( override fun onStop(utteranceId: String?, interrupted: Boolean) { _uiState.update { currentState -> currentState.copy( - isButtonEnabled = ButtonStatusType.PLAY + buttonStatus = ButtonStatusType.PLAY ) } } @@ -72,9 +76,10 @@ class TextToSpeechViewModel @Inject internal constructor( // Speech has finished, re-enable the button _uiState.update { currentState -> currentState.copy( - isButtonEnabled = ButtonStatusType.PLAY + buttonStatus = ButtonStatusType.PLAY ) } + clearText() } @Deprecated("Deprecated in Java") @@ -82,7 +87,7 @@ class TextToSpeechViewModel @Inject internal constructor( // There was an error, re-enable the button _uiState.update { currentState -> currentState.copy( - isButtonEnabled = ButtonStatusType.PLAY + buttonStatus = ButtonStatusType.PLAY ) } } @@ -99,7 +104,7 @@ class TextToSpeechViewModel @Inject internal constructor( // Initialization failed, re-enable the button _uiState.update { currentState -> currentState.copy( - isButtonEnabled = ButtonStatusType.PLAY + buttonStatus = ButtonStatusType.PLAY ) } } diff --git a/frontend/app/src/main/res/values/strings.xml b/frontend/app/src/main/res/values/strings.xml index 47fcb67a..ea272080 100644 --- a/frontend/app/src/main/res/values/strings.xml +++ b/frontend/app/src/main/res/values/strings.xml @@ -41,6 +41,5 @@ 소리로 말해보아요 키보드로 텍스트를 입력해주세요 재생하기 - 일시정지 정지 \ No newline at end of file From 7217eb83a2cb67eebf390061cb0703361cbd783c Mon Sep 17 00:00:00 2001 From: YeonJeong Kim Date: Sun, 29 Oct 2023 20:19:38 +0900 Subject: [PATCH 5/6] :art: Remove redundant input checking in LoginViewModel --- .../java/com/example/speechbuddy/viewmodel/LoginViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/LoginViewModel.kt b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/LoginViewModel.kt index 059b4cef..b6aec871 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/LoginViewModel.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/viewmodel/LoginViewModel.kt @@ -81,7 +81,7 @@ class LoginViewModel @Inject internal constructor( } fun login() { - if (!isValidEmail(emailInput) && emailInput.isEmpty()) { + if (!isValidEmail(emailInput)) { _uiState.update { currentState -> currentState.copy( isValidEmail = false, From 0b046bbdf56c930ebb1169ba39ed3193c3a1ab37 Mon Sep 17 00:00:00 2001 From: Sukchan Lee Date: Mon, 30 Oct 2023 00:28:36 +0900 Subject: [PATCH 6/6] :art: Refactor navigation in SpeechBuddyApp --- .../com/example/speechbuddy/compose/SpeechBuddyApp.kt | 9 ++++----- .../compose/texttospeech/TextToSpeechScreen.kt | 2 -- frontend/app/src/main/res/values/strings.xml | 3 --- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyApp.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyApp.kt index 4c6fa35e..3e5e4015 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyApp.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/SpeechBuddyApp.kt @@ -25,14 +25,14 @@ fun SpeechBuddyNavHost( navController: NavHostController ) { // val activity = (LocalContext.current as Activity) - NavHost(navController = navController, startDestination = "home") { + NavHost(navController = navController, startDestination = "landing") { composable("landing") { LandingScreen( + onGuestClick = { + navController.navigate("home") + }, onLoginClick = { navController.navigate("login") - }, - onGuestClick = { - navController.navigate("text_to_speech") } ) } @@ -74,7 +74,6 @@ fun SpeechBuddyNavHost( navController.navigateUp() }, onNextClick = {} - ) } composable("home") { diff --git a/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeech/TextToSpeechScreen.kt b/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeech/TextToSpeechScreen.kt index 9751255b..c24caa37 100644 --- a/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeech/TextToSpeechScreen.kt +++ b/frontend/app/src/main/java/com/example/speechbuddy/compose/texttospeech/TextToSpeechScreen.kt @@ -1,6 +1,5 @@ package com.example.speechbuddy.compose.texttospeech - import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -39,7 +38,6 @@ import com.example.speechbuddy.compose.utils.TitleUi import com.example.speechbuddy.ui.models.ButtonStatusType import com.example.speechbuddy.viewmodel.TextToSpeechViewModel - @OptIn(ExperimentalMaterial3Api::class) @Composable fun TextToSpeechScreen( diff --git a/frontend/app/src/main/res/values/strings.xml b/frontend/app/src/main/res/values/strings.xml index 70dac577..b54016c1 100644 --- a/frontend/app/src/main/res/values/strings.xml +++ b/frontend/app/src/main/res/values/strings.xml @@ -33,9 +33,6 @@ 새 비밀번호 확인 8자 이상 입력해주세요 비밀번호가 일치하지 않습니다 - 소리로 말해보아요 - 키보드로 텍스트를 입력해주세요 - 재생하기 상징으로 말하기 음성으로 말하기 새 상징 만들기