diff --git a/Prezel/core/data/src/main/java/com/team/prezel/core/data/mapper/PresentationMapper.kt b/Prezel/core/data/src/main/java/com/team/prezel/core/data/mapper/PresentationMapper.kt index 5c8726ca..54e71082 100644 --- a/Prezel/core/data/src/main/java/com/team/prezel/core/data/mapper/PresentationMapper.kt +++ b/Prezel/core/data/src/main/java/com/team/prezel/core/data/mapper/PresentationMapper.kt @@ -102,7 +102,7 @@ internal fun PresentationSentenceAnalysisResponse.toDomain(): SentenceAnalysisDe status = WordAnalysisStatus.from(value = status), mainFeedback = mainFeedback, subFeedback = subFeedback, - guideScript = guideScript, + guideScript = guideScript.orEmpty(), accuracy = accuracy, startTimeMs = startTimeMs, endTimeMs = endTimeMs, diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/actions/area/PrezelButtonArea.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/actions/area/PrezelButtonArea.kt index 8928b2e1..4020ff48 100644 --- a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/actions/area/PrezelButtonArea.kt +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/actions/area/PrezelButtonArea.kt @@ -41,6 +41,7 @@ fun PrezelButtonArea( isVertical: Boolean = true, isStrongStrength: Boolean = true, showBackground: Boolean = false, + showDivider: Boolean = showBackground, isNested: Boolean = false, config: PrezelButtonAreaDefault = PrezelButtonAreaDefaults.getDefault(), mainButton: @Composable (Modifier) -> Unit, @@ -53,7 +54,7 @@ fun PrezelButtonArea( .fillMaxWidth() .then(if (showBackground) Modifier.background(config.backgroundColor) else Modifier), ) { - if (showBackground) { + if (showDivider) { PrezelHorizontalDivider(type = PrezelDividerType.THICK, color = config.borderColor) } diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/voice/PrezelVoiceChromeWave.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/voice/PrezelVoiceChromeWave.kt index e4a163a2..a78224ea 100644 --- a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/voice/PrezelVoiceChromeWave.kt +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/voice/PrezelVoiceChromeWave.kt @@ -1,8 +1,6 @@ package com.team.prezel.core.designsystem.component.voice import androidx.annotation.FloatRange -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -35,6 +33,9 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlin.math.roundToInt +private const val MIN_REACTIVE_VOLUME = 0.12f +private const val MIN_WAVE_VOLUME = 0.1f + @Composable fun PrezelVoiceChromeWave( modifier: Modifier = Modifier, @@ -48,33 +49,23 @@ fun PrezelVoiceChromeWave( VoiceChromeStatus.LISTENING, VoiceChromeStatus.WAITING, -> { - val clippedVolumes = volumes.map { volume -> - volume.coerceIn( - minimumValue = 0.1f, - maximumValue = 1f, - ) - } + val clippedVolumes = volumes + .filter { volume -> volume > MIN_REACTIVE_VOLUME } + .map { volume -> + volume.coerceIn( + minimumValue = MIN_WAVE_VOLUME, + maximumValue = 1f, + ) + } clippedVolumes.toImmutableList() } } - val activationProgress by animateFloatAsState( - targetValue = if (status == VoiceChromeStatus.IDLE) 0f else 1f, - animationSpec = tween(durationMillis = 6400), - label = "VoiceChromeWaveActivationProgress", - ) - val volumeProgress by animateFloatAsState( - targetValue = if (status == VoiceChromeStatus.IDLE) 0f else 1f, - animationSpec = tween(durationMillis = 440), - label = "VoiceChromeWaveVolumeProgress", - ) Spacer( modifier = modifier.drawVoiceChromeWave( status = status, volumes = adjustedVolumes, - activationProgress = activationProgress, - volumeProgress = volumeProgress, showBaseline = showBaseline, ), ) @@ -84,8 +75,6 @@ fun PrezelVoiceChromeWave( private fun Modifier.drawVoiceChromeWave( status: VoiceChromeStatus, volumes: ImmutableList, - activationProgress: Float, - volumeProgress: Float, showBaseline: Boolean, ): Modifier { val colors = PrezelTheme.colors @@ -122,8 +111,6 @@ private fun Modifier.drawVoiceChromeWave( status = status, volumes = volumes, config = drawConfig, - activationProgress = activationProgress, - volumeProgress = volumeProgress, showBaseline = showBaseline, baselineColor = colors.borderRegular, ) @@ -147,35 +134,14 @@ private fun DrawScope.drawVoiceChromeWaveContent( status: VoiceChromeStatus, volumes: ImmutableList, config: VoiceChromeWaveDrawConfig, - activationProgress: Float, - volumeProgress: Float, showBaseline: Boolean, baselineColor: Color, ) { - if (status == VoiceChromeStatus.LISTENING && activationProgress < 1f) { - drawVoiceChromeWaveBars( - status = VoiceChromeStatus.IDLE, - volumes = persistentListOf(), - config = config, - xOffset = -size.width * activationProgress, - volumeProgress = 0f, - ) - drawVoiceChromeWaveBars( - status = VoiceChromeStatus.LISTENING, - volumes = volumes, - config = config, - xOffset = size.width * (1f - activationProgress), - volumeProgress = volumeProgress, - ) - } else { - drawVoiceChromeWaveBars( - status = status, - volumes = volumes, - config = config, - xOffset = 0f, - volumeProgress = volumeProgress, - ) - } + drawVoiceChromeWaveBars( + status = status, + volumes = volumes, + config = config, + ) drawVoiceChromeWaveBaseline( visible = showBaseline, @@ -188,10 +154,8 @@ private fun DrawScope.drawVoiceChromeWaveBars( status: VoiceChromeStatus, volumes: ImmutableList, config: VoiceChromeWaveDrawConfig, - xOffset: Float, - volumeProgress: Float, ) { - var barX = -config.barWidth + xOffset + var barX = -config.barWidth var barIndex = 0 val barCount = (size.width / config.barSpacing).roundToInt() + 1 @@ -200,10 +164,7 @@ private fun DrawScope.drawVoiceChromeWaveBars( index = barIndex, sampleCount = barCount, ) - val barHeight = config.volumeToBarHeight( - volume = volume, - progress = volumeProgress, - ) + val barHeight = config.volumeToBarHeight(volume) val barTop = (size.height - barHeight) / 2f val topLeft = Offset(x = barX, y = barTop) val barSize = Size(width = config.barWidth, height = barHeight) @@ -251,25 +212,22 @@ private fun DrawScope.drawVoiceChromeWaveBaseline( ) } -private fun VoiceChromeWaveDrawConfig.volumeToBarHeight( - volume: Float, - progress: Float, -): Float { - val volumeProgress = (volume - 0.1f) / (1f - 0.1f) - val targetHeight = minBarHeight + volumeProgress * (maxBarHeight - minBarHeight) +private fun VoiceChromeWaveDrawConfig.volumeToBarHeight(volume: Float): Float { + val volumeProgress = (volume - MIN_WAVE_VOLUME) / (1f - MIN_WAVE_VOLUME) - return minBarHeight + (targetHeight - minBarHeight) * progress + return minBarHeight + volumeProgress * (maxBarHeight - minBarHeight) } private fun ImmutableList.sampleVolume( index: Int, sampleCount: Int, ): Float { - if (isEmpty()) return 0.1f - if (size == 1 || sampleCount <= 1) return first() + if (isEmpty() || sampleCount <= 0) return MIN_WAVE_VOLUME + + val firstVisibleIndex = (size - sampleCount).coerceAtLeast(0) + val sampleIndex = firstVisibleIndex + index - val sampleIndex = (index * (lastIndex.toFloat() / (sampleCount - 1))).roundToInt() - return get(sampleIndex.coerceIn(indices)) + return getOrElse(sampleIndex) { MIN_WAVE_VOLUME } } @LargeDevicePreview diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/presentation/PresentationWordDetailResponse.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/presentation/PresentationWordDetailResponse.kt index e7174251..3317637e 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/presentation/PresentationWordDetailResponse.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/presentation/PresentationWordDetailResponse.kt @@ -24,7 +24,7 @@ data class PresentationSentenceAnalysisResponse( @SerialName("subFeedback") val subFeedback: String, @SerialName("guideScript") - val guideScript: String = "", + val guideScript: String?, @SerialName("accuracy") val accuracy: Double, @SerialName("startTimeMs") diff --git a/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/AnalysisScreen.kt b/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/AnalysisScreen.kt index da066b0b..5c7fa220 100644 --- a/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/AnalysisScreen.kt +++ b/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/AnalysisScreen.kt @@ -170,7 +170,6 @@ private fun VoiceRecordingFeedback.toChromeUi(): VoiceRecordingChromeUi = titleResId = R.string.feature_analysis_impl_voice_recording_ready_to_continue_feedback, status = VoiceChromeStatus.LISTENING, gradient = VoiceChromeGradient.NONE, - hideWaveform = true, ) VoiceRecordingFeedback.SPEAK_LOUDER -> VoiceRecordingChromeUi( diff --git a/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/recording/VoiceRecordingButtonArea.kt b/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/recording/VoiceRecordingButtonArea.kt index 7f127c3b..2c9b4d6c 100644 --- a/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/recording/VoiceRecordingButtonArea.kt +++ b/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/recording/VoiceRecordingButtonArea.kt @@ -67,6 +67,8 @@ private fun IdleRecordingButtonArea( ) { PrezelButtonArea( modifier = modifier, + showBackground = true, + showDivider = false, mainButton = { buttonModifier -> RecordingIconButton( iconResId = recordingState.actionIconResId, diff --git a/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/recording/VoiceRecordingContent.kt b/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/recording/VoiceRecordingContent.kt index fa9a6eb7..c2567950 100644 --- a/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/recording/VoiceRecordingContent.kt +++ b/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/recording/VoiceRecordingContent.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn @@ -35,6 +36,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import com.team.prezel.core.audio.AudioSessionState +import com.team.prezel.core.audio.AudioSource import com.team.prezel.core.designsystem.component.actions.button.PrezelIconButton import com.team.prezel.core.designsystem.component.actions.button.config.ButtonHierarchy import com.team.prezel.core.designsystem.component.actions.button.config.ButtonSize @@ -42,6 +44,7 @@ import com.team.prezel.core.designsystem.component.actions.button.config.ButtonT import com.team.prezel.core.designsystem.component.actions.button.config.PrezelButtonDefaults import com.team.prezel.core.designsystem.component.voice.PrezelVoiceChromeWave import com.team.prezel.core.designsystem.icon.PrezelIcons +import com.team.prezel.core.designsystem.preview.BasicPreview import com.team.prezel.core.designsystem.theme.PrezelTheme import com.team.prezel.core.ui.util.noRippleClickable import com.team.prezel.feature.analysis.impl.R @@ -65,7 +68,7 @@ internal fun VoiceRecordingContent( modifier = modifier .fillMaxWidth() .background(PrezelTheme.colors.bgRegular) - .padding(vertical = PrezelTheme.spacing.V16), + .padding(vertical = PrezelTheme.spacing.V4), horizontalAlignment = Alignment.CenterHorizontally, ) { if (!recordingState.isCompleted || isScriptExpanded) { @@ -73,7 +76,7 @@ internal fun VoiceRecordingContent( isScriptExpanded = isScriptExpanded, onToggleScriptExpanded = onToggleScriptExpanded, ) - Spacer(modifier = Modifier.height(PrezelTheme.spacing.V16)) + Spacer(modifier = Modifier.height(PrezelTheme.spacing.V4)) } VoiceRecordingScriptBody( @@ -104,7 +107,7 @@ private fun VoiceRecordingScriptHeader( Box( modifier = Modifier .fillMaxWidth() - .height(20.dp) + .height(48.dp) .padding(horizontal = PrezelTheme.spacing.V20), ) { Text( @@ -240,6 +243,7 @@ private fun VoiceRecordingStatusArea( modifier = Modifier.fillMaxWidth(), ) } + Spacer(modifier = Modifier.height(PrezelTheme.spacing.V12)) } @Composable @@ -262,7 +266,7 @@ private fun ScriptZoomButton( ), contentDescription = null, modifier = Modifier.size(24.dp), - tint = PrezelTheme.colors.iconRegular, + tint = PrezelTheme.colors.iconDisabled, ) } } @@ -275,14 +279,10 @@ private fun RecordingWaveform( modifier: Modifier = Modifier, ) { val playbackProgress = recordingState.playbackProgress() - val visibleVolumes = if (voiceChromeUi?.hideWaveform == true) { - persistentListOf() - } else { - recordingState.visibleRecordingVolumes( - recordingVolumes = recordingVolumes, - playbackProgress = playbackProgress, - ) - } + val visibleVolumes = recordingState.visibleRecordingVolumes( + recordingVolumes = recordingVolumes, + playbackProgress = playbackProgress, + ) PrezelVoiceChromeWave( status = voiceChromeUi?.status ?: recordingState.toVoiceChromeStatus(), @@ -447,3 +447,58 @@ private fun RecordingRoundIconButton( onClick = onClick, ) } + +@BasicPreview +@Composable +private fun VoiceRecordingContentIdlePreview() { + VoiceRecordingContentPreview(recordingState = AudioSessionState.Idle) +} + +@BasicPreview +@Composable +private fun VoiceRecordingContentEmptyScriptPreview() { + VoiceRecordingContentPreview( + recordingState = AudioSessionState.Idle, + script = "", + useMinimumScriptHeight = true, + ) +} + +@BasicPreview +@Composable +private fun VoiceRecordingContentRecordingPreview() { + VoiceRecordingContentPreview( + recordingState = AudioSessionState.Recording(elapsedSeconds = 12), + ) +} + +@BasicPreview +@Composable +private fun VoiceRecordingContentCompletedPreview() { + VoiceRecordingContentPreview( + recordingState = AudioSessionState.ReadyToPlay( + source = AudioSource.RecordedFile(filePath = "preview.m4a"), + durationSeconds = 75, + ), + ) +} + +@Composable +private fun VoiceRecordingContentPreview( + recordingState: AudioSessionState, + script: String = "한 번쯤 발표하면서 긴장하신 경험 있으시죠. 오늘도 다들 긴장되는 마음으로 오셨을 것 같습니다.", + useMinimumScriptHeight: Boolean = false, +) { + PrezelTheme { + VoiceRecordingContent( + script = script, + recordingState = recordingState, + recordingVolumes = persistentListOf(0.2f, 0.45f, 0.7f, 0.35f, 0.8f, 0.55f), + isScriptExpanded = false, + onToggleScriptExpanded = {}, + onClickRecordingControl = {}, + modifier = if (useMinimumScriptHeight) Modifier.fillMaxWidth() else Modifier.fillMaxSize(), + useMinimumScriptHeight = useMinimumScriptHeight, + ) + } +} diff --git a/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/recording/VoiceRecordingStateProperties.kt b/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/recording/VoiceRecordingStateProperties.kt index bf0feabf..ec367729 100644 --- a/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/recording/VoiceRecordingStateProperties.kt +++ b/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/recording/VoiceRecordingStateProperties.kt @@ -15,7 +15,6 @@ internal data class VoiceRecordingChromeUi( @param:StringRes val titleResId: Int? = null, val status: VoiceChromeStatus? = null, val gradient: VoiceChromeGradient? = null, - val hideWaveform: Boolean = false, ) internal val AudioSessionState.currentSeconds: Int diff --git a/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/schedule/PresentationScheduleScreen.kt b/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/schedule/PresentationScheduleScreen.kt index 3a3dd52f..cbce2916 100644 --- a/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/schedule/PresentationScheduleScreen.kt +++ b/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/schedule/PresentationScheduleScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.team.prezel.core.designsystem.component.datepicker.PrezelDatePicker import com.team.prezel.core.designsystem.component.textfield.PrezelTextField +import com.team.prezel.core.designsystem.component.textfield.PrezelTextFieldStatus import com.team.prezel.core.designsystem.icon.PrezelIcons import com.team.prezel.core.designsystem.preview.BasicPreview import com.team.prezel.core.designsystem.theme.PrezelTheme @@ -93,6 +94,9 @@ private fun PresentationScheduleScreen( onValueChange = onTitleChange, placeholder = stringResource(R.string.feature_analysis_impl_presentation_name_placeholder), label = stringResource(R.string.feature_analysis_impl_presentation_name_label), + status = PrezelTextFieldStatus.Default( + message = stringResource(R.string.feature_analysis_impl_presentation_name_helper), + ), maxLength = 10, ) diff --git a/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/situation/PresentationSituationScreen.kt b/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/situation/PresentationSituationScreen.kt index 7261e63a..0e313b05 100644 --- a/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/situation/PresentationSituationScreen.kt +++ b/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/situation/PresentationSituationScreen.kt @@ -174,10 +174,7 @@ private fun CategorySituationAccordion( CategoryOptionGrid( selectedValue = form.category, options = categoryOptions, - onSelect = { selectedCategory -> - onSelectCategory(selectedCategory) - onExpandedTypeChange(selectedCategory.nextExpandedType(SituationAccordionType.CATEGORY)) - }, + onSelect = onSelectCategory, ) } } @@ -201,9 +198,7 @@ private fun PurposeSituationAccordion( options = purposeOptions.toChipContentOptions(form.purpose), onSelect = { index -> val selectedPurpose = purposeOptions[index].value - val nextPurpose = selectedPurpose.toggleIfSelected(form.purpose) - onSelectPurpose(nextPurpose) - onExpandedTypeChange(nextPurpose.nextExpandedType(SituationAccordionType.PURPOSE)) + onSelectPurpose(selectedPurpose.toggleIfSelected(form.purpose)) }, ) } @@ -228,9 +223,7 @@ private fun StyleSituationAccordion( options = styleOptions.toChipContentOptions(form.style), onSelect = { index -> val selectedStyle = styleOptions[index].value - val nextStyle = selectedStyle.toggleIfSelected(form.style) - onSelectStyle(nextStyle) - onExpandedTypeChange(nextStyle.nextExpandedType(SituationAccordionType.STYLE)) + onSelectStyle(selectedStyle.toggleIfSelected(form.style)) }, ) } @@ -256,9 +249,7 @@ private fun AudienceSituationAccordion( options = audienceOptions.toChipContentOptions(form.audience), onSelect = { index -> val selectedAudience = audienceOptions[index].value - val nextAudience = selectedAudience.toggleIfSelected(form.audience) - onSelectAudience(nextAudience) - onExpandedTypeChange(nextAudience.nextExpandedType(SituationAccordionType.AUDIENCE)) + onSelectAudience(selectedAudience.toggleIfSelected(form.audience)) }, ) } @@ -333,21 +324,10 @@ private enum class SituationAccordionType { PURPOSE, STYLE, AUDIENCE, - ; - - val next: SituationAccordionType? - get() = when (this) { - CATEGORY -> PURPOSE - PURPOSE -> STYLE - STYLE -> AUDIENCE - AUDIENCE -> null - } } private fun T.toggleIfSelected(selectedValue: T?): T? = if (this == selectedValue) null else this -private fun Any?.nextExpandedType(currentType: SituationAccordionType): SituationAccordionType? = if (this == null) currentType else currentType.next - @Composable private fun CategoryOptionCard( option: SituationCategoryOption, diff --git a/Prezel/feature/analysis/impl/src/main/res/values/strings.xml b/Prezel/feature/analysis/impl/src/main/res/values/strings.xml index e6fbc2da..9ca200dc 100644 --- a/Prezel/feature/analysis/impl/src/main/res/values/strings.xml +++ b/Prezel/feature/analysis/impl/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ 발표 이름과 날짜를 입력해주세요. 발표 이름 발표의 이름을 설정해주세요 + 2자부터 10자 이내로 입력해주세요 발표 날짜 발표하는 날짜를 알려주세요