From 3e10e49b9866fea00f524e9611402b24f8dca7c6 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Sun, 2 Aug 2026 21:15:23 +0900 Subject: [PATCH 1/3] =?UTF-8?q?style:=20PrezelTabs=20=EA=B5=AC=EB=B6=84?= =?UTF-8?q?=EC=84=A0=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=9D=B8=EB=94=94?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=ED=84=B0=20=EB=91=90=EA=BB=98=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `SecondaryTabRow`에 `HorizontalDivider`를 추가하여 탭 바 하단 구분선 구현 - 탭 인디케이터의 높이를 하드코딩된 값(2.dp)에서 `PrezelTheme.stroke.V2` 테마 토큰으로 변경하여 디자인 일관성 확보 --- .../core/designsystem/component/navigations/PrezelTabs.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/navigations/PrezelTabs.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/navigations/PrezelTabs.kt index 732c8996..8bb3d443 100644 --- a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/navigations/PrezelTabs.kt +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/navigations/PrezelTabs.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.pager.PagerState +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.SecondaryTabRow import androidx.compose.material3.Tab import androidx.compose.material3.TabIndicatorScope @@ -30,6 +31,7 @@ fun PrezelTabs( selectedTabIndex = pagerState.currentPage, modifier = modifier.fillMaxWidth(), containerColor = Color.Transparent, + divider = { HorizontalDivider(thickness = PrezelTheme.stroke.V2) }, indicator = { PrezelTabIndicator(selectedTabIndex = pagerState.currentPage) }, ) { tabs.forEachIndexed { index, label -> @@ -51,7 +53,7 @@ private fun TabIndicatorScope.PrezelTabIndicator( Spacer( modifier = modifier .fillMaxWidth() - .height(2.dp) + .height(PrezelTheme.stroke.V2) .tabIndicatorOffset(selectedTabIndex = selectedTabIndex) .background(PrezelTheme.colors.solidBlack), ) From 97c96d6f6ed3d2a066b6efa40ee443daa0a142b4 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Sun, 2 Aug 2026 22:56:42 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EB=8C=80=EB=B3=B8=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EC=8A=A4=ED=81=AC=EB=A1=A4=EB=B0=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20PrezelTextArea=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `PrezelTextArea`를 `TextFieldState` 기반의 최신 Compose API로 전환하여 상태 관리 개선 - 텍스트 입력 영역 내 커스텀 스크롤바(`PrezelTextAreaScrollbar`) 구현 및 적용 - 스크롤 위치에 따라 하단 버튼 영역의 구분선(`alwaysShowButtonAreaDivider`) 노출 로직 추가 - `ScriptInputScreen`에서 입력창과 레이아웃 간의 `ScrollState` 공유 및 제어 로직 최적화 --- .../component/textfield/PrezelTextArea.kt | 196 +++++++++++++++--- .../analysis/impl/script/ScriptInputScreen.kt | 18 +- 2 files changed, 179 insertions(+), 35 deletions(-) diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/textfield/PrezelTextArea.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/textfield/PrezelTextArea.kt index 3ba202ed..a0adb29a 100644 --- a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/textfield/PrezelTextArea.kt +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/textfield/PrezelTextArea.kt @@ -1,5 +1,7 @@ package com.team.prezel.core.designsystem.component.textfield +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -10,9 +12,17 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActionScope import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.KeyboardActionHandler +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.maxLength +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -20,13 +30,17 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -37,6 +51,7 @@ import com.team.prezel.core.designsystem.preview.BasicPreview import com.team.prezel.core.designsystem.preview.PreviewColumn import com.team.prezel.core.designsystem.preview.PreviewSurface import com.team.prezel.core.designsystem.theme.PrezelTheme +import kotlin.math.min @Composable fun PrezelTextArea( @@ -51,33 +66,38 @@ fun PrezelTextArea( showCount: Boolean = false, minHeight: Dp = 72.dp, fillContainerHeight: Boolean = false, + scrollState: ScrollState = rememberScrollState(), keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, ) { + require(maxLength >= 0) { "maxLength는 0 이상이어야 합니다." } + var focused by remember { mutableStateOf(false) } - var textFieldValue by remember { mutableStateOf(TextFieldValue(text = value, selection = TextRange(value.length))) } + val textFieldState = remember { TextFieldState(initialText = value) } + val currentValue by rememberUpdatedState(value) + val currentOnValueChange by rememberUpdatedState(onValueChange) LaunchedEffect(value) { - if (value != textFieldValue.text) { - textFieldValue = TextFieldValue(text = value, selection = TextRange(value.length)) + if (value != textFieldState.text.toString()) { + textFieldState.setTextAndPlaceCursorAtEnd(value) } } + LaunchedEffect(textFieldState) { + snapshotFlow { textFieldState.text.toString() } + .collect { newValue -> + if (newValue != currentValue) currentOnValueChange(newValue) + } + } + val style = rememberPrezelTextFieldState( - value = textFieldValue.text, + value = textFieldState.text.toString(), enabled = enabled, focused = focused, ).let { state -> PrezelTextFieldStyle(state = state, status = status) } PrezelTextArea( - value = textFieldValue, - onValueChange = { newValue -> - val applied = applyPrezelTextInputPolicy(currentValue = textFieldValue, newValue = newValue, maxLength = maxLength) - if (applied != textFieldValue) { - textFieldValue = applied - if (applied.text != value) onValueChange(applied.text) - } - }, + textFieldState = textFieldState, placeholder = placeholder, style = style, maxLength = maxLength, @@ -89,6 +109,7 @@ fun PrezelTextArea( showCount = showCount, minHeight = minHeight, fillContainerHeight = fillContainerHeight, + scrollState = scrollState, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, ) @@ -96,8 +117,7 @@ fun PrezelTextArea( @Composable private fun PrezelTextArea( - value: TextFieldValue, - onValueChange: (TextFieldValue) -> Unit, + textFieldState: TextFieldState, placeholder: String, maxLength: Int, style: PrezelTextFieldStyle, @@ -106,12 +126,17 @@ private fun PrezelTextArea( label: String?, enabled: Boolean, showCount: Boolean, + modifier: Modifier = Modifier, minHeight: Dp = 72.dp, fillContainerHeight: Boolean, + scrollState: ScrollState, keyboardOptions: KeyboardOptions, keyboardActions: KeyboardActions, - modifier: Modifier = Modifier, ) { + val keyboardActionHandler = remember(keyboardActions, keyboardOptions.imeAction) { + keyboardActions.toKeyboardActionHandler(keyboardOptions.imeAction) + } + Column(modifier = modifier.fillMaxWidth()) { label?.let { PrezelTextFieldLabel(label = it) @@ -119,8 +144,7 @@ private fun PrezelTextArea( } BasicTextField( - value = value, - onValueChange = onValueChange, + state = textFieldState, enabled = enabled, modifier = Modifier .fillMaxWidth() @@ -134,20 +158,26 @@ private fun PrezelTextArea( textStyle = PrezelTheme.typography.body2Regular.copy(color = style.textColor()), cursorBrush = SolidColor(PrezelTheme.colors.interactiveRegular), keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - decorationBox = { innerTextField -> + onKeyboardAction = keyboardActionHandler, + inputTransformation = InputTransformation.maxLength(maxLength), + scrollState = scrollState, + decorator = { innerTextField -> PrezelTextAreaDecorationBox( innerTextField = innerTextField, - showPlaceholder = !focused && value.text.isEmpty(), + showPlaceholder = !focused && textFieldState.text.isEmpty(), placeholder = placeholder, state = style, showCounter = showCount, + scrollState = scrollState, counter = { if (showCount) { - Counter(currentLength = value.text.length, maxLength = maxLength, state = style) + Counter(currentLength = textFieldState.text.length, maxLength = maxLength, state = style) } }, - modifier = if (fillContainerHeight) Modifier.fillMaxHeight() else Modifier.heightIn(min = minHeight), + modifier = when { + fillContainerHeight -> Modifier.fillMaxHeight() + else -> Modifier.heightIn(min = minHeight) + }, fillContainerHeight = fillContainerHeight, ) }, @@ -184,6 +214,7 @@ private fun PrezelTextAreaDecorationBox( placeholder: String, state: PrezelTextFieldStyle, showCounter: Boolean, + scrollState: ScrollState, modifier: Modifier = Modifier, fillContainerHeight: Boolean = false, ) { @@ -194,15 +225,27 @@ private fun PrezelTextAreaDecorationBox( border = state.borderStroke(), contentColor = state.textColor(), ) { + val showScrollbar = scrollState.maxValue > 0 + val endPadding = if (showScrollbar) PrezelTheme.spacing.V8 else PrezelTheme.spacing.V12 + val scrollbarContentPadding = if (showScrollbar) PrezelTheme.spacing.V8 else 0.dp + Box( modifier = Modifier .then(if (fillContainerHeight) Modifier.fillMaxSize() else Modifier.fillMaxWidth()) - .padding(PrezelTheme.spacing.V12), + .padding( + start = PrezelTheme.spacing.V12, + top = PrezelTheme.spacing.V12, + end = endPadding, + bottom = PrezelTheme.spacing.V12, + ), ) { Box( modifier = Modifier .then(if (fillContainerHeight) Modifier.fillMaxSize() else Modifier.fillMaxWidth()) - .padding(bottom = if (showCounter) PrezelTheme.spacing.V24 else 0.dp), + .padding( + end = scrollbarContentPadding, + bottom = if (showCounter) PrezelTheme.spacing.V24 else 0.dp, + ), ) { innerTextField() if (showPlaceholder) { @@ -210,13 +253,74 @@ private fun PrezelTextAreaDecorationBox( } } - Box(modifier = Modifier.align(Alignment.BottomEnd)) { + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = scrollbarContentPadding), + ) { counter() } + + if (showScrollbar) { + PrezelTextAreaScrollbar( + scrollState = scrollState, + modifier = Modifier + .align(Alignment.CenterEnd) + .fillMaxHeight() + .width(PrezelTheme.spacing.V4), + ) + } } } } +@Composable +private fun PrezelTextAreaScrollbar( + scrollState: ScrollState, + modifier: Modifier = Modifier, +) { + val color = PrezelTheme.colors.borderLarge + val minThumbHeight = PrezelTheme.spacing.V24 + + Canvas(modifier = modifier) { + if (scrollState.maxValue <= 0 || scrollState.viewportSize <= 0) return@Canvas + + val viewportHeight = scrollState.viewportSize.toFloat() + val contentHeight = viewportHeight + scrollState.maxValue + val minThumbHeightPx = min(minThumbHeight.toPx(), size.height) + val thumbHeight = (size.height * viewportHeight / contentHeight) + .coerceIn(minThumbHeightPx, size.height) + val thumbOffset = (size.height - thumbHeight) * scrollState.value / scrollState.maxValue + + drawRoundRect( + color = color, + topLeft = Offset(x = 0f, y = thumbOffset), + size = Size(width = size.width, height = thumbHeight), + cornerRadius = CornerRadius(size.width / 2f), + ) + } +} + +private fun KeyboardActions.toKeyboardActionHandler(imeAction: ImeAction): KeyboardActionHandler? { + val action = when (imeAction) { + ImeAction.Done -> onDone + ImeAction.Go -> onGo + ImeAction.Next -> onNext + ImeAction.Previous -> onPrevious + ImeAction.Search -> onSearch + ImeAction.Send -> onSend + else -> null + } ?: return null + + return KeyboardActionHandler { performDefaultAction -> + action.invoke( + object : KeyboardActionScope { + override fun defaultKeyboardAction(imeAction: ImeAction) = performDefaultAction() + }, + ) + } +} + @BasicPreview @Composable private fun PrezelTextAreaDefaultStatePreview() { @@ -310,6 +414,28 @@ private fun PrezelTextAreaTypedStatePreview() { } } +@BasicPreview +@Composable +private fun PrezelTextAreaScrollbarPreview() { + PreviewTextAreaState(title = "Type - With Scrollbar") { + PrezelTextAreaPreviewItem( + label = "Label", + value = "Lorem ipsum dolor sit amet consectetur. Consequat quis viverra nulla in aliquam sed " + + "scelerisque odio gravida. At urna congue vulputate facilisis id et viverra pellentesque " + + "tempus. Blandit et faucibus iaculis dictum pharetra. Magna elit lacus nullam facilisi amet " + + "urna pulvinar.", + state = PrezelTextFieldStyle( + state = PrezelTextFieldState.TYPED, + status = PrezelTextFieldStatus.Default("Helper"), + ), + modifier = Modifier.height(200.dp), + showCount = false, + fillContainerHeight = true, + maxLength = 500, + ) + } +} + @Composable private fun PreviewTextAreaState( title: String, @@ -338,20 +464,26 @@ private fun PrezelTextAreaPreviewItem( modifier: Modifier = Modifier, enabled: Boolean = true, focused: Boolean = false, + showCount: Boolean = true, + fillContainerHeight: Boolean = false, + maxLength: Int = 100, ) { + val textFieldState = remember { TextFieldState(initialText = value) } + PrezelTextArea( - value = TextFieldValue(text = value, selection = TextRange(value.length)), - onValueChange = {}, + textFieldState = textFieldState, placeholder = "Placeholder", label = label, style = state, - maxLength = 100, + maxLength = maxLength, focused = focused, modifier = modifier, onFocusChange = {}, enabled = enabled, - showCount = true, - fillContainerHeight = false, + showCount = showCount, + minHeight = 72.dp, + fillContainerHeight = fillContainerHeight, + scrollState = rememberScrollState(), keyboardOptions = KeyboardOptions.Default, keyboardActions = KeyboardActions.Default, ) diff --git a/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/script/ScriptInputScreen.kt b/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/script/ScriptInputScreen.kt index eb3d6562..897ddfbc 100644 --- a/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/script/ScriptInputScreen.kt +++ b/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/script/ScriptInputScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.Image +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer @@ -14,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -134,6 +136,7 @@ private fun ScriptInputScreen( onBack: () -> Unit, ) { var pendingInputTypeChange by remember { mutableStateOf(null) } + val textAreaScrollState = rememberScrollState() pendingInputTypeChange?.let { inputType -> ScriptInputTypeChangeDialog( @@ -157,7 +160,9 @@ private fun ScriptInputScreen( isHiddenOptions = isHiddenOptions, trailingText = stringResource(R.string.feature_analysis_impl_skip), onTrailingTextClick = onSkip, - contentScrollable = form.scriptInputType == ScriptInputType.DIRECT_INPUT, + contentScrollable = false, + alwaysShowButtonAreaDivider = + form.scriptInputType == ScriptInputType.DIRECT_INPUT && textAreaScrollState.maxValue > 0, ) { AnalysisStepTitle( title = stringResource(R.string.feature_analysis_impl_script_headline), @@ -175,6 +180,7 @@ private fun ScriptInputScreen( pendingInputTypeChange = inputType }, onScriptChange = onScriptChange, + textAreaScrollState = textAreaScrollState, onScriptFileUploadClick = onScriptFileUploadClick, onScriptFileClear = onScriptFileClear, ) @@ -189,6 +195,7 @@ private fun ColumnScope.ScriptInputContent( onSelectInputType: (ScriptInputType) -> Unit, onRequestInputTypeChange: (ScriptInputType) -> Unit, onScriptChange: (String) -> Unit, + textAreaScrollState: ScrollState, onScriptFileUploadClick: () -> Unit, onScriptFileClear: () -> Unit, ) { @@ -221,6 +228,8 @@ private fun ColumnScope.ScriptInputContent( ScriptInputType.DIRECT_INPUT -> DirectScriptInput( script = form.script, onScriptChange = onScriptChange, + scrollState = textAreaScrollState, + modifier = Modifier.weight(weight = 1f, fill = false), ) } } @@ -229,14 +238,17 @@ private fun ColumnScope.ScriptInputContent( private fun DirectScriptInput( script: String, onScriptChange: (String) -> Unit, + scrollState: ScrollState, + modifier: Modifier = Modifier, ) { PrezelTextArea( value = script, onValueChange = onScriptChange, placeholder = stringResource(R.string.feature_analysis_impl_script_placeholder), maxLength = SCRIPT_MAX_LENGTH, - minHeight = 122.dp, - modifier = Modifier.fillMaxWidth(), + minHeight = 82.dp, + scrollState = scrollState, + modifier = modifier.fillMaxWidth(), ) Spacer(modifier = Modifier.height(PrezelTheme.spacing.V16)) } From f02029cadb97aade1a689229023f46fd9bda0727 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Mon, 3 Aug 2026 00:41:33 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EC=A0=95=ED=99=95=EB=8F=84=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EC=B5=9C=EC=A0=81=ED=99=94=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B0=80=EC=9D=B4=EB=93=9C=20=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=20=ED=95=98=EC=9D=B4=EB=9D=BC=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `BoxWithConstraints`를 활용하여 화면 크기에 따른 바텀 시트 최대 높이 계산 방식 개선 - 가이드 스크립트 내에서 분석 결과에 따라 특정 단어를 강조(Highlight)하는 기능 추가 - 바텀 시트 축소 상태에서 스크립트 일치 항목을 아코디언 대신 카드 형태로 노출하도록 UI 수정 - 탭 전환 시 현재 선택된 문장 또는 첫 번째 오류 항목이 우선적으로 강조되도록 로직 개선 - 확장 상태에 따른 `sheetShadowElevation` 조정 및 불필요한 레이아웃 상수 제거 --- .../accuracydetail/AccuracyDetailScreen.kt | 100 +++++++++--------- .../component/AccuracyDetailPlayerSheet.kt | 70 +++++++++--- 2 files changed, 106 insertions(+), 64 deletions(-) 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 61043964..fcf5354f 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,7 +1,7 @@ package com.team.prezel.feature.report.impl.accuracydetail -import android.annotation.SuppressLint import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.heightIn @@ -22,10 +22,8 @@ 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 @@ -138,7 +136,6 @@ private fun AccuracyDetailScreenContent( AccuracyDetailTab.SCRIPT_MATCH -> sentenceDetails.filter { detail -> detail.isScriptMatchIssue } }.toImmutableList() } - val sheetPeekHeight = AccuracyDetailPlayerSheetPeekHeight val playerState = rememberDetailPlayerState( selectedTab = selectedTab, sentenceDetails = sentenceDetails, @@ -165,7 +162,6 @@ private fun AccuracyDetailScreenContent( sentenceDetails = sentenceDetails, playerState = playerState, expanded = isSheetExpanded, - sheetPeekHeight = sheetPeekHeight, onClose = onClose, tabLabels = tabLabels, onClickTab = { index -> pagerState.requestScrollToPage(index) }, @@ -259,7 +255,6 @@ private fun PlaybackEffect( } } -@SuppressLint("ConfigurationScreenWidthHeight") @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AccuracyDetailScaffold( @@ -269,64 +264,65 @@ private fun AccuracyDetailScaffold( sentenceDetails: ImmutableList, playerState: PrezelPlayerState, expanded: Boolean, - sheetPeekHeight: Dp, onClose: () -> Unit, tabLabels: ImmutableList, onClickTab: (Int) -> Unit, pagerState: PagerState, ) { - val configuration = LocalConfiguration.current - val expandedSheetMaxHeight = configuration.screenHeightDp.dp - AccuracyDetailExpandedSheetTopGap + BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + val expandedSheetMaxHeight = (maxHeight - 48.dp).coerceAtLeast(0.dp) - BottomSheetScaffold( - modifier = Modifier.fillMaxSize(), - scaffoldState = scaffoldState, - sheetPeekHeight = sheetPeekHeight, - sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), - sheetContainerColor = PrezelTheme.colors.solidWhite, - sheetShadowElevation = 12.dp, - sheetContent = { - AccuracyDetailPlayerSheet( - selectedTab = selectedTab, - selectedSentence = selectedSentence, - sentenceDetails = sentenceDetails, - playerState = playerState, - expanded = expanded, - modifier = if (expanded) { - Modifier.heightIn(max = expandedSheetMaxHeight) - } else { - Modifier - }, - ) - }, - sheetDragHandle = null, - containerColor = PrezelTheme.colors.bgRegular, - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding), - ) { - if (!expanded) { - AccuracyDetailTopAppBar(onClose = onClose) + BottomSheetScaffold( + modifier = Modifier.fillMaxSize(), + scaffoldState = scaffoldState, + sheetPeekHeight = 276.dp, + sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + sheetContainerColor = PrezelTheme.colors.solidWhite, + sheetShadowElevation = if (expanded) 0.dp else 12.dp, + sheetContent = { + AccuracyDetailPlayerSheet( + selectedTab = selectedTab, + selectedSentence = selectedSentence, + sentenceDetails = sentenceDetails, + playerState = playerState, + expanded = expanded, + modifier = Modifier + .then( + if (expanded) { + Modifier.heightIn(max = expandedSheetMaxHeight) + } else { + Modifier + }, + ), + ) + }, + sheetDragHandle = null, + containerColor = PrezelTheme.colors.bgRegular, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + ) { + if (!expanded) { + AccuracyDetailTopAppBar(onClose = onClose) + } + PrezelTabs( + tabs = tabLabels, + pagerState = pagerState, + onClickTab = onClickTab, + ) + ScriptDetailList( + selectedTab = selectedTab, + selectedSentence = selectedSentence, + sentenceDetails = sentenceDetails, + ) } - PrezelTabs( - tabs = tabLabels, - pagerState = pagerState, - onClickTab = onClickTab, - ) - ScriptDetailList( - selectedTab = selectedTab, - selectedSentence = selectedSentence, - sentenceDetails = sentenceDetails, - ) } } } private const val SEEK_SYNC_THRESHOLD_MILLIS = 750L -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 33cad7bd..d559038d 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 @@ -9,6 +9,7 @@ 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 import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -21,6 +22,8 @@ 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.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.unit.dp import com.team.prezel.core.designsystem.component.PrezelAccordion import com.team.prezel.core.designsystem.component.chip.chip.ChipHierarchy @@ -37,6 +40,7 @@ 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.WordAnalysisUiModel +import com.team.prezel.feature.report.impl.accuracydetail.model.isScriptMatchIssue import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -144,7 +148,6 @@ private fun SheetDetailContent( } } } - Spacer(modifier = Modifier.height(PrezelTheme.spacing.V12)) } private fun ImmutableList.hasNoVisibleDetails( @@ -199,6 +202,9 @@ private fun SpeechDetailContent( selectedSentence = selectedSentence, expanded = expanded, ) + val highlightedDetail = selectedSentence + ?.takeIf { detail -> detail.isSpeechAccuracyIssue } + ?: visibleAccuracyDetails.firstOrNull() if (visibleAccuracyDetails.isEmpty()) { EmptyDetailText(text = stringResource(R.string.feature_report_impl_accuracy_detail_sheet_empty_speech)) @@ -206,7 +212,7 @@ private fun SpeechDetailContent( visibleAccuracyDetails.forEach { detail -> SentenceAnalysisCard( detail = detail, - highlighted = detail == selectedSentence, + highlighted = detail == highlightedDetail, subText = detail.subFeedback.takeIf { expanded }, useStatusTextColor = false, status = detail.speechAccuracyStatus, @@ -237,16 +243,30 @@ private fun ScriptMatchDetailContent( selectedSentence = selectedSentence, expanded = expanded, ) + val highlightedDetail = selectedSentence + ?.takeIf { detail -> detail.isScriptMatchIssue } + ?: visibleMismatchDetails.firstOrNull() if (visibleMismatchDetails.isEmpty()) { EmptyDetailText(text = stringResource(R.string.feature_report_impl_accuracy_detail_sheet_empty_script)) } visibleMismatchDetails.forEach { detail -> - ScriptMatchAnalysisAccordion( - detail = detail, - highlighted = detail == selectedSentence, - ) + if (expanded) { + ScriptMatchAnalysisAccordion( + detail = detail, + highlighted = detail == highlightedDetail, + ) + } else { + SentenceAnalysisCard( + detail = detail, + highlighted = detail == highlightedDetail, + modifier = Modifier.heightIn(min = 104.dp), + text = detail.mainFeedback, + useStatusTextColor = false, + status = detail.scriptMatchStatus, + ) + } } } @@ -282,14 +302,40 @@ private fun ScriptMatchAnalysisAccordion( title = detail.mainFeedback, initiallyExpanded = false, ) { - Text( - text = detail.guideScript, - modifier = Modifier.padding(PrezelTheme.spacing.V12), - style = PrezelTheme.typography.body3Regular, - color = PrezelTheme.colors.textMedium, - ) + GuideScriptText(detail = detail) + } + } +} + +@Composable +private fun GuideScriptText(detail: SentenceAnalysisUiModel) { + val feedbackGoodColor = PrezelTheme.colors.feedbackGoodRegular + val guideScript = buildAnnotatedString { + append(detail.guideScript) + + var searchFrom = 0 + detail.wordDetails.forEach { wordDetail -> + val start = detail.guideScript.indexOf(wordDetail.word, startIndex = searchFrom) + if (start < 0) return@forEach + + val end = start + wordDetail.word.length + searchFrom = end + if (wordDetail.status.isScriptMatchIssue) { + addStyle( + style = SpanStyle(color = feedbackGoodColor), + start = start, + end = end, + ) + } } } + + Text( + text = guideScript, + modifier = Modifier.padding(PrezelTheme.spacing.V12), + style = PrezelTheme.typography.body3Regular, + color = PrezelTheme.colors.textMedium, + ) } private fun ImmutableList.visibleScriptMatchDetails(