diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 933fbfe3..25834d1a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(projects.feature.setting) implementation(projects.feature.webview) implementation(platform(libs.firebase.bom)) + implementation(libs.compose.material) implementation(libs.firebase.analytics) implementation(libs.firebase.crashlytics) implementation(libs.play.services.ads) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3b4a973a..780b61bb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,8 +22,9 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Orbit" + android:enableOnBackInvokedCallback="true" android:usesCleartextTraffic="true" - tools:targetApi="31"> + tools:targetApi="33"> Unit diff --git a/core/ui/src/main/java/com/yapp/ui/component/OrbitBottomSheet.kt b/core/ui/src/main/java/com/yapp/ui/component/bottomsheet/OrbitBottomSheetLayout.kt similarity index 69% rename from core/ui/src/main/java/com/yapp/ui/component/OrbitBottomSheet.kt rename to core/ui/src/main/java/com/yapp/ui/component/bottomsheet/OrbitBottomSheetLayout.kt index baf7e50b..ff048441 100644 --- a/core/ui/src/main/java/com/yapp/ui/component/OrbitBottomSheet.kt +++ b/core/ui/src/main/java/com/yapp/ui/component/bottomsheet/OrbitBottomSheetLayout.kt @@ -1,22 +1,19 @@ -package com.yapp.ui.component +package com.yapp.ui.component.bottomsheet import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +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.shape.RoundedCornerShape +import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetState import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect @@ -33,42 +30,31 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable -fun OrbitBottomSheet( +fun OrbitBottomSheetLayout( modifier: Modifier = Modifier, - sheetState: SheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true, - ), - isSheetOpen: Boolean, - onDismissRequest: () -> Unit = {}, + sheetState: OrbitBottomSheetState, shape: Shape = RoundedCornerShape(topStart = 30.dp, topEnd = 30.dp), containerColor: Color = OrbitTheme.colors.gray_800, strokeColor: Color = OrbitTheme.colors.gray_700, strokeThickness: Dp = 1.dp, content: @Composable () -> Unit, ) { - val scope = rememberCoroutineScope() - if (isSheetOpen) { - ModalBottomSheet( - modifier = modifier, - sheetState = sheetState, - shape = shape, - onDismissRequest = { - scope.launch { - sheetState.hide() - onDismissRequest() - } - }, - containerColor = containerColor, - dragHandle = null, - ) { + ModalBottomSheetLayout( + modifier = modifier.navigationBarsPadding(), + sheetState = sheetState.state, + sheetShape = shape, + sheetBackgroundColor = containerColor, + sheetContent = { Box { - content() + sheetState.content?.invoke(this) BottomSheetTopRoundedStroke( strokeColor = strokeColor, strokeThickness = strokeThickness, ) } - } + }, + ) { + content() } } @@ -139,43 +125,31 @@ fun BottomSheetTopRoundedStroke( @Preview @Composable fun OrbitBottomSheetPreview() { - var isSheetOpen by rememberSaveable { mutableStateOf(true) } - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val sheetState = rememberOrbitBottomSheetState() val scope = rememberCoroutineScope() OrbitTheme { - Button( - onClick = { - scope.launch { - sheetState.show() - }.invokeOnCompletion { - if (!isSheetOpen) { - isSheetOpen = true - } - } - }, - ) { - Text("Toggle Bottom Sheet") - } - - OrbitBottomSheet( - isSheetOpen = isSheetOpen, + OrbitBottomSheetLayout( sheetState = sheetState, - onDismissRequest = { isSheetOpen = !isSheetOpen }, content = { Box( modifier = Modifier - .fillMaxWidth() - .height(600.dp), + .fillMaxSize() + .background(color = OrbitTheme.colors.white), contentAlignment = Alignment.Center, ) { Button( onClick = { scope.launch { - sheetState.hide() - }.invokeOnCompletion { - if (isSheetOpen) { - isSheetOpen = false + sheetState.show { + Box( + modifier = Modifier + .fillMaxWidth() + .height(500.dp), + contentAlignment = Alignment.Center, + ) { + Text("This is a bottom sheet content") + } } } }, diff --git a/core/ui/src/main/java/com/yapp/ui/component/bottomsheet/OrbitBottomSheetState.kt b/core/ui/src/main/java/com/yapp/ui/component/bottomsheet/OrbitBottomSheetState.kt new file mode 100644 index 00000000..b558e1d8 --- /dev/null +++ b/core/ui/src/main/java/com/yapp/ui/component/bottomsheet/OrbitBottomSheetState.kt @@ -0,0 +1,49 @@ +package com.yapp.ui.component.bottomsheet + +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember + +@Composable +fun rememberOrbitBottomSheetState(): OrbitBottomSheetState { + val contentState = remember { mutableStateOf(null) } + + val bottomSheetState = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Hidden, + confirmValueChange = { value -> + if (value == ModalBottomSheetValue.Hidden) { + contentState.value = null + } + true + }, + skipHalfExpanded = true, + ) + + return remember(contentState, bottomSheetState) { + OrbitBottomSheetState( + state = bottomSheetState, + contentState = contentState, + setContent = { contentState.value = it }, + ) + } +} + +class OrbitBottomSheetState( + val state: ModalBottomSheetState, + val contentState: State, + private val setContent: (BottomSheetContent?) -> Unit, +) { + val content: BottomSheetContent? + get() = contentState.value + + suspend fun show(sheetContent: BottomSheetContent) { + setContent(sheetContent) + state.show() + } + + suspend fun hide() = state.hide() +} diff --git a/feature/home/src/main/java/com/yapp/home/HomeNavGraph.kt b/feature/home/src/main/java/com/yapp/home/HomeNavGraph.kt index 5e5fae4a..c46e3458 100644 --- a/feature/home/src/main/java/com/yapp/home/HomeNavGraph.kt +++ b/feature/home/src/main/java/com/yapp/home/HomeNavGraph.kt @@ -8,6 +8,7 @@ import com.yapp.common.navigation.OrbitNavigator import com.yapp.common.navigation.route.HomeBaseRoute import com.yapp.common.navigation.route.HomeDestination import com.yapp.home.alarm.addedit.AlarmAddEditRoute +import com.yapp.ui.component.bottomsheet.OrbitBottomSheetState const val ADD_ALARM_RESULT_KEY = "addAlarmResult" const val UPDATE_ALARM_RESULT_KEY = "updateAlarmResult" @@ -15,6 +16,7 @@ const val DELETE_ALARM_RESULT_KEY = "deleteAlarmResult" fun NavGraphBuilder.homeNavGraph( navigator: OrbitNavigator, + bottomSheetState: OrbitBottomSheetState, snackBarHostState: SnackbarHostState, ) { navigation( @@ -30,6 +32,7 @@ fun NavGraphBuilder.homeNavGraph( composable { AlarmAddEditRoute( navigator = navigator, + bottomSheetState = bottomSheetState, snackBarHostState = snackBarHostState, ) } diff --git a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditContract.kt b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditContract.kt index eb99cf01..0f128d45 100644 --- a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditContract.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditContract.kt @@ -51,10 +51,8 @@ sealed class AlarmAddEditContract { data class AlarmSnoozeState( val isSnoozeEnabled: Boolean = true, - val snoozeIntervalIndex: Int = 2, - val snoozeCountIndex: Int = 2, - val snoozeIntervals: List = listOf("1분", "3분", "5분", "10분", "15분"), - val snoozeCounts: List = listOf("1회", "3회", "5회", "10회", "무한"), + val snoozeInterval: Int = 5, + val snoozeCount: Int = 5, ) data class AlarmSoundState( @@ -83,16 +81,25 @@ sealed class AlarmAddEditContract { data object ToggleWeekendsSelection : Action() data class ToggleSpecificDaySelection(val day: AlarmDay) : Action() data object ToggleHolidaySkipOption : Action() - data object ToggleSnoozeOption : Action() - data class SaveMission(val type: MissionType, val count: Int) : Action() + data class SaveMissionSetting(val type: MissionType, val count: Int) : Action() + data class SaveSnoozeSetting( + val enabled: Boolean, + val interval: Int, + val count: Int, + ) : Action() + data class SaveSoundSetting( + val vibrationEnabled: Boolean, + val soundEnabled: Boolean, + val soundVolume: Int, + val soundIndex: Int, + ) : Action() + data class ToggleVibrationEnabled(val enabled: Boolean) : Action() + data class ToggleSoundEnabled(val enabled: Boolean) : Action() + data class SetSoundVolume(val volume: Int) : Action() + data class SetSoundIndex(val index: Int) : Action() + data class ShowBottomSheet(val sheetType: BottomSheetType) : Action() data class NavigateToMissionPreview(val missionType: MissionType, val missionCount: Int) : Action() - data class SetSnoozeInterval(val index: Int) : Action() - data class SetSnoozeRepeatCount(val index: Int) : Action() - data object ToggleVibrationOption : Action() - data object ToggleSoundOption : Action() - data class AdjustSoundVolume(val volume: Int) : Action() - data class SelectAlarmSound(val index: Int) : Action() - data class ToggleBottomSheet(val sheetType: BottomSheetType) : Action() + data object HideBottomSheet : Action() } sealed class BottomSheetType { @@ -109,6 +116,12 @@ sealed class AlarmAddEditContract { val missionCount: Int, ) : SideEffect() + data class ShowBottomSheet( + val sheetType: BottomSheetType, + ) : SideEffect() + + data object HideBottomSheet : SideEffect() + data class SaveAlarm(val id: Long) : SideEffect() data class UpdateAlarm(val id: Long) : SideEffect() @@ -137,13 +150,8 @@ internal fun AlarmAddEditContract.State.toAlarm(id: Long = 0): Alarm { missionType = missionState.missionType, missionCount = missionState.missionCount, isSnoozeEnabled = snoozeState.isSnoozeEnabled, - snoozeInterval = snoozeState.snoozeIntervals.getOrNull(snoozeState.snoozeIntervalIndex) - ?.filter { it.isDigit() } - ?.toIntOrNull() - ?: 5, - snoozeCount = snoozeState.snoozeCounts.getOrNull(snoozeState.snoozeCountIndex) - ?.let { if (it == "무한") -1 else it.filter { char -> char.isDigit() }.toIntOrNull() ?: 1 } - ?: 1, + snoozeInterval = snoozeState.snoozeInterval, + snoozeCount = snoozeState.snoozeCount, isVibrationEnabled = soundState.isVibrationEnabled, isSoundEnabled = soundState.isSoundEnabled, soundUri = soundState.sounds.getOrNull(soundState.soundIndex)?.uri.toString(), diff --git a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt index 8bc32ab5..ec6dcefe 100644 --- a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt @@ -28,7 +28,6 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -62,6 +61,8 @@ import com.yapp.home.alarm.component.bottomsheet.AlarmMissionBottomSheet import com.yapp.home.alarm.component.bottomsheet.AlarmSnoozeBottomSheet import com.yapp.home.alarm.component.bottomsheet.AlarmSoundBottomSheet import com.yapp.home.alarm.getLabelStringRes +import com.yapp.ui.component.bottomsheet.OrbitBottomSheetState +import com.yapp.ui.component.bottomsheet.rememberOrbitBottomSheetState import com.yapp.ui.component.button.OrbitButton import com.yapp.ui.component.dialog.OrbitDialog import com.yapp.ui.component.lottie.LottieAnimation @@ -70,7 +71,6 @@ import com.yapp.ui.component.switch.OrbitSwitch import com.yapp.ui.component.timepicker.OrbitPicker import feature.home.R import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import org.orbitmvi.orbit.compose.collectSideEffect import java.time.LocalTime @@ -78,56 +78,159 @@ import java.time.LocalTime fun AlarmAddEditRoute( viewModel: AlarmAddEditViewModel = hiltViewModel(), navigator: OrbitNavigator, + bottomSheetState: OrbitBottomSheetState, snackBarHostState: SnackbarHostState, ) { val state by viewModel.container.stateFlow.collectAsStateWithLifecycle() val coroutineScope = rememberCoroutineScope() - viewModel.collectSideEffect { - handleSideEffect(it, navigator, snackBarHostState, coroutineScope) + viewModel.collectSideEffect { sideEffect -> + handleSideEffect( + sideEffect = sideEffect, + navigator = navigator, + bottomSheetState = bottomSheetState, + snackBarHostState = snackBarHostState, + coroutineScope = coroutineScope, + state = state, + processAction = viewModel::processAction, + ) } AlarmAddEditScreen( - stateProvider = { state }, - eventDispatcher = viewModel::processAction, + state = state, + bottomSheetState = bottomSheetState, + processAction = viewModel::processAction, ) } private suspend fun handleSideEffect( sideEffect: AlarmAddEditContract.SideEffect, navigator: OrbitNavigator, + bottomSheetState: OrbitBottomSheetState, snackBarHostState: SnackbarHostState, coroutineScope: CoroutineScope, + state: AlarmAddEditContract.State, + processAction: (AlarmAddEditContract.Action) -> Unit, ) { when (sideEffect) { is AlarmAddEditContract.SideEffect.NavigateBack -> { navigator.navigateBack() } + is AlarmAddEditContract.SideEffect.NavigateToMissionPreview -> { navigator.navigateToMissionPreview( missionType = sideEffect.missionType.value, missionCount = sideEffect.missionCount, ) } + + is AlarmAddEditContract.SideEffect.ShowBottomSheet -> { + bottomSheetState.show { + when (sideEffect.sheetType) { + AlarmAddEditContract.BottomSheetType.MissionSetting -> { + AlarmMissionBottomSheet( + missionState = state.missionState, + onDismiss = { + processAction(AlarmAddEditContract.Action.HideBottomSheet) + }, + onSaveMission = { missionType, missionCount -> + processAction( + AlarmAddEditContract.Action.SaveMissionSetting( + type = missionType, + count = missionCount, + ), + ) + }, + onPreviewMission = { missionType, missionCount -> + processAction( + AlarmAddEditContract.Action.NavigateToMissionPreview( + missionType = missionType, + missionCount = missionCount, + ), + ) + }, + ) + } + + AlarmAddEditContract.BottomSheetType.SnoozeSetting -> { + AlarmSnoozeBottomSheet( + snoozeState = state.snoozeState, + onDismiss = { + processAction(AlarmAddEditContract.Action.HideBottomSheet) + }, + onComplete = { enabled, interval, count -> + processAction( + AlarmAddEditContract.Action.SaveSnoozeSetting( + enabled = enabled, + interval = interval, + count = count, + ), + ) + processAction(AlarmAddEditContract.Action.HideBottomSheet) + }, + ) + } + + AlarmAddEditContract.BottomSheetType.SoundSetting -> { + AlarmSoundBottomSheet( + soundState = state.soundState, + onVibrationToggle = { enabled -> + processAction(AlarmAddEditContract.Action.ToggleVibrationEnabled(enabled)) + }, + onSoundToggle = { enabled -> + processAction(AlarmAddEditContract.Action.ToggleSoundEnabled(enabled)) + }, + onVolumeChanged = { volume -> + processAction(AlarmAddEditContract.Action.SetSoundVolume(volume)) + }, + onSoundSelected = { index -> + processAction(AlarmAddEditContract.Action.SetSoundIndex(index)) + }, + onDismiss = { + processAction(AlarmAddEditContract.Action.HideBottomSheet) + }, + onComplete = { vibrationEnabled, soundEnabled, soundVolume, soundIndex -> + processAction( + AlarmAddEditContract.Action.SaveSoundSetting( + vibrationEnabled = vibrationEnabled, + soundEnabled = soundEnabled, + soundVolume = soundVolume, + soundIndex = soundIndex, + ), + ) + }, + ) + } + } + } + } + + is AlarmAddEditContract.SideEffect.HideBottomSheet -> { + bottomSheetState.hide() + } + is AlarmAddEditContract.SideEffect.SaveAlarm -> { navigator.navController.previousBackStackEntry ?.savedStateHandle ?.set(ADD_ALARM_RESULT_KEY, sideEffect.id) navigator.navController.popBackStack() } + is AlarmAddEditContract.SideEffect.UpdateAlarm -> { navigator.navController.previousBackStackEntry ?.savedStateHandle ?.set(UPDATE_ALARM_RESULT_KEY, sideEffect.id) navigator.navigateBack() } + is AlarmAddEditContract.SideEffect.DeleteAlarm -> { navigator.navController.previousBackStackEntry ?.savedStateHandle ?.set(DELETE_ALARM_RESULT_KEY, sideEffect.id) navigator.navigateBack() } + is AlarmAddEditContract.SideEffect.ShowSnackBar -> { val result = showCustomSnackBar( scope = coroutineScope, @@ -149,17 +252,17 @@ private suspend fun handleSideEffect( @Composable fun AlarmAddEditScreen( - stateProvider: () -> AlarmAddEditContract.State, - eventDispatcher: (AlarmAddEditContract.Action) -> Unit, + state: AlarmAddEditContract.State, + bottomSheetState: OrbitBottomSheetState, + processAction: (AlarmAddEditContract.Action) -> Unit, ) { - val state = stateProvider() - if (state.initialLoading) { AlarmAddEditLoadingScreen() } else { AlarmAddEditContent( state = state, - eventDispatcher = eventDispatcher, + bottomSheetState = bottomSheetState, + processAction = processAction, ) } } @@ -168,19 +271,17 @@ fun AlarmAddEditScreen( @Composable fun AlarmAddEditContent( state: AlarmAddEditContract.State, - eventDispatcher: (AlarmAddEditContract.Action) -> Unit, + bottomSheetState: OrbitBottomSheetState, + processAction: (AlarmAddEditContract.Action) -> Unit, ) { BackHandler { - eventDispatcher(AlarmAddEditContract.Action.CheckUnsavedChangesBeforeExit) + if (bottomSheetState.state.isVisible) { + processAction(AlarmAddEditContract.Action.HideBottomSheet) + } else { + processAction(AlarmAddEditContract.Action.CheckUnsavedChangesBeforeExit) + } } - val missionState = state.missionState - val snoozeState = state.snoozeState - val missionBottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val snoozeBottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val soundBottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val scope = rememberCoroutineScope() - Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, @@ -188,8 +289,8 @@ fun AlarmAddEditContent( AlarmAddEditTopBar( mode = state.mode, title = state.timeState.alarmMessage, - onBack = { eventDispatcher(AlarmAddEditContract.Action.CheckUnsavedChangesBeforeExit) }, - onDelete = { eventDispatcher(AlarmAddEditContract.Action.ShowDeleteDialog) }, + onBack = { processAction(AlarmAddEditContract.Action.CheckUnsavedChangesBeforeExit) }, + onDelete = { processAction(AlarmAddEditContract.Action.ShowDeleteDialog) }, ) Box( modifier = Modifier.weight(1f), @@ -198,25 +299,25 @@ fun AlarmAddEditContent( OrbitPicker( initialTime = state.timeState.initialTime, ) { newTime -> - eventDispatcher(AlarmAddEditContract.Action.SetAlarmTime(newTime)) + processAction(AlarmAddEditContract.Action.SetAlarmTime(newTime)) } } AlarmAddEditSelectDaysSection( modifier = Modifier.padding(horizontal = 20.dp), daysSelectionState = state.daySelectionState, holidayState = state.holidayState, - processAction = eventDispatcher, + processAction = processAction, ) Spacer(modifier = Modifier.height(12.dp)) AlarmAddEditSettingsSection( modifier = Modifier.padding(horizontal = 20.dp), state = state, - processAction = eventDispatcher, + processAction = processAction, ) Spacer(modifier = Modifier.height(24.dp)) OrbitButton( label = stringResource(R.string.alarm_add_edit_save), - onClick = { eventDispatcher(AlarmAddEditContract.Action.SaveAlarm) }, + onClick = { processAction(AlarmAddEditContract.Action.SaveAlarm) }, enabled = true, modifier = Modifier .padding( @@ -227,101 +328,6 @@ fun AlarmAddEditContent( ) } - AlarmMissionBottomSheet( - sheetState = missionBottomSheetState, - missionType = missionState.missionType, - missionCount = missionState.missionCount, - isSheetOpen = state.bottomSheetState == AlarmAddEditContract.BottomSheetType.MissionSetting, - onDismiss = { - scope.launch { - missionBottomSheetState.hide() - }.invokeOnCompletion { - eventDispatcher(AlarmAddEditContract.Action.ToggleBottomSheet(AlarmAddEditContract.BottomSheetType.MissionSetting)) - } - }, - onSaveMission = { missionType, missionCount -> - eventDispatcher( - AlarmAddEditContract.Action.SaveMission( - type = missionType, - count = missionCount, - ), - ) - }, - onPreviewMission = { missionType, missionCount -> - eventDispatcher( - AlarmAddEditContract.Action.NavigateToMissionPreview( - missionType = missionType, - missionCount = missionCount, - ), - ) - }, - ) - - AlarmSnoozeBottomSheet( - snoozeEnabled = snoozeState.isSnoozeEnabled, - snoozeIntervalIndex = snoozeState.snoozeIntervalIndex, - snoozeCountIndex = snoozeState.snoozeCountIndex, - snoozeIntervals = snoozeState.snoozeIntervals, - snoozeCounts = snoozeState.snoozeCounts, - onSnoozeToggle = { eventDispatcher(AlarmAddEditContract.Action.ToggleSnoozeOption) }, - onIntervalSelected = { index -> - eventDispatcher( - AlarmAddEditContract.Action.SetSnoozeInterval( - index, - ), - ) - }, - onCountSelected = { index -> - eventDispatcher( - AlarmAddEditContract.Action.SetSnoozeRepeatCount( - index, - ), - ) - }, - onComplete = { - scope.launch { - snoozeBottomSheetState.hide() - }.invokeOnCompletion { - eventDispatcher(AlarmAddEditContract.Action.ToggleBottomSheet(AlarmAddEditContract.BottomSheetType.SnoozeSetting)) - } - }, - isSheetOpen = state.bottomSheetState == AlarmAddEditContract.BottomSheetType.SnoozeSetting, - onDismiss = { - scope.launch { - snoozeBottomSheetState.hide() - }.invokeOnCompletion { - eventDispatcher(AlarmAddEditContract.Action.ToggleBottomSheet(AlarmAddEditContract.BottomSheetType.SnoozeSetting)) - } - }, - ) - - AlarmSoundBottomSheet( - vibrationEnabled = state.soundState.isVibrationEnabled, - soundEnabled = state.soundState.isSoundEnabled, - soundVolume = state.soundState.soundVolume, - soundIndex = state.soundState.soundIndex, - sounds = state.soundState.sounds, - onVibrationToggle = { eventDispatcher(AlarmAddEditContract.Action.ToggleVibrationOption) }, - onSoundToggle = { eventDispatcher(AlarmAddEditContract.Action.ToggleSoundOption) }, - onVolumeChanged = { eventDispatcher(AlarmAddEditContract.Action.AdjustSoundVolume(it)) }, - onSoundSelected = { eventDispatcher(AlarmAddEditContract.Action.SelectAlarmSound(it)) }, - onComplete = { - scope.launch { - soundBottomSheetState.hide() - }.invokeOnCompletion { - eventDispatcher(AlarmAddEditContract.Action.ToggleBottomSheet(AlarmAddEditContract.BottomSheetType.SoundSetting)) - } - }, - isSheetOpen = state.bottomSheetState == AlarmAddEditContract.BottomSheetType.SoundSetting, - onDismiss = { - scope.launch { - soundBottomSheetState.hide() - }.invokeOnCompletion { - eventDispatcher(AlarmAddEditContract.Action.ToggleBottomSheet(AlarmAddEditContract.BottomSheetType.SoundSetting)) - } - }, - ) - if (state.isDeleteDialogVisible) { OrbitDialog( title = stringResource(id = R.string.alarm_delete_dialog_title), @@ -329,10 +335,10 @@ fun AlarmAddEditContent( confirmText = stringResource(id = R.string.alarm_delete_dialog_btn_delete), cancelText = stringResource(id = R.string.alarm_delete_dialog_btn_cancel), onConfirm = { - eventDispatcher(AlarmAddEditContract.Action.DeleteAlarm) + processAction(AlarmAddEditContract.Action.DeleteAlarm) }, onCancel = { - eventDispatcher(AlarmAddEditContract.Action.HideDeleteDialog) + processAction(AlarmAddEditContract.Action.HideDeleteDialog) }, ) } @@ -344,10 +350,10 @@ fun AlarmAddEditContent( confirmText = stringResource(id = R.string.alarm_unsaved_changes_dialog_btn_discard), cancelText = stringResource(id = R.string.alarm_unsaved_changes_dialog_btn_cancel), onConfirm = { - eventDispatcher(AlarmAddEditContract.Action.NavigateBack) + processAction(AlarmAddEditContract.Action.NavigateBack) }, onCancel = { - eventDispatcher(AlarmAddEditContract.Action.HideUnsavedChangesDialog) + processAction(AlarmAddEditContract.Action.HideUnsavedChangesDialog) }, ) } @@ -480,7 +486,7 @@ private fun AlarmAddEditSettingsSection( }, onClick = { processAction( - AlarmAddEditContract.Action.ToggleBottomSheet( + AlarmAddEditContract.Action.ShowBottomSheet( AlarmAddEditContract.BottomSheetType.MissionSetting, ), ) @@ -496,17 +502,29 @@ private fun AlarmAddEditSettingsSection( AlarmAddEditSettingItem( label = stringResource(id = R.string.alarm_add_edit_alarm_snooze), description = if (state.snoozeState.isSnoozeEnabled) { + val interval = stringResource( + id = R.string.alarm_add_edit_interval_minute, + state.snoozeState.snoozeInterval, + ) + val count = if (state.snoozeState.snoozeCount == -1) { + stringResource(id = R.string.alarm_add_edit_repeat_count_infinite) + } else { + stringResource( + id = R.string.alarm_add_edit_repeat_count_times, + state.snoozeState.snoozeCount, + ) + } stringResource( id = R.string.alarm_add_edit_alarm_selected_option, - state.snoozeState.snoozeIntervals[state.snoozeState.snoozeIntervalIndex], - state.snoozeState.snoozeCounts[state.snoozeState.snoozeCountIndex], + interval, + count, ) } else { stringResource(id = R.string.alarm_add_edit_alarm_selected_option_none) }, onClick = { processAction( - AlarmAddEditContract.Action.ToggleBottomSheet( + AlarmAddEditContract.Action.ShowBottomSheet( AlarmAddEditContract.BottomSheetType.SnoozeSetting, ), ) @@ -542,7 +560,7 @@ private fun AlarmAddEditSettingsSection( }, onClick = { processAction( - AlarmAddEditContract.Action.ToggleBottomSheet( + AlarmAddEditContract.Action.ShowBottomSheet( AlarmAddEditContract.BottomSheetType.SoundSetting, ), ) @@ -771,24 +789,23 @@ fun AlarmAddEditScreenPreview() { ), ) { AlarmAddEditScreen( - stateProvider = { - AlarmAddEditContract.State( - initialLoading = false, - timeState = AlarmAddEditContract.AlarmTimeState( - currentTime = LocalTime.of(19, 30), - ), - daySelectionState = AlarmAddEditContract.AlarmDaySelectionState( - isWeekdaysChecked = true, - isWeekendsChecked = false, - selectedDays = setOf(AlarmDay.MON, AlarmDay.TUE), - days = AlarmDay.entries.toSet(), - ), - holidayState = AlarmAddEditContract.AlarmHolidayState( - isDisableHolidayChecked = false, - ), - ) - }, - eventDispatcher = { }, + state = AlarmAddEditContract.State( + initialLoading = false, + timeState = AlarmAddEditContract.AlarmTimeState( + currentTime = LocalTime.of(19, 30), + ), + daySelectionState = AlarmAddEditContract.AlarmDaySelectionState( + isWeekdaysChecked = true, + isWeekendsChecked = false, + selectedDays = setOf(AlarmDay.MON, AlarmDay.TUE), + days = AlarmDay.entries.toSet(), + ), + holidayState = AlarmAddEditContract.AlarmHolidayState( + isDisableHolidayChecked = false, + ), + ), + bottomSheetState = rememberOrbitBottomSheetState(), + processAction = { }, ) } } diff --git a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt index fc68790e..b0f8dfb5 100644 --- a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt @@ -149,17 +149,11 @@ class AlarmAddEditViewModel @Inject constructor( ): AlarmAddEditContract.AlarmSnoozeState { return currentState.snoozeState.copy( isSnoozeEnabled = alarm.isSnoozeEnabled, - snoozeIntervalIndex = findSnoozeIndex(alarm.snoozeInterval, currentState.snoozeState.snoozeIntervals), - snoozeCountIndex = findSnoozeIndex(alarm.snoozeCount, currentState.snoozeState.snoozeCounts), + snoozeInterval = alarm.snoozeInterval, + snoozeCount = alarm.snoozeCount, ) } - private fun findSnoozeIndex(value: Int, list: List): Int { - return list.indexOfFirst { - it == "무한" && value == -1 || it.filter { char -> char.isDigit() }.toIntOrNull() == value - }.takeIf { it >= 0 } ?: 0 - } - override fun onCleared() { super.onCleared() alarmUseCase.releaseSoundPlayer() @@ -180,16 +174,25 @@ class AlarmAddEditViewModel @Inject constructor( is AlarmAddEditContract.Action.ToggleWeekendsSelection -> toggleWeekendsSelection() is AlarmAddEditContract.Action.ToggleSpecificDaySelection -> toggleSpecificDaySelection(action.day) is AlarmAddEditContract.Action.ToggleHolidaySkipOption -> toggleHolidaySkipOption() - is AlarmAddEditContract.Action.SaveMission -> saveMission(action.type, action.count) + is AlarmAddEditContract.Action.SaveMissionSetting -> saveMissionSetting(action.type, action.count) + is AlarmAddEditContract.Action.SaveSnoozeSetting -> saveSnoozeSetting( + action.enabled, + action.interval, + action.count, + ) + is AlarmAddEditContract.Action.SaveSoundSetting -> saveSoundSetting( + action.vibrationEnabled, + action.soundEnabled, + action.soundVolume, + action.soundIndex, + ) + is AlarmAddEditContract.Action.ToggleVibrationEnabled -> toggleVibrationEnabled(action.enabled) + is AlarmAddEditContract.Action.ToggleSoundEnabled -> toggleSoundEnabled(action.enabled) + is AlarmAddEditContract.Action.SetSoundVolume -> setSoundVolume(action.volume) + is AlarmAddEditContract.Action.SetSoundIndex -> setSoundIndex(action.index) is AlarmAddEditContract.Action.NavigateToMissionPreview -> navigateToMissionPreview(action.missionType, action.missionCount) - is AlarmAddEditContract.Action.ToggleSnoozeOption -> toggleSnoozeOption() - is AlarmAddEditContract.Action.SetSnoozeInterval -> setSnoozeInterval(action.index) - is AlarmAddEditContract.Action.SetSnoozeRepeatCount -> setSnoozeRepeatCount(action.index) - is AlarmAddEditContract.Action.ToggleVibrationOption -> toggleVibrationOption() - is AlarmAddEditContract.Action.ToggleSoundOption -> toggleSoundOption() - is AlarmAddEditContract.Action.AdjustSoundVolume -> adjustSoundVolume(action.volume) - is AlarmAddEditContract.Action.SelectAlarmSound -> selectAlarmSound(action.index) - is AlarmAddEditContract.Action.ToggleBottomSheet -> toggleBottomSheet(action.sheetType) + is AlarmAddEditContract.Action.ShowBottomSheet -> showBottomSheet(action.sheetType) + is AlarmAddEditContract.Action.HideBottomSheet -> hideBottomSheet() } } @@ -453,7 +456,7 @@ class AlarmAddEditViewModel @Inject constructor( } } - private fun saveMission(type: MissionType, count: Int) = intent { + private fun saveMissionSetting(type: MissionType, count: Int) = intent { val newMissionState = state.missionState.copy( missionType = type, missionCount = count, @@ -463,81 +466,68 @@ class AlarmAddEditViewModel @Inject constructor( } } - private fun toggleSnoozeOption() = intent { + private fun saveSnoozeSetting( + isSnoozeEnabled: Boolean, + snoozeInterval: Int, + snoozeCount: Int, + ) = intent { val newSnoozeState = state.snoozeState.copy( - isSnoozeEnabled = !state.snoozeState.isSnoozeEnabled, + isSnoozeEnabled = isSnoozeEnabled, + snoozeInterval = snoozeInterval, + snoozeCount = snoozeCount, ) - reduce { - state.copy(snoozeState = newSnoozeState) - } - } - private fun setSnoozeInterval(index: Int) = intent { - val newSnoozeState = state.snoozeState.copy(snoozeIntervalIndex = index) reduce { state.copy(snoozeState = newSnoozeState) } } - private fun setSnoozeRepeatCount(index: Int) = intent { - val newSnoozeState = state.snoozeState.copy(snoozeCountIndex = index) + private fun saveSoundSetting( + vibrationEnabled: Boolean, + soundEnabled: Boolean, + soundVolume: Int, + soundIndex: Int, + ) = intent { + val newSoundState = state.soundState.copy( + isVibrationEnabled = vibrationEnabled, + isSoundEnabled = soundEnabled, + soundVolume = soundVolume, + soundIndex = soundIndex, + ) + reduce { - state.copy(snoozeState = newSnoozeState) + state.copy(soundState = newSoundState) } } - private fun toggleVibrationOption() = intent { - val newSoundState = state.soundState.copy(isVibrationEnabled = !state.soundState.isVibrationEnabled) - - if (newSoundState.isVibrationEnabled) { + private fun toggleVibrationEnabled(enabled: Boolean) = intent { + if (enabled) { hapticFeedbackManager.performHapticFeedback(HapticType.SUCCESS) } - reduce { - state.copy(soundState = newSoundState) - } } - private fun toggleSoundOption() = intent { - val newSoundState = state.soundState.copy(isSoundEnabled = !state.soundState.isSoundEnabled) - if (!newSoundState.isSoundEnabled) { + private fun toggleSoundEnabled(enabled: Boolean) = intent { + if (!enabled) { alarmUseCase.stopAlarmSound() } - reduce { - state.copy(soundState = newSoundState) - } } - private fun adjustSoundVolume(volume: Int) = intent { - val newSoundState = state.soundState.copy(soundVolume = volume) + private fun setSoundVolume(volume: Int) = intent { alarmUseCase.updateAlarmVolume(volume) - reduce { - state.copy(soundState = newSoundState) - } } - private fun selectAlarmSound(index: Int) = intent { - val newSoundState = state.soundState.copy(soundIndex = index) - reduce { - state.copy(soundState = newSoundState) - } - + private fun setSoundIndex(index: Int) = intent { val selectedSound = state.soundState.sounds[index] alarmUseCase.initializeSoundPlayer(selectedSound.uri) alarmUseCase.playAlarmSound(state.soundState.soundVolume) } - private fun toggleBottomSheet(sheetType: AlarmAddEditContract.BottomSheetType) = intent { - val newBottomSheetState = if (state.bottomSheetState == sheetType) { - if (state.bottomSheetState == AlarmAddEditContract.BottomSheetType.SoundSetting) { - alarmUseCase.stopAlarmSound() - } - null - } else { - sheetType - } - reduce { - state.copy(bottomSheetState = newBottomSheetState) - } + private fun showBottomSheet(sheetType: AlarmAddEditContract.BottomSheetType) = intent { + postSideEffect(AlarmAddEditContract.SideEffect.ShowBottomSheet(sheetType)) + } + + private fun hideBottomSheet() = intent { + postSideEffect(AlarmAddEditContract.SideEffect.HideBottomSheet) } private fun getAlarmMessage(currentTime: LocalTime, selectedDays: Set): String { diff --git a/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmMissionBottomSheet.kt b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmMissionBottomSheet.kt index 9cb8669e..0c84f840 100644 --- a/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmMissionBottomSheet.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmMissionBottomSheet.kt @@ -19,9 +19,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.SheetState import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -38,8 +36,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.yapp.designsystem.theme.OrbitTheme import com.yapp.domain.model.MissionType +import com.yapp.home.alarm.addedit.AlarmAddEditContract import com.yapp.home.alarm.component.SelectorItems -import com.yapp.ui.component.OrbitBottomSheet import com.yapp.ui.component.button.OrbitButton import com.yapp.ui.component.lottie.LottieAnimation import com.yapp.ui.extensions.customClickable @@ -59,99 +57,84 @@ private fun MissionType.displayData(): Pair = when (this) { else -> throw IllegalStateException("Invalid mission type") } -@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun AlarmMissionBottomSheet( - sheetState: SheetState, - missionType: MissionType, - missionCount: Int, - isSheetOpen: Boolean, + missionState: AlarmAddEditContract.AlarmMissionState, onDismiss: () -> Unit, onSaveMission: (MissionType, Int) -> Unit, onPreviewMission: (MissionType, Int) -> Unit, ) { - var currentStep by remember { mutableStateOf(AlarmMissionSelectBottomSheetType.MISSION_SETTING) } - - var selectedMissionType by remember { mutableStateOf(missionType) } - var selectedMissionCount by remember { mutableIntStateOf(missionCount) } - - OrbitBottomSheet( - isSheetOpen = isSheetOpen, - sheetState = sheetState, - onDismissRequest = { - currentStep = AlarmMissionSelectBottomSheetType.MISSION_SETTING - onDismiss() - }, - ) { - when (currentStep) { - AlarmMissionSelectBottomSheetType.MISSION_SETTING -> { - if (selectedMissionType == MissionType.NONE) { - MissionAddContent { - currentStep = AlarmMissionSelectBottomSheetType.MISSION_SELECT - } - } else { - MissionSettingContent( - missionType = missionType, - missionCount = missionCount, - onDetail = { - currentStep = AlarmMissionSelectBottomSheetType.MISSION_DETAIL - }, - onDelete = { - selectedMissionType = MissionType.NONE - onSaveMission(selectedMissionType, selectedMissionCount) - }, - onChange = { - currentStep = AlarmMissionSelectBottomSheetType.MISSION_SELECT - }, - onDone = { - onSaveMission(selectedMissionType, selectedMissionCount) - onDismiss() - }, - ) - } - } + var stepStack by remember { mutableStateOf(listOf(AlarmMissionSelectBottomSheetType.MISSION_SETTING)) } + var selectedMissionType by remember { mutableStateOf(missionState.missionType) } + var selectedMissionCount by remember { mutableIntStateOf(missionState.missionCount) } - AlarmMissionSelectBottomSheetType.MISSION_SELECT -> { - MissionSelectContent( - onBack = { - currentStep = AlarmMissionSelectBottomSheetType.MISSION_SETTING - }, - onClose = { - currentStep = AlarmMissionSelectBottomSheetType.MISSION_SETTING - onDismiss() - }, - onSelect = { mission -> - selectedMissionType = mission - currentStep = AlarmMissionSelectBottomSheetType.MISSION_DETAIL - }, - ) - } + fun push(step: AlarmMissionSelectBottomSheetType) { + stepStack = stepStack + step + } - AlarmMissionSelectBottomSheetType.MISSION_DETAIL -> { - MissionDetailContent( + fun pop() { + if (stepStack.size > 1) { + stepStack = stepStack.dropLast(1) + } + } + + val currentStep = stepStack.last() + + when (currentStep) { + AlarmMissionSelectBottomSheetType.MISSION_SETTING -> { + if (selectedMissionType == MissionType.NONE) { + MissionAddContent { + push(AlarmMissionSelectBottomSheetType.MISSION_SELECT) + } + } else { + MissionSettingContent( missionType = selectedMissionType, - selectedMissionCount = selectedMissionCount, - onCountChange = { count -> - selectedMissionCount = count - }, - onBack = { - currentStep = AlarmMissionSelectBottomSheetType.MISSION_SELECT - }, - onClose = { - currentStep = AlarmMissionSelectBottomSheetType.MISSION_SETTING - onDismiss() + missionCount = selectedMissionCount, + onDetail = { push(AlarmMissionSelectBottomSheetType.MISSION_DETAIL) }, + onDelete = { + selectedMissionType = MissionType.NONE + onSaveMission(selectedMissionType, selectedMissionCount) }, - onSave = { - currentStep = AlarmMissionSelectBottomSheetType.MISSION_SETTING + onChange = { push(AlarmMissionSelectBottomSheetType.MISSION_SELECT) }, + onDone = { onSaveMission(selectedMissionType, selectedMissionCount) onDismiss() }, - onPreview = { - onPreviewMission(selectedMissionType, selectedMissionCount) - }, ) } } + + AlarmMissionSelectBottomSheetType.MISSION_SELECT -> { + MissionSelectContent( + onBack = { pop() }, + onClose = { + onDismiss() + }, + onSelect = { mission -> + selectedMissionType = mission + push(AlarmMissionSelectBottomSheetType.MISSION_DETAIL) + }, + ) + } + + AlarmMissionSelectBottomSheetType.MISSION_DETAIL -> { + MissionDetailContent( + missionType = selectedMissionType, + selectedMissionCount = selectedMissionCount, + onCountChange = { selectedMissionCount = it }, + onBack = { pop() }, + onClose = { + onDismiss() + }, + onSave = { + onSaveMission(selectedMissionType, selectedMissionCount) + onDismiss() + }, + onPreview = { + onPreviewMission(selectedMissionType, selectedMissionCount) + }, + ) + } } } @@ -505,9 +488,11 @@ private fun MissionDetailContent( .fillMaxWidth() .padding( horizontal = 20.dp, - vertical = 24.dp, + vertical = 12.dp, ), ) { + Spacer(modifier = Modifier.height(12.dp)) + Box( modifier = Modifier .fillMaxWidth() @@ -646,14 +631,9 @@ private fun MissionSelectTopAppBar( @Composable private fun AlarmMissionSelectBottomSheetPreview() { OrbitTheme { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - AlarmMissionBottomSheet( - sheetState = sheetState, - missionType = MissionType.SHAKE, - missionCount = 15, - isSheetOpen = true, - onDismiss = {}, + missionState = AlarmAddEditContract.AlarmMissionState(), + onDismiss = { }, onSaveMission = { _, _ -> }, onPreviewMission = { _, _ -> }, ) diff --git a/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSnoozeBottomSheet.kt b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSnoozeBottomSheet.kt index 82e4caeb..ef6643c0 100644 --- a/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSnoozeBottomSheet.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSnoozeBottomSheet.kt @@ -11,13 +11,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -25,102 +23,80 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.yapp.designsystem.theme.OrbitTheme +import com.yapp.home.alarm.addedit.AlarmAddEditContract import com.yapp.home.alarm.component.SelectorItems -import com.yapp.ui.component.OrbitBottomSheet import com.yapp.ui.component.button.OrbitButton import com.yapp.ui.component.switch.OrbitSwitch import feature.home.R -import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun AlarmSnoozeBottomSheet( - snoozeEnabled: Boolean, - snoozeIntervalIndex: Int, - snoozeIntervals: List, - onIntervalSelected: (Int) -> Unit, - snoozeCountIndex: Int, - snoozeCounts: List, - onSnoozeToggle: () -> Unit, - onCountSelected: (Int) -> Unit, - onComplete: () -> Unit, - isSheetOpen: Boolean, - onDismiss: () -> Unit, + snoozeState: AlarmAddEditContract.AlarmSnoozeState, + onDismiss: () -> Unit = {}, + onComplete: (enabled: Boolean, interval: Int, count: Int) -> Unit, ) { - val scope = rememberCoroutineScope() - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val snoozeIntervalOptions = listOf(1, 3, 5, 10, 15) + val snoozeCountOptions = listOf(1, 3, 5, 10, -1) - OrbitBottomSheet( - isSheetOpen = isSheetOpen, - sheetState = sheetState, - onDismissRequest = { - scope.launch { - sheetState.hide() - }.invokeOnCompletion { onDismiss() } - }, - ) { - BottomSheetContent( - isSnoozeEnabled = snoozeEnabled, - snoozeIntervalIndex = snoozeIntervalIndex, - snoozeIntervals = snoozeIntervals, - onIntervalSelected = onIntervalSelected, - snoozeCountIndex = snoozeCountIndex, - snoozeCounts = snoozeCounts, - onSnoozeToggle = onSnoozeToggle, - onCountSelected = onCountSelected, - onComplete = { - scope.launch { - sheetState.hide() - }.invokeOnCompletion { onComplete() } + val snoozeIntervals = snoozeIntervalOptions.map { + stringResource(id = R.string.alarm_add_edit_interval_minute, it) + } + val snoozeCounts = listOf( + stringResource(id = R.string.alarm_add_edit_repeat_count_times, 1), + stringResource(id = R.string.alarm_add_edit_repeat_count_times, 3), + stringResource(id = R.string.alarm_add_edit_repeat_count_times, 5), + stringResource(id = R.string.alarm_add_edit_repeat_count_times, 10), + stringResource(id = R.string.alarm_add_edit_repeat_count_infinite), + ) + + var selectedSnoozeEnabled by remember { mutableStateOf(snoozeState.isSnoozeEnabled) } + var selectedSnoozeIntervalIndex by remember { mutableIntStateOf(snoozeIntervalOptions.indexOf(snoozeState.snoozeInterval)) } + var selectedSnoozeCountIndex by remember { + mutableIntStateOf( + if (snoozeState.snoozeCount == -1) { + snoozeCountOptions.lastIndex + } else { + snoozeCountOptions.indexOf(snoozeState.snoozeCount) }, ) } -} -@Composable -private fun BottomSheetContent( - isSnoozeEnabled: Boolean, - snoozeIntervalIndex: Int, - snoozeIntervals: List, - onIntervalSelected: (Int) -> Unit, - snoozeCountIndex: Int, - snoozeCounts: List, - onSnoozeToggle: () -> Unit, - onCountSelected: (Int) -> Unit, - onComplete: () -> Unit, -) { Column( modifier = Modifier .fillMaxWidth() - .padding( - horizontal = 24.dp, - vertical = 12.dp, - ), + .padding(horizontal = 24.dp, vertical = 12.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(modifier = Modifier.height(6.dp)) - VibrationSection(isSnoozeEnabled, onSnoozeToggle) + VibrationSection(selectedSnoozeEnabled) { + selectedSnoozeEnabled = !selectedSnoozeEnabled + } Spacer(modifier = Modifier.height(20.dp)) SelectorSection( title = stringResource(id = R.string.alarm_add_edit_interval), - selectedIndex = snoozeIntervalIndex, + selectedIndex = selectedSnoozeIntervalIndex, items = snoozeIntervals, - enabled = isSnoozeEnabled, - onItemSelected = onIntervalSelected, + enabled = selectedSnoozeEnabled, + onItemSelected = { + selectedSnoozeIntervalIndex = it + }, ) Spacer(modifier = Modifier.height(32.dp)) SelectorSection( title = stringResource(id = R.string.alarm_add_edit_repeat_count), - selectedIndex = snoozeCountIndex, + selectedIndex = selectedSnoozeCountIndex, items = snoozeCounts, - enabled = isSnoozeEnabled, - onItemSelected = onCountSelected, + enabled = selectedSnoozeEnabled, + onItemSelected = { + selectedSnoozeCountIndex = it + }, ) Spacer(modifier = Modifier.height(20.dp)) - if (isSnoozeEnabled) { + if (selectedSnoozeEnabled) { AlarmSnoozeMessage( - interval = snoozeIntervals[snoozeIntervalIndex], - count = snoozeCounts[snoozeCountIndex], + interval = snoozeIntervals[selectedSnoozeIntervalIndex], + count = snoozeCounts[selectedSnoozeCountIndex], ) } Spacer(modifier = Modifier.height(40.dp)) @@ -131,7 +107,14 @@ private fun BottomSheetContent( contentColor = OrbitTheme.colors.white, pressedContainerColor = OrbitTheme.colors.gray_500, pressedContentColor = OrbitTheme.colors.white.copy(alpha = 0.7f), - onClick = onComplete, + onClick = { + onDismiss() + onComplete( + selectedSnoozeEnabled, + snoozeIntervalOptions[selectedSnoozeIntervalIndex], + snoozeCountOptions[selectedSnoozeCountIndex], + ) + }, ) } } @@ -207,31 +190,17 @@ private fun AlarmSnoozeMessage(interval: String, count: String) { @Composable private fun AlarmSnoozeBottomSheetPreview() { var isSnoozeEnabled by remember { mutableStateOf(true) } - var snoozeIntervalIndex by remember { mutableIntStateOf(2) } - var snoozeCountIndex by remember { mutableIntStateOf(1) } - var isSheetOpen by remember { mutableStateOf(true) } + var snoozeInterval by remember { mutableIntStateOf(5) } + var snoozeCount by remember { mutableIntStateOf(5) } OrbitTheme { AlarmSnoozeBottomSheet( - snoozeEnabled = isSnoozeEnabled, - snoozeIntervalIndex = snoozeIntervalIndex, - snoozeCountIndex = snoozeCountIndex, - snoozeIntervals = listOf(1, 3, 5, 10, 15).map { - stringResource(id = R.string.alarm_add_edit_interval_minute, it) - }, - snoozeCounts = listOf( - stringResource(id = R.string.alarm_add_edit_repeat_count_times, 1), - stringResource(id = R.string.alarm_add_edit_repeat_count_times, 3), - stringResource(id = R.string.alarm_add_edit_repeat_count_times, 5), - stringResource(id = R.string.alarm_add_edit_repeat_count_times, 10), - stringResource(id = R.string.alarm_add_edit_repeat_count_infinite), + snoozeState = AlarmAddEditContract.AlarmSnoozeState( + isSnoozeEnabled = isSnoozeEnabled, + snoozeInterval = snoozeInterval, + snoozeCount = snoozeCount, ), - onSnoozeToggle = { isSnoozeEnabled = !isSnoozeEnabled }, - onIntervalSelected = { index -> snoozeIntervalIndex = index }, - onCountSelected = { index -> snoozeCountIndex = index }, - onComplete = { isSheetOpen = false }, - isSheetOpen = isSheetOpen, - onDismiss = { isSheetOpen = false }, + onComplete = { _, _, _ -> }, ) } } diff --git a/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSoundBottomSheet.kt b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSoundBottomSheet.kt index e98fe962..7b706435 100644 --- a/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSoundBottomSheet.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSoundBottomSheet.kt @@ -10,21 +10,17 @@ 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.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -34,88 +30,41 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.yapp.designsystem.theme.OrbitTheme import com.yapp.domain.model.AlarmSound -import com.yapp.ui.component.OrbitBottomSheet +import com.yapp.home.alarm.addedit.AlarmAddEditContract import com.yapp.ui.component.button.OrbitButton import com.yapp.ui.component.radiobutton.OrbitRadioButton import com.yapp.ui.component.slider.OrbitSlider import com.yapp.ui.component.switch.OrbitSwitch import feature.home.R -import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun AlarmSoundBottomSheet( - vibrationEnabled: Boolean, - soundEnabled: Boolean, - soundVolume: Int, - soundIndex: Int, - sounds: List, - onVibrationToggle: () -> Unit, - onSoundToggle: () -> Unit, + soundState: AlarmAddEditContract.AlarmSoundState, + onVibrationToggle: (Boolean) -> Unit, + onSoundToggle: (Boolean) -> Unit, onVolumeChanged: (Int) -> Unit, onSoundSelected: (Int) -> Unit, - onComplete: () -> Unit, - isSheetOpen: Boolean, - onDismiss: () -> Unit, + onDismiss: () -> Unit = {}, + onComplete: (vibrationEnabled: Boolean, soundEnabled: Boolean, soundVolume: Int, soundIndex: Int) -> Unit, ) { - val scope = rememberCoroutineScope() - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - - OrbitBottomSheet( - modifier = Modifier.statusBarsPadding(), - isSheetOpen = isSheetOpen, - sheetState = sheetState, - onDismissRequest = { - scope.launch { - sheetState.hide() - }.invokeOnCompletion { onDismiss() } - }, - ) { - BottomSheetContent( - vibrationEnabled = vibrationEnabled, - soundEnabled = soundEnabled, - soundVolume = soundVolume, - soundIndex = soundIndex, - sounds = sounds, - onVibrationToggle = onVibrationToggle, - onSoundToggle = onSoundToggle, - onVolumeChanged = onVolumeChanged, - onSoundSelected = onSoundSelected, - onComplete = { - scope.launch { - sheetState.hide() - }.invokeOnCompletion { onComplete() } - }, - ) - } -} + var selectedVibrationEnabled by remember { mutableStateOf(soundState.isVibrationEnabled) } + var selectedSoundEnabled by remember { mutableStateOf(soundState.isSoundEnabled) } + var selectedSoundVolume by remember { mutableIntStateOf(soundState.soundVolume) } + var selectedSoundIndex by remember { mutableIntStateOf(soundState.soundIndex) } -@Composable -private fun BottomSheetContent( - vibrationEnabled: Boolean, - soundEnabled: Boolean, - soundVolume: Int, - soundIndex: Int, - sounds: List, - onVibrationToggle: () -> Unit, - onSoundToggle: () -> Unit, - onVolumeChanged: (Int) -> Unit, - onSoundSelected: (Int) -> Unit, - onComplete: () -> Unit, -) { Column( modifier = Modifier .fillMaxWidth() - .padding( - horizontal = 24.dp, - vertical = 12.dp, - ), + .padding(horizontal = 24.dp, vertical = 12.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(modifier = Modifier.height(6.dp)) VibrationSection( - isVibrationEnabled = vibrationEnabled, - onVibrationToggle = onVibrationToggle, + isVibrationEnabled = selectedVibrationEnabled, + onVibrationToggle = { + selectedVibrationEnabled = !selectedVibrationEnabled + onVibrationToggle(selectedVibrationEnabled) + }, ) Spacer( modifier = Modifier @@ -125,13 +74,22 @@ private fun BottomSheetContent( ) SoundSection( modifier = Modifier.weight(1f), - soundEnabled = soundEnabled, - onSoundToggle = onSoundToggle, - soundVolume = soundVolume, - onVolumeChanged = onVolumeChanged, - soundIndex = soundIndex, - sounds = sounds, - onSoundSelected = { onSoundSelected(it) }, + soundEnabled = selectedSoundEnabled, + onSoundToggle = { + selectedSoundEnabled = !selectedSoundEnabled + onSoundToggle(selectedSoundEnabled) + }, + soundVolume = selectedSoundVolume, + onVolumeChanged = { + selectedSoundVolume = it + onVolumeChanged(it) + }, + soundIndex = selectedSoundIndex, + sounds = soundState.sounds, + onSoundSelected = { + selectedSoundIndex = it + onSoundSelected(it) + }, ) OrbitButton( @@ -141,7 +99,15 @@ private fun BottomSheetContent( contentColor = OrbitTheme.colors.white, pressedContainerColor = OrbitTheme.colors.gray_500, pressedContentColor = OrbitTheme.colors.white.copy(alpha = 0.7f), - onClick = onComplete, + onClick = { + onDismiss() + onComplete( + selectedVibrationEnabled, + selectedSoundEnabled, + selectedSoundVolume, + selectedSoundIndex, + ) + }, ) } } @@ -300,27 +266,17 @@ private fun SoundSelectionItem( @Preview @Composable private fun AlarmSoundBottomSheetPreview() { - var isVibrationEnabled by remember { mutableStateOf(true) } - var isSoundEnabled by remember { mutableStateOf(true) } - var soundVolume by remember { mutableIntStateOf(0) } - var soundIndex by remember { mutableIntStateOf(0) } - val sounds by remember { mutableStateOf((1..20).map { AlarmSound("sound $it", Uri.EMPTY) }) } - var isSheetOpen by remember { mutableStateOf(true) } - OrbitTheme { AlarmSoundBottomSheet( - vibrationEnabled = isVibrationEnabled, - soundEnabled = isSoundEnabled, - soundVolume = soundVolume, - soundIndex = soundIndex, - sounds = sounds, - onVibrationToggle = { isVibrationEnabled = !isVibrationEnabled }, - onSoundToggle = { isSoundEnabled = !isSoundEnabled }, - onVolumeChanged = { soundVolume = it }, - onSoundSelected = { soundIndex = it }, - onComplete = { isSheetOpen = false }, - isSheetOpen = isSheetOpen, - onDismiss = { isSheetOpen = false }, + soundState = AlarmAddEditContract.AlarmSoundState( + sounds = (1..20).map { AlarmSound("sound $it", Uri.EMPTY) }, + ), + onVibrationToggle = {}, + onSoundToggle = {}, + onVolumeChanged = {}, + onSoundSelected = {}, + onComplete = { _, _, _, _ -> + }, ) } } diff --git a/feature/onboarding/build.gradle.kts b/feature/onboarding/build.gradle.kts index 8a44d192..e573da87 100644 --- a/feature/onboarding/build.gradle.kts +++ b/feature/onboarding/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(libs.orbit.core) implementation(libs.orbit.compose) implementation(libs.orbit.viewmodel) + implementation(libs.compose.material) implementation(libs.coil.compose) implementation(libs.coil.gif) implementation(libs.accompanist.permission) diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingContract.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingContract.kt index 86bf3029..95dc24db 100644 --- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingContract.kt +++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingContract.kt @@ -19,7 +19,6 @@ sealed class OnboardingContract { val isBirthDateValid: Boolean = false, val isBirthTimeValid: Boolean = false, val isValid: Boolean = false, - val isBottomSheetOpen: Boolean = false, val isShowWarningDialog: Boolean = false, ) : UiState { val birthDateFormatted: String @@ -54,7 +53,8 @@ sealed class OnboardingContract { data object Submit : Action() data class UpdateGender(val gender: String) : Action() data class UpdateBirthDate(val lunar: String, val year: Int, val month: Int, val day: Int) : Action() - data object ToggleBottomSheet : Action() + data object ShowBottomSheet : Action() + data object HideBottomSheet : Action() data object CompleteOnboarding : Action() data class OpenWebView(val url: String) : Action() data object ShowWarningDialog : Action() @@ -71,6 +71,10 @@ sealed class OnboardingContract { data object NavigateBack : SideEffect() + data object ShowBottomSheet : SideEffect() + + data object HideBottomSheet : SideEffect() + data object OnboardingCompleted : SideEffect() data class OpenWebView(val url: String) : SideEffect() diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingGenderScreen.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingGenderScreen.kt index c9ba857d..06a2ec94 100644 --- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingGenderScreen.kt +++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingGenderScreen.kt @@ -1,5 +1,6 @@ package com.yapp.onboarding +import android.net.Uri import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -21,18 +22,26 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.navOptions import com.yapp.analytics.AnalyticsEvent import com.yapp.analytics.LocalAnalyticsHelper +import com.yapp.common.navigation.OrbitNavigator +import com.yapp.common.navigation.route.OnboardingBaseRoute import com.yapp.designsystem.theme.OrbitTheme import com.yapp.onboarding.component.UserInfoBottomSheet +import com.yapp.ui.component.bottomsheet.OrbitBottomSheetState +import com.yapp.ui.component.bottomsheet.rememberOrbitBottomSheetState import com.yapp.ui.component.dialog.OrbitDialog import com.yapp.ui.toggle.OrbitGenderToggle import com.yapp.ui.utils.heightForScreenPercentage import com.yapp.ui.utils.paddingForScreenPercentage import feature.onboarding.R +import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun OnboardingGenderRoute( + navigator: OrbitNavigator, + bottomSheetState: OrbitBottomSheetState, viewModel: OnboardingViewModel, ) { val state by viewModel.container.stateFlow.collectAsStateWithLifecycle() @@ -50,55 +59,110 @@ fun OnboardingGenderRoute( ) } - BackHandler { - viewModel.processAction(OnboardingContract.Action.PreviousStep) + viewModel.collectSideEffect { sideEffect -> + handleSideEffect( + sideEffect = sideEffect, + navigator = navigator, + bottomSheetState = bottomSheetState, + state = state, + processAction = viewModel::processAction, + ) } OnboardingGenderScreen( state = state, + bottomSheetState = bottomSheetState, currentStep = 5, totalSteps = 6, - onNextClick = { viewModel.processAction(OnboardingContract.Action.ToggleBottomSheet) }, - onBackClick = { viewModel.processAction(OnboardingContract.Action.PreviousStep) }, - onGenderSelect = { gender -> - analyticsHelper.logEvent( - AnalyticsEvent( - type = "onboarding_gender_select", - properties = mapOf( - AnalyticsEvent.OnboardingPropertiesKeys.GENDER to gender, - ), - ), - ) - viewModel.processAction(OnboardingContract.Action.UpdateGender(gender)) - }, - onDismissRequest = { - viewModel.processAction(OnboardingContract.Action.ToggleBottomSheet) - }, - onConfirmRequest = { - viewModel.processAction(OnboardingContract.Action.ToggleBottomSheet) - viewModel.processAction(OnboardingContract.Action.Submit) - }, + processAction = viewModel::processAction, ) } +private suspend fun handleSideEffect( + sideEffect: OnboardingContract.SideEffect, + navigator: OrbitNavigator, + bottomSheetState: OrbitBottomSheetState, + state: OnboardingContract.State, + processAction: (OnboardingContract.Action) -> Unit, +) { + when (sideEffect) { + is OnboardingContract.SideEffect.NavigateToNextStep -> { + navigator.navigateToOnboardingNextStep(sideEffect.currentStep) + } + + OnboardingContract.SideEffect.NavigateBack -> { + processAction(OnboardingContract.Action.Reset) + navigator.navigateBack() + } + + is OnboardingContract.SideEffect.ShowBottomSheet -> { + bottomSheetState.show { + UserInfoBottomSheet( + name = state.userName, + gender = state.selectedGender ?: "무지개", + birthDate = state.birthDateFormatted, + birthTime = state.birthTimeFormatted, + onDismiss = { + processAction(OnboardingContract.Action.HideBottomSheet) + }, + onConfirm = { + processAction(OnboardingContract.Action.HideBottomSheet) + processAction(OnboardingContract.Action.Submit) + }, + ) + } + } + + is OnboardingContract.SideEffect.HideBottomSheet -> { + bottomSheetState.hide() + } + + OnboardingContract.SideEffect.OnboardingCompleted -> { + navigator.navigateToHome( + navOptions = navOptions { + popUpTo(OnboardingBaseRoute) { + inclusive = true + } + }, + ) + } + + is OnboardingContract.SideEffect.OpenWebView -> { + navigator.navigateToWebView(Uri.encode(sideEffect.url)) + } + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun OnboardingGenderScreen( state: OnboardingContract.State, + bottomSheetState: OrbitBottomSheetState, currentStep: Int, totalSteps: Int, - onNextClick: () -> Unit, - onBackClick: () -> Unit, - onGenderSelect: (String) -> Unit, - onDismissRequest: () -> Unit, - onConfirmRequest: () -> Unit, + processAction: (OnboardingContract.Action) -> Unit, + logEvent: (AnalyticsEvent) -> Unit = { }, ) { + BackHandler { + if (state.isShowWarningDialog) { + processAction(OnboardingContract.Action.HideWarningDialog) + } else if (bottomSheetState.state.isVisible) { + processAction(OnboardingContract.Action.HideBottomSheet) + } else { + processAction(OnboardingContract.Action.PreviousStep) + } + } + OnboardingScreen( currentStep = currentStep, totalSteps = totalSteps, isButtonEnabled = state.selectedGender != null, - onNextClick = onNextClick, - onBackClick = onBackClick, + onNextClick = { + processAction(OnboardingContract.Action.ShowBottomSheet) + }, + onBackClick = { + processAction(OnboardingContract.Action.PreviousStep) + }, buttonLabel = "다음", ) { Column( @@ -121,19 +185,24 @@ fun OnboardingGenderScreen( .paddingForScreenPercentage(topPercentage = 0.11f), horizontalArrangement = Arrangement.spacedBy(15.dp), ) { - Box(modifier = Modifier.weight(1f)) { - OrbitGenderToggle( - label = "남성", - isSelected = state.selectedGender == "남성", - onToggle = { onGenderSelect("남성") }, - ) - } - Box(modifier = Modifier.weight(1f)) { - OrbitGenderToggle( - label = "여성", - isSelected = state.selectedGender == "여성", - onToggle = { onGenderSelect("여성") }, - ) + listOf("남성", "여성").forEach { gender -> + Box(modifier = Modifier.weight(1f)) { + OrbitGenderToggle( + label = gender, + isSelected = state.selectedGender == gender, + onToggle = { + logEvent( + AnalyticsEvent( + type = "onboarding_gender_select", + properties = mapOf( + AnalyticsEvent.OnboardingPropertiesKeys.GENDER to gender, + ), + ), + ) + processAction(OnboardingContract.Action.UpdateGender(gender)) + }, + ) + } } } } @@ -144,21 +213,9 @@ fun OnboardingGenderScreen( title = stringResource(id = R.string.onboarding_warning_dialog_title), message = stringResource(id = R.string.onboarding_warning_dialog_message), confirmText = stringResource(id = R.string.onboarding_warning_dialog_btn_confirm), - onConfirm = { - onConfirmRequest() - }, + onConfirm = { processAction(OnboardingContract.Action.HideWarningDialog) }, ) } - - UserInfoBottomSheet( - isSheetOpen = state.isBottomSheetOpen, - onDismissRequest = onDismissRequest, - onConfirmRequest = onConfirmRequest, - name = state.userName, - gender = state.selectedGender ?: "무지개", - birthDate = state.birthDateFormatted, - birthTime = state.birthTimeFormatted, - ) } @Composable @@ -168,12 +225,9 @@ fun OnboardingGenderScreenPreview() { state = OnboardingContract.State( isButtonEnabled = true, ), + bottomSheetState = rememberOrbitBottomSheetState(), currentStep = 0, totalSteps = 0, - onNextClick = {}, - onBackClick = {}, - onGenderSelect = {}, - onDismissRequest = {}, - onConfirmRequest = {}, + processAction = {}, ) } diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingNavGraph.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingNavGraph.kt index 7b0677fa..ff7417d7 100644 --- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingNavGraph.kt +++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingNavGraph.kt @@ -9,90 +9,106 @@ import com.yapp.common.navigation.OrbitNavigator import com.yapp.common.navigation.extensions.sharedHiltViewModel import com.yapp.common.navigation.route.OnboardingBaseRoute import com.yapp.common.navigation.route.OnboardingDestination +import com.yapp.ui.component.bottomsheet.OrbitBottomSheetState import org.orbitmvi.orbit.compose.collectSideEffect fun NavGraphBuilder.onboardingNavGraph( navigator: OrbitNavigator, + bottomSheetState: OrbitBottomSheetState, ) { navigation(startDestination = OnboardingDestination.Explain) { composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - viewModel.collectSideEffect { - handleSideEffect(it, navigator, viewModel) + + viewModel.collectSideEffect { sideEffect -> + handleOnboardingCommonSideEffect(sideEffect, navigator, viewModel::processAction) } + OnboardingExplainRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - viewModel.collectSideEffect { - handleSideEffect(it, navigator, viewModel) + + viewModel.collectSideEffect { sideEffect -> + handleOnboardingCommonSideEffect(sideEffect, navigator, viewModel::processAction) } + OnboardingAlarmTimeSelectionRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - viewModel.collectSideEffect { - handleSideEffect(it, navigator, viewModel) + + viewModel.collectSideEffect { sideEffect -> + handleOnboardingCommonSideEffect(sideEffect, navigator, viewModel::processAction) } + OnboardingBirthdayRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - viewModel.collectSideEffect { - handleSideEffect(it, navigator, viewModel) + + viewModel.collectSideEffect { sideEffect -> + handleOnboardingCommonSideEffect(sideEffect, navigator, viewModel::processAction) } + OnboardingTimeOfBirthRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - viewModel.collectSideEffect { - handleSideEffect(it, navigator, viewModel) + + viewModel.collectSideEffect { sideEffect -> + handleOnboardingCommonSideEffect(sideEffect, navigator, viewModel::processAction) } + OnboardingNameRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - viewModel.collectSideEffect { - handleSideEffect(it, navigator, viewModel) - } - OnboardingGenderRoute(viewModel) + + OnboardingGenderRoute(navigator, bottomSheetState, viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - viewModel.collectSideEffect { - handleSideEffect(it, navigator, viewModel) + + viewModel.collectSideEffect { sideEffect -> + handleOnboardingCommonSideEffect(sideEffect, navigator, viewModel::processAction) } + OnboardingAccessRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - viewModel.collectSideEffect { - handleSideEffect(it, navigator, viewModel) + + viewModel.collectSideEffect { sideEffect -> + handleOnboardingCommonSideEffect(sideEffect, navigator, viewModel::processAction) } + OnboardingCompleteRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - viewModel.collectSideEffect { - handleSideEffect(it, navigator, viewModel) + + viewModel.collectSideEffect { sideEffect -> + handleOnboardingCommonSideEffect(sideEffect, navigator, viewModel::processAction) } + OnboardingCompleteRoute2(viewModel) } } } -private fun handleSideEffect( +private fun handleOnboardingCommonSideEffect( sideEffect: OnboardingContract.SideEffect, navigator: OrbitNavigator, - viewModel: OnboardingViewModel, + processAction: (OnboardingContract.Action) -> Unit, ) { when (sideEffect) { is OnboardingContract.SideEffect.NavigateToNextStep -> { @@ -100,7 +116,7 @@ private fun handleSideEffect( } OnboardingContract.SideEffect.NavigateBack -> { - viewModel.processAction(OnboardingContract.Action.Reset) + processAction(OnboardingContract.Action.Reset) navigator.navigateBack() } @@ -117,5 +133,7 @@ private fun handleSideEffect( is OnboardingContract.SideEffect.OpenWebView -> { navigator.navigateToWebView(Uri.encode(sideEffect.url)) } + + else -> { } } } diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingViewModel.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingViewModel.kt index 7281a9a5..24f3ba73 100644 --- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingViewModel.kt +++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingViewModel.kt @@ -57,7 +57,8 @@ class OnboardingViewModel @Inject constructor( is OnboardingContract.Action.Reset -> resetFields() is OnboardingContract.Action.Submit -> submitUserInfo() is OnboardingContract.Action.UpdateGender -> updateGender(action.gender) - is OnboardingContract.Action.ToggleBottomSheet -> toggleBottomSheet() + is OnboardingContract.Action.ShowBottomSheet -> showBottomSheet() + is OnboardingContract.Action.HideBottomSheet -> hideBottomSheet() is OnboardingContract.Action.CompleteOnboarding -> completeOnboarding() is OnboardingContract.Action.OpenWebView -> openWebView(action.url) is OnboardingContract.Action.ShowWarningDialog -> showWarningDialog() @@ -90,7 +91,6 @@ class OnboardingViewModel @Inject constructor( ), ) - reduce { state.copy(isBottomSheetOpen = false) } moveToNextStep() } else { showWarningDialog() @@ -225,9 +225,12 @@ class OnboardingViewModel @Inject constructor( reduce { state.copy(selectedGender = gender, isButtonEnabled = true) } } - private fun toggleBottomSheet() = intent { - val isCurrentlyOpen = state.isBottomSheetOpen - reduce { state.copy(isBottomSheetOpen = !isCurrentlyOpen) } + private fun showBottomSheet() = intent { + postSideEffect(OnboardingContract.SideEffect.ShowBottomSheet) + } + + private fun hideBottomSheet() = intent { + postSideEffect(OnboardingContract.SideEffect.HideBottomSheet) } private fun completeOnboarding() = intent { diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/component/UserInfoBottomSheet.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/component/UserInfoBottomSheet.kt index ce6b6629..af935235 100644 --- a/feature/onboarding/src/main/java/com/yapp/onboarding/component/UserInfoBottomSheet.kt +++ b/feature/onboarding/src/main/java/com/yapp/onboarding/component/UserInfoBottomSheet.kt @@ -7,9 +7,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SheetState import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -17,7 +15,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.yapp.designsystem.theme.OrbitTheme -import com.yapp.ui.component.OrbitBottomSheet import com.yapp.ui.component.button.OrbitButton import com.yapp.ui.utils.paddingForScreenPercentage import com.yapp.ui.utils.widthForScreenPercentage @@ -26,80 +23,70 @@ import feature.onboarding.R @OptIn(ExperimentalMaterial3Api::class) @Composable fun UserInfoBottomSheet( - sheetState: SheetState = rememberModalBottomSheetState(), - isSheetOpen: Boolean, - onDismissRequest: () -> Unit, - onConfirmRequest: () -> Unit, name: String, gender: String, birthDate: String, birthTime: String, + onDismiss: () -> Unit, + onConfirm: () -> Unit, ) { - if (isSheetOpen) { - OrbitBottomSheet( - isSheetOpen = isSheetOpen, - sheetState = sheetState, - onDismissRequest = onDismissRequest, - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .paddingForScreenPercentage(allPercentage = 0.03f), - ) { - Text( - text = stringResource(R.string.onboarding_step6_bs_title), - modifier = Modifier - .paddingForScreenPercentage( - topPercentage = 0.005f, - bottomPercentage = 0.027f, - ), - style = OrbitTheme.typography.heading2SemiBold, - color = OrbitTheme.colors.white, - ) - UserInfoRow(label = stringResource(R.string.onboarding_step6_bs_name), value = name) - UserInfoRow( - label = stringResource(R.string.onboarding_step6_bs_gender), - value = gender, - ) - UserInfoRow( - label = stringResource(R.string.onboarding_step6_bs_birth), - value = birthDate, - ) - UserInfoRow( - label = stringResource(R.string.onboarding_step6_bs_time), - value = birthTime, - ) + Column( + modifier = Modifier + .fillMaxWidth() + .paddingForScreenPercentage(allPercentage = 0.03f), + ) { + Text( + text = stringResource(R.string.onboarding_step6_bs_title), + modifier = Modifier + .paddingForScreenPercentage( + topPercentage = 0.005f, + bottomPercentage = 0.027f, + ), + style = OrbitTheme.typography.heading2SemiBold, + color = OrbitTheme.colors.white, + ) + UserInfoRow(label = stringResource(R.string.onboarding_step6_bs_name), value = name) + UserInfoRow( + label = stringResource(R.string.onboarding_step6_bs_gender), + value = gender, + ) + UserInfoRow( + label = stringResource(R.string.onboarding_step6_bs_birth), + value = birthDate, + ) + UserInfoRow( + label = stringResource(R.string.onboarding_step6_bs_time), + value = birthTime, + ) - Row( - modifier = Modifier - .fillMaxWidth() - .paddingForScreenPercentage(topPercentage = 0.032f), - verticalAlignment = Alignment.CenterVertically, - ) { - OrbitButton( - label = stringResource(R.string.onboarding_step6_bs_btn_dismiss), - modifier = Modifier.weight(1f), - onClick = onDismissRequest, - enabled = true, - containerColor = OrbitTheme.colors.gray_600, - contentColor = OrbitTheme.colors.white, - pressedContainerColor = OrbitTheme.colors.gray_500, - pressedContentColor = OrbitTheme.colors.white.copy(alpha = 0.7f), - shape = RoundedCornerShape(12.dp), - ) - Spacer(modifier = Modifier.widthForScreenPercentage(0.032f)) - OrbitButton( - label = stringResource(R.string.onboarding_step6_bs_btn_confirm), - modifier = Modifier.weight(1f), - onClick = onConfirmRequest, - enabled = true, - pressedContainerColor = OrbitTheme.colors.main.copy(alpha = 0.8f), - pressedContentColor = OrbitTheme.colors.gray_600, - shape = RoundedCornerShape(12.dp), + Row( + modifier = Modifier + .fillMaxWidth() + .paddingForScreenPercentage(topPercentage = 0.032f), + verticalAlignment = Alignment.CenterVertically, + ) { + OrbitButton( + label = stringResource(R.string.onboarding_step6_bs_btn_dismiss), + modifier = Modifier.weight(1f), + onClick = onDismiss, + enabled = true, + containerColor = OrbitTheme.colors.gray_600, + contentColor = OrbitTheme.colors.white, + pressedContainerColor = OrbitTheme.colors.gray_500, + pressedContentColor = OrbitTheme.colors.white.copy(alpha = 0.7f), + shape = RoundedCornerShape(12.dp), + ) + Spacer(modifier = Modifier.widthForScreenPercentage(0.032f)) + OrbitButton( + label = stringResource(R.string.onboarding_step6_bs_btn_confirm), + modifier = Modifier.weight(1f), + onClick = onConfirm, + enabled = true, + pressedContainerColor = OrbitTheme.colors.main.copy(alpha = 0.8f), + pressedContentColor = OrbitTheme.colors.gray_600, + shape = RoundedCornerShape(12.dp), - ) - } - } + ) } } } @@ -134,12 +121,11 @@ fun UserInfoRow( @Preview fun UserInfoBottomSheetPreview() { UserInfoBottomSheet( - isSheetOpen = true, - onDismissRequest = { }, - onConfirmRequest = { }, name = "홍길동", gender = "남성", birthDate = "1990년 1월 1일", birthTime = "12:00", + onDismiss = { }, + onConfirm = { }, ) }