Skip to content

Commit 82a794b

Browse files
authored
Merge pull request #20961 from wordpress-mobile/issue/v2c-initial-UI
[Voice to Content] Initial UI
2 parents 0fda3a4 + 82bf5ed commit 82a794b

16 files changed

Lines changed: 1066 additions & 212 deletions

.idea/checkstyle-idea.xml

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package org.wordpress.android.ui.voicetocontent
2+
3+
import android.content.res.Configuration
4+
import androidx.compose.animation.AnimatedContent
5+
import androidx.compose.animation.ExperimentalAnimationApi
6+
import androidx.compose.animation.animateColorAsState
7+
import androidx.compose.animation.core.tween
8+
import androidx.compose.animation.fadeIn
9+
import androidx.compose.animation.fadeOut
10+
import androidx.compose.animation.with
11+
import androidx.compose.foundation.Image
12+
import androidx.compose.foundation.background
13+
import androidx.compose.foundation.clickable
14+
import androidx.compose.foundation.isSystemInDarkTheme
15+
import androidx.compose.foundation.layout.Box
16+
import androidx.compose.foundation.layout.size
17+
import androidx.compose.foundation.shape.CircleShape
18+
import androidx.compose.material.ContentAlpha
19+
import androidx.compose.material.MaterialTheme
20+
import androidx.compose.runtime.Composable
21+
import androidx.compose.runtime.getValue
22+
import androidx.compose.runtime.mutableStateOf
23+
import androidx.compose.runtime.remember
24+
import androidx.compose.runtime.setValue
25+
import androidx.compose.ui.Alignment
26+
import androidx.compose.ui.Modifier
27+
import androidx.compose.ui.graphics.Color
28+
import androidx.compose.ui.graphics.ColorFilter
29+
import androidx.compose.ui.graphics.painter.Painter
30+
import androidx.compose.ui.res.painterResource
31+
import androidx.compose.ui.tooling.preview.Devices
32+
import androidx.compose.ui.tooling.preview.Preview
33+
import androidx.compose.ui.unit.dp
34+
import org.wordpress.android.R
35+
import org.wordpress.android.ui.compose.theme.AppTheme
36+
37+
@OptIn(ExperimentalAnimationApi::class)
38+
@Suppress("DEPRECATION")
39+
@Composable
40+
fun MicToStopIcon(model: RecordingPanelUIModel) {
41+
val isEnabled = model.isEnabled
42+
var isMic by remember { mutableStateOf(true) }
43+
val isLight = !isSystemInDarkTheme()
44+
45+
val circleColor by animateColorAsState(
46+
targetValue = if (!isEnabled) MaterialTheme.colors.onSurface.copy(alpha = 0.3f)
47+
else if (isMic) MaterialTheme.colors.primary
48+
else if (isLight) Color.Black
49+
else Color.White, label = ""
50+
)
51+
52+
val iconColor by animateColorAsState(
53+
targetValue = if (!isEnabled) MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
54+
else if (isMic) Color.White
55+
else if (isLight) Color.White
56+
else Color.Black, label = ""
57+
)
58+
59+
val micIcon: Painter = painterResource(id = R.drawable.ic_mic_none_24)
60+
val stopIcon: Painter = painterResource(id = R.drawable.v2c_stop)
61+
62+
Box(
63+
contentAlignment = Alignment.Center,
64+
modifier = Modifier
65+
.size(100.dp)
66+
.background(Color.Transparent) // Ensure transparent background
67+
.clickable(
68+
enabled = isEnabled,
69+
onClick = {
70+
if (model.hasPermission) {
71+
if (isMic) {
72+
model.onMicTap?.invoke()
73+
} else {
74+
model.onStopTap?.invoke()
75+
}
76+
// isMic = !isMic
77+
} else {
78+
model.onRequestPermission?.invoke()
79+
}
80+
isMic = !isMic
81+
}
82+
)
83+
) {
84+
Box(
85+
modifier = Modifier
86+
.size(100.dp)
87+
.background(circleColor, shape = CircleShape)
88+
)
89+
if (model.hasPermission) {
90+
AnimatedContent(
91+
targetState = isMic,
92+
transitionSpec = {
93+
fadeIn(animationSpec = tween(300)) with fadeOut(animationSpec = tween(300))
94+
}, label = ""
95+
) { targetState ->
96+
val icon: Painter = if (targetState) micIcon else stopIcon
97+
val iconSize = if (targetState) 50.dp else 35.dp
98+
Image(
99+
painter = icon,
100+
contentDescription = null,
101+
modifier = Modifier.size(iconSize),
102+
colorFilter = ColorFilter.tint(iconColor)
103+
)
104+
}
105+
} else {
106+
// Display mic icon statically if permission is not granted
107+
Image(
108+
painter = micIcon,
109+
contentDescription = null,
110+
modifier = Modifier.size(50.dp),
111+
colorFilter = ColorFilter.tint(iconColor)
112+
)
113+
}
114+
}
115+
}
116+
117+
@Preview(showBackground = true)
118+
@Preview(showBackground = true, device = Devices.PIXEL_4_XL)
119+
@Preview(showBackground = true, device = Devices.PIXEL_4_XL, uiMode = Configuration.UI_MODE_NIGHT_YES)
120+
@Composable
121+
fun ExistingLayoutPreview() {
122+
AppTheme {
123+
MicToStopIcon(
124+
RecordingPanelUIModel(
125+
isEligibleForFeature = true,
126+
onMicTap = {},
127+
onStopTap = {},
128+
hasPermission = true,
129+
onRequestPermission = {},
130+
actionLabel = R.string.voice_to_content_base_header_label, isEnabled = false
131+
)
132+
)
133+
}
134+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.wordpress.android.ui.voicetocontent
2+
3+
import kotlinx.coroutines.Dispatchers
4+
import kotlinx.coroutines.withContext
5+
import org.wordpress.android.fluxc.model.SiteModel
6+
import org.wordpress.android.fluxc.model.jetpackai.JetpackAIAssistantFeature
7+
import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAIAssistantFeatureResponse
8+
import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore
9+
import javax.inject.Inject
10+
11+
class PrepareVoiceToContentUseCase @Inject constructor(
12+
private val jetpackAIStore: JetpackAIStore
13+
) {
14+
suspend fun execute(site: SiteModel): PrepareVoiceToContentResult =
15+
withContext(Dispatchers.IO) {
16+
when (val response = jetpackAIStore.fetchJetpackAIAssistantFeature(site)) {
17+
is JetpackAIAssistantFeatureResponse.Success -> {
18+
PrepareVoiceToContentResult.Success(model = response.model)
19+
}
20+
is JetpackAIAssistantFeatureResponse.Error -> {
21+
PrepareVoiceToContentResult.Error
22+
}
23+
}
24+
}
25+
}
26+
27+
sealed class PrepareVoiceToContentResult {
28+
data class Success(val model: JetpackAIAssistantFeature) : PrepareVoiceToContentResult()
29+
data object Error : PrepareVoiceToContentResult()
30+
}
Lines changed: 20 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,62 @@
11
package org.wordpress.android.ui.voicetocontent
22

33
import android.content.Intent
4-
import android.content.pm.PackageManager
54
import android.net.Uri
65
import android.os.Bundle
76
import android.view.LayoutInflater
87
import android.view.View
98
import android.view.ViewGroup
109
import androidx.activity.result.contract.ActivityResultContracts
1110
import androidx.appcompat.app.AlertDialog
12-
import androidx.compose.foundation.clickable
13-
import androidx.compose.runtime.Composable
14-
import androidx.compose.runtime.getValue
15-
import androidx.compose.runtime.livedata.observeAsState
1611
import androidx.compose.ui.platform.ComposeView
1712
import androidx.fragment.app.viewModels
1813
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
1914
import dagger.hilt.android.AndroidEntryPoint
2015
import org.wordpress.android.ui.compose.theme.AppTheme
21-
import androidx.compose.foundation.layout.Column
22-
import androidx.compose.foundation.layout.Spacer
23-
import androidx.compose.foundation.layout.fillMaxWidth
24-
import androidx.compose.foundation.layout.height
25-
import androidx.compose.foundation.layout.padding
26-
import androidx.compose.foundation.layout.size
27-
import androidx.compose.material.Icon
28-
import androidx.compose.material.Text
29-
import androidx.compose.ui.Alignment
30-
import androidx.compose.ui.Modifier
31-
import androidx.compose.ui.res.painterResource
32-
import androidx.compose.ui.text.font.FontWeight
33-
import androidx.compose.ui.unit.dp
34-
import androidx.compose.ui.unit.sp
35-
import androidx.core.content.ContextCompat
3616
import org.wordpress.android.R
3717
import org.wordpress.android.util.audio.IAudioRecorder.Companion.REQUIRED_RECORDING_PERMISSIONS
3818
import android.provider.Settings
19+
import androidx.compose.material.ExperimentalMaterialApi
3920

4021
@AndroidEntryPoint
4122
class VoiceToContentDialogFragment : BottomSheetDialogFragment() {
4223
private val viewModel: VoiceToContentViewModel by viewModels()
4324

25+
@ExperimentalMaterialApi
4426
override fun onCreateView(
4527
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
4628
): View = ComposeView(requireContext()).apply {
4729
setContent {
4830
AppTheme {
4931
VoiceToContentScreen(
50-
viewModel = viewModel,
51-
onRequestPermission = { requestAllPermissionsForRecording() },
52-
hasPermission = { hasAllPermissionsForRecording() }
32+
viewModel = viewModel
5333
)
5434
}
5535
}
5636
}
5737

38+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
39+
super.onViewCreated(view, savedInstanceState)
40+
observeViewModel()
41+
viewModel.start()
42+
}
43+
44+
private fun observeViewModel() {
45+
viewModel.requestPermission.observe(viewLifecycleOwner) {
46+
requestAllPermissionsForRecording()
47+
}
48+
49+
viewModel.dismiss.observe(viewLifecycleOwner) {
50+
dismiss()
51+
}
52+
}
53+
5854
private val requestMultiplePermissionsLauncher = registerForActivityResult(
5955
ActivityResultContracts.RequestMultiplePermissions()
6056
) { permissions ->
6157
val areAllPermissionsGranted = permissions.entries.all { it.value }
6258
if (areAllPermissionsGranted) {
63-
viewModel.startRecording()
59+
viewModel.onPermissionGranted()
6460
} else {
6561
// Check if any permissions were denied permanently
6662
if (permissions.entries.any { !it.value }) {
@@ -69,15 +65,6 @@ class VoiceToContentDialogFragment : BottomSheetDialogFragment() {
6965
}
7066
}
7167

72-
private fun hasAllPermissionsForRecording(): Boolean {
73-
return REQUIRED_RECORDING_PERMISSIONS.all {
74-
ContextCompat.checkSelfPermission(
75-
requireContext(),
76-
it
77-
) == PackageManager.PERMISSION_GRANTED
78-
}
79-
}
80-
8168
private fun requestAllPermissionsForRecording() {
8269
requestMultiplePermissionsLauncher.launch(REQUIRED_RECORDING_PERMISSIONS)
8370
}
@@ -104,63 +91,3 @@ class VoiceToContentDialogFragment : BottomSheetDialogFragment() {
10491
fun newInstance() = VoiceToContentDialogFragment()
10592
}
10693
}
107-
108-
@Composable
109-
fun VoiceToContentScreen(
110-
viewModel: VoiceToContentViewModel,
111-
onRequestPermission: () -> Unit,
112-
hasPermission: () -> Boolean
113-
) {
114-
val result by viewModel.uiState.observeAsState()
115-
val assistantFeature by viewModel.aiAssistantFeatureState.observeAsState()
116-
Column(
117-
horizontalAlignment = Alignment.CenterHorizontally,
118-
modifier = Modifier
119-
.fillMaxWidth()
120-
.padding(16.dp)
121-
) {
122-
when {
123-
result?.isError == true -> {
124-
Text(text = "Error happened", fontSize = 20.sp, fontWeight = FontWeight.Bold)
125-
}
126-
127-
result?.content != null -> {
128-
Text(text = result?.content!!, fontSize = 20.sp, fontWeight = FontWeight.Bold)
129-
}
130-
131-
assistantFeature != null -> {
132-
Text(text = "Assistant Feature Returned Successfully", fontSize = 20.sp, fontWeight = FontWeight.Bold)
133-
Spacer(modifier = Modifier.height(16.dp))
134-
}
135-
136-
else -> {
137-
Text(text = "Ready to fake record - tap microphone", fontSize = 20.sp, fontWeight = FontWeight.Bold)
138-
Spacer(modifier = Modifier.height(16.dp))
139-
Icon(
140-
painterResource(id = R.drawable.ic_mic_white_24dp),
141-
contentDescription = "Microphone",
142-
modifier = Modifier
143-
.size(64.dp)
144-
.clickable {
145-
if (hasPermission()) {
146-
viewModel.startRecording()
147-
} else {
148-
onRequestPermission()
149-
}
150-
}
151-
)
152-
153-
Spacer(modifier = Modifier.height(16.dp))
154-
Icon(
155-
painterResource(id = com.google.android.exoplayer2.ui.R.drawable.exo_icon_stop),
156-
contentDescription = "Stop",
157-
modifier = Modifier
158-
.size(64.dp)
159-
.clickable {
160-
viewModel.stopRecording()
161-
}
162-
)
163-
}
164-
}
165-
}
166-
}

0 commit comments

Comments
 (0)