diff --git a/Prezel/app/src/main/ic_launcher-playstore.png b/Prezel/app/src/main/ic_launcher-playstore.png index 6f92d02b..1de4ce25 100644 Binary files a/Prezel/app/src/main/ic_launcher-playstore.png and b/Prezel/app/src/main/ic_launcher-playstore.png differ diff --git a/Prezel/app/src/main/res/drawable/ic_launcher_background.xml b/Prezel/app/src/main/res/drawable/ic_launcher_background.xml index ca3826a4..eae2f6cf 100644 --- a/Prezel/app/src/main/res/drawable/ic_launcher_background.xml +++ b/Prezel/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,4 +1,18 @@ - + + - \ No newline at end of file + + diff --git a/Prezel/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Prezel/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index c4a603d4..07607028 100644 --- a/Prezel/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/Prezel/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,6 @@ - - - \ No newline at end of file + + + + diff --git a/Prezel/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/Prezel/app/src/main/res/mipmap-hdpi/ic_launcher.webp index 91804b9a..7c1ba40b 100644 Binary files a/Prezel/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/Prezel/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/Prezel/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/Prezel/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp index bf87d73d..d520b037 100644 Binary files a/Prezel/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and b/Prezel/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/Prezel/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/Prezel/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index c209b945..5cba0acd 100644 Binary files a/Prezel/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/Prezel/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/Prezel/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/Prezel/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 5d1031db..17a04734 100644 Binary files a/Prezel/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/Prezel/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/Prezel/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/Prezel/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp index 6c2eef30..2553b414 100644 Binary files a/Prezel/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and b/Prezel/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/Prezel/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/Prezel/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 80047873..e4ed25dc 100644 Binary files a/Prezel/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/Prezel/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/Prezel/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/Prezel/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index b441e097..95f9d8e5 100644 Binary files a/Prezel/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/Prezel/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/Prezel/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/Prezel/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp index bfa1fb08..b49e401e 100644 Binary files a/Prezel/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and b/Prezel/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/Prezel/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/Prezel/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 787b8d0c..99e65162 100644 Binary files a/Prezel/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/Prezel/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/Prezel/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/Prezel/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 52f11073..5dddd65c 100644 Binary files a/Prezel/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/Prezel/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/Prezel/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/Prezel/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp index 4e6c0bf3..80f5764b 100644 Binary files a/Prezel/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and b/Prezel/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/Prezel/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/Prezel/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index f513ee3d..3b8bb4e7 100644 Binary files a/Prezel/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/Prezel/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/Prezel/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/Prezel/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index b6382107..f978ab45 100644 Binary files a/Prezel/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/Prezel/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/Prezel/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/Prezel/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp index b2d09799..52a46bdd 100644 Binary files a/Prezel/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and b/Prezel/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/Prezel/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/Prezel/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 37023b10..5de2809b 100644 Binary files a/Prezel/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/Prezel/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ 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 a89832be..af647911 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,6 +102,7 @@ internal fun PresentationSentenceAnalysisResponse.toDomain(): SentenceAnalysisDe status = WordAnalysisStatus.from(value = status), mainFeedback = mainFeedback, subFeedback = subFeedback, + guideScript = guideScript, accuracy = accuracy, startTimeMs = startTimeMs, endTimeMs = endTimeMs, diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelAccordion.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelAccordion.kt index 5853181b..812597d5 100644 --- a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelAccordion.kt +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelAccordion.kt @@ -84,6 +84,7 @@ fun PrezelAccordion( ), size = if (nested) PrezelListSize.SMALL else PrezelListSize.REGULAR, nested = nested, + titleTextColor = PrezelTheme.colors.textLarge, leadingContent = leadingContent, trailingContent = { trailingContent?.invoke() diff --git a/Prezel/core/model/src/main/java/com/team/prezel/core/model/presentation/PresentationWordDetail.kt b/Prezel/core/model/src/main/java/com/team/prezel/core/model/presentation/PresentationWordDetail.kt index 1497b74a..f50e9e58 100644 --- a/Prezel/core/model/src/main/java/com/team/prezel/core/model/presentation/PresentationWordDetail.kt +++ b/Prezel/core/model/src/main/java/com/team/prezel/core/model/presentation/PresentationWordDetail.kt @@ -13,6 +13,7 @@ data class SentenceAnalysisDetail( val status: WordAnalysisStatus, val mainFeedback: String, val subFeedback: String, + val guideScript: String = "", val accuracy: Double, val startTimeMs: Long, val endTimeMs: Long, @@ -32,10 +33,10 @@ enum class WordAnalysisStatus( ) { EXCELLENT("Excellent"), GOOD("Good"), - STUTTER("Stutter"), INSERTION("Insertion"), - OMISSION("Omission"), + STUTTER("Stutter"), MISPRONUNCIATION("Mispronunciation"), + OMISSION("Omission"), UNKNOWN("Unknown"), ; 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 47327b5a..e7174251 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 @@ -23,6 +23,8 @@ data class PresentationSentenceAnalysisResponse( val mainFeedback: String, @SerialName("subFeedback") val subFeedback: String, + @SerialName("guideScript") + val guideScript: String = "", @SerialName("accuracy") val accuracy: Double, @SerialName("startTimeMs") diff --git a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/AccuracyDetailScreen.kt b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/AccuracyDetailScreen.kt index 2c2e483f..61043964 100644 --- a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/AccuracyDetailScreen.kt +++ b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/AccuracyDetailScreen.kt @@ -1,8 +1,10 @@ package com.team.prezel.feature.report.impl.accuracydetail +import android.annotation.SuppressLint import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState @@ -20,13 +22,13 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.team.prezel.core.designsystem.component.feedback.snackbar.showPrezelSnackbar -import com.team.prezel.core.designsystem.component.navigations.PrezelTabSize import com.team.prezel.core.designsystem.component.navigations.PrezelTabs import com.team.prezel.core.designsystem.component.player.PrezelPlayerItem import com.team.prezel.core.designsystem.component.player.PrezelPlayerState @@ -136,7 +138,7 @@ private fun AccuracyDetailScreenContent( AccuracyDetailTab.SCRIPT_MATCH -> sentenceDetails.filter { detail -> detail.isScriptMatchIssue } }.toImmutableList() } - val sheetPeekHeight = rememberPlayerSheetPeekHeight(markerSentenceDetails = playerMarkerSentenceDetails) + val sheetPeekHeight = AccuracyDetailPlayerSheetPeekHeight val playerState = rememberDetailPlayerState( selectedTab = selectedTab, sentenceDetails = sentenceDetails, @@ -180,16 +182,6 @@ private fun rememberAccuracyDetailTabs(): List = ) } -@Composable -private fun rememberPlayerSheetPeekHeight(markerSentenceDetails: ImmutableList): Dp = - remember(markerSentenceDetails) { - if (markerSentenceDetails.isEmpty()) { - AccuracyDetailPlayerSheetDefaultPeekHeight - } else { - AccuracyDetailPlayerSheetLargePeekHeight - } - } - @Composable private fun rememberDetailPlayerState( selectedTab: AccuracyDetailTab, @@ -267,6 +259,7 @@ private fun PlaybackEffect( } } +@SuppressLint("ConfigurationScreenWidthHeight") @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AccuracyDetailScaffold( @@ -282,6 +275,9 @@ private fun AccuracyDetailScaffold( onClickTab: (Int) -> Unit, pagerState: PagerState, ) { + val configuration = LocalConfiguration.current + val expandedSheetMaxHeight = configuration.screenHeightDp.dp - AccuracyDetailExpandedSheetTopGap + BottomSheetScaffold( modifier = Modifier.fillMaxSize(), scaffoldState = scaffoldState, @@ -296,6 +292,11 @@ private fun AccuracyDetailScaffold( sentenceDetails = sentenceDetails, playerState = playerState, expanded = expanded, + modifier = if (expanded) { + Modifier.heightIn(max = expandedSheetMaxHeight) + } else { + Modifier + }, ) }, sheetDragHandle = null, @@ -306,11 +307,12 @@ private fun AccuracyDetailScaffold( .fillMaxSize() .padding(innerPadding), ) { - AccuracyDetailTopAppBar(onClose = onClose) + if (!expanded) { + AccuracyDetailTopAppBar(onClose = onClose) + } PrezelTabs( tabs = tabLabels, pagerState = pagerState, - size = PrezelTabSize.SMALL, onClickTab = onClickTab, ) ScriptDetailList( @@ -323,8 +325,8 @@ private fun AccuracyDetailScaffold( } private const val SEEK_SYNC_THRESHOLD_MILLIS = 750L -private val AccuracyDetailPlayerSheetDefaultPeekHeight = 220.dp -private val AccuracyDetailPlayerSheetLargePeekHeight = 336.dp +private val AccuracyDetailPlayerSheetPeekHeight = 252.dp +private val AccuracyDetailExpandedSheetTopGap = 56.dp @BasicPreview @Composable diff --git a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/component/AccuracyDetailPlayerSheet.kt b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/component/AccuracyDetailPlayerSheet.kt index c795eb80..33cad7bd 100644 --- a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/component/AccuracyDetailPlayerSheet.kt +++ b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/component/AccuracyDetailPlayerSheet.kt @@ -4,20 +4,28 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement 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.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.team.prezel.core.designsystem.component.PrezelAccordion +import com.team.prezel.core.designsystem.component.chip.chip.ChipHierarchy +import com.team.prezel.core.designsystem.component.chip.chip.ChipSize +import com.team.prezel.core.designsystem.component.chip.chip.PrezelChip import com.team.prezel.core.designsystem.component.player.PrezelPlayer import com.team.prezel.core.designsystem.component.player.PrezelPlayerItem import com.team.prezel.core.designsystem.component.player.PrezelPlayerState @@ -40,11 +48,16 @@ internal fun AccuracyDetailPlayerSheet( sentenceDetails: ImmutableList, playerState: PrezelPlayerState, expanded: Boolean, + modifier: Modifier = Modifier, ) { Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(), + modifier = modifier.then( + if (expanded) { + Modifier.fillMaxSize() + } else { + Modifier.fillMaxWidth() + }, + ), ) { SheetHandle() SheetDetailContent( @@ -57,6 +70,7 @@ internal fun AccuracyDetailPlayerSheet( PrezelPlayer( state = playerState, trackContentDescription = stringResource(R.string.feature_report_impl_script_detail_player_track_desc), + modifier = Modifier.navigationBarsPadding(), ) } } @@ -86,6 +100,25 @@ private fun SheetDetailContent( expanded: Boolean, modifier: Modifier = Modifier, ) { + val emptyDetailTextResId = when (selectedTab) { + AccuracyDetailTab.SPEECH -> R.string.feature_report_impl_accuracy_detail_sheet_empty_speech + AccuracyDetailTab.SCRIPT_MATCH -> R.string.feature_report_impl_accuracy_detail_sheet_empty_script + } + val isEmptyDetail = sentenceDetails.hasNoVisibleDetails(selectedTab, selectedSentence, expanded) + + if (expanded && isEmptyDetail) { + Box( + modifier = modifier + .fillMaxSize() + .padding(horizontal = PrezelTheme.spacing.V20), + contentAlignment = Alignment.Center, + ) { + ExpandedEmptyDetailText(text = stringResource(emptyDetailTextResId)) + } + Spacer(modifier = Modifier.height(PrezelTheme.spacing.V12)) + return + } + Column( modifier = modifier .fillMaxWidth() @@ -93,35 +126,79 @@ private fun SheetDetailContent( .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(PrezelTheme.spacing.V16), ) { - when (selectedTab) { - AccuracyDetailTab.SPEECH -> SpeechDetailContent( - selectedSentence = selectedSentence, - sentenceDetails = sentenceDetails, - expanded = expanded, - ) + if (isEmptyDetail) { + CollapsedEmptyDetailCard(text = stringResource(emptyDetailTextResId)) + } else { + when (selectedTab) { + AccuracyDetailTab.SPEECH -> SpeechDetailContent( + selectedSentence = selectedSentence, + sentenceDetails = sentenceDetails, + expanded = expanded, + ) - AccuracyDetailTab.SCRIPT_MATCH -> ScriptMatchDetailContent( - selectedSentence = selectedSentence, - sentenceDetails = sentenceDetails, - expanded = expanded, - ) + AccuracyDetailTab.SCRIPT_MATCH -> ScriptMatchDetailContent( + selectedSentence = selectedSentence, + sentenceDetails = sentenceDetails, + expanded = expanded, + ) + } } } Spacer(modifier = Modifier.height(PrezelTheme.spacing.V12)) } +private fun ImmutableList.hasNoVisibleDetails( + selectedTab: AccuracyDetailTab, + selectedSentence: SentenceAnalysisUiModel?, + expanded: Boolean, +): Boolean = + when (selectedTab) { + AccuracyDetailTab.SPEECH -> visibleSpeechAccuracyDetails(selectedSentence, expanded) + AccuracyDetailTab.SCRIPT_MATCH -> visibleScriptMatchDetails(selectedSentence, expanded) + }.isEmpty() + +@Composable +private fun CollapsedEmptyDetailCard(text: String) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(PrezelTheme.shapes.V8) + .background(PrezelTheme.colors.bgMedium) + .padding(PrezelTheme.spacing.V12), + verticalArrangement = Arrangement.spacedBy(PrezelTheme.spacing.V8), + ) { + PrezelChip( + text = stringResource(R.string.feature_report_impl_accuracy_detail_sheet_empty_card_label), + size = ChipSize.SMALL, + hierarchy = ChipHierarchy.SECONDARY, + ) + Text( + text = text, + style = PrezelTheme.typography.body2Medium, + color = PrezelTheme.colors.textLarge, + ) + } +} + +@Composable +private fun ExpandedEmptyDetailText(text: String) { + Text( + text = text, + style = PrezelTheme.typography.body3Medium, + color = PrezelTheme.colors.textMedium, + ) +} + @Composable private fun SpeechDetailContent( selectedSentence: SentenceAnalysisUiModel?, sentenceDetails: ImmutableList, expanded: Boolean, ) { - val accuracyDetails = sentenceDetails.filter { it.isSpeechAccuracyIssue }.toImmutableList() - val visibleAccuracyDetails = if (expanded) { - accuracyDetails - } else { - listOfNotNull(selectedSentence?.takeIf { it.isSpeechAccuracyIssue } ?: accuracyDetails.firstOrNull()) - } + val visibleAccuracyDetails = sentenceDetails.visibleSpeechAccuracyDetails( + selectedSentence = selectedSentence, + expanded = expanded, + ) if (visibleAccuracyDetails.isEmpty()) { EmptyDetailText(text = stringResource(R.string.feature_report_impl_accuracy_detail_sheet_empty_speech)) @@ -130,8 +207,7 @@ private fun SpeechDetailContent( SentenceAnalysisCard( detail = detail, highlighted = detail == selectedSentence, - text = detail.mainFeedback, - subText = detail.subFeedback, + subText = detail.subFeedback.takeIf { expanded }, useStatusTextColor = false, status = detail.speechAccuracyStatus, ) @@ -139,6 +215,18 @@ private fun SpeechDetailContent( } } +private fun ImmutableList.visibleSpeechAccuracyDetails( + selectedSentence: SentenceAnalysisUiModel?, + expanded: Boolean, +): ImmutableList { + val accuracyDetails = filter { it.isSpeechAccuracyIssue }.toImmutableList() + return if (expanded) { + accuracyDetails + } else { + listOfNotNull(selectedSentence?.takeIf { it.isSpeechAccuracyIssue } ?: accuracyDetails.firstOrNull()).toImmutableList() + } +} + @Composable private fun ScriptMatchDetailContent( selectedSentence: SentenceAnalysisUiModel?, @@ -155,17 +243,55 @@ private fun ScriptMatchDetailContent( } visibleMismatchDetails.forEach { detail -> - SentenceAnalysisCard( + ScriptMatchAnalysisAccordion( detail = detail, highlighted = detail == selectedSentence, - text = detail.mainFeedback, - subText = detail.subFeedback, - useStatusTextColor = false, - status = detail.scriptMatchStatus, ) } } +@Composable +private fun ScriptMatchAnalysisAccordion( + detail: SentenceAnalysisUiModel, + highlighted: Boolean, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(PrezelTheme.shapes.V8) + .background(if (highlighted) PrezelTheme.colors.bgMedium else Color.Transparent), + ) { + Row( + modifier = Modifier.padding( + start = PrezelTheme.spacing.V12, + top = PrezelTheme.spacing.V12, + end = PrezelTheme.spacing.V12, + ), + horizontalArrangement = Arrangement.spacedBy(PrezelTheme.spacing.V8), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = detail.startTimeMs.toPlayerTimeText(), + style = PrezelTheme.typography.caption1Regular, + color = PrezelTheme.colors.textRegular, + ) + StatusChip(status = detail.scriptMatchStatus) + } + + PrezelAccordion( + title = detail.mainFeedback, + initiallyExpanded = false, + ) { + Text( + text = detail.guideScript, + modifier = Modifier.padding(PrezelTheme.spacing.V12), + style = PrezelTheme.typography.body3Regular, + color = PrezelTheme.colors.textMedium, + ) + } + } +} + private fun ImmutableList.visibleScriptMatchDetails( selectedSentence: SentenceAnalysisUiModel?, expanded: Boolean, @@ -206,6 +332,42 @@ private fun AccuracyDetailPlayerSheetScriptMatchPreview() { } } +@BasicPreview +@Composable +private fun AccuracyDetailPlayerSheetAllAccuratePreview() { + PrezelTheme { + AccuracyDetailPlayerSheet( + selectedTab = AccuracyDetailTab.SPEECH, + selectedSentence = PreviewAllAccurateSentenceDetails.first(), + sentenceDetails = PreviewAllAccurateSentenceDetails, + playerState = rememberPrezelPlayerState( + durationMillis = 11_300L, + currentMillis = 0L, + initialItems = persistentListOf(), + ), + expanded = false, + ) + } +} + +@BasicPreview +@Composable +private fun AccuracyDetailPlayerSheetAllScriptMatchedPreview() { + PrezelTheme { + AccuracyDetailPlayerSheet( + selectedTab = AccuracyDetailTab.SCRIPT_MATCH, + selectedSentence = PreviewAllAccurateSentenceDetails.first(), + sentenceDetails = PreviewAllAccurateSentenceDetails, + playerState = rememberPrezelPlayerState( + durationMillis = 11_300L, + currentMillis = 7_230L, + initialItems = persistentListOf(), + ), + expanded = false, + ) + } +} + @BasicPreview @Composable private fun AccuracyDetailPlayerSheetExpandedPreview() { @@ -274,7 +436,7 @@ private val PreviewSentenceDetails = persistentListOf( SentenceAnalysisUiModel( sentence = "오늘도 다들 긴장되는 마음으로 오셨을 것 같습니다.", status = WordAnalysisStatus.OMISSION, - mainFeedback = "오늘도 다들 긴장되는 마음으로 오셨을 것 같습니다.", + mainFeedback = "말하지 않고 넘어갔어요", subFeedback = "대본에 있으나 읽지 않은 구간이에요.", accuracy = 0.0, startTimeMs = 9_400L, @@ -290,3 +452,44 @@ private val PreviewSentenceDetails = persistentListOf( ), ), ) + +private val PreviewAllAccurateSentenceDetails = persistentListOf( + SentenceAnalysisUiModel( + sentence = "오늘도 다들 긴장되는 마음으로 오셨을 것 같습니다.", + status = WordAnalysisStatus.EXCELLENT, + mainFeedback = "말하지 않고 넘어갔어요", + subFeedback = "모든 단어가 정확하게 발음되었어요.", + accuracy = 98.0, + startTimeMs = 0L, + endTimeMs = 6_800L, + wordDetails = persistentListOf( + WordAnalysisUiModel( + word = "발음", + status = WordAnalysisStatus.EXCELLENT, + accuracy = 99.0, + startTimeMs = 1_200L, + endTimeMs = 1_700L, + ), + ), + ), + SentenceAnalysisUiModel( + sentence = "오늘도 다들 긴장되는 마음으로 오셨을 것 같습니다.", + status = WordAnalysisStatus.GOOD, + mainFeedback = "말하지 않고 넘어갔어요", + subFeedback = "모든 단어가 안정적으로 전달되었어요.", + accuracy = 94.0, + startTimeMs = 7_000L, + endTimeMs = 8_800L, + wordDetails = persistentListOf(), + ), + SentenceAnalysisUiModel( + sentence = "오늘도 다들 긴장되는 마음으로 오셨을 것 같습니다.", + status = WordAnalysisStatus.EXCELLENT, + mainFeedback = "말하지 않고 넘어갔어요", + subFeedback = "또렷한 발음을 유지해주세요.", + accuracy = 97.0, + startTimeMs = 9_000L, + endTimeMs = 11_300L, + wordDetails = persistentListOf(), + ), +) diff --git a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/component/ScriptDetailList.kt b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/component/ScriptDetailList.kt index bbd02dc6..bba24717 100644 --- a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/component/ScriptDetailList.kt +++ b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/component/ScriptDetailList.kt @@ -17,8 +17,11 @@ import com.team.prezel.core.model.presentation.WordAnalysisStatus import com.team.prezel.feature.report.impl.R import com.team.prezel.feature.report.impl.accuracydetail.AccuracyDetailTab import com.team.prezel.feature.report.impl.accuracydetail.model.SentenceAnalysisUiModel +import com.team.prezel.feature.report.impl.accuracydetail.model.isScriptMatchIssue +import com.team.prezel.feature.report.impl.accuracydetail.model.isSpeechAccuracyIssue import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList @Composable internal fun ScriptDetailList( @@ -27,6 +30,9 @@ internal fun ScriptDetailList( sentenceDetails: ImmutableList, ) { val scrollState = rememberScrollState() + val visibleDetails = sentenceDetails + .filter { detail -> detail.isVisibleIn(selectedTab) } + .toImmutableList() LaunchedEffect(selectedTab) { scrollState.scrollTo(0) @@ -39,10 +45,10 @@ internal fun ScriptDetailList( .padding(all = PrezelTheme.spacing.V20), verticalArrangement = Arrangement.spacedBy(PrezelTheme.spacing.V16), ) { - if (sentenceDetails.isNotEmpty()) { - sentenceDetails.forEach { detail -> + if (visibleDetails.isNotEmpty()) { + visibleDetails.forEach { detail -> SentenceAnalysisCard( - detail = detail, + detail = detail.withVisibleWordDetails(selectedTab), highlighted = detail == selectedSentence, showStatusChip = detail.showsStatusChip(selectedTab), highlightWordDetails = true, @@ -55,6 +61,23 @@ internal fun ScriptDetailList( } } +private fun SentenceAnalysisUiModel.isVisibleIn(selectedTab: AccuracyDetailTab): Boolean = + when (selectedTab) { + AccuracyDetailTab.SPEECH -> isSpeechAccuracyIssue + AccuracyDetailTab.SCRIPT_MATCH -> isScriptMatchIssue + } + +private fun SentenceAnalysisUiModel.withVisibleWordDetails(selectedTab: AccuracyDetailTab): SentenceAnalysisUiModel = + copy( + wordDetails = wordDetails + .filter { word -> + when (selectedTab) { + AccuracyDetailTab.SPEECH -> word.status.isSpeechAccuracyIssue + AccuracyDetailTab.SCRIPT_MATCH -> word.status.isScriptMatchIssue + } + }.toImmutableList(), + ) + private val AccuracyDetailTab.emptyDetailTextResId: Int get() = when (this) { AccuracyDetailTab.SPEECH -> R.string.feature_report_impl_accuracy_detail_sheet_empty_speech @@ -63,7 +86,7 @@ private val AccuracyDetailTab.emptyDetailTextResId: Int private fun SentenceAnalysisUiModel.showsStatusChip(selectedTab: AccuracyDetailTab): Boolean = when (selectedTab) { - AccuracyDetailTab.SPEECH -> hasSpeechAccuracyStatus + AccuracyDetailTab.SPEECH -> isSpeechAccuracyIssue AccuracyDetailTab.SCRIPT_MATCH -> isScriptMatchIssue } diff --git a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/component/SentenceAnalysisCard.kt b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/component/SentenceAnalysisCard.kt index ee36acc9..a75febce 100644 --- a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/component/SentenceAnalysisCard.kt +++ b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/component/SentenceAnalysisCard.kt @@ -196,9 +196,10 @@ private fun WordAnalysisStatus.chipTextColor(): Color = when (this) { WordAnalysisStatus.INSERTION, WordAnalysisStatus.MISPRONUNCIATION, + WordAnalysisStatus.STUTTER, + WordAnalysisStatus.OMISSION, -> PrezelTheme.colors.feedbackWarningRegular - WordAnalysisStatus.OMISSION -> PrezelTheme.colors.textRegular else -> PrezelTheme.colors.interactiveRegular } @@ -207,9 +208,10 @@ private fun WordAnalysisStatus.chipBackgroundColor(): Color = when (this) { WordAnalysisStatus.INSERTION, WordAnalysisStatus.MISPRONUNCIATION, + WordAnalysisStatus.STUTTER, + WordAnalysisStatus.OMISSION, -> PrezelTheme.colors.feedbackWarningSmall - WordAnalysisStatus.OMISSION -> PrezelTheme.colors.bgLarge else -> PrezelTheme.colors.interactiveXSmall } diff --git a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/model/SentenceAnalysisUiModel.kt b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/model/SentenceAnalysisUiModel.kt index 5134a9ce..e7d5d2da 100644 --- a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/model/SentenceAnalysisUiModel.kt +++ b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/accuracydetail/model/SentenceAnalysisUiModel.kt @@ -13,6 +13,7 @@ internal data class SentenceAnalysisUiModel( val status: WordAnalysisStatus, val mainFeedback: String, val subFeedback: String, + val guideScript: String = "", val accuracy: Double, val startTimeMs: Long, val endTimeMs: Long, @@ -24,14 +25,11 @@ internal data class SentenceAnalysisUiModel( val isSpeechAccuracyIssue: Boolean get() = status.isSpeechAccuracyIssue || wordDetails.any { word -> word.status.isSpeechAccuracyIssue } - val hasSpeechAccuracyStatus: Boolean - get() = status.isSpeechAccuracyStatus || wordDetails.any { word -> word.status.isSpeechAccuracyStatus } - val scriptMatchStatus: WordAnalysisStatus get() = wordDetails.firstOrNull { word -> word.status.isScriptMatchIssue }?.status ?: status val speechAccuracyStatus: WordAnalysisStatus - get() = wordDetails.firstOrNull { word -> word.status.isSpeechAccuracyStatus }?.status ?: status + get() = wordDetails.firstOrNull { word -> word.status.isSpeechAccuracyIssue }?.status ?: status } @Immutable @@ -43,18 +41,14 @@ internal data class WordAnalysisUiModel( val endTimeMs: Long, ) -private val WordAnalysisStatus.isScriptMatchIssue: Boolean - get() = this == WordAnalysisStatus.INSERTION || - this == WordAnalysisStatus.OMISSION - -private val WordAnalysisStatus.isSpeechAccuracyIssue: Boolean - get() = this == WordAnalysisStatus.STUTTER || +internal val WordAnalysisStatus.isScriptMatchIssue: Boolean + get() = this == WordAnalysisStatus.OMISSION || this == WordAnalysisStatus.MISPRONUNCIATION -private val WordAnalysisStatus.isSpeechAccuracyStatus: Boolean +internal val WordAnalysisStatus.isSpeechAccuracyIssue: Boolean get() = this == WordAnalysisStatus.EXCELLENT || - this == WordAnalysisStatus.GOOD || - this == WordAnalysisStatus.STUTTER + this == WordAnalysisStatus.STUTTER || + this == WordAnalysisStatus.INSERTION internal fun ImmutableList.toUiModels(): ImmutableList = map { detail -> detail.toUiModel() }.toImmutableList() @@ -65,6 +59,7 @@ private fun SentenceAnalysisDetail.toUiModel(): SentenceAnalysisUiModel = status = status, mainFeedback = mainFeedback, subFeedback = subFeedback, + guideScript = guideScript, accuracy = accuracy, startTimeMs = startTimeMs, endTimeMs = endTimeMs, diff --git a/Prezel/feature/report/impl/src/main/res/values/strings.xml b/Prezel/feature/report/impl/src/main/res/values/strings.xml index 085ec0df..58883545 100644 --- a/Prezel/feature/report/impl/src/main/res/values/strings.xml +++ b/Prezel/feature/report/impl/src/main/res/values/strings.xml @@ -66,8 +66,9 @@ 닫기 정확도 상세를 불러오지 못했습니다. 발화 분석 상세가 없어요. - 모든 단어가 정확하게 발음되었어요. - 대본과 일치하지 않는 부분이 없어요. + 안정적으로 발음했어요 + 대본과 동일하게 말했어요 + 전체 정확도 상세 오디오 트랙 발음 불필요한 표현