diff --git a/app/src/main/kotlin/com/chipichipi/dobedobe/navigation/DobeDobeNavHost.kt b/app/src/main/kotlin/com/chipichipi/dobedobe/navigation/DobeDobeNavHost.kt index 56bf36c7..ee093c2c 100644 --- a/app/src/main/kotlin/com/chipichipi/dobedobe/navigation/DobeDobeNavHost.kt +++ b/app/src/main/kotlin/com/chipichipi/dobedobe/navigation/DobeDobeNavHost.kt @@ -1,42 +1,35 @@ package com.chipichipi.dobedobe.navigation -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import com.chipichipi.dobedobe.feature.dashboard.navigation.DashboardRoute import com.chipichipi.dobedobe.feature.dashboard.navigation.dashboardScreen +import com.chipichipi.dobedobe.feature.setting.navigation.navigateToSetting +import com.chipichipi.dobedobe.feature.setting.navigation.settingScreen import com.chipichipi.dobedobe.ui.DobeDobeAppState @Composable -fun DobeDobeNavHost( +internal fun DobeDobeNavHost( appState: DobeDobeAppState, onShowSnackbar: suspend (String, String?) -> Boolean, modifier: Modifier = Modifier, ) { val navController = appState.navController - Box( - modifier = - Modifier.windowInsetsPadding( - WindowInsets.safeDrawing.only( - WindowInsetsSides.Top, - ), - ), + NavHost( + navController = navController, + startDestination = DashboardRoute, + modifier = modifier, ) { - NavHost( - navController = navController, - startDestination = DashboardRoute, - modifier = modifier, - ) { - dashboardScreen( - onShowSnackbar = onShowSnackbar, - ) - } + dashboardScreen( + onShowSnackbar = onShowSnackbar, + navigateToSetting = navController::navigateToSetting, + ) + + settingScreen( + onShowSnackbar = onShowSnackbar, + navigateToBack = navController::popBackStack, + ) } } diff --git a/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Switch.kt b/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Switch.kt new file mode 100644 index 00000000..fc64967e --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/chipichipi/dobedobe/core/designsystem/component/Switch.kt @@ -0,0 +1,47 @@ +package com.chipichipi.dobedobe.core.designsystem.component + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.chipichipi.dobedobe.core.designsystem.theme.DobeDobeTheme + +/** + * TODO : Switch 컴포넌트 단순 Wrapper 임시 처리, 각 상태 디자인 정의 필요 + */ +@Composable +fun DobeDobeSwitch( + checked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource? = null, +) { + // TODO : 디자인시스템 나오면 컬러 정의 필요 + Switch( + modifier = modifier, + checked = checked, + onCheckedChange = onCheckedChange, + enabled = enabled, + interactionSource = interactionSource, + colors = SwitchDefaults.colors( + checkedThumbColor = Color.White, + checkedTrackColor = Color.Red, + uncheckedThumbColor = Color.Gray, + uncheckedTrackColor = Color.Transparent, + ), + ) +} + +@ThemePreviews +@Composable +private fun DobeDobeSwitchPreview() { + DobeDobeTheme { + DobeDobeSwitch( + checked = true, + onCheckedChange = {}, + ) + } +} diff --git a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt index 4e72647d..2de2f485 100644 --- a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt +++ b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/DashboardScreen.kt @@ -43,6 +43,7 @@ import org.koin.androidx.compose.koinViewModel @Composable internal fun DashboardRoute( onShowSnackbar: suspend (String, String?) -> Boolean, + navigateToSetting: () -> Unit, modifier: Modifier = Modifier, viewModel: DashboardViewModel = koinViewModel(), ) { @@ -54,6 +55,7 @@ internal fun DashboardRoute( uiState = uiState, setGoalNotificationEnabled = viewModel::setGoalNotificationEnabled, disableSystemNotificationDialog = viewModel::disableSystemNotificationDialog, + navigateToSetting = navigateToSetting, ) } @@ -64,6 +66,7 @@ private fun DashboardScreen( uiState: DashboardUiState, setGoalNotificationEnabled: (Boolean) -> Unit, disableSystemNotificationDialog: () -> Unit, + navigateToSetting: () -> Unit, modifier: Modifier = Modifier, ) { val bottomSheetScaffoldState = rememberBottomSheetScaffoldState( @@ -90,7 +93,7 @@ private fun DashboardScreen( // TODO: 기능 추가 필요 DashboardTopAppBar( onEditClick = {}, - onSettingClick = {}, + navigateToSetting = navigateToSetting, ) }, ) { innerPadding -> diff --git a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/component/DashboardTopAppBar.kt b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/component/DashboardTopAppBar.kt index 19ad6b2b..960c64a6 100644 --- a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/component/DashboardTopAppBar.kt +++ b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/component/DashboardTopAppBar.kt @@ -22,7 +22,7 @@ import com.chipichipi.dobedobe.core.designsystem.theme.DobeDobeTheme @Composable internal fun DashboardTopAppBar( onEditClick: () -> Unit, - onSettingClick: () -> Unit, + navigateToSetting: () -> Unit, modifier: Modifier = Modifier, ) { DobeDobeTopAppBar( @@ -43,7 +43,7 @@ internal fun DashboardTopAppBar( } // TODO: 아이콘 교체 필요 IconButton( - onClick = onSettingClick, + onClick = navigateToSetting, ) { Icon( Icons.AutoMirrored.Filled.AltRoute, @@ -65,7 +65,7 @@ private fun DashboardTopAppBarPreview() { DobeDobeTheme { DashboardTopAppBar( onEditClick = {}, - onSettingClick = {}, + navigateToSetting = {}, ) } } diff --git a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/navigation/DashboardNavigation.kt b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/navigation/DashboardNavigation.kt index 74d2ae66..bca04968 100644 --- a/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/navigation/DashboardNavigation.kt +++ b/feature/dashboard/src/main/kotlin/com/chipichipi/dobedobe/feature/dashboard/navigation/DashboardNavigation.kt @@ -16,10 +16,12 @@ fun NavController.navigateToDashboard( fun NavGraphBuilder.dashboardScreen( onShowSnackbar: suspend (String, String?) -> Boolean, + navigateToSetting: () -> Unit, ) { composable { DashboardRoute( onShowSnackbar = onShowSnackbar, + navigateToSetting = navigateToSetting, ) } } diff --git a/feature/setting/build.gradle.kts b/feature/setting/build.gradle.kts index e02383ad..b27602b9 100644 --- a/feature/setting/build.gradle.kts +++ b/feature/setting/build.gradle.kts @@ -9,4 +9,5 @@ android { dependencies { implementation(projects.core.data) + implementation(projects.core.notifications) } diff --git a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingScreen.kt b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingScreen.kt new file mode 100644 index 00000000..94359713 --- /dev/null +++ b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingScreen.kt @@ -0,0 +1,144 @@ +package com.chipichipi.dobedobe.feature.setting + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowForwardIos +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.LifecycleResumeEffect +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.chipichipi.dobedobe.core.designsystem.component.DobeDobeSwitch +import com.chipichipi.dobedobe.core.notifications.NotificationUtil +import com.chipichipi.dobedobe.core.notifications.NotificationUtil.checkSystemNotificationEnabled +import com.chipichipi.dobedobe.feature.setting.component.SettingRow +import com.chipichipi.dobedobe.feature.setting.component.SettingTopAppBar +import com.chipichipi.dobedobe.feature.setting.util.openPlayStore +import org.koin.androidx.compose.koinViewModel + +@Composable +internal fun SettingRoute( + onShowSnackbar: suspend (String, String?) -> Boolean, + navigateToBack: () -> Unit, + modifier: Modifier = Modifier, + viewModel: SettingViewModel = koinViewModel(), +) { + val isGoalNotificationChecked by viewModel.isGoalNotificationChecked.collectAsStateWithLifecycle() + + SettingScreen( + modifier = modifier, + isGoalNotificationChecked = isGoalNotificationChecked, + navigateToBack = navigateToBack, + onNotificationToggled = viewModel::setGoalNotificationChecked, + ) +} + +@Composable +private fun SettingScreen( + isGoalNotificationChecked: Boolean, + navigateToBack: () -> Unit, + onNotificationToggled: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + SettingTopAppBar( + navigateToBack = navigateToBack, + ) + }, + ) { innerPadding -> + SettingBody( + isGoalNotificationChecked = isGoalNotificationChecked, + onNotificationToggled = onNotificationToggled, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + ) + } + + GoalNotificationEffect( + onNotificationToggled = onNotificationToggled, + ) +} + +@Composable +private fun SettingBody( + isGoalNotificationChecked: Boolean, + onNotificationToggled: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + Column( + modifier = modifier, + ) { + // TODO : 언어 대응 필요 + SettingRow( + label = "알림", + ) { + DobeDobeSwitch( + modifier = Modifier.padding(end = 8.dp), + checked = isGoalNotificationChecked, + onCheckedChange = { checked -> + NotificationUtil.handleNotificationToggle( + context = context, + checked = checked, + onNotificationToggled = onNotificationToggled, + ) + }, + ) + } + + // TODO : 언어 대응 필요 + SettingRow( + label = "앱 피드백 남기기", + ) { + IconButton( + modifier = Modifier.size(42.dp), + onClick = { openPlayStore(context) }, + ) { + // TODO: 아이콘 변경 필요 + Icon( + modifier = Modifier.size(24.dp), + imageVector = Icons.Default.ArrowForwardIos, + contentDescription = null, + ) + } + } + } +} + +@Composable +private fun GoalNotificationEffect( + onNotificationToggled: (Boolean) -> Unit, +) { + val context = LocalContext.current + + var systemNotificationEnabled by remember { + mutableStateOf(checkSystemNotificationEnabled(context)) + } + + LifecycleResumeEffect(Unit) { + val updatedSystemNotificationEnabled = checkSystemNotificationEnabled(context) + + if (systemNotificationEnabled != updatedSystemNotificationEnabled) { + systemNotificationEnabled = updatedSystemNotificationEnabled + if (updatedSystemNotificationEnabled) { + onNotificationToggled(true) + } + } + onPauseOrDispose { } + } +} diff --git a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingViewModel.kt b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingViewModel.kt index 9f5196bd..9c09f308 100644 --- a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingViewModel.kt +++ b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/SettingViewModel.kt @@ -1,5 +1,27 @@ package com.chipichipi.dobedobe.feature.setting import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.chipichipi.dobedobe.core.data.repository.UserRepository +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch -class SettingViewModel : ViewModel() +internal class SettingViewModel( + private val userRepository: UserRepository, +) : ViewModel() { + val isGoalNotificationChecked = userRepository.userData + .map { it.isGoalNotificationChecked } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = false, + ) + + fun setGoalNotificationChecked(checked: Boolean) { + viewModelScope.launch { + userRepository.setGoalNotificationChecked(checked) + } + } +} diff --git a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/component/SettingRow.kt b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/component/SettingRow.kt new file mode 100644 index 00000000..780c6870 --- /dev/null +++ b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/component/SettingRow.kt @@ -0,0 +1,76 @@ +package com.chipichipi.dobedobe.feature.setting.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.chipichipi.dobedobe.core.designsystem.component.ThemePreviews +import com.chipichipi.dobedobe.core.designsystem.theme.DobeDobeTheme + +@Composable +internal fun SettingRow( + label: String, + modifier: Modifier = Modifier, + trailingContent: @Composable () -> Unit, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.Center, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(52.dp) + .padding( + start = 24.dp, + end = 8.dp, + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + // TODO : 폰트 스타일 변경 필요 + Text( + text = label, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = Color.Black, + ) + + trailingContent() + } + + // TODO : 색상 변경 필요 + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + thickness = 8.dp, + color = Color(0xFFF2F3F6), + ) + } +} + +@ThemePreviews +@Composable +private fun SettingRowPreview() { + DobeDobeTheme { + SettingRow( + label = "TEST", + ) { + Row( + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = "TEST") + } + } + } +} diff --git a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/component/SettingTopAppBar.kt b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/component/SettingTopAppBar.kt new file mode 100644 index 00000000..a972b3cf --- /dev/null +++ b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/component/SettingTopAppBar.kt @@ -0,0 +1,47 @@ +package com.chipichipi.dobedobe.feature.setting.component + +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.chipichipi.dobedobe.core.designsystem.component.DobeDobeTopAppBar +import com.chipichipi.dobedobe.core.designsystem.component.ThemePreviews +import com.chipichipi.dobedobe.core.designsystem.theme.DobeDobeTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SettingTopAppBar( + navigateToBack: () -> Unit, + modifier: Modifier = Modifier, +) { + DobeDobeTopAppBar( + modifier = modifier, + navigationIcon = { + IconButton( + modifier = Modifier.size(48.dp), + onClick = navigateToBack, + ) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = Icons.Default.ArrowBackIosNew, + contentDescription = null, + ) + } + }, + ) +} + +@ThemePreviews +@Composable +private fun SettingTopAppBarPreview() { + DobeDobeTheme { + SettingTopAppBar( + navigateToBack = {}, + ) + } +} diff --git a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/navigation/SettingNavigation.kt b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/navigation/SettingNavigation.kt new file mode 100644 index 00000000..3352961f --- /dev/null +++ b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/navigation/SettingNavigation.kt @@ -0,0 +1,27 @@ +package com.chipichipi.dobedobe.feature.setting.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.chipichipi.dobedobe.feature.setting.SettingRoute +import kotlinx.serialization.Serializable + +@Serializable +data object SettingRoute + +fun NavController.navigateToSetting( + navOptions: NavOptions? = null, +) = navigate(route = SettingRoute, navOptions) + +fun NavGraphBuilder.settingScreen( + onShowSnackbar: suspend (String, String?) -> Boolean, + navigateToBack: () -> Unit, +) { + composable { + SettingRoute( + onShowSnackbar = onShowSnackbar, + navigateToBack = navigateToBack, + ) + } +} diff --git a/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/util/SettingUtil.kt b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/util/SettingUtil.kt new file mode 100644 index 00000000..bb84ea71 --- /dev/null +++ b/feature/setting/src/main/kotlin/com/chipichipi/dobedobe/feature/setting/util/SettingUtil.kt @@ -0,0 +1,15 @@ +package com.chipichipi.dobedobe.feature.setting.util + +import android.content.Context +import android.content.Intent +import android.net.Uri + +// TODO : 주소 확인 필요 +internal fun openPlayStore(context: Context) { + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse("https://play.google.com/store/apps/details?id=com.chipichipi.dobedobe"), + ) + + context.startActivity(intent) +}