Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Voice to Content] Initial UI #20961

Merged
merged 46 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
302ef26
Add UiState for voice to content
zwarm May 30, 2024
0540358
Move VoiceToContent screen into it's own class with initial UI pass
zwarm May 30, 2024
1fc33fa
Refactor: Move VoiceToContent screen into it's own class, move some p…
zwarm May 30, 2024
426528b
Add string placeholders for voiceToContent
zwarm May 30, 2024
4ba9d75
Add placeholder methods for uiState.
zwarm May 30, 2024
70d29cf
Merge branch 'trunk' into issue/v2c-initial-UI
zwarm May 31, 2024
78b11f3
Move the call to jetpack ai assistant into its own use case
zwarm May 31, 2024
4ebfe65
Update string placeholders
zwarm May 31, 2024
4f3032f
Add ErrorView, FinishedView, and update existing
zwarm May 31, 2024
aebf427
Add ErrorView and FinishedView to support the UI building
zwarm May 31, 2024
129a892
Observe dismiss action, refactor method names, and call vm.start
zwarm May 31, 2024
ff013f9
Refactor: remove uiState and aiAssistantFeatureState. Add prepareVoic…
zwarm May 31, 2024
78ad550
A bunch of POC commits that I will come back to later
zwarm Jun 3, 2024
d8f6f5f
Merge branch 'trunk' into issue/v2c-initial-UI
zwarm Jun 8, 2024
9b97d8b
Add amplitudes to RecordingUpdate to be passed back to caller
zwarm Jun 9, 2024
fd307b7
Initial shot as waveform
zwarm Jun 9, 2024
79a46fb
Refactor: uiState, models, and views
zwarm Jun 9, 2024
a8b7aaf
Address detekt issues
zwarm Jun 9, 2024
7266580
+ Adds: text styles for all fonts
AjeshRPai Jun 10, 2024
ab59085
→ Moves: styles to top of the previews
AjeshRPai Jun 10, 2024
313882e
↑ Updates: the logic of actionLabelStyle to enable or disable
AjeshRPai Jun 10, 2024
a660514
+ Adds: the missing style for disabled action label
AjeshRPai Jun 10, 2024
a28c33d
+ Adds: url message to the state
AjeshRPai Jun 10, 2024
4d1ed6a
+ Adds: the logic for showing upgrade to use this feature link view
AjeshRPai Jun 10, 2024
8c92669
Add support for upgradeURL
zwarm Jun 10, 2024
ae00938
Fix checkstyle comment
zwarm Jun 10, 2024
ee5cf02
Rename properties for messages related to ineligibility and upgrade
zwarm Jun 10, 2024
eaccd7e
Support renamed properties for messages related to ineligibility and …
zwarm Jun 10, 2024
35a9994
Remove delay and incorrect check
zwarm Jun 10, 2024
57de539
Refactor: clean up unused strings
zwarm Jun 10, 2024
e98ada2
Apply correct strings
zwarm Jun 10, 2024
50221d9
Update FluxC hash
zwarm Jun 10, 2024
e1f3ed3
Merge branch 'trunk' into issue/v2c-initial-UI
zwarm Jun 10, 2024
fac2e05
Fix references to broken strings
zwarm Jun 10, 2024
2ddd71f
Merge branch 'trunk' into issue/v2c-initial-UI
pantstamp Jun 11, 2024
c415bc5
Updates FluxC hash
pantstamp Jun 11, 2024
784aa42
Fix unit tests
pantstamp Jun 11, 2024
c435cbe
Fix unit tests
pantstamp Jun 11, 2024
27f8afd
Add microphone vector asset
zwarm Jun 11, 2024
c315755
Update to use ic mic
zwarm Jun 11, 2024
8c76a46
Revert back to normal after accidently commit
zwarm Jun 11, 2024
5af902d
Add start test for the view model
zwarm Jun 11, 2024
bf16b77
Fix checkstyle
zwarm Jun 11, 2024
7859334
Remove unused resource
zwarm Jun 11, 2024
fbc2040
Address failing test
zwarm Jun 11, 2024
82bf5ed
Address detekt issues
zwarm Jun 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions .idea/checkstyle-idea.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package org.wordpress.android.ui.voicetocontent

import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.with
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.wordpress.android.R
import org.wordpress.android.ui.compose.theme.AppTheme

@OptIn(ExperimentalAnimationApi::class)
@Suppress("DEPRECATION")
@Composable
fun MicToStopIcon(model: RecordingPanelUIModel) {
val isEnabled = model.isEnabled
var isMic by remember { mutableStateOf(true) }
val isLight = !isSystemInDarkTheme()

val circleColor by animateColorAsState(
targetValue = if (!isEnabled) MaterialTheme.colors.onSurface.copy(alpha = 0.3f)
else if (isMic) MaterialTheme.colors.primary
else if (isLight) Color.Black
else Color.White, label = ""
)

val iconColor by animateColorAsState(
targetValue = if (!isEnabled) MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
else if (isMic) Color.White
else if (isLight) Color.White
else Color.Black, label = ""
)

val micIcon: Painter = painterResource(id = R.drawable.v2c_mic)
val stopIcon: Painter = painterResource(id = R.drawable.v2c_stop)

Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(100.dp)
.background(Color.Transparent) // Ensure transparent background
.clickable(
enabled = isEnabled,
onClick = {
if (model.hasPermission) {
if (isMic) {
model.onMicTap?.invoke()
} else {
model.onStopTap?.invoke()
}
// isMic = !isMic
} else {
model.onRequestPermission?.invoke()
}
isMic = !isMic
}
)
) {
Box(
modifier = Modifier
.size(100.dp)
.background(circleColor, shape = CircleShape)
)
if (model.hasPermission) {
AnimatedContent(
targetState = isMic,
transitionSpec = {
fadeIn(animationSpec = tween(300)) with fadeOut(animationSpec = tween(300))
}, label = ""
) { targetState ->
val icon: Painter = if (targetState) micIcon else stopIcon
val iconSize = if (targetState) 50.dp else 35.dp
Image(
painter = icon,
contentDescription = null,
modifier = Modifier.size(iconSize),
colorFilter = ColorFilter.tint(iconColor)
)
}
} else {
// Display mic icon statically if permission is not granted
Image(
painter = micIcon,
contentDescription = null,
modifier = Modifier.size(50.dp),
colorFilter = ColorFilter.tint(iconColor)
)
}
}
}

@Preview(showBackground = true)
@Preview(showBackground = true, device = Devices.PIXEL_4_XL)
@Preview(showBackground = true, device = Devices.PIXEL_4_XL, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun ExistingLayoutPreview() {
AppTheme {
MicToStopIcon(
RecordingPanelUIModel(
isEligibleForFeature = true,
onMicTap = {},
onStopTap = {},
hasPermission = true,
onRequestPermission = {},
actionLabel = R.string.voice_to_content_base_header_label, isEnabled = false
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.wordpress.android.ui.voicetocontent

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.model.jetpackai.JetpackAIAssistantFeature
import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAIAssistantFeatureResponse
import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore
import javax.inject.Inject

class PrepareVoiceToContentUseCase @Inject constructor(
private val jetpackAIStore: JetpackAIStore
) {
suspend fun execute(site: SiteModel): PrepareVoiceToContentResult =
withContext(Dispatchers.IO) {
when (val response = jetpackAIStore.fetchJetpackAIAssistantFeature(site)) {
is JetpackAIAssistantFeatureResponse.Success -> {
PrepareVoiceToContentResult.Success(model = response.model)
}
is JetpackAIAssistantFeatureResponse.Error -> {
PrepareVoiceToContentResult.Error
}
}
}
}

sealed class PrepareVoiceToContentResult {
data class Success(val model: JetpackAIAssistantFeature) : PrepareVoiceToContentResult()
data object Error : PrepareVoiceToContentResult()
}
Original file line number Diff line number Diff line change
@@ -1,66 +1,62 @@
package org.wordpress.android.ui.voicetocontent

import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.compose.foundation.clickable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.viewModels
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import org.wordpress.android.ui.compose.theme.AppTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.material.Icon
import androidx.compose.material.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import org.wordpress.android.R
import org.wordpress.android.util.audio.IAudioRecorder.Companion.REQUIRED_RECORDING_PERMISSIONS
import android.provider.Settings
import androidx.compose.material.ExperimentalMaterialApi

@AndroidEntryPoint
class VoiceToContentDialogFragment : BottomSheetDialogFragment() {
private val viewModel: VoiceToContentViewModel by viewModels()

@ExperimentalMaterialApi
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View = ComposeView(requireContext()).apply {
setContent {
AppTheme {
VoiceToContentScreen(
viewModel = viewModel,
onRequestPermission = { requestAllPermissionsForRecording() },
hasPermission = { hasAllPermissionsForRecording() }
viewModel = viewModel
)
}
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observeViewModel()
viewModel.start()
}

private fun observeViewModel() {
viewModel.requestPermission.observe(viewLifecycleOwner) {
requestAllPermissionsForRecording()
}

viewModel.dismiss.observe(viewLifecycleOwner) {
dismiss()
}
}

private val requestMultiplePermissionsLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val areAllPermissionsGranted = permissions.entries.all { it.value }
if (areAllPermissionsGranted) {
viewModel.startRecording()
viewModel.onPermissionGranted()
} else {
// Check if any permissions were denied permanently
if (permissions.entries.any { !it.value }) {
Expand All @@ -69,15 +65,6 @@ class VoiceToContentDialogFragment : BottomSheetDialogFragment() {
}
}

private fun hasAllPermissionsForRecording(): Boolean {
return REQUIRED_RECORDING_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
requireContext(),
it
) == PackageManager.PERMISSION_GRANTED
}
}

private fun requestAllPermissionsForRecording() {
requestMultiplePermissionsLauncher.launch(REQUIRED_RECORDING_PERMISSIONS)
}
Expand All @@ -104,63 +91,3 @@ class VoiceToContentDialogFragment : BottomSheetDialogFragment() {
fun newInstance() = VoiceToContentDialogFragment()
}
}

@Composable
fun VoiceToContentScreen(
viewModel: VoiceToContentViewModel,
onRequestPermission: () -> Unit,
hasPermission: () -> Boolean
) {
val result by viewModel.uiState.observeAsState()
val assistantFeature by viewModel.aiAssistantFeatureState.observeAsState()
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
when {
result?.isError == true -> {
Text(text = "Error happened", fontSize = 20.sp, fontWeight = FontWeight.Bold)
}

result?.content != null -> {
Text(text = result?.content!!, fontSize = 20.sp, fontWeight = FontWeight.Bold)
}

assistantFeature != null -> {
Text(text = "Assistant Feature Returned Successfully", fontSize = 20.sp, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(16.dp))
}

else -> {
Text(text = "Ready to fake record - tap microphone", fontSize = 20.sp, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(16.dp))
Icon(
painterResource(id = R.drawable.ic_mic_white_24dp),
contentDescription = "Microphone",
modifier = Modifier
.size(64.dp)
.clickable {
if (hasPermission()) {
viewModel.startRecording()
} else {
onRequestPermission()
}
}
)

Spacer(modifier = Modifier.height(16.dp))
Icon(
painterResource(id = com.google.android.exoplayer2.ui.R.drawable.exo_icon_stop),
contentDescription = "Stop",
modifier = Modifier
.size(64.dp)
.clickable {
viewModel.stopRecording()
}
)
}
}
}
}
Loading
Loading