diff --git a/composeApp/src/androidMain/composeResources/drawable/ic_audio.xml b/composeApp/src/androidMain/composeResources/drawable/ic_audio.xml new file mode 100644 index 0000000..2777d1e --- /dev/null +++ b/composeApp/src/androidMain/composeResources/drawable/ic_audio.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt index c53a642..90736da 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt @@ -1,8 +1,17 @@ package su.reya.coop.screens +import android.app.Activity +import android.content.Intent import android.net.Uri +import android.speech.RecognizerIntent import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -66,6 +75,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.ic_add_circle import coop.composeapp.generated.resources.ic_arrow_back +import coop.composeapp.generated.resources.ic_audio import coop.composeapp.generated.resources.ic_cancel import coop.composeapp.generated.resources.ic_check_circle import coop.composeapp.generated.resources.ic_send @@ -153,9 +163,19 @@ fun ChatScreen(id: Long, screening: Boolean = false) { } } - val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> - uri?.let { sendFile(it) } - } + val fileLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let { sendFile(it) } + } + + val sttLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val data = result.data + val results = data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS) + if (!results.isNullOrEmpty()) text = results[0] + } + } LaunchedEffect(id) { // Get messages @@ -356,7 +376,24 @@ fun ChatScreen(id: Long, screening: Boolean = false) { text = "" }, onUpload = { - launcher.launch("image/*") + fileLauncher.launch("image/*") + }, + onMicClick = { + val intent = + Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { + putExtra( + RecognizerIntent.EXTRA_LANGUAGE_MODEL, + RecognizerIntent.LANGUAGE_MODEL_FREE_FORM + ) + putExtra(RecognizerIntent.EXTRA_PROMPT, "Speak now...") + } + try { + sttLauncher.launch(intent) + } catch (e: Exception) { + scope.launch { + snackbarHostState.showSnackbar("Speech recognition not available") + } + } } ) } @@ -550,7 +587,8 @@ fun ChatInput( value: String, onValueChange: (String) -> Unit, onSend: () -> Unit, - onUpload: () -> Unit + onUpload: () -> Unit, + onMicClick: () -> Unit ) { Row( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), @@ -576,18 +614,36 @@ fun ChatInput( }, ) Spacer(modifier = Modifier.size(8.dp)) - FilledTonalIconButton( - onClick = onSend, - modifier = Modifier.size(56.dp), - colors = IconButtonDefaults.filledTonalIconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) { - Icon( - painter = painterResource(Res.drawable.ic_send), - contentDescription = "Send" - ) + AnimatedContent( + targetState = value.isNotEmpty(), + transitionSpec = { (scaleIn() + fadeIn()) togetherWith (scaleOut() + fadeOut()) }, + label = "send_mic_transition" + ) { isNotEmpty -> + if (isNotEmpty) { + IconButton( + onClick = onSend, + modifier = Modifier.size(56.dp), + colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Icon( + painter = painterResource(Res.drawable.ic_send), + contentDescription = "Send" + ) + } + } else { + FilledTonalIconButton( + onClick = onMicClick, + modifier = Modifier.size(56.dp), + ) { + Icon( + painter = painterResource(Res.drawable.ic_audio), + contentDescription = "Speech to Text" + ) + } + } } } }