Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
87a4e84
[REFACTOR/#237] OrbitBottomSheet 내부 isSheetOpen 상태 간리 로직 제거
DongChyeon Jul 27, 2025
72e0073
[REFACTOR/#237] when 문을 사용하여 BottomSheetType에 따라 바텀시트 호출
DongChyeon Jul 27, 2025
184b2d2
[REFACTOR/#237] AlarmSnoozeBottomSheet의 활성화 여부, 간격, 횟수 상태를 내부 상태로 이동
DongChyeon Jul 27, 2025
848916d
[REFACTOR/#237] AlarmAddEditViewModel에서 스누즈 상태를 인덱스가 아닌 실제 값 기반으로 관리
DongChyeon Jul 27, 2025
043ada5
[FEAT/#237] OrbitBottomSheetState 정의
DongChyeon Jul 28, 2025
df41065
[FEAT/#237] ModalBottomSheet -> ModalBottomSheetLayout 마이그레이션
DongChyeon Jul 28, 2025
1ef65d0
[FEAT/#237] AlarmMissionBottomSheet 단계 상태를 스택으로 관리
DongChyeon Jul 28, 2025
300cfef
[FIX/#237] OrbitBottomSheetState가 닫힐 때마다 content를 null로 초기화
DongChyeon Jul 28, 2025
1bd3606
[REFACTOR/#237] 알람 소리 바텀시트에서 진동, 소리, 음량, 선택 상태를 내부 상태로 이동
DongChyeon Jul 28, 2025
98f8017
[REFACTOR/#237] AlarmSoundBottomSheet에서 soundState를 상태 객체로 통합하여 관리
DongChyeon Jul 28, 2025
9165446
[REFACTOR/#237] AlarmSnoozeBottomSheet에서 Snooze 상태를 AlarmSnoozeState …
DongChyeon Jul 28, 2025
ec3d5f2
[REFACTOR/#237] AlarmMissionBottomSheet의 초기 미션 상태를 AlarmMissionState로 변경
DongChyeon Jul 28, 2025
f51ce57
[REFACTOR/#237] 바텀시트에서 설정 완료 시 상태를 한번에 업데이트하도록 수정
DongChyeon Jul 28, 2025
13c4132
[FIX/#239] 바텀 시트나 다이얼로그 노출 시 뒤로가기 버튼 누르면 닫기
DongChyeon Jul 28, 2025
99908c4
[UI/#237] AlarmMissionBottomSheet 횟수 설정 화면 하단 패딩 축소
DongChyeon Jul 28, 2025
e9bafd0
[REFACTOR/#237] OrbitBottomSheetState.setContent를 private으로 변경
DongChyeon Jul 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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">

<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
Expand Down
60 changes: 37 additions & 23 deletions app/src/main/java/com/yapp/orbit/OrbitNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import com.yapp.mission.missionScreen
import com.yapp.onboarding.onboardingNavGraph
import com.yapp.setting.settingNavGraph
import com.yapp.splash.splashScreen
import com.yapp.ui.component.bottomsheet.OrbitBottomSheetLayout
import com.yapp.ui.component.bottomsheet.OrbitBottomSheetState
import com.yapp.ui.component.bottomsheet.rememberOrbitBottomSheetState
import com.yapp.ui.component.snackbar.CustomSnackBarVisuals
import com.yapp.ui.component.snackbar.OrbitSnackBar
import com.yapp.webview.webViewScreen
Expand All @@ -34,34 +37,45 @@ import com.yapp.webview.webViewScreen
internal fun OrbitNavHost(
modifier: Modifier = Modifier,
navigator: OrbitNavigator = rememberOrbitNavigator(),
bottomSheetState: OrbitBottomSheetState = rememberOrbitBottomSheetState(),
) {
val snackBarHostState = remember { SnackbarHostState() }

Scaffold(
modifier = modifier,
snackbarHost = {
OrbitSnackBarHost(snackBarHostState = snackBarHostState)
},
containerColor = OrbitTheme.colors.gray_900,
OrbitBottomSheetLayout(
sheetState = bottomSheetState,
) {
NavHost(
navController = navigator.navController,
startDestination = navigator.startDestination,
modifier = Modifier.navigationBarsPadding(),
Scaffold(
modifier = modifier,
snackbarHost = {
OrbitSnackBarHost(snackBarHostState = snackBarHostState)
},
containerColor = OrbitTheme.colors.gray_900,
) {
splashScreen(navigator = navigator)
onboardingNavGraph(navigator = navigator)
homeNavGraph(
navigator = navigator,
snackBarHostState = snackBarHostState,
)
missionScreen(navigator = navigator)
fortuneNavGraph(
navigator = navigator,
snackBarHostState = snackBarHostState,
)
settingNavGraph(navigator = navigator)
webViewScreen(navigator = navigator)
NavHost(
navController = navigator.navController,
startDestination = navigator.startDestination,
modifier = Modifier.navigationBarsPadding(),
) {
splashScreen(
navigator = navigator,
)
onboardingNavGraph(
navigator = navigator,
bottomSheetState = bottomSheetState,
)
homeNavGraph(
navigator = navigator,
bottomSheetState = bottomSheetState,
snackBarHostState = snackBarHostState,
)
missionScreen(navigator = navigator)
fortuneNavGraph(
navigator = navigator,
snackBarHostState = snackBarHostState,
)
settingNavGraph(navigator = navigator)
webViewScreen(navigator = navigator)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.yapp.ui.component.bottomsheet

import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable

typealias BottomSheetContent = @Composable BoxScope.() -> Unit
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
}
}

Expand Down Expand Up @@ -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")
}
}
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BottomSheetContent?>(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<BottomSheetContent?>,
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()
}
3 changes: 3 additions & 0 deletions feature/home/src/main/java/com/yapp/home/HomeNavGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ 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"
const val DELETE_ALARM_RESULT_KEY = "deleteAlarmResult"

fun NavGraphBuilder.homeNavGraph(
navigator: OrbitNavigator,
bottomSheetState: OrbitBottomSheetState,
snackBarHostState: SnackbarHostState,
) {
navigation<HomeBaseRoute>(
Expand All @@ -30,6 +32,7 @@ fun NavGraphBuilder.homeNavGraph(
composable<HomeDestination.AlarmAddEdit> {
AlarmAddEditRoute(
navigator = navigator,
bottomSheetState = bottomSheetState,
snackBarHostState = snackBarHostState,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = listOf("1분", "3분", "5분", "10분", "15분"),
val snoozeCounts: List<String> = listOf("1회", "3회", "5회", "10회", "무한"),
val snoozeInterval: Int = 5,
val snoozeCount: Int = 5,
)

data class AlarmSoundState(
Expand Down Expand Up @@ -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 {
Expand All @@ -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()
Expand Down Expand Up @@ -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(),
Expand Down
Loading