diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/Emotion.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/Emotion.kt index 5a4f72a3..931074a1 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/Emotion.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/Emotion.kt @@ -34,3 +34,19 @@ val Emotion.graphicRes: Int Emotion.SAD -> R.drawable.img_emotion_sadness Emotion.INSIGHT -> R.drawable.img_emotion_insight } + +val Emotion.graphicResV2: Int + get() = when (this) { + Emotion.WARM -> R.drawable.img_category_warm + Emotion.JOY -> R.drawable.img_category_joy + Emotion.SAD -> R.drawable.img_category_sad + Emotion.INSIGHT -> R.drawable.img_category_insight + } + +val Emotion.descriptionRes: Int + get() = when (this) { + Emotion.WARM -> R.string.emotion_warm_description + Emotion.JOY -> R.string.emotion_joy_description + Emotion.SAD -> R.string.emotion_sad_description + Emotion.INSIGHT -> R.string.emotion_insight_description + } diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/chip/ChipSizeStyle.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/chip/ChipSizeStyle.kt new file mode 100644 index 00000000..360c41b5 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/chip/ChipSizeStyle.kt @@ -0,0 +1,29 @@ +package com.ninecraft.booket.core.designsystem.component.chip + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.TextStyle +import com.ninecraft.booket.core.designsystem.theme.ReedTheme + +data class ChipSizeStyle( + val paddingValues: PaddingValues, + val textStyle: TextStyle, +) + +val mediumChipStyle: ChipSizeStyle + @Composable get() = ChipSizeStyle( + paddingValues = PaddingValues( + horizontal = ReedTheme.spacing.spacing3, + vertical = ReedTheme.spacing.spacing2, + ), + textStyle = ReedTheme.typography.body2Medium, + ) + +val smallChipStyle: ChipSizeStyle + @Composable get() = ChipSizeStyle( + paddingValues = PaddingValues( + horizontal = ReedTheme.spacing.spacing3, + vertical = ReedTheme.spacing.spacing15, + ), + textStyle = ReedTheme.typography.label1Medium, + ) diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/chip/ReedRemovableChip.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/chip/ReedRemovableChip.kt new file mode 100644 index 00000000..df72cccf --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/chip/ReedRemovableChip.kt @@ -0,0 +1,75 @@ +package com.ninecraft.booket.core.designsystem.component.chip + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +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.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.ninecraft.booket.core.common.extensions.noRippleClickable +import com.ninecraft.booket.core.designsystem.ComponentPreview +import com.ninecraft.booket.core.designsystem.R +import com.ninecraft.booket.core.designsystem.theme.ReedTheme + +@Composable +fun ReedRemovableChip( + label: String, + chipSizeStyle: ChipSizeStyle, + onRemove: () -> Unit, + modifier: Modifier = Modifier, +) { + val cornerShape = RoundedCornerShape(ReedTheme.radius.full) + + Row( + modifier = modifier + .clip(cornerShape) + .background(color = ReedTheme.colors.bgTertiary) + .border( + width = 1.dp, + color = ReedTheme.colors.borderBrand, + shape = cornerShape, + ) + .padding(chipSizeStyle.paddingValues), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + color = ReedTheme.colors.contentBrand, + style = chipSizeStyle.textStyle, + ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing1)) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_close), + contentDescription = "Icon Close", + tint = ReedTheme.colors.contentBrand, + modifier = Modifier + .size(14.dp) + .noRippleClickable { + onRemove() + }, + ) + } +} + +@ComponentPreview +@Composable +private fun ReedRemovableChipPreview() { + ReedTheme { + ReedRemovableChip( + label = "text", + chipSizeStyle = mediumChipStyle, + onRemove = {}, + ) + } +} diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/chip/ReedSelectableChip.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/chip/ReedSelectableChip.kt new file mode 100644 index 00000000..c75629a9 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/chip/ReedSelectableChip.kt @@ -0,0 +1,89 @@ +package com.ninecraft.booket.core.designsystem.component.chip + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +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.unit.dp +import com.ninecraft.booket.core.common.extensions.noRippleClickable +import com.ninecraft.booket.core.designsystem.ComponentPreview +import com.ninecraft.booket.core.designsystem.theme.ReedTheme + +@Composable +fun ReedSelectableChip( + label: String, + chipSizeStyle: ChipSizeStyle, + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val cornerShape = RoundedCornerShape(ReedTheme.radius.full) + val backgroundColor = if (selected) ReedTheme.colors.bgTertiary else ReedTheme.colors.basePrimary + val borderColor = if (selected) ReedTheme.colors.borderBrand else ReedTheme.colors.borderPrimary + val textColor = if (selected) ReedTheme.colors.contentBrand else ReedTheme.colors.contentSecondary + + Row( + modifier = modifier + .clip(cornerShape) + .background(color = backgroundColor) + .noRippleClickable { + onClick() + } + .border( + width = 1.dp, + color = borderColor, + shape = cornerShape, + ) + .padding(chipSizeStyle.paddingValues), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + color = textColor, + style = chipSizeStyle.textStyle, + ) + } +} + +@ComponentPreview +@Composable +private fun ReedSelectableChipPreview() { + ReedTheme { + Column( + verticalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing1), + ) { + ReedSelectableChip( + label = "text", + chipSizeStyle = mediumChipStyle, + selected = false, + onClick = {}, + ) + ReedSelectableChip( + label = "text", + chipSizeStyle = mediumChipStyle, + selected = true, + onClick = {}, + ) + ReedSelectableChip( + label = "text", + chipSizeStyle = smallChipStyle, + selected = false, + onClick = {}, + ) + ReedSelectableChip( + label = "text", + chipSizeStyle = smallChipStyle, + selected = true, + onClick = {}, + ) + } + } +} diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Spacing.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Spacing.kt index a4dfd5fd..9595ba84 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Spacing.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Spacing.kt @@ -9,6 +9,7 @@ data class ReedSpacing( val spacing0: Dp = 0.dp, val spacing05: Dp = 2.dp, val spacing1: Dp = 4.dp, + val spacing15: Dp = 6.dp, val spacing2: Dp = 8.dp, val spacing3: Dp = 12.dp, val spacing4: Dp = 16.dp, diff --git a/core/designsystem/src/main/res/drawable/img_category_insight.webp b/core/designsystem/src/main/res/drawable/img_category_insight.webp new file mode 100644 index 00000000..b0ad9df0 Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_category_insight.webp differ diff --git a/core/designsystem/src/main/res/drawable/img_category_joy.webp b/core/designsystem/src/main/res/drawable/img_category_joy.webp new file mode 100644 index 00000000..70036262 Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_category_joy.webp differ diff --git a/core/designsystem/src/main/res/drawable/img_category_sad.webp b/core/designsystem/src/main/res/drawable/img_category_sad.webp new file mode 100644 index 00000000..016a7321 Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_category_sad.webp differ diff --git a/core/designsystem/src/main/res/drawable/img_category_warm.webp b/core/designsystem/src/main/res/drawable/img_category_warm.webp new file mode 100644 index 00000000..5ad834e2 Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_category_warm.webp differ diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index a2793c60..674c8c55 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -4,4 +4,8 @@ 알 수 없는 오류가 발생하였습니다. 도서 검색 후 내 서재에 담아보세요 로그인이 필요한 기능입니다 + 공감과 위로가 된 순간 + 흥미롭고 유쾌한 순간 + 눈물이 고인 순간 + 생각이 깊어지는 순간 diff --git a/core/designsystem/stability/designsystem.stability b/core/designsystem/stability/designsystem.stability index 2bf67bed..9261a073 100644 --- a/core/designsystem/stability/designsystem.stability +++ b/core/designsystem/stability/designsystem.stability @@ -163,6 +163,39 @@ public fun com.ninecraft.booket.core.designsystem.component.checkbox.TickOnlyChe - onCheckedChange: STABLE (function type) - modifier: STABLE (marked @Stable or @Immutable) +@Composable +public fun com.ninecraft.booket.core.designsystem.component.chip.(): com.ninecraft.booket.core.designsystem.component.chip.ChipSizeStyle + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.chip.(): com.ninecraft.booket.core.designsystem.component.chip.ChipSizeStyle + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.chip.ReedRemovableChip(label: kotlin.String, chipSizeStyle: com.ninecraft.booket.core.designsystem.component.chip.ChipSizeStyle, onRemove: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - label: STABLE (String is immutable) + - chipSizeStyle: STABLE (class with no mutable properties) + - onRemove: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +public fun com.ninecraft.booket.core.designsystem.component.chip.ReedSelectableChip(label: kotlin.String, chipSizeStyle: com.ninecraft.booket.core.designsystem.component.chip.ChipSizeStyle, selected: kotlin.Boolean, onClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - label: STABLE (String is immutable) + - chipSizeStyle: STABLE (class with no mutable properties) + - selected: STABLE (primitive type) + - onClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + @Composable public fun com.ninecraft.booket.core.designsystem.component.textfield.ReedRecordTextField(recordState: androidx.compose.foundation.text.input.TextFieldState, recordHintRes: kotlin.Int, modifier: androidx.compose.ui.Modifier, inputTransformation: androidx.compose.foundation.text.input.InputTransformation?, keyboardOptions: androidx.compose.foundation.text.KeyboardOptions, lineLimits: androidx.compose.foundation.text.input.TextFieldLineLimits, isError: kotlin.Boolean, errorMessage: kotlin.String, onClear: kotlin.Function0?, onNext: kotlin.Function0, backgroundColor: androidx.compose.ui.graphics.Color, textColor: androidx.compose.ui.graphics.Color, cornerShape: androidx.compose.foundation.shape.RoundedCornerShape, borderStroke: androidx.compose.foundation.BorderStroke): kotlin.Unit skippable: true diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt index 62581d60..8a01ac5c 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt @@ -31,6 +31,9 @@ import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch @@ -61,12 +64,8 @@ class RecordRegisterPresenter( @Composable override fun present(): RecordRegisterUiState { - val scope = rememberCoroutineScope() - var isLoading by rememberRetained { mutableStateOf(false) } - var sideEffect by rememberRetained { mutableStateOf(null) } - var currentStep by rememberRetained { mutableStateOf(RecordStep.QUOTE) } - val recordPageState = rememberTextFieldState() - val recordSentenceState = rememberTextFieldState() + /** 2차 고도화 삭제 예정 ===================================================================== */ + val impressionState = rememberTextFieldState() val impressionGuideList by rememberRetained { mutableStateOf( listOf( @@ -80,13 +79,28 @@ class RecordRegisterPresenter( ).toPersistentList(), ) } - val emotions by rememberRetained { mutableStateOf(Emotion.entries.toPersistentList()) } - var selectedEmotion by rememberRetained { mutableStateOf(null) } var selectedImpressionGuide by rememberRetained { mutableStateOf("") } var beforeSelectedImpressionGuide by rememberRetained { mutableStateOf(selectedImpressionGuide) } - val impressionState = rememberTextFieldState() - var savedRecordId by rememberRetained { mutableStateOf("") } var isImpressionGuideBottomSheetVisible by rememberRetained { mutableStateOf(false) } + var isScanTooltipVisible by rememberRetained { mutableStateOf(true) } + var isImpressionGuideTooltipVisible by rememberRetained { mutableStateOf(true) } + + /** ====================================================================================== */ + val scope = rememberCoroutineScope() + var isLoading by rememberRetained { mutableStateOf(false) } + var sideEffect by rememberRetained { mutableStateOf(null) } + var currentStep by rememberRetained { mutableStateOf(RecordStep.QUOTE) } + val recordPageState = rememberTextFieldState() + val recordSentenceState = rememberTextFieldState() + val memoState = rememberTextFieldState() + val emotions by rememberRetained { mutableStateOf(Emotion.entries.toPersistentList()) } + var emotionDetails by rememberRetained { mutableStateOf(persistentListOf()) } + var selectedEmotion by rememberRetained { mutableStateOf(null) } + var selectedEmotionDetails by rememberRetained { mutableStateOf>>(emptyMap()) } + var committedEmotion by rememberRetained { mutableStateOf(null) } + var committedEmotionDetails by rememberRetained { mutableStateOf>>(emptyMap()) } + var isEmotionDetailBottomSheetVisible by rememberRetained { mutableStateOf(false) } + var savedRecordId by rememberRetained { mutableStateOf("") } var isExitDialogVisible by rememberRetained { mutableStateOf(false) } var isRecordSavedDialogVisible by rememberRetained { mutableStateOf(false) } val isPageError by remember { @@ -99,19 +113,17 @@ class RecordRegisterPresenter( derivedStateOf { when (currentStep) { RecordStep.QUOTE -> { - recordPageState.text.isNotEmpty() && recordSentenceState.text.isNotEmpty() && !isPageError + recordSentenceState.text.isNotEmpty() && !isPageError } RecordStep.EMOTION -> { - selectedEmotion != null + committedEmotion != null } RecordStep.IMPRESSION -> true } } } - var isScanTooltipVisible by rememberRetained { mutableStateOf(true) } - var isImpressionGuideTooltipVisible by rememberRetained { mutableStateOf(true) } val ocrNavigator = rememberAnsweringNavigator(navigator) { result -> recordSentenceState.edit { @@ -161,6 +173,19 @@ class RecordRegisterPresenter( } } + fun provideEmotionDetailMap(): Map> { + return mapOf( + Emotion.WARM to persistentListOf("위로받은", "포근한", "다정한", "고마운", "마음이 놓이는", "편안한"), + Emotion.JOY to persistentListOf("설레는", "뿌듯한", "유쾌한", "기쁜", "흥미진진한"), + Emotion.SAD to persistentListOf("허무함", "외로운", "아쉬운", "먹먹한", "애틋한", "안타까운", "그리운"), + Emotion.INSIGHT to persistentListOf("감탄한", "통찰력을 얻은", "영감을 받은", "생각이 깊어진", "새롭게 이해한"), + ) + } + + fun getEmotionDetails(emotion: Emotion): ImmutableList { + return provideEmotionDetailMap()[emotion] ?: persistentListOf() + } + fun handleEvent(event: RecordRegisterUiEvent) { when (event) { is RecordRegisterUiEvent.OnBackButtonClick -> { @@ -203,6 +228,56 @@ class RecordRegisterPresenter( selectedEmotion = event.emotion } + is RecordRegisterUiEvent.OnSelectEmotionV2 -> { + selectedEmotion = event.emotion + emotionDetails = getEmotionDetails(event.emotion).toPersistentList() + isEmotionDetailBottomSheetVisible = true + } + + is RecordRegisterUiEvent.OnEmotionDetailToggled -> { + val emotionKey = selectedEmotion ?: return + val currentDetails = selectedEmotionDetails[selectedEmotion].orEmpty() + val updatedDetails = if (event.detail in currentDetails) { + currentDetails - event.detail + } else { + currentDetails + event.detail + } + + selectedEmotionDetails = selectedEmotionDetails + (emotionKey to updatedDetails.toPersistentList()) + } + + is RecordRegisterUiEvent.OnEmotionDetailRemoved -> { + val emotionKey = selectedEmotion ?: return + val currentDetails = committedEmotionDetails[selectedEmotion].orEmpty() + val updatedDetails = currentDetails - event.detail + + committedEmotionDetails = committedEmotionDetails + (emotionKey to updatedDetails.toPersistentList()) + selectedEmotionDetails = selectedEmotionDetails + (emotionKey to updatedDetails.toPersistentList()) + } + + is RecordRegisterUiEvent.OnEmotionDetailSkipped -> { + committedEmotion = selectedEmotion + // 건너뛰기 시 세부감정 선택 초기화 + committedEmotionDetails = persistentMapOf() + selectedEmotionDetails = persistentMapOf() + isEmotionDetailBottomSheetVisible = false + } + + is RecordRegisterUiEvent.OnEmotionDetailCommitted -> { + val emotionKey = selectedEmotion ?: return + val details = selectedEmotionDetails[emotionKey] ?: persistentListOf() + + committedEmotion = emotionKey + committedEmotionDetails = mapOf(emotionKey to details) + selectedEmotionDetails = mapOf(emotionKey to details) + isEmotionDetailBottomSheetVisible = false + } + + is RecordRegisterUiEvent.OnEmotionDetailBottomSheetDismiss -> { + isEmotionDetailBottomSheetVisible = false + } + + /** 2차 고도화 삭제 예정 ===================================================================== */ is RecordRegisterUiEvent.OnImpressionGuideButtonClick -> { analyticsHelper.logScreenView(RECORD_INPUT_HELP) isImpressionGuideTooltipVisible = false @@ -245,6 +320,7 @@ class RecordRegisterPresenter( is RecordRegisterUiEvent.OnImpressionGuideBottomSheetDismiss -> { isImpressionGuideBottomSheetVisible = false } + /** ====================================================================================== */ is RecordRegisterUiEvent.OnNextButtonClick -> { when (currentStep) { @@ -298,9 +374,15 @@ class RecordRegisterPresenter( currentStep = currentStep, recordPageState = recordPageState, recordSentenceState = recordSentenceState, + memoState = memoState, isPageError = isPageError, emotions = emotions, + emotionDetails = emotionDetails, selectedEmotion = selectedEmotion, + selectedEmotionDetails = selectedEmotionDetails, + committedEmotion = committedEmotion, + committedEmotionDetails = committedEmotionDetails, + isEmotionDetailBottomSheetVisible = isEmotionDetailBottomSheetVisible, impressionState = impressionState, impressionGuideList = impressionGuideList, selectedImpressionGuide = selectedImpressionGuide, diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt index 16732566..2aed1fe4 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt @@ -16,8 +16,14 @@ data class RecordRegisterUiState( val recordPageState: TextFieldState = TextFieldState(), val recordSentenceState: TextFieldState = TextFieldState(), val isPageError: Boolean = false, + val memoState: TextFieldState = TextFieldState(), val emotions: ImmutableList = persistentListOf(), + val emotionDetails: ImmutableList = persistentListOf(), val selectedEmotion: Emotion? = null, + val selectedEmotionDetails: Map> = emptyMap(), + val committedEmotion: Emotion? = null, + val committedEmotionDetails: Map> = emptyMap(), + val isEmotionDetailBottomSheetVisible: Boolean = false, val impressionState: TextFieldState = TextFieldState(), val impressionGuideList: ImmutableList = persistentListOf(), val selectedImpressionGuide: String = "", @@ -47,6 +53,12 @@ sealed interface RecordRegisterUiEvent : CircuitUiEvent { data object OnNextButtonClick : RecordRegisterUiEvent data object OnSentenceScanButtonClick : RecordRegisterUiEvent data class OnSelectEmotion(val emotion: Emotion) : RecordRegisterUiEvent + data class OnSelectEmotionV2(val emotion: Emotion) : RecordRegisterUiEvent + data class OnEmotionDetailToggled(val detail: String) : RecordRegisterUiEvent + data class OnEmotionDetailRemoved(val detail: String) : RecordRegisterUiEvent + data object OnEmotionDetailSkipped : RecordRegisterUiEvent + data object OnEmotionDetailCommitted : RecordRegisterUiEvent + data object OnEmotionDetailBottomSheetDismiss : RecordRegisterUiEvent data object OnImpressionGuideButtonClick : RecordRegisterUiEvent data object OnImpressionGuideBottomSheetDismiss : RecordRegisterUiEvent data class OnSelectImpressionGuide(val index: Int) : RecordRegisterUiEvent diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionDetailBottomSheet.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionDetailBottomSheet.kt new file mode 100644 index 00000000..19994c8a --- /dev/null +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionDetailBottomSheet.kt @@ -0,0 +1,176 @@ +package com.ninecraft.booket.feature.record.step_v2 + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import com.ninecraft.booket.core.common.extensions.clickableSingle +import com.ninecraft.booket.core.designsystem.ComponentPreview +import com.ninecraft.booket.core.designsystem.component.button.ReedButton +import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle +import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle +import com.ninecraft.booket.core.designsystem.component.chip.ReedSelectableChip +import com.ninecraft.booket.core.designsystem.component.chip.mediumChipStyle +import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.ninecraft.booket.core.model.Emotion +import com.ninecraft.booket.core.ui.component.ReedBottomSheet +import com.ninecraft.booket.feature.record.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import com.ninecraft.booket.core.designsystem.R as designR + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun EmotionDetailBottomSheet( + emotion: Emotion, + emotionDetails: ImmutableList, + selectedEmotionDetail: ImmutableList, + onDismissRequest: () -> Unit, + sheetState: SheetState, + onCloseButtonClick: () -> Unit, + onEmotionDetailToggled: (String) -> Unit, + onSkipButtonClick: () -> Unit, + onConfirmButtonClick: () -> Unit, +) { + val emotionCategoryName = "'${emotion.displayName}'" + + ReedBottomSheet( + onDismissRequest = { + onDismissRequest() + }, + sheetState = sheetState, + ) { + Column( + modifier = Modifier + .padding( + start = ReedTheme.spacing.spacing5, + top = ReedTheme.spacing.spacing5, + end = ReedTheme.spacing.spacing5, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.emotion_detail_title, emotionCategoryName), + color = ReedTheme.colors.contentPrimary, + textAlign = TextAlign.Center, + style = ReedTheme.typography.heading2SemiBold, + ) + Icon( + imageVector = ImageVector.vectorResource(designR.drawable.ic_close), + contentDescription = "Close Icon", + modifier = Modifier.clickableSingle { + onCloseButtonClick() + }, + ) + } + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing1)) + Text( + text = stringResource(R.string.emotion_detail_description), + modifier = Modifier.fillMaxWidth(), + color = ReedTheme.colors.contentSecondary, + style = ReedTheme.typography.label1Medium, + ) + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding( + start = ReedTheme.spacing.spacing5, + end = ReedTheme.spacing.spacing5, + top = ReedTheme.spacing.spacing6, + bottom = ReedTheme.spacing.spacing3, + ), + horizontalArrangement = Arrangement.spacedBy( + ReedTheme.spacing.spacing2, + Alignment.CenterHorizontally, + ), + verticalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing2), + ) { + emotionDetails.forEach { detail -> + ReedSelectableChip( + label = detail, + chipSizeStyle = mediumChipStyle, + selected = detail in selectedEmotionDetail, + onClick = { + onEmotionDetailToggled(detail) + }, + ) + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = ReedTheme.spacing.spacing4), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + ReedButton( + onClick = { + onSkipButtonClick() + }, + text = stringResource(R.string.emotion_detail_skip), + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.SECONDARY, + modifier = Modifier.weight(1f), + ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) + ReedButton( + onClick = { + onConfirmButtonClick() + }, + text = stringResource(R.string.emotion_detail_confirm), + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.PRIMARY, + modifier = Modifier.weight(1f), + enabled = selectedEmotionDetail.isNotEmpty(), + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@ComponentPreview +@Composable +private fun EmotionDetailBottomSheetPreview() { + val emotionDetails = persistentListOf("위로받은", "포근한", "다정한", "고마운", "마음이 놓이는", "편안한") + + val sheetState = SheetState( + skipPartiallyExpanded = true, + initialValue = SheetValue.Expanded, + positionalThreshold = { 0f }, + velocityThreshold = { 0f }, + ) + ReedTheme { + EmotionDetailBottomSheet( + emotion = Emotion.WARM, + emotionDetails = emotionDetails, + selectedEmotionDetail = persistentListOf(), + onDismissRequest = {}, + sheetState = sheetState, + onCloseButtonClick = {}, + onSkipButtonClick = {}, + onConfirmButtonClick = {}, + onEmotionDetailToggled = {}, + ) + } +} diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionItem.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionItem.kt new file mode 100644 index 00000000..8311cbd6 --- /dev/null +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionItem.kt @@ -0,0 +1,140 @@ +package com.ninecraft.booket.feature.record.step_v2 + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +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.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.ninecraft.booket.core.designsystem.ComponentPreview +import com.ninecraft.booket.core.designsystem.R +import com.ninecraft.booket.core.designsystem.component.chip.ReedRemovableChip +import com.ninecraft.booket.core.designsystem.component.chip.smallChipStyle +import com.ninecraft.booket.core.designsystem.descriptionRes +import com.ninecraft.booket.core.designsystem.graphicResV2 +import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.ninecraft.booket.core.model.Emotion +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun EmotionItem( + emotion: Emotion, + selectedEmotionDetails: ImmutableList, + onClick: () -> Unit, + isSelected: Boolean, + onEmotionDetailRemove: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val cornerShape = RoundedCornerShape(ReedTheme.radius.md) + val iconRes = if (isSelected) R.drawable.ic_check else R.drawable.ic_chevron_right + val iconTint = if (isSelected) ReedTheme.colors.borderBrand else ReedTheme.colors.contentTertiary + + Column( + modifier = modifier + .fillMaxWidth() + .clip(cornerShape) + .clickable { + onClick() + } + .background(color = ReedTheme.colors.baseSecondary) + .then( + if (isSelected) Modifier.border( + width = ReedTheme.border.border15, + color = ReedTheme.colors.borderBrand, + shape = cornerShape, + ) + else Modifier, + ) + .padding( + horizontal = ReedTheme.spacing.spacing4, + vertical = ReedTheme.spacing.spacing3, + ), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(emotion.graphicResV2), + contentDescription = "Emotion Image", + modifier = Modifier + .size(60.dp) + .clip(CircleShape), + ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing4)) + Column { + Text( + text = emotion.displayName, + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.headline1SemiBold, + ) + Text( + text = stringResource(emotion.descriptionRes), + color = ReedTheme.colors.contentTertiary, + style = ReedTheme.typography.label1Medium, + ) + } + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = ImageVector.vectorResource(iconRes), + contentDescription = "Chevron Right", + tint = iconTint, + ) + } + + if (selectedEmotionDetails.isNotEmpty()) { + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing2), + verticalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing2), + ) { + selectedEmotionDetails.forEach { detail -> + ReedRemovableChip( + label = detail, + chipSizeStyle = smallChipStyle, + onRemove = { + onEmotionDetailRemove(detail) + }, + ) + } + } + } + } +} + +@ComponentPreview +@Composable +private fun EmotionItemPreview() { + val selectedEmotionDetails = persistentListOf("위로받은", "포근한", "다정한", "고마운", "마음이 놓이는", "편안한") + + ReedTheme { + EmotionItem( + emotion = Emotion.WARM, + selectedEmotionDetails = selectedEmotionDetails, + onClick = {}, + isSelected = false, + onEmotionDetailRemove = {}, + modifier = Modifier.fillMaxWidth(), + ) + } +} diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionStepV2.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionStepV2.kt new file mode 100644 index 00000000..175b6b55 --- /dev/null +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionStepV2.kt @@ -0,0 +1,158 @@ +package com.ninecraft.booket.feature.record.step_v2 + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +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.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.ninecraft.booket.core.designsystem.ComponentPreview +import com.ninecraft.booket.core.designsystem.component.button.ReedButton +import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle +import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle +import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.ninecraft.booket.core.designsystem.theme.White +import com.ninecraft.booket.core.model.Emotion +import com.ninecraft.booket.feature.record.R +import com.ninecraft.booket.feature.record.register.RecordRegisterUiEvent +import com.ninecraft.booket.feature.record.register.RecordRegisterUiState +import com.skydoves.compose.stability.runtime.TraceRecomposition +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@TraceRecomposition +@Composable +internal fun EmotionStepV2( + state: RecordRegisterUiState, + modifier: Modifier = Modifier, +) { + val emotionDetailBottomSheetState = rememberModalBottomSheetState() + val coroutineScope = rememberCoroutineScope() + + Box( + modifier = modifier + .fillMaxSize() + .background(color = White), + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = ReedTheme.spacing.spacing5) + .padding(bottom = 80.dp), + ) { + item { + Text( + text = stringResource(R.string.emotion_step_title), + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.heading1Bold, + ) + } + item { + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing1)) + } + item { + Text( + text = stringResource(R.string.emotion_step_description), + color = ReedTheme.colors.contentTertiary, + style = ReedTheme.typography.label1Medium, + ) + } + item { + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) + } + + items(state.emotions) { emotion -> + EmotionItem( + emotion = emotion, + selectedEmotionDetails = state.committedEmotionDetails[emotion] ?: persistentListOf(), + onClick = { + state.eventSink(RecordRegisterUiEvent.OnSelectEmotionV2(emotion)) + }, + isSelected = state.committedEmotion == emotion, + onEmotionDetailRemove = { detail -> + state.eventSink(RecordRegisterUiEvent.OnEmotionDetailRemoved(detail)) + }, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + } + } + + ReedButton( + onClick = { + state.eventSink(RecordRegisterUiEvent.OnNextButtonClick) + }, + colorStyle = ReedButtonColorStyle.PRIMARY, + sizeStyle = largeButtonStyle, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(horizontal = ReedTheme.spacing.spacing5) + .padding(bottom = ReedTheme.spacing.spacing4), + enabled = state.isNextButtonEnabled, + text = stringResource(R.string.record_finish_button_text), + multipleEventsCutterEnabled = false, + ) + } + + if (state.isEmotionDetailBottomSheetVisible) { + EmotionDetailBottomSheet( + emotion = state.selectedEmotion ?: Emotion.WARM, + emotionDetails = state.emotionDetails, + selectedEmotionDetail = state.selectedEmotionDetails[state.selectedEmotion] ?: persistentListOf(), + onDismissRequest = { + state.eventSink(RecordRegisterUiEvent.OnEmotionDetailBottomSheetDismiss) + }, + sheetState = emotionDetailBottomSheetState, + onCloseButtonClick = { + coroutineScope.launch { + emotionDetailBottomSheetState.hide() + state.eventSink(RecordRegisterUiEvent.OnEmotionDetailBottomSheetDismiss) + } + }, + onEmotionDetailToggled = { detail -> + state.eventSink(RecordRegisterUiEvent.OnEmotionDetailToggled(detail)) + }, + onSkipButtonClick = { + coroutineScope.launch { + emotionDetailBottomSheetState.hide() + state.eventSink(RecordRegisterUiEvent.OnEmotionDetailSkipped) + } + }, + onConfirmButtonClick = { + coroutineScope.launch { + emotionDetailBottomSheetState.hide() + state.eventSink(RecordRegisterUiEvent.OnEmotionDetailCommitted) + } + }, + ) + } +} + +@ComponentPreview +@Composable +private fun EmotionStepV2Preview() { + val emotions = Emotion.entries.toPersistentList() + + ReedTheme { + EmotionStepV2( + state = RecordRegisterUiState( + emotions = emotions, + eventSink = {}, + ), + ) + } +} diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/QuoteStepV2.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/QuoteStepV2.kt new file mode 100644 index 00000000..1c5b59f9 --- /dev/null +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/QuoteStepV2.kt @@ -0,0 +1,245 @@ +package com.ninecraft.booket.feature.record.step_v2 + +import androidx.compose.foundation.background +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.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.ninecraft.booket.core.designsystem.ComponentPreview +import com.ninecraft.booket.core.designsystem.component.button.ReedButton +import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle +import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle +import com.ninecraft.booket.core.designsystem.component.button.mediumButtonStyle +import com.ninecraft.booket.core.designsystem.component.textfield.ReedRecordTextField +import com.ninecraft.booket.core.designsystem.component.textfield.digitOnlyInputTransformation +import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.ninecraft.booket.core.designsystem.theme.White +import com.ninecraft.booket.feature.record.R +import com.ninecraft.booket.feature.record.register.RecordRegisterUiEvent +import com.ninecraft.booket.feature.record.register.RecordRegisterUiState +import com.skydoves.compose.stability.runtime.TraceRecomposition +import kotlinx.coroutines.delay +import tech.thdev.compose.extensions.keyboard.state.foundation.rememberKeyboardVisible +import com.ninecraft.booket.core.designsystem.R as designR + +@TraceRecomposition +@Composable +internal fun QuoteStepV2( + state: RecordRegisterUiState, + modifier: Modifier = Modifier, +) { + val focusManager = LocalFocusManager.current + val scrollState = rememberScrollState() + val bringIntoViewRequester = remember { BringIntoViewRequester() } + val keyboardState by rememberKeyboardVisible() + var isSentenceTextFieldFocused by remember { mutableStateOf(false) } + + LaunchedEffect(keyboardState, isSentenceTextFieldFocused) { + if (keyboardState && isSentenceTextFieldFocused) { + delay(100) + bringIntoViewRequester.bringIntoView() + } + } + + Column( + modifier = modifier + .fillMaxSize() + .background(color = White) + .imePadding(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = ReedTheme.spacing.spacing5) + .verticalScroll(scrollState), + ) { + Text( + text = stringResource(R.string.quote_step_title), + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.heading1Bold, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing10)) + Text( + text = stringResource(R.string.quote_step_sentence_label), + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.body1Medium, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + ReedRecordTextField( + recordState = state.recordSentenceState, + recordHintRes = R.string.quote_step_sentence_hint, + modifier = Modifier + .fillMaxWidth() + .height(140.dp) + .onFocusChanged { focusState -> + isSentenceTextFieldFocused = focusState.isFocused + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Default, + ), + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) + ReedButton( + onClick = { + state.eventSink(RecordRegisterUiEvent.OnSentenceScanButtonClick) + }, + colorStyle = ReedButtonColorStyle.TERTIARY, + sizeStyle = mediumButtonStyle, + text = stringResource(R.string.quote_step_scan_sentence), + modifier = Modifier + .fillMaxWidth() + .bringIntoViewRequester(bringIntoViewRequester), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(designR.drawable.ic_maximize), + contentDescription = "Scan Icon", + ) + }, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing12)) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.quote_step_page_label), + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.body1Medium, + ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) + Text( + text = stringResource(R.string.select), + modifier = Modifier + .clip(RoundedCornerShape(ReedTheme.radius.xs)) + .background(color = ReedTheme.colors.bgSecondary) + .padding( + start = ReedTheme.spacing.spacing2, + top = ReedTheme.spacing.spacing05, + end = ReedTheme.spacing.spacing2, + bottom = ReedTheme.spacing.spacing05, + ), + color = ReedTheme.colors.contentTertiary, + style = ReedTheme.typography.caption1Medium, + ) + } + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + ReedRecordTextField( + recordState = state.recordPageState, + recordHintRes = R.string.quote_step_page_hint, + inputTransformation = digitOnlyInputTransformation, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + lineLimits = TextFieldLineLimits.SingleLine, + isError = state.isPageError, + errorMessage = stringResource(R.string.quote_step_page_input_error), + onClear = { + state.eventSink(RecordRegisterUiEvent.OnClearClick) + }, + onNext = { + focusManager.moveFocus(FocusDirection.Down) + }, + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing12)) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.quote_step_memo_label), + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.body1Medium, + ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) + Text( + text = stringResource(R.string.select), + modifier = Modifier + .clip(RoundedCornerShape(ReedTheme.radius.xs)) + .background(color = ReedTheme.colors.bgSecondary) + .padding( + start = ReedTheme.spacing.spacing2, + top = ReedTheme.spacing.spacing05, + end = ReedTheme.spacing.spacing2, + bottom = ReedTheme.spacing.spacing05, + ), + color = ReedTheme.colors.contentTertiary, + style = ReedTheme.typography.caption1Medium, + ) + } + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + ReedRecordTextField( + recordState = state.memoState, + recordHintRes = R.string.quote_step_memo_hint, + modifier = Modifier + .fillMaxWidth() + .height(140.dp), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Default, + ), + ) + } + + ReedButton( + onClick = { + state.eventSink(RecordRegisterUiEvent.OnNextButtonClick) + }, + colorStyle = ReedButtonColorStyle.PRIMARY, + sizeStyle = largeButtonStyle, + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = ReedTheme.spacing.spacing5, + vertical = ReedTheme.spacing.spacing4, + ), + enabled = state.isNextButtonEnabled, + text = stringResource(R.string.record_next_button_text), + multipleEventsCutterEnabled = false, + ) + } +} + +@ComponentPreview +@Composable +private fun QuoteStepV2Preview() { + ReedTheme { + QuoteStepV2( + state = RecordRegisterUiState( + eventSink = {}, + ), + ) + } +} diff --git a/feature/record/src/main/res/values/strings.xml b/feature/record/src/main/res/values/strings.xml index c24a72f7..7f8837e4 100644 --- a/feature/record/src/main/res/values/strings.xml +++ b/feature/record/src/main/res/values/strings.xml @@ -24,6 +24,8 @@ 문장 스캔하기 기록하고 싶은 페이지를 작성해보세요 기록하고 싶은 문장을 작성해보세요 + 메모 + 기록하고 싶은 메모가 있다면 작성해보세요 문장에 대해 어떤 감정이 드셨나요? 대표 감정을 한 가지 선택해주세요 문장에 대한 감상을 남겨주세요 @@ -42,4 +44,8 @@ 예시 문장을 알려드려요 스캔으로 빠르게 입력해요 선택 + 어떤 %1$s을 느꼈나요? + 더 자세한 감정을 선택 기록할 수 있어요. + 건너뛰기 + 선택 완료 diff --git a/feature/record/stability/record.stability b/feature/record/stability/record.stability index 0198cadd..ca90752c 100644 --- a/feature/record/stability/record.stability +++ b/feature/record/stability/record.stability @@ -134,10 +134,10 @@ private fun com.ninecraft.booket.feature.record.ocr.component.SentenceBoxPreview @Composable internal fun com.ninecraft.booket.feature.record.register.HandleRecordRegisterSideEffects(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState): kotlin.Unit - skippable: true + skippable: false restartable: true params: - - state: STABLE (class with no mutable properties) + - state: RUNTIME (requires runtime check) @Composable public fun com.ninecraft.booket.feature.record.register.RecordRegisterPresenter.present(): com.ninecraft.booket.feature.record.register.RecordRegisterUiState @@ -153,10 +153,10 @@ private fun com.ninecraft.booket.feature.record.register.RecordRegisterPreview() @Composable internal fun com.ninecraft.booket.feature.record.register.RecordRegisterUi(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit - skippable: true + skippable: false restartable: true params: - - state: STABLE (class with no mutable properties) + - state: RUNTIME (requires runtime check) - modifier: STABLE (marked @Stable or @Immutable) @Composable @@ -171,18 +171,18 @@ private fun com.ninecraft.booket.feature.record.step.EmotionItem(emotion: com.ni @Composable public fun com.ninecraft.booket.feature.record.step.EmotionStep(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit - skippable: true + skippable: false restartable: true params: - - state: STABLE (class with no mutable properties) + - state: RUNTIME (requires runtime check) - modifier: STABLE (marked @Stable or @Immutable) @Composable public fun com.ninecraft.booket.feature.record.step.ImpressionStep(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit - skippable: true + skippable: false restartable: true params: - - state: STABLE (class with no mutable properties) + - state: RUNTIME (requires runtime check) - modifier: STABLE (marked @Stable or @Immutable) @Composable @@ -193,10 +193,10 @@ private fun com.ninecraft.booket.feature.record.step.ImpressionStepPreview(): ko @Composable internal fun com.ninecraft.booket.feature.record.step.QuoteStep(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit - skippable: true + skippable: false restartable: true params: - - state: STABLE (class with no mutable properties) + - state: RUNTIME (requires runtime check) - modifier: STABLE (marked @Stable or @Immutable) @Composable @@ -211,3 +211,70 @@ private fun com.ninecraft.booket.feature.record.step.RecordRegisterPreview(): ko restartable: true params: +@Composable +internal fun com.ninecraft.booket.feature.record.step_v2.EmotionDetailBottomSheet(emotion: com.ninecraft.booket.core.model.Emotion, emotionDetails: kotlinx.collections.immutable.ImmutableList, selectedEmotionDetail: kotlinx.collections.immutable.ImmutableList, onDismissRequest: kotlin.Function0, sheetState: androidx.compose.material3.SheetState, onCloseButtonClick: kotlin.Function0, onEmotionDetailToggled: kotlin.Function1, onSkipButtonClick: kotlin.Function0, onConfirmButtonClick: kotlin.Function0): kotlin.Unit + skippable: true + restartable: true + params: + - emotion: STABLE (class with no mutable properties) + - emotionDetails: STABLE (known stable type) + - selectedEmotionDetail: STABLE (known stable type) + - onDismissRequest: STABLE (function type) + - sheetState: STABLE (marked @Stable or @Immutable) + - onCloseButtonClick: STABLE (function type) + - onEmotionDetailToggled: STABLE (function type) + - onSkipButtonClick: STABLE (function type) + - onConfirmButtonClick: STABLE (function type) + +@Composable +private fun com.ninecraft.booket.feature.record.step_v2.EmotionDetailBottomSheetPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.record.step_v2.EmotionItem(emotion: com.ninecraft.booket.core.model.Emotion, selectedEmotionDetails: kotlinx.collections.immutable.ImmutableList, onClick: kotlin.Function0, isSelected: kotlin.Boolean, onEmotionDetailRemove: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - emotion: STABLE (class with no mutable properties) + - selectedEmotionDetails: STABLE (known stable type) + - onClick: STABLE (function type) + - isSelected: STABLE (primitive type) + - onEmotionDetailRemove: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.record.step_v2.EmotionItemPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.record.step_v2.EmotionStepV2(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: false + restartable: true + params: + - state: RUNTIME (requires runtime check) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.record.step_v2.EmotionStepV2Preview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.record.step_v2.QuoteStepV2(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: false + restartable: true + params: + - state: RUNTIME (requires runtime check) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.record.step_v2.QuoteStepV2Preview(): kotlin.Unit + skippable: true + restartable: true + params: +