Skip to content

Commit 6ef308e

Browse files
authored
Merge pull request #188 from YAPP-Github/BOOK-354-feature/#186
feat: 알림 설정 화면 UI 구현
2 parents e49c214 + a8a6add commit 6ef308e

11 files changed

Lines changed: 367 additions & 0 deletions

File tree

feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ data object SettingsScreen : ReedScreen(name = ScreenNames.SETTINGS)
3333
@Parcelize
3434
data object OssLicensesScreen : ReedScreen(name = "OssLicenses()")
3535

36+
@Parcelize
37+
data object NotificationScreen : ReedScreen(name = "Notification()")
38+
3639
@Parcelize
3740
data class RecordScreen(val userBookId: String) : ReedScreen(name = ScreenNames.RECORD)
3841

feature/settings/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,7 @@ ksp {
1717
dependencies {
1818
implementations(
1919
libs.logger,
20+
21+
libs.androidx.activity.compose,
2022
)
2123
}

feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.ninecraft.booket.core.data.api.repository.AuthRepository
1212
import com.ninecraft.booket.core.data.api.repository.RemoteConfigRepository
1313
import com.ninecraft.booket.core.model.UserState
1414
import com.ninecraft.booket.feature.screens.LoginScreen
15+
import com.ninecraft.booket.feature.screens.NotificationScreen
1516
import com.ninecraft.booket.feature.screens.OssLicensesScreen
1617
import com.ninecraft.booket.feature.screens.SettingsScreen
1718
import com.ninecraft.booket.feature.screens.WebViewScreen
@@ -153,6 +154,10 @@ class SettingsPresenter @AssistedInject constructor(
153154
navigator.goTo(WebViewScreen(url = policy.url, title = policy.title))
154155
}
155156

157+
is SettingsUiEvent.OnNotificationClick -> {
158+
navigator.goTo(NotificationScreen)
159+
}
160+
156161
is SettingsUiEvent.OnTermClick -> {
157162
val terms = WebViewConstants.TERMS_OF_SERVICE
158163
navigator.goTo(WebViewScreen(url = terms.url, title = terms.title))

feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUi.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,19 @@ internal fun SettingsUi(
9898
)
9999
},
100100
)
101+
SettingItem(
102+
title = stringResource(R.string.settings_notification),
103+
onItemClick = {
104+
state.eventSink(SettingsUiEvent.OnNotificationClick)
105+
},
106+
action = {
107+
Icon(
108+
imageVector = ImageVector.vectorResource(id = designR.drawable.ic_chevron_right),
109+
contentDescription = "Right Chevron Icon",
110+
tint = Color.Unspecified,
111+
)
112+
},
113+
)
101114
SettingItem(
102115
title = stringResource(R.string.settings_terms_of_service),
103116
onItemClick = {

feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ sealed interface SettingsUiEvent : CircuitUiEvent {
3232
data object InitSideEffect : SettingsUiEvent
3333
data object OnBackClick : SettingsUiEvent
3434
data object OnPolicyClick : SettingsUiEvent
35+
data object OnNotificationClick : SettingsUiEvent
3536
data object OnTermClick : SettingsUiEvent
3637
data object OnOssLicensesClick : SettingsUiEvent
3738
data object OnLogoutClick : SettingsUiEvent
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.ninecraft.booket.feature.settings.component
2+
3+
import android.annotation.SuppressLint
4+
import androidx.compose.animation.animateColor
5+
import androidx.compose.animation.core.animateDp
6+
import androidx.compose.animation.core.updateTransition
7+
import androidx.compose.foundation.background
8+
import androidx.compose.foundation.layout.Box
9+
import androidx.compose.foundation.layout.height
10+
import androidx.compose.foundation.layout.offset
11+
import androidx.compose.foundation.layout.size
12+
import androidx.compose.foundation.layout.width
13+
import androidx.compose.foundation.shape.CircleShape
14+
import androidx.compose.foundation.shape.RoundedCornerShape
15+
import androidx.compose.runtime.Composable
16+
import androidx.compose.runtime.getValue
17+
import androidx.compose.runtime.mutableStateOf
18+
import androidx.compose.runtime.remember
19+
import androidx.compose.runtime.setValue
20+
import androidx.compose.ui.Alignment
21+
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.draw.clip
23+
import androidx.compose.ui.draw.shadow
24+
import androidx.compose.ui.graphics.Color
25+
import androidx.compose.ui.unit.dp
26+
import com.ninecraft.booket.core.common.extensions.noRippleClickable
27+
import com.ninecraft.booket.core.designsystem.DevicePreview
28+
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
29+
30+
@SuppressLint("UseOfNonLambdaOffsetOverload")
31+
@Composable
32+
internal fun ReedSwitch(
33+
checked: Boolean,
34+
onCheckedChange: (Boolean) -> Unit,
35+
modifier: Modifier = Modifier,
36+
) {
37+
val transition = updateTransition(checked, label = "switchTransition")
38+
39+
val trackColor by transition.animateColor(label = "trackColor") {
40+
if (it) ReedTheme.colors.contentBrand else Color(0xFFE9E9EB)
41+
}
42+
43+
val thumbOffset by transition.animateDp(label = "thumbOffset") {
44+
if (it) 22.dp else 2.dp
45+
}
46+
47+
Box(
48+
modifier = modifier
49+
.width(51.dp)
50+
.height(31.dp)
51+
.clip(RoundedCornerShape(ReedTheme.radius.full))
52+
.background(trackColor)
53+
.noRippleClickable { onCheckedChange(!checked) },
54+
contentAlignment = Alignment.CenterStart,
55+
) {
56+
Box(
57+
modifier = Modifier
58+
.offset(x = thumbOffset)
59+
.size(27.dp)
60+
.shadow(elevation = 1.dp, shape = CircleShape)
61+
.clip(CircleShape)
62+
.background(ReedTheme.colors.contentInverse),
63+
)
64+
}
65+
}
66+
67+
@DevicePreview
68+
@Composable
69+
private fun ReedSwitchPreview() {
70+
var isChecked by remember { mutableStateOf(true) }
71+
72+
ReedTheme {
73+
ReedSwitch(
74+
checked = isChecked,
75+
onCheckedChange = { isChecked = it },
76+
)
77+
}
78+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.ninecraft.booket.feature.settings.component
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Row
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.material3.Text
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.ui.Alignment
11+
import androidx.compose.ui.Modifier
12+
import com.ninecraft.booket.core.designsystem.DevicePreview
13+
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
14+
15+
@Composable
16+
internal fun ToggleItem(
17+
title: String,
18+
description: String,
19+
isChecked: Boolean,
20+
onCheckedChange: (Boolean) -> Unit,
21+
modifier: Modifier = Modifier,
22+
) {
23+
Row(
24+
modifier = modifier
25+
.fillMaxWidth()
26+
.padding(
27+
vertical = ReedTheme.spacing.spacing4,
28+
horizontal = ReedTheme.spacing.spacing5,
29+
),
30+
verticalAlignment = Alignment.CenterVertically,
31+
horizontalArrangement = Arrangement.SpaceBetween,
32+
) {
33+
Column {
34+
Text(
35+
text = title,
36+
color = ReedTheme.colors.contentPrimary,
37+
style = ReedTheme.typography.body1Medium,
38+
)
39+
Text(
40+
text = description,
41+
color = ReedTheme.colors.contentTertiary,
42+
style = ReedTheme.typography.label1Medium,
43+
)
44+
}
45+
ReedSwitch(
46+
checked = isChecked,
47+
onCheckedChange = {
48+
onCheckedChange(!isChecked)
49+
},
50+
)
51+
}
52+
}
53+
54+
@DevicePreview
55+
@Composable
56+
private fun ToggleItemPreview() {
57+
ReedTheme {
58+
ToggleItem(
59+
title = "알림 받기",
60+
description = "리드에서 알림을 보내드려요",
61+
isChecked = true,
62+
onCheckedChange = {},
63+
)
64+
}
65+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.ninecraft.booket.feature.settings.notification
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.getValue
5+
import androidx.compose.runtime.mutableStateOf
6+
import androidx.compose.runtime.setValue
7+
import com.ninecraft.booket.feature.screens.NotificationScreen
8+
import com.slack.circuit.codegen.annotations.CircuitInject
9+
import com.slack.circuit.retained.rememberRetained
10+
import com.slack.circuit.runtime.Navigator
11+
import com.slack.circuit.runtime.presenter.Presenter
12+
import dagger.assisted.Assisted
13+
import dagger.assisted.AssistedFactory
14+
import dagger.assisted.AssistedInject
15+
import dagger.hilt.android.components.ActivityRetainedComponent
16+
17+
class NotificationPresenter @AssistedInject constructor(
18+
@Assisted val navigator: Navigator,
19+
) : Presenter<NotificationUiState> {
20+
@Composable
21+
override fun present(): NotificationUiState {
22+
var isNotificationEnabled by rememberRetained { mutableStateOf(false) }
23+
24+
fun handleEvent(event: NotificationUiEvent) {
25+
when (event) {
26+
is NotificationUiEvent.OnBackClick -> {
27+
navigator.pop()
28+
}
29+
30+
is NotificationUiEvent.OnNotificationToggle -> {
31+
isNotificationEnabled = event.enabled
32+
}
33+
}
34+
}
35+
return NotificationUiState(
36+
isNotificationEnabled = isNotificationEnabled,
37+
eventSink = ::handleEvent,
38+
)
39+
}
40+
41+
@CircuitInject(NotificationScreen::class, ActivityRetainedComponent::class)
42+
@AssistedFactory
43+
fun interface Factory {
44+
fun create(navigator: Navigator): NotificationPresenter
45+
}
46+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package com.ninecraft.booket.feature.settings.notification
2+
3+
import android.content.Intent
4+
import android.net.Uri
5+
import android.provider.Settings
6+
import androidx.activity.compose.rememberLauncherForActivityResult
7+
import androidx.activity.result.contract.ActivityResultContracts
8+
import androidx.compose.foundation.background
9+
import androidx.compose.foundation.layout.Arrangement
10+
import androidx.compose.foundation.layout.Column
11+
import androidx.compose.foundation.layout.Row
12+
import androidx.compose.foundation.layout.Spacer
13+
import androidx.compose.foundation.layout.fillMaxSize
14+
import androidx.compose.foundation.layout.fillMaxWidth
15+
import androidx.compose.foundation.layout.height
16+
import androidx.compose.foundation.layout.padding
17+
import androidx.compose.foundation.shape.RoundedCornerShape
18+
import androidx.compose.material3.Icon
19+
import androidx.compose.material3.Text
20+
import androidx.compose.runtime.Composable
21+
import androidx.compose.ui.Alignment
22+
import androidx.compose.ui.Modifier
23+
import androidx.compose.ui.graphics.vector.ImageVector
24+
import androidx.compose.ui.platform.LocalContext
25+
import androidx.compose.ui.res.stringResource
26+
import androidx.compose.ui.res.vectorResource
27+
import com.ninecraft.booket.core.common.extensions.noRippleClickable
28+
import com.ninecraft.booket.core.designsystem.DevicePreview
29+
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
30+
import com.ninecraft.booket.core.designsystem.theme.White
31+
import com.ninecraft.booket.core.ui.ReedScaffold
32+
import com.ninecraft.booket.core.ui.component.ReedBackTopAppBar
33+
import com.ninecraft.booket.feature.screens.NotificationScreen
34+
import com.ninecraft.booket.feature.settings.R
35+
import com.ninecraft.booket.feature.settings.component.ToggleItem
36+
import com.slack.circuit.codegen.annotations.CircuitInject
37+
import dagger.hilt.android.components.ActivityRetainedComponent
38+
import com.ninecraft.booket.core.designsystem.R as designR
39+
40+
@CircuitInject(NotificationScreen::class, ActivityRetainedComponent::class)
41+
@Composable
42+
internal fun NotificationUi(
43+
state: NotificationUiState,
44+
modifier: Modifier = Modifier,
45+
) {
46+
ReedScaffold(
47+
modifier = modifier.fillMaxSize(),
48+
containerColor = White,
49+
) { innerPadding ->
50+
Column(
51+
modifier = modifier
52+
.fillMaxSize()
53+
.padding(innerPadding),
54+
) {
55+
ReedBackTopAppBar(
56+
modifier = modifier.fillMaxWidth(),
57+
title = stringResource(R.string.settings_notification),
58+
onBackClick = {
59+
state.eventSink(NotificationUiEvent.OnBackClick)
60+
},
61+
)
62+
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2))
63+
NotificationGuideItem()
64+
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4))
65+
ToggleItem(
66+
title = stringResource(R.string.notification_toggle_title),
67+
description = stringResource(R.string.notification_toggle_description),
68+
isChecked = state.isNotificationEnabled,
69+
onCheckedChange = { enabled ->
70+
state.eventSink(NotificationUiEvent.OnNotificationToggle(enabled))
71+
},
72+
)
73+
}
74+
}
75+
}
76+
77+
@Composable
78+
internal fun NotificationGuideItem() {
79+
val context = LocalContext.current
80+
val settingsLauncher = rememberLauncherForActivityResult(
81+
contract = ActivityResultContracts.StartActivityForResult(),
82+
) { _ -> }
83+
84+
Row(
85+
modifier = Modifier
86+
.padding(horizontal = ReedTheme.spacing.spacing5)
87+
.fillMaxWidth()
88+
.background(
89+
color = ReedTheme.colors.baseSecondary,
90+
shape = RoundedCornerShape(ReedTheme.radius.md),
91+
)
92+
.noRippleClickable {
93+
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
94+
data = Uri.fromParts("package", context.packageName, null)
95+
}
96+
settingsLauncher.launch(intent)
97+
}
98+
.padding(
99+
vertical = ReedTheme.spacing.spacing6,
100+
horizontal = ReedTheme.spacing.spacing5,
101+
),
102+
verticalAlignment = Alignment.CenterVertically,
103+
horizontalArrangement = Arrangement.SpaceBetween,
104+
) {
105+
Column {
106+
Text(
107+
text = stringResource(R.string.notification_guide_title),
108+
color = ReedTheme.colors.contentBrand,
109+
style = ReedTheme.typography.body1SemiBold,
110+
)
111+
Text(
112+
text = stringResource(R.string.notification_guide_description),
113+
color = ReedTheme.colors.contentTertiary,
114+
style = ReedTheme.typography.label2Regular,
115+
)
116+
}
117+
Icon(
118+
imageVector = ImageVector.vectorResource(designR.drawable.ic_chevron_right),
119+
contentDescription = "Chevron Right Icon",
120+
tint = ReedTheme.colors.contentBrand,
121+
)
122+
}
123+
}
124+
125+
@DevicePreview
126+
@Composable
127+
private fun NotificationUiPreview() {
128+
ReedTheme {
129+
NotificationUi(
130+
state = NotificationUiState(
131+
eventSink = {},
132+
),
133+
)
134+
}
135+
}

0 commit comments

Comments
 (0)