diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/data/datastore/TimerDataStore.kt b/app/src/main/java/com/ggaebiz/ggaebiz/data/datastore/TimerDataStore.kt index 008ef24..8e3f24f 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/data/datastore/TimerDataStore.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/data/datastore/TimerDataStore.kt @@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -16,7 +17,12 @@ class TimerDataStore(private val dataStore: DataStore) { private val levelKey = intPreferencesKey("level_data") private val levelIdxKey = intPreferencesKey("level_idx_data") private val hourKey = intPreferencesKey("hour_data") + private val settingHourKey = intPreferencesKey("setting_hour_data") private val minuteKey = intPreferencesKey("minute_data") + private val settingMinuteKey = intPreferencesKey("setting_minute_data") + private val isRestCompletedKey = booleanPreferencesKey("is_rest_completed__data") + private val timerModeKey = stringPreferencesKey("timer_mode") + private val timerTypeKey = stringPreferencesKey("timer_type") private val snoozeCountKey = intPreferencesKey("snooze_count_data") companion object { @@ -26,6 +32,9 @@ class TimerDataStore(private val dataStore: DataStore) { const val DEFAULT_LEVEL_IDX = 0 const val DEFAULT_HOUR = 0 const val DEFAULT_MINUTE = 30 + const val DEFAULT_IS_REST_COMPLETED = false + const val DEFAULT_TIMER_MODE = "REST" + const val DEFAULT_TIMER_TYPE = "NORMAL" const val DEFAULT_SNOOZE_COUNT = 0 } @@ -105,6 +114,66 @@ class TimerDataStore(private val dataStore: DataStore) { } } + fun getSettingHour(): Flow { + return dataStore.data.map { preferences -> + preferences[settingHourKey] ?: DEFAULT_HOUR + } + } + + suspend fun setSettingHour(settingHour: Int) { + dataStore.edit { preferences -> + preferences[settingHourKey] = settingHour + } + } + + fun getSettingMinute(): Flow { + return dataStore.data.map { preferences -> + preferences[settingMinuteKey] ?: DEFAULT_MINUTE + } + } + + suspend fun setSettingMinute(settingMinute: Int) { + dataStore.edit { preferences -> + preferences[settingMinuteKey] = settingMinute + } + } + + fun getIsRestCompleted(): Flow { + return dataStore.data.map { preferences -> + preferences[isRestCompletedKey] ?: DEFAULT_IS_REST_COMPLETED + } + } + + suspend fun seIsRestCompleted(isRestCompleted: Boolean) { + dataStore.edit { preferences -> + preferences[isRestCompletedKey] = isRestCompleted + } + } + + fun getTimerMode(): Flow { + return dataStore.data.map { preferences -> + preferences[timerModeKey] ?: DEFAULT_TIMER_MODE + } + } + + suspend fun setTimerMode(timerMode: String) { + dataStore.edit { preferences -> + preferences[timerModeKey] = timerMode + } + } + + fun getTimerType(): Flow { + return dataStore.data.map { preferences -> + preferences[timerTypeKey] + } + } + + suspend fun setTimerType(timerType: String) { + dataStore.edit { preferences -> + preferences[timerTypeKey] = timerType + } + } + fun getSnoozeCount(): Flow { return dataStore.data.map { preferences -> preferences[snoozeCountKey] ?: DEFAULT_SNOOZE_COUNT diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/data/repository/TimerRepositoryImpl.kt b/app/src/main/java/com/ggaebiz/ggaebiz/data/repository/TimerRepositoryImpl.kt index c3f171e..bf32311 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/data/repository/TimerRepositoryImpl.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/data/repository/TimerRepositoryImpl.kt @@ -4,10 +4,14 @@ import android.util.Log import com.ggaebiz.ggaebiz.data.datastore.TimerDataStore import com.ggaebiz.ggaebiz.data.datastore.TimerDataStore.Companion.DEFAULT_CHARACTER_IDX import com.ggaebiz.ggaebiz.data.datastore.TimerDataStore.Companion.DEFAULT_HOUR +import com.ggaebiz.ggaebiz.data.datastore.TimerDataStore.Companion.DEFAULT_IS_REST_COMPLETED import com.ggaebiz.ggaebiz.data.datastore.TimerDataStore.Companion.DEFAULT_IS_SETTING_TIMER import com.ggaebiz.ggaebiz.data.datastore.TimerDataStore.Companion.DEFAULT_LEVEL import com.ggaebiz.ggaebiz.data.datastore.TimerDataStore.Companion.DEFAULT_MINUTE import com.ggaebiz.ggaebiz.domain.repository.TimerRepository +import com.ggaebiz.ggaebiz.presentation.ui.setting.TimerMode +import com.ggaebiz.ggaebiz.presentation.ui.setting.toTimerMode +import com.ggaebiz.ggaebiz.presentation.ui.setting.toTimerModeString import kotlinx.coroutines.flow.first class TimerRepositoryImpl( @@ -99,6 +103,78 @@ class TimerRepositoryImpl( } } + override suspend fun getSettingHour(): Int { + return try { + timerDataStore.getSettingHour().first() + } catch (e: Exception) { + Log.e("TimerRepositoryImpl", "Error fetching timer data", e) + DEFAULT_HOUR + } + } + + override suspend fun setSettingHour(settingHour: Int) { + try { + timerDataStore.setSettingHour(settingHour) + } catch (e: Exception) { + Log.e("TimerRepositoryImpl", "Error updating timer data", e) + } + } + + override suspend fun getSettingMinute(): Int { + return try { + timerDataStore.getSettingMinute().first() + } catch (e: Exception) { + Log.e("TimerRepositoryImpl", "Error fetching timer data", e) + DEFAULT_MINUTE + } + } + + override suspend fun setSettingMinute(settingMinute: Int) { + try { + timerDataStore.setSettingMinute(settingMinute) + } catch (e: Exception) { + Log.e("TimerRepositoryImpl", "Error updating timer data", e) + } + } + + override suspend fun getIsRestCompleted(): Boolean { + return try { + timerDataStore.getIsRestCompleted().first() + } catch (e: Exception) { + Log.e("TimerRepositoryImpl", "Error fetching timer data", e) + DEFAULT_IS_REST_COMPLETED + } + } + + override suspend fun setIsRestCompleted(isRestCompleted: Boolean) { + try { + timerDataStore.seIsRestCompleted(isRestCompleted) + } catch (e: Exception) { + Log.e("TimerRepositoryImpl", "Error updating timer data", e) + } + } + + override suspend fun getTimerMode(): TimerMode { + return try { + val modeValue = timerDataStore.getTimerMode().first() + val typeValue = timerDataStore.getTimerType().first() + toTimerMode(modeValue, typeValue) + } catch (e: Exception) { + Log.e("TimerRepositoryImpl", "Error fetching timer data", e) + toTimerMode(TimerDataStore.DEFAULT_TIMER_MODE, TimerDataStore.DEFAULT_TIMER_TYPE) + } + } + + override suspend fun setTimerMode(timerMode: TimerMode) { + try { + val (modeValue, typeValue) = toTimerModeString(timerMode) + timerDataStore.setTimerMode(modeValue) + timerDataStore.setTimerType(typeValue) + } catch (e: Exception) { + Log.e("TimerRepositoryImpl", "Error updating timer data", e) + } + } + override suspend fun getSnoozeCount(): Int { return try { timerDataStore.getSnoozeCount().first() diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/di/AppModule.kt b/app/src/main/java/com/ggaebiz/ggaebiz/di/AppModule.kt index 9440d85..bbb299c 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/di/AppModule.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/di/AppModule.kt @@ -21,10 +21,14 @@ import com.ggaebiz.ggaebiz.domain.usecase.EndTimerUseCase import com.ggaebiz.ggaebiz.domain.usecase.GetAudioResIdUseCase import com.ggaebiz.ggaebiz.domain.usecase.GetCharacterIdxUseCase import com.ggaebiz.ggaebiz.domain.usecase.GetSnoozeCountUseCase -import com.ggaebiz.ggaebiz.domain.usecase.GetTimerSettingUseCase +import com.ggaebiz.ggaebiz.domain.usecase.GetCurrentTimerUseCase +import com.ggaebiz.ggaebiz.domain.usecase.GetIsRestCompletedUseCase +import com.ggaebiz.ggaebiz.domain.usecase.GetSettingTimerUseCase import com.ggaebiz.ggaebiz.domain.usecase.SelectCharacterIdxUseCase import com.ggaebiz.ggaebiz.domain.usecase.SetSnoozeCountUseCase -import com.ggaebiz.ggaebiz.domain.usecase.SetTimerSettingUseCase +import com.ggaebiz.ggaebiz.domain.usecase.SetCurrentTimerUseCase +import com.ggaebiz.ggaebiz.domain.usecase.SetIsRestCompletedUseCase +import com.ggaebiz.ggaebiz.domain.usecase.SetSettingTimerUseCase import com.ggaebiz.ggaebiz.presentation.service.TimerServiceManager import com.ggaebiz.ggaebiz.presentation.ui.alarm.AlarmViewModel import com.ggaebiz.ggaebiz.presentation.ui.config.ConfigViewModel @@ -58,18 +62,22 @@ val appModule = module { factory { GetAudioResIdUseCase(get()) } factory { SelectCharacterIdxUseCase(get()) } factory { GetCharacterIdxUseCase(get()) } - factory { SetTimerSettingUseCase(get()) } + factory { GetCurrentTimerUseCase(get()) } + factory { SetCurrentTimerUseCase(get()) } factory { EndTimerUseCase(get()) } - factory { GetTimerSettingUseCase(get()) } + factory { GetSettingTimerUseCase(get()) } + factory { SetSettingTimerUseCase(get()) } + factory { GetIsRestCompletedUseCase(get()) } + factory { SetIsRestCompletedUseCase(get()) } factory { SetSnoozeCountUseCase(get()) } factory { GetSnoozeCountUseCase(get()) } viewModel { HomeViewModel(get(), get(), get()) } - viewModel { SettingViewModel(get(), get(), get()) } + viewModel { SettingViewModel(get(), get(), get(), get(), get()) } viewModel { SplashViewModel(get()) } viewModel { OnboardingViewModel(get()) } - viewModel { TimerViewModel(get(), get(), get(), get(), get(), get()) } - viewModel { AlarmViewModel(get(), get(), get(), get(), get()) } + viewModel { TimerViewModel(get(), get(), get(), get(), get(), get(), get(), get()) } + viewModel { AlarmViewModel(get(), get(), get(), get(), get(), get(), get(), get()) } viewModel { ConfigViewModel(get()) } } diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/domain/repository/TimerRepository.kt b/app/src/main/java/com/ggaebiz/ggaebiz/domain/repository/TimerRepository.kt index 9f0fe8d..00e4159 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/domain/repository/TimerRepository.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/domain/repository/TimerRepository.kt @@ -1,5 +1,7 @@ package com.ggaebiz.ggaebiz.domain.repository +import com.ggaebiz.ggaebiz.presentation.ui.setting.TimerMode + interface TimerRepository { suspend fun getIsSettingTimer(): Boolean @@ -17,6 +19,18 @@ interface TimerRepository { suspend fun getMinute(): Int suspend fun setMinute(minute: Int) + suspend fun getSettingHour(): Int + suspend fun setSettingHour(settingHour: Int) + + suspend fun getSettingMinute(): Int + suspend fun setSettingMinute(settingMinute: Int) + + suspend fun getIsRestCompleted(): Boolean + suspend fun setIsRestCompleted(isRestCompleted: Boolean) + + suspend fun getTimerMode(): TimerMode + suspend fun setTimerMode(timerMode: TimerMode) + suspend fun getSnoozeCount(): Int suspend fun setSnoozeCount(count: Int) diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/GetCurrentTimerUseCase.kt b/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/GetCurrentTimerUseCase.kt new file mode 100644 index 0000000..9b7cc75 --- /dev/null +++ b/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/GetCurrentTimerUseCase.kt @@ -0,0 +1,19 @@ +package com.ggaebiz.ggaebiz.domain.usecase + +import com.ggaebiz.ggaebiz.domain.repository.TimerRepository +import com.ggaebiz.ggaebiz.presentation.ui.home.Quadruple +import com.ggaebiz.ggaebiz.presentation.ui.setting.TimerMode + +class GetCurrentTimerUseCase(private val repository: TimerRepository) { + suspend operator fun invoke(): Quadruple { + repository.run { + return Quadruple(getLevel(), getHour(), getMinute(), getTimerMode()) + } + } + + suspend fun getLevelIdx(): Int { + repository.run { + return getLevelIdx() + } + } +} diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/GetIsRestCompletedUseCase.kt b/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/GetIsRestCompletedUseCase.kt new file mode 100644 index 0000000..4426b76 --- /dev/null +++ b/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/GetIsRestCompletedUseCase.kt @@ -0,0 +1,9 @@ +package com.ggaebiz.ggaebiz.domain.usecase + +import com.ggaebiz.ggaebiz.domain.repository.TimerRepository + +class GetIsRestCompletedUseCase(private val repository: TimerRepository) { + suspend operator fun invoke(): Boolean{ + return repository.getIsRestCompleted() + } +} diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/GetSettingTimerUseCase.kt b/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/GetSettingTimerUseCase.kt new file mode 100644 index 0000000..7a82911 --- /dev/null +++ b/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/GetSettingTimerUseCase.kt @@ -0,0 +1,11 @@ +package com.ggaebiz.ggaebiz.domain.usecase + +import com.ggaebiz.ggaebiz.domain.repository.TimerRepository + +class GetSettingTimerUseCase(private val repository: TimerRepository) { + suspend operator fun invoke(): Pair { + repository.run { + return Pair(getSettingHour(), getSettingMinute()) + } + } +} diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/GetTimerSettingUseCase.kt b/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/GetTimerSettingUseCase.kt deleted file mode 100644 index bf1ccb2..0000000 --- a/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/GetTimerSettingUseCase.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.ggaebiz.ggaebiz.domain.usecase - -import com.ggaebiz.ggaebiz.domain.repository.TimerRepository - -class GetTimerSettingUseCase(private val repository: TimerRepository) { - suspend operator fun invoke(): Triple { - repository.run { - return Triple(getLevel(), getHour(), getMinute()) - } - } - - suspend fun getLevelIdx(): Int { - repository.run { - return getLevelIdx() - } - } -} diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/SetTimerSettingUseCase.kt b/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/SetCurrentTimerUseCase.kt similarity index 62% rename from app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/SetTimerSettingUseCase.kt rename to app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/SetCurrentTimerUseCase.kt index 9aaacc7..a26a635 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/SetTimerSettingUseCase.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/SetCurrentTimerUseCase.kt @@ -1,14 +1,16 @@ package com.ggaebiz.ggaebiz.domain.usecase import com.ggaebiz.ggaebiz.domain.repository.TimerRepository +import com.ggaebiz.ggaebiz.presentation.ui.setting.TimerMode -class SetTimerSettingUseCase(private val repository: TimerRepository) { - suspend operator fun invoke(level: Int, hour: Int, minute: Int, snoozeCount : Int) { +class SetCurrentTimerUseCase(private val repository: TimerRepository) { + suspend operator fun invoke(level: Int, hour: Int, minute: Int, timerMode: TimerMode?) { repository.run { setIsSettingTimer(true) setLevel(level) setHour(hour) setMinute(minute) + timerMode?.let { setTimerMode(it) } } } } diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/SetIsRestCompletedUseCase.kt b/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/SetIsRestCompletedUseCase.kt new file mode 100644 index 0000000..777867f --- /dev/null +++ b/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/SetIsRestCompletedUseCase.kt @@ -0,0 +1,9 @@ +package com.ggaebiz.ggaebiz.domain.usecase + +import com.ggaebiz.ggaebiz.domain.repository.TimerRepository + +class SetIsRestCompletedUseCase(private val repository: TimerRepository) { + suspend operator fun invoke(isRestCompleted: Boolean) { + repository.setIsRestCompleted(isRestCompleted) + } +} diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/SetSettingTimerUseCase.kt b/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/SetSettingTimerUseCase.kt new file mode 100644 index 0000000..ee46443 --- /dev/null +++ b/app/src/main/java/com/ggaebiz/ggaebiz/domain/usecase/SetSettingTimerUseCase.kt @@ -0,0 +1,12 @@ +package com.ggaebiz.ggaebiz.domain.usecase + +import com.ggaebiz.ggaebiz.domain.repository.TimerRepository + +class SetSettingTimerUseCase(private val repository: TimerRepository) { + suspend operator fun invoke(hour: Int, minute: Int) { + repository.run { + setSettingHour(hour) + setSettingMinute(minute) + } + } +} diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/chip/CategoryChip.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/chip/CategoryChip.kt new file mode 100644 index 0000000..96fe52e --- /dev/null +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/chip/CategoryChip.kt @@ -0,0 +1,119 @@ +package com.ggaebiz.ggaebiz.presentation.designsystem.component.chip + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.ggaebiz.ggaebiz.R +import com.ggaebiz.ggaebiz.presentation.designsystem.component.icon.GaeBizIcon +import com.ggaebiz.ggaebiz.presentation.designsystem.theme.GaeBizTheme + +@Composable +fun CategoryChip( + text: String, + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + leadingIcon: ImageVector? = null, + enabled: Boolean = true, + shape: Shape = RoundedCornerShape(15.dp), + height: Dp = 40.dp, + horizontalPadding: Dp = 10.dp, + spacing: Dp = 4.dp, + unselectedChipBackgroundColor: Color = GaeBizTheme.colors.gray50, + selectedChipBackgroundColor: Color = GaeBizTheme.colors.gray900, + unselectedTextColor: Color = GaeBizTheme.colors.gray900, + selectedTextColor: Color = GaeBizTheme.colors.white, + unselectedTextStyle: TextStyle = GaeBizTheme.typography.body2Medium, + selectedTextStyle: TextStyle = GaeBizTheme.typography.body2SemiBold, +) { + val backgroundColor by animateColorAsState( + if (selected) selectedChipBackgroundColor else unselectedChipBackgroundColor, + label = "backgroundColor" + ) + val textColor by animateColorAsState( + if (selected) selectedTextColor else unselectedTextColor, + label = "textColor" + ) + val textStyle = if (selected) selectedTextStyle else unselectedTextStyle + + Row( + modifier = modifier + .height(height) + .clip(shape) + .background(backgroundColor) + .clickable(enabled = enabled) { onClick() } + .padding(horizontal = horizontalPadding), + verticalAlignment = Alignment.CenterVertically, + ) { + if (leadingIcon != null) { + Image( + imageVector = leadingIcon, + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + } + + Spacer(Modifier.width(spacing)) + + Text( + text = text, + color = textColor, + style = textStyle, + ) + + Spacer(Modifier.width(spacing)) + + AnimatedVisibility( + visible = selected, + enter = fadeIn() + scaleIn(), + exit = fadeOut() + scaleOut(), + ) { + if (selected) { + Image( + imageVector = GaeBizIcon.icCheck, + contentDescription = null, + modifier = Modifier.size(18.dp), + colorFilter = ColorFilter.tint(GaeBizTheme.colors.primaryOrange), + ) + } + } + } +} + +@Preview("Chip") +@Composable +private fun CategoryChipPreview() { + CategoryChip( + text = stringResource(R.string.normal_mode_text), + selected = true, + onClick = { } + ) +} diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/icon/GaeBizIcon.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/icon/GaeBizIcon.kt index d8fd53a..96105bb 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/icon/GaeBizIcon.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/icon/GaeBizIcon.kt @@ -47,4 +47,28 @@ object GaeBizIcon { val icProofCard : ImageVector @Composable get() = ImageVector.vectorResource(R.drawable.icon_proof_card) + + val icBasketBall : ImageVector + @Composable + get() = ImageVector.vectorResource(R.drawable.ic_basketball) + + val icPencil : ImageVector + @Composable + get() = ImageVector.vectorResource(R.drawable.ic_pencil) + + val icCheck : ImageVector + @Composable + get() = ImageVector.vectorResource(R.drawable.ic_check) + + val icTopBottomArrow : ImageVector + @Composable + get() = ImageVector.vectorResource(R.drawable.ic_top_bottom_arrow) + + val icResume : ImageVector + @Composable + get() = ImageVector.vectorResource(R.drawable.ic_resume) + + val icPause : ImageVector + @Composable + get() = ImageVector.vectorResource(R.drawable.ic_pause) } diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/picker/Picker.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/picker/Picker.kt index da600fc..acd0d51 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/picker/Picker.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/picker/Picker.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -35,6 +36,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ggaebiz.ggaebiz.presentation.designsystem.theme.GaeBizTheme +import com.ggaebiz.ggaebiz.presentation.ui.setting.RestType +import com.ggaebiz.ggaebiz.presentation.ui.setting.TimerMode import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -42,7 +45,8 @@ import kotlinx.coroutines.launch @Composable fun GaeBizPicker( modifier: Modifier = Modifier, - pickerState: PickerState, + selectedValue: String, + timerMode: TimerMode, list: List, visibleItemsCount: Int, centerTextStyle: TextStyle, @@ -52,132 +56,130 @@ fun GaeBizPicker( dividerHeight: Int, normalDividerColor: Color, pressedDividerColor: Color, + onSelected: (String) -> Unit, ) { - val listScrollCount = Integer.MAX_VALUE - val listScrollMiddle = listScrollCount / 2 - val visibleItemsMiddle = visibleItemsCount / 2 - val listStartIndex = listScrollMiddle - listScrollMiddle % list.size - visibleItemsMiddle + list.indexOf(pickerState.selectedItem) + + val period = list.size + val safeSelected = + if (list.contains(selectedValue)) selectedValue else list.firstOrNull() ?: "00" + val safeIndexInList = list.indexOf(safeSelected).coerceAtLeast(0) + + val listScrollCount = Int.MAX_VALUE + val listScrollMiddle = listScrollCount / 2 + val targetFirstIndex = + listScrollMiddle - (listScrollMiddle % period) - visibleItemsMiddle + safeIndexInList val coroutineScope = rememberCoroutineScope() var dividerColor by remember { mutableStateOf(normalDividerColor) } - val listState = rememberLazyListState(initialFirstVisibleItemIndex = listStartIndex) - val flingBehavior = rememberSnapFlingBehavior(lazyListState = listState) - - val normalItemHeightPixels = remember { mutableIntStateOf(0) } - val normalItemHeightDp = pixelsToDp(normalItemHeightPixels.intValue) - val centerItemHeightPixels = remember { mutableIntStateOf(0) } - val centerIemHeightDp = pixelsToDp(centerItemHeightPixels.intValue) - - LaunchedEffect(Unit) { - snapshotFlow { listState.isScrollInProgress } - .distinctUntilChanged() - .collect { - dividerColor = if (listState.isScrollInProgress) { - pressedDividerColor - } else { - normalDividerColor + + val normalItemHeightPx = remember { mutableIntStateOf(0) } + val centerItemHeightPx = remember { mutableIntStateOf(0) } + val normalItemHeightDp = pixelsToDp(normalItemHeightPx.intValue) + val centerItemHeightDp = pixelsToDp(centerItemHeightPx.intValue) + + key(timerMode, period) { + + val listState = rememberLazyListState( + initialFirstVisibleItemIndex = targetFirstIndex, + initialFirstVisibleItemScrollOffset = 0 + ) + val flingBehavior = rememberSnapFlingBehavior(lazyListState = listState) + + LaunchedEffect(Unit) { + snapshotFlow { listState.isScrollInProgress } + .distinctUntilChanged() + .collect { inProgress -> + dividerColor = if (inProgress) pressedDividerColor else normalDividerColor } - } - } + } - LaunchedEffect(Unit) { - snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } - .map { (index, offset) -> index + visibleItemsMiddle to offset } - .distinctUntilChanged() - .collect { (index, offset) -> - pickerState.selectedItem = list[index % list.size] - - if (offset > 0) { - coroutineScope.launch { - val scrollAmount = -offset.toFloat() - listState.animateScrollBy( - value = scrollAmount, - animationSpec = spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium, - ), - ) + LaunchedEffect(Unit) { + snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } + .map { (index, offset) -> index + visibleItemsMiddle to offset } + .distinctUntilChanged() + .collect { (centerIndex, offset) -> + onSelected(list[centerIndex % period]) + + if (offset > 0) { + coroutineScope.launch { + listState.animateScrollBy( + value = -offset.toFloat(), + animationSpec = spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessMedium + ) + ) + } } } - } - } + } - Box(modifier = modifier) { - LazyColumn( - state = listState, - flingBehavior = flingBehavior, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .height(normalItemHeightDp * (visibleItemsCount - 1) + centerIemHeightDp + dividerHeight.dp * 2), - ) { - items(listScrollCount) { index -> - val item = list[index % list.size] - val isSelected = item == pickerState.selectedItem - val color = if (isSelected) centerTextColor else normalTextColor - val textStyle = if (isSelected) centerTextStyle else normalTextStyle - - Column( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center) - .wrapContentWidth(Alignment.CenterHorizontally) - .wrapContentHeight(Alignment.CenterVertically), - ) { - if (isSelected) Spacer(modifier = Modifier.height(dividerHeight.dp)) - Text( - text = item, - style = textStyle.copy(color = color), - textAlign = TextAlign.Center, + Box(modifier = modifier) { + LazyColumn( + state = listState, + flingBehavior = flingBehavior, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .height( + normalItemHeightDp * (visibleItemsCount - 1) + centerItemHeightDp + dividerHeight.dp * 2 + ), + ) { + items(listScrollCount) { index -> + val item = list[index % period] + val isSelected = item == safeSelected + val color = if (isSelected) centerTextColor else normalTextColor + val textStyle = if (isSelected) centerTextStyle else normalTextStyle + + Column( modifier = Modifier - .onSizeChanged { size -> - if (isSelected) { - centerItemHeightPixels.value = size.height - } else { - normalItemHeightPixels.value = size.height - } + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally) + .wrapContentHeight(Alignment.CenterVertically), + ) { + if (isSelected) Spacer(Modifier.height(dividerHeight.dp)) + Text( + text = item, + style = textStyle.copy(color = color), + textAlign = TextAlign.Center, + modifier = Modifier.onSizeChanged { size -> + if (isSelected) centerItemHeightPx.value = size.height + else normalItemHeightPx.value = size.height } - .height(textStyle.fontSize.value.dp), - ) - if (isSelected) Spacer(modifier = Modifier.height(dividerHeight.dp)) + ) + if (isSelected) Spacer(Modifier.height(dividerHeight.dp)) + } } } - } - - HorizontalDivider( - modifier = Modifier - .offset(y = normalItemHeightDp * visibleItemsMiddle) - .height(dividerHeight.dp), - color = dividerColor, - ) - HorizontalDivider( - modifier = Modifier - .offset(y = normalItemHeightDp * visibleItemsMiddle + centerIemHeightDp + dividerHeight.dp * 2) - .height(dividerHeight.dp), - color = dividerColor, - ) + HorizontalDivider( + modifier = Modifier + .offset(y = normalItemHeightDp * visibleItemsMiddle) + .height(dividerHeight.dp), + color = dividerColor, + ) + HorizontalDivider( + modifier = Modifier + .offset(y = normalItemHeightDp * visibleItemsMiddle + centerItemHeightDp + dividerHeight.dp * 2) + .height(dividerHeight.dp), + color = dividerColor, + ) + } } } @Composable private fun pixelsToDp(pixels: Int) = with(LocalDensity.current) { pixels.toDp() } -@Composable -fun rememberPickerState(defaultValue: String = "") = remember { PickerState(defaultValue) } - -class PickerState(defaultValue: String) { - var selectedItem by mutableStateOf(defaultValue) -} - @Preview("Picker") @Composable private fun GaeBizPickerPreview() { val hours = (0..6).toList().map { it.toString().padStart(2, '0') } GaeBizPicker( - pickerState = PickerState(""), + selectedValue = "", + timerMode = TimerMode.Rest(RestType.NORMAL), list = hours, visibleItemsCount = 3, centerTextStyle = GaeBizTheme.typography.timer2, @@ -187,5 +189,6 @@ private fun GaeBizPickerPreview() { dividerHeight = 2, normalDividerColor = GaeBizTheme.colors.gray75, pressedDividerColor = GaeBizTheme.colors.primaryOrange, + onSelected = { }, ) } diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/popup/GaeBizBasePopup.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/popup/GaeBizBasePopup.kt index ed90974..f340efc 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/popup/GaeBizBasePopup.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/popup/GaeBizBasePopup.kt @@ -140,7 +140,8 @@ fun GaeBizBasePopup( ) { Box( modifier = modifier - .then(Modifier.widthIn(max = maxWidth)) + .fillMaxWidth(0.912f) + .widthIn(max = maxWidth) .clip(shape) .background(containerColor) .clickable(enabled = false) {} diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/textswitch/TextSwitchBox.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/textswitch/TextSwitchBox.kt new file mode 100644 index 0000000..1988cd9 --- /dev/null +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/component/textswitch/TextSwitchBox.kt @@ -0,0 +1,34 @@ +package com.ggaebiz.ggaebiz.presentation.designsystem.component.textswitch + +import androidx.compose.foundation.layout.Box +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.TextStyle + +@Composable +fun TextSwitchBox( + modifier: Modifier, + text: String, + isSelected: Boolean, + selectedTextColor: Color, + unSelectedTextColor: Color, + selectedTextStyle: TextStyle, + unSelectedTextStyle: TextStyle, +) { + val textColor = if (isSelected) selectedTextColor else unSelectedTextColor + val textStyle = if (isSelected) selectedTextStyle else unSelectedTextStyle + + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Text( + text = text, + color = textColor, + style = textStyle, + ) + } +} diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/theme/Color.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/theme/Color.kt index 1bd7a26..10f8427 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/theme/Color.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/theme/Color.kt @@ -7,6 +7,9 @@ import androidx.compose.ui.graphics.Color @Immutable data class GaeBizColors( val primaryOrange: Color, + val primaryOrange50: Color, + val primaryOrange100: Color, + val primaryOrange600: Color, val splashStatusBarColor: Color, val gradientOrange: List, @@ -46,6 +49,9 @@ data class GaeBizColors( val LocalGaeBizColor = staticCompositionLocalOf { GaeBizColors( primaryOrange = Color.Unspecified, + primaryOrange50 = Color.Unspecified, + primaryOrange100 = Color.Unspecified, + primaryOrange600 = Color.Unspecified, splashStatusBarColor = Color.Unspecified, gradientOrange = listOf(Color.Unspecified, Color.Unspecified), @@ -85,6 +91,10 @@ val LocalGaeBizColor = staticCompositionLocalOf { val GaeBizColorScheme = GaeBizColors( primaryOrange = Color(0xFFFC6F3D), + primaryOrange50 = Color(0xFFFFF1EC), + primaryOrange100 = Color(0xFFFED2C3), + primaryOrange600 = Color(0xFFE56538), + splashStatusBarColor = Color( 0xFFFD8258), gradientOrange = listOf( Color(0xFFFF9A76).copy(alpha = 0.5f), diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/CategoryArea.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/CategoryArea.kt new file mode 100644 index 0000000..ed3dcf6 --- /dev/null +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/CategoryArea.kt @@ -0,0 +1,43 @@ +package com.ggaebiz.ggaebiz.presentation.designsystem.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.ggaebiz.ggaebiz.R +import com.ggaebiz.ggaebiz.presentation.designsystem.component.chip.CategoryChip +import com.ggaebiz.ggaebiz.presentation.designsystem.component.icon.GaeBizIcon +import com.ggaebiz.ggaebiz.presentation.ui.setting.ConcentrateType +import com.ggaebiz.ggaebiz.presentation.ui.setting.TimerMode + +@Composable +fun CategoryArea( + modifier: Modifier, + selected: TimerMode.Concentrate, + onSelect: (ConcentrateType) -> Unit, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + CategoryChip( + text = stringResource(R.string.normal_mode_text), + selected = selected.type == ConcentrateType.NORMAL, + onClick = { onSelect(ConcentrateType.NORMAL) } + ) + CategoryChip( + text = stringResource(R.string.study_mode_text), + leadingIcon = GaeBizIcon.icPencil, + selected = selected.type == ConcentrateType.STUDY, + onClick = { onSelect(ConcentrateType.STUDY) } + ) + CategoryChip( + text = stringResource(R.string.exercise_mode_text), + leadingIcon = GaeBizIcon.icBasketBall, + selected = selected.type == ConcentrateType.EXERCISE, + onClick = { onSelect(ConcentrateType.EXERCISE) } + ) + } +} diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/LevelItem.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/LevelItem.kt new file mode 100644 index 0000000..159fec2 --- /dev/null +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/LevelItem.kt @@ -0,0 +1,64 @@ +package com.ggaebiz.ggaebiz.presentation.designsystem.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.ggaebiz.ggaebiz.presentation.designsystem.theme.GaeBizTheme + +@Composable +fun LevelItem( + modifier: Modifier = Modifier, + title: String, + isSelected: Boolean, + selectedIcon: Painter, + unSelectedIcon: Painter, + onClick: (() -> Unit), + cornerRadius: Dp = 16.dp, + selectedTextStyle: TextStyle = GaeBizTheme.typography.body2Bold, + unSelectedTextStyle: TextStyle = GaeBizTheme.typography.body2Medium, + selectedTextColor: Color = GaeBizTheme.colors.gray800, + unSelectedTextColor: Color = GaeBizTheme.colors.gray300, +) { + Column( + modifier = modifier.wrapContentWidth(Alignment.CenterHorizontally), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .fillMaxWidth(1f) + .clip(RoundedCornerShape(cornerRadius)) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center + ) { + Image( + painter = if (isSelected) selectedIcon else unSelectedIcon, + contentDescription = title, + modifier = Modifier.fillMaxWidth(1f), + ) + } + + Spacer(Modifier.height(6.dp)) + + Text( + text = title, + style = if (isSelected) selectedTextStyle else unSelectedTextStyle, + color = if (isSelected) selectedTextColor else unSelectedTextColor, + ) + } +} diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/LevelSlider.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/LevelSlider.kt index d903144..3981642 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/LevelSlider.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/LevelSlider.kt @@ -19,6 +19,7 @@ import com.ggaebiz.ggaebiz.R import com.ggaebiz.ggaebiz.presentation.designsystem.component.slider.GaeBizSlider import com.ggaebiz.ggaebiz.presentation.designsystem.theme.GaeBizTheme +@Deprecated("use levelItem") @Composable fun GaeBizLevelSlider( modifier: Modifier = Modifier, diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/RestMent.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/RestMent.kt new file mode 100644 index 0000000..e1ccdfa --- /dev/null +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/RestMent.kt @@ -0,0 +1,108 @@ +package com.ggaebiz.ggaebiz.presentation.designsystem.ui + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.ggaebiz.ggaebiz.R +import com.ggaebiz.ggaebiz.presentation.designsystem.component.icon.GaeBizIcon +import com.ggaebiz.ggaebiz.presentation.designsystem.theme.GaeBizTheme + +@Composable +fun RestMent( + modifier: Modifier = Modifier, + text: String, + level: Int, + onClick: (() -> Unit), + textStyle: TextStyle = GaeBizTheme.typography.bodySemiBold, + levelTextStyle: TextStyle = GaeBizTheme.typography.label3, + textColor: Color = GaeBizTheme.colors.gray800, + levelTextColor: Color = GaeBizTheme.colors.primaryOrange, + radius: Dp = 16.dp, + levelRadius: Dp = 10.dp, + backgroundColor: Color = GaeBizTheme.colors.white, + pressedBackgroundColor: Color = GaeBizTheme.colors.gray50, + levelBackgroundColor: Color = GaeBizTheme.colors.primaryOrange50, +) { + val interaction = remember { MutableInteractionSource() } + val isPressed by interaction.collectIsPressedAsState() + + val containerBackgroundColor by animateColorAsState( + if (isPressed) pressedBackgroundColor else backgroundColor, + label = "containerBackgroundColor" + ) + + Row( + modifier = modifier + .wrapContentSize() + .background( + color = containerBackgroundColor, + shape = RoundedCornerShape(radius), + ) + .padding( + horizontal = 12.dp, + vertical = 12.dp, + ) + .clickable( + interactionSource = interaction, + indication = null, + onClick = onClick, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = modifier + .wrapContentSize() + .background( + color = levelBackgroundColor, + shape = RoundedCornerShape(levelRadius), + ) + .padding( + horizontal = 6.dp, + vertical = 8.dp, + ), + ) { + Text( + text = stringResource(R.string.ment_level_text, level), + color = levelTextColor, + style = levelTextStyle, + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = text, + maxLines = 2, + color = textColor, + style = textStyle, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Icon( + imageVector = GaeBizIcon.icTopBottomArrow, + contentDescription = null, + ) + } +} diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/SettingSwitch.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/SettingSwitch.kt new file mode 100644 index 0000000..8b35ecd --- /dev/null +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/SettingSwitch.kt @@ -0,0 +1,128 @@ +package com.ggaebiz.ggaebiz.presentation.designsystem.ui + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.ggaebiz.ggaebiz.R +import com.ggaebiz.ggaebiz.presentation.designsystem.component.textswitch.TextSwitchBox +import com.ggaebiz.ggaebiz.presentation.designsystem.theme.GaeBizTheme +import com.ggaebiz.ggaebiz.presentation.ui.setting.ConcentrateType +import com.ggaebiz.ggaebiz.presentation.ui.setting.RestType +import com.ggaebiz.ggaebiz.presentation.ui.setting.TimerMode + +@Composable +fun SettingSwitch( + timerMode: TimerMode, + onToggle: (TimerMode) -> Unit, + modifier: Modifier = Modifier, + containerColor: Color = GaeBizTheme.colors.gray75, + selectedBgColor: Color = GaeBizTheme.colors.white, + unselectedBgColor: Color = GaeBizTheme.colors.gray75, + selectedTextColor: Color = GaeBizTheme.colors.gray900, + unselectedTextColor: Color = GaeBizTheme.colors.gray600, + cornerRadius: Dp = 50.dp, + height: Dp = 40.dp, + selectedWeight: Float = 1.14f, + unselectedWeight: Float = 0.86f, +) { + val shape = RoundedCornerShape(cornerRadius) + + val restWeight by animateFloatAsState( + targetValue = if (timerMode is TimerMode.Rest) selectedWeight else unselectedWeight, + animationSpec = spring(stiffness = Spring.StiffnessMediumLow), + label = "restWeight" + ) + val concentrateWeight by animateFloatAsState( + targetValue = if (timerMode is TimerMode.Rest) unselectedWeight else selectedWeight, + animationSpec = spring(stiffness = Spring.StiffnessMediumLow), + label = "concentrateWeight" + ) + + val restBackground by animateColorAsState( + targetValue = if (timerMode is TimerMode.Rest) selectedBgColor else unselectedBgColor, + animationSpec = spring(stiffness = Spring.StiffnessMediumLow), + label = "restBackground" + ) + val concentrateBackground by animateColorAsState( + targetValue = if (timerMode is TimerMode.Concentrate) selectedBgColor else unselectedBgColor, + animationSpec = spring(stiffness = Spring.StiffnessMediumLow), + label = "concentrateBackground" + ) + + Row( + modifier = modifier + .clip(shape) + .border( + width = 2.dp, + color = GaeBizTheme.colors.gray50, + shape = RoundedCornerShape(cornerRadius) + ) + .background(containerColor) + .padding(vertical = 2.dp) + .height(height) + .width(132.dp), + verticalAlignment = Alignment.CenterVertically + ) { + TextSwitchBox( + modifier = Modifier + .padding(start = if (timerMode is TimerMode.Concentrate) 8.dp else 0.dp) + .weight(restWeight) + .fillMaxHeight() + .clip(shape) + .background(restBackground) + .clickable { + if (timerMode is TimerMode.Concentrate) { + onToggle(TimerMode.Rest(RestType.NORMAL)) + } + }, + text = stringResource(R.string.rest_mode_text), + isSelected = timerMode is TimerMode.Rest, + selectedTextColor = selectedTextColor, + unSelectedTextColor = unselectedTextColor, + selectedTextStyle = GaeBizTheme.typography.body2Bold, + unSelectedTextStyle = GaeBizTheme.typography.body2SemiBold, + ) + + Spacer(Modifier.width(4.dp)) + + TextSwitchBox( + modifier = Modifier + .padding(end = if (timerMode is TimerMode.Rest) 8.dp else 0.dp) + .weight(concentrateWeight) + .fillMaxHeight() + .clip(shape) + .background(concentrateBackground) + .clickable { + if (timerMode is TimerMode.Rest) { + onToggle(TimerMode.Concentrate(ConcentrateType.NORMAL)) + } + }, + text = stringResource(R.string.concentration_mode_text), + isSelected = timerMode is TimerMode.Concentrate, + selectedTextColor = selectedTextColor, + unSelectedTextColor = unselectedTextColor, + selectedTextStyle = GaeBizTheme.typography.body2Bold, + unSelectedTextStyle = GaeBizTheme.typography.body2SemiBold, + ) + } +} diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/TImePicker.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/TImePicker.kt index 4909dd8..89b42bc 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/TImePicker.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/TImePicker.kt @@ -20,15 +20,19 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ggaebiz.ggaebiz.R import com.ggaebiz.ggaebiz.presentation.designsystem.component.picker.GaeBizPicker -import com.ggaebiz.ggaebiz.presentation.designsystem.component.picker.PickerState import com.ggaebiz.ggaebiz.presentation.designsystem.component.timer.TimerSmallColon import com.ggaebiz.ggaebiz.presentation.designsystem.theme.GaeBizTheme +import com.ggaebiz.ggaebiz.presentation.ui.setting.RestType +import com.ggaebiz.ggaebiz.presentation.ui.setting.TimerMode @Composable fun GaeBizTimePicker( modifier: Modifier = Modifier, - hourPickerState: PickerState, - minutePickerState: PickerState, + selectedHour: String, + selectedMinute: String, + maxHour: Int = 5, + maxMinute: Int = 59, + timerMode: TimerMode, visibleItemsCount: Int = 3, centerTextStyle: TextStyle = GaeBizTheme.typography.timer2, centerTextColor: Color = GaeBizTheme.colors.gray900, @@ -37,14 +41,16 @@ fun GaeBizTimePicker( dividerHeight: Int = 2, normalDividerColor: Color = GaeBizTheme.colors.gray75, pressedDividerColor: Color = GaeBizTheme.colors.primaryOrange, + onHourSelected: (String) -> Unit, + onMinuteSelected: (String) -> Unit, ) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, modifier = Modifier.wrapContentSize(), ) { - val hours = (0..5).toList().map { it.toString().padStart(2, '0') } - val minutes = (0..59).toList().map { it.toString().padStart(2, '0') } + val hours = (0..maxHour).toList().map { it.toString().padStart(2, '0') } + val minutes = (0..maxMinute).toList().map { it.toString().padStart(2, '0') } Row( modifier = Modifier.fillMaxWidth(), @@ -53,7 +59,8 @@ fun GaeBizTimePicker( ) { Column(modifier = Modifier.weight(0.5f)) { GaeBizPicker( - pickerState = hourPickerState, + selectedValue = selectedHour, + timerMode = timerMode, list = hours, visibleItemsCount = visibleItemsCount, centerTextStyle = centerTextStyle, @@ -63,6 +70,7 @@ fun GaeBizTimePicker( dividerHeight = dividerHeight, normalDividerColor = normalDividerColor, pressedDividerColor = pressedDividerColor, + onSelected = onHourSelected, ) Spacer(Modifier.height(12.dp)) Text( @@ -80,7 +88,8 @@ fun GaeBizTimePicker( Column(modifier = Modifier.weight(0.5f)) { GaeBizPicker( - pickerState = minutePickerState, + selectedValue = selectedMinute, + timerMode = timerMode, list = minutes, visibleItemsCount = visibleItemsCount, centerTextStyle = centerTextStyle, @@ -90,6 +99,7 @@ fun GaeBizTimePicker( dividerHeight = dividerHeight, normalDividerColor = normalDividerColor, pressedDividerColor = pressedDividerColor, + onSelected = onMinuteSelected, ) Spacer(Modifier.height(12.dp)) Text( @@ -108,7 +118,10 @@ fun GaeBizTimePicker( @Composable private fun GaeBizTimePickerPreview() { GaeBizTimePicker( - hourPickerState = PickerState(""), - minutePickerState = PickerState(""), + selectedHour = "", + selectedMinute = "", + timerMode = TimerMode.Rest(RestType.NORMAL), + onHourSelected = { }, + onMinuteSelected = { }, ) } diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/TimerActionButton.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/TimerActionButton.kt new file mode 100644 index 0000000..258a79f --- /dev/null +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/designsystem/ui/TimerActionButton.kt @@ -0,0 +1,108 @@ +package com.ggaebiz.ggaebiz.presentation.designsystem.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.ggaebiz.ggaebiz.R +import com.ggaebiz.ggaebiz.presentation.designsystem.component.icon.GaeBizIcon +import com.ggaebiz.ggaebiz.presentation.designsystem.theme.GaeBizTheme + +@Immutable +enum class TimerActionType { Pause, Resume } + +@Composable +fun TimerActionButton( + type: TimerActionType, + visible: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) { + if (!visible) return + + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + val background = when (type) { + TimerActionType.Pause -> + if (isPressed) GaeBizTheme.colors.gray50 else GaeBizTheme.colors.gray100 + + TimerActionType.Resume -> + if (isPressed) GaeBizTheme.colors.primaryOrange100 else GaeBizTheme.colors.primaryOrange50 + } + val textColor = when (type) { + TimerActionType.Pause -> GaeBizTheme.colors.black + TimerActionType.Resume -> if (isPressed) GaeBizTheme.colors.primaryOrange600 else GaeBizTheme.colors.primaryOrange + } + + val text = when (type) { + TimerActionType.Pause -> stringResource(R.string.pause_text) + TimerActionType.Resume -> stringResource(R.string.resume_text) + } + + val icon = when (type) { + TimerActionType.Pause -> GaeBizIcon.icPause + TimerActionType.Resume -> GaeBizIcon.icResume + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .clip(RoundedCornerShape(10.dp)) + .background(background) + .defaultMinSize(minHeight = 38.dp) + .padding(horizontal = 12.dp, vertical = 9.dp) + .clickable( + enabled = enabled, + interactionSource = interactionSource, + indication = null, + onClick = onClick + ) + ) { + Text( + text = text, + color = textColor, + fontSize = 14.sp, + style = GaeBizTheme.typography.body2SemiBold + ) + + Spacer(Modifier.width(2.dp)) + + Image( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + } +} + +@Preview("TimerActionButton") +@Composable +private fun TimerActionButtonPreview() { + TimerActionButton( + visible = true, + type = TimerActionType.Resume, + onClick = { }, + ) +} diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/model/AlarmCharacterData.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/model/AlarmCharacterData.kt index db40b62..45b97b1 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/model/AlarmCharacterData.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/model/AlarmCharacterData.kt @@ -4,22 +4,56 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import com.ggaebiz.ggaebiz.R import com.ggaebiz.ggaebiz.data.model.CharacterName +import com.ggaebiz.ggaebiz.presentation.ui.setting.TimerMode import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.serialization.Serializable - @Serializable data class AlarmCharacterData( val characterName: CharacterName, - val mentAudioList: PersistentList>, + val restMentAudioList: PersistentList>, + val concentrateNormalMentAudioList: PersistentList, + val concentrateStudyMentAudioList: PersistentList, + val concentrateExerciseMentAudioList: PersistentList, @DrawableRes val alarmBackgroundImageList: PersistentList, ) { + fun getMentAudio(timerMode: TimerMode, level: Int? = null, levelIdx: Int? = null): MentAudio { + return when { + timerMode.isRestTimer() -> { + if (level != null && levelIdx != null) { + restMentAudioList[level][levelIdx] + } else { + restMentAudioList[0][0] + } + } + timerMode.isConcentrateTimer() -> { + when { + (timerMode as TimerMode.Concentrate).isNormal() -> { + concentrateNormalMentAudioList[0] + } + (timerMode as TimerMode.Concentrate).isStudy() -> { + concentrateStudyMentAudioList[0] + } + (timerMode as TimerMode.Concentrate).isExercise() -> { + concentrateExerciseMentAudioList[0] + } + + else -> concentrateNormalMentAudioList[0] + } + } + + else -> restMentAudioList[0][0] + } + } + companion object { + + private val ALARM_CHARACTER_DATA = listOf( AlarmCharacterData( characterName = CharacterName.KIKI, - mentAudioList = persistentListOf( + restMentAudioList = persistentListOf( persistentListOf( MentAudio( ment = R.string.alarm_ment_kiki_level1_1, @@ -73,6 +107,48 @@ data class AlarmCharacterData( ), ) ), + concentrateNormalMentAudioList = persistentListOf( + MentAudio( + ment = R.string.focus_ment_kiki_1, + audioPath = "raw/kiki_focus_1" + ), + MentAudio( + ment = R.string.focus_ment_kiki_2, + audioPath = "raw/kiki_focus_2" + ), + MentAudio( + ment = R.string.focus_ment_kiki_3, + audioPath = "raw/kiki_focus_3" + ), + ), + concentrateStudyMentAudioList = persistentListOf( + MentAudio( + ment = R.string.study_ment_kiki_1, + audioPath = "raw/kiki_study_1" + ), + MentAudio( + ment = R.string.study_ment_kiki_2, + audioPath = "raw/kiki_study_2" + ), + MentAudio( + ment = R.string.study_ment_kiki_3, + audioPath = "raw/kiki_study_3" + ), + ), + concentrateExerciseMentAudioList = persistentListOf( + MentAudio( + ment = R.string.exercise_ment_kiki_1, + audioPath = "raw/kiki_exercise_1" + ), + MentAudio( + ment = R.string.exercise_ment_kiki_2, + audioPath = "raw/kiki_exercise_2" + ), + MentAudio( + ment = R.string.exercise_ment_kiki_3, + audioPath = "raw/kiki_exercise_3" + ), + ), alarmBackgroundImageList = persistentListOf( R.drawable.fullpage_kiki_lev_1, R.drawable.fullpage_kiki_lev_24, @@ -82,7 +158,7 @@ data class AlarmCharacterData( ), AlarmCharacterData( characterName = CharacterName.BOBO, - mentAudioList = persistentListOf( + restMentAudioList = persistentListOf( persistentListOf( MentAudio( ment = R.string.alarm_ment_bobo_level1_1, @@ -136,6 +212,48 @@ data class AlarmCharacterData( ), ) ), + concentrateNormalMentAudioList = persistentListOf( + MentAudio( + ment = R.string.focus_ment_bobo_1, + audioPath = "raw/bobo_focus_1" + ), + MentAudio( + ment = R.string.focus_ment_bobo_2, + audioPath = "raw/bobo_focus_2" + ), + MentAudio( + ment = R.string.focus_ment_bobo_3, + audioPath = "raw/bobo_focus_3" + ), + ), + concentrateStudyMentAudioList = persistentListOf( + MentAudio( + ment = R.string.study_ment_bobo_1, + audioPath = "raw/bobo_study_1" + ), + MentAudio( + ment = R.string.study_ment_bobo_2, + audioPath = "raw/bobo_study_2" + ), + MentAudio( + ment = R.string.study_ment_bobo_3, + audioPath = "raw/bobo_study_3" + ), + ), + concentrateExerciseMentAudioList = persistentListOf( + MentAudio( + ment = R.string.exercise_ment_bobo_1, + audioPath = "raw/bobo_exercise_1" + ), + MentAudio( + ment = R.string.exercise_ment_bobo_2, + audioPath = "raw/bobo_exercise_2" + ), + MentAudio( + ment = R.string.exercise_ment_bobo_3, + audioPath = "raw/bobo_exercise_3" + ), + ), alarmBackgroundImageList = persistentListOf( R.drawable.fullpage_bobo_lev_1, R.drawable.fullpage_bobo_lev_24, @@ -145,7 +263,7 @@ data class AlarmCharacterData( ), AlarmCharacterData( characterName = CharacterName.NANA, - mentAudioList = persistentListOf( + restMentAudioList = persistentListOf( persistentListOf( MentAudio( ment = R.string.alarm_ment_nana_level1_1, @@ -199,6 +317,48 @@ data class AlarmCharacterData( ), ) ), + concentrateNormalMentAudioList = persistentListOf( + MentAudio( + ment = R.string.focus_ment_nana_1, + audioPath = "raw/nana_focus_1" + ), + MentAudio( + ment = R.string.focus_ment_nana_2, + audioPath = "raw/nana_focus_2" + ), + MentAudio( + ment = R.string.focus_ment_nana_3, + audioPath = "raw/nana_focus_3" + ), + ), + concentrateStudyMentAudioList = persistentListOf( + MentAudio( + ment = R.string.study_ment_nana_1, + audioPath = "raw/nana_study_1" + ), + MentAudio( + ment = R.string.study_ment_nana_2, + audioPath = "raw/nana_study_2" + ), + MentAudio( + ment = R.string.study_ment_nana_3, + audioPath = "raw/nana_study_3" + ), + ), + concentrateExerciseMentAudioList = persistentListOf( + MentAudio( + ment = R.string.exercise_ment_nana_1, + audioPath = "raw/nana_exercise_1" + ), + MentAudio( + ment = R.string.exercise_ment_nana_2, + audioPath = "raw/nana_exercise_2" + ), + MentAudio( + ment = R.string.exercise_ment_nana_3, + audioPath = "raw/nana_exercise_3" + ), + ), alarmBackgroundImageList = persistentListOf( R.drawable.fullpage_nana_lev_1, R.drawable.fullpage_nana_lev_24, @@ -208,7 +368,7 @@ data class AlarmCharacterData( ), AlarmCharacterData( characterName = CharacterName.CHACHA, - mentAudioList = persistentListOf( + restMentAudioList = persistentListOf( persistentListOf( MentAudio( ment = R.string.alarm_ment_chacha_level1_1, @@ -262,6 +422,48 @@ data class AlarmCharacterData( ), ) ), + concentrateNormalMentAudioList = persistentListOf( + MentAudio( + ment = R.string.focus_ment_chacha_1, + audioPath = "raw/chacha_focus_1" + ), + MentAudio( + ment = R.string.focus_ment_chacha_2, + audioPath = "raw/chacha_focus_2" + ), + MentAudio( + ment = R.string.focus_ment_chacha_3, + audioPath = "raw/chacha_focus_3" + ), + ), + concentrateStudyMentAudioList = persistentListOf( + MentAudio( + ment = R.string.study_ment_chacha_1, + audioPath = "raw/chacha_study_1" + ), + MentAudio( + ment = R.string.study_ment_chacha_2, + audioPath = "raw/chacha_study_2" + ), + MentAudio( + ment = R.string.study_ment_chacha_3, + audioPath = "raw/chacha_study_3" + ), + ), + concentrateExerciseMentAudioList = persistentListOf( + MentAudio( + ment = R.string.exercise_ment_chacha_1, + audioPath = "raw/chacha_exercise_1" + ), + MentAudio( + ment = R.string.exercise_ment_chacha_2, + audioPath = "raw/chacha_exercise_2" + ), + MentAudio( + ment = R.string.exercise_ment_chacha_3, + audioPath = "raw/chacha_exercise_3" + ), + ), alarmBackgroundImageList = persistentListOf( R.drawable.fullpage_chacha_lev_1, R.drawable.fullpage_chacha_lev_24, @@ -271,7 +473,7 @@ data class AlarmCharacterData( ), AlarmCharacterData( characterName = CharacterName.BOOBOO, - mentAudioList = persistentListOf( + restMentAudioList = persistentListOf( persistentListOf( MentAudio( ment = R.string.alarm_ment_booboo_level1_1, @@ -325,6 +527,48 @@ data class AlarmCharacterData( ), ) ), + concentrateNormalMentAudioList = persistentListOf( + MentAudio( + ment = R.string.focus_ment_booboo_1, + audioPath = "raw/booboo_focus_1" + ), + MentAudio( + ment = R.string.focus_ment_booboo_2, + audioPath = "raw/booboo_focus_2" + ), + MentAudio( + ment = R.string.focus_ment_booboo_3, + audioPath = "raw/booboo_focus_3" + ), + ), + concentrateStudyMentAudioList = persistentListOf( + MentAudio( + ment = R.string.study_ment_booboo_1, + audioPath = "raw/booboo_study_1" + ), + MentAudio( + ment = R.string.study_ment_booboo_2, + audioPath = "raw/booboo_study_2" + ), + MentAudio( + ment = R.string.study_ment_booboo_3, + audioPath = "raw/booboo_study_3" + ), + ), + concentrateExerciseMentAudioList = persistentListOf( + MentAudio( + ment = R.string.exercise_ment_booboo_1, + audioPath = "raw/booboo_exercise_1" + ), + MentAudio( + ment = R.string.exercise_ment_booboo_2, + audioPath = "raw/booboo_exercise_2" + ), + MentAudio( + ment = R.string.exercise_ment_booboo_3, + audioPath = "raw/booboo_exercise_3" + ), + ), alarmBackgroundImageList = persistentListOf( R.drawable.fullpage_booboo_lev_1, R.drawable.fullpage_booboo_lev_24, @@ -342,4 +586,4 @@ data class AlarmCharacterData( data class MentAudio( @StringRes val ment: Int, val audioPath: String, -) \ No newline at end of file +) diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/model/Character.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/model/Character.kt index df86984..48d7ef2 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/model/Character.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/model/Character.kt @@ -20,6 +20,9 @@ data class Character( @StringRes val timerMentResId: Int, @DrawableRes val imageResId: PersistentList, @DrawableRes val selectedImageResId: PersistentList, + @DrawableRes val selectedConcentrateNormalImageResId: Int, + @DrawableRes val selectedConcentrateStudyImageResId: Int, + @DrawableRes val selectedConcentrateExerciseImageResId: Int, @StringRes val traitsResIdList: PersistentList, @RawRes val lottieResId: Int, @RawRes val initMentAudioResId: Int, @@ -44,6 +47,9 @@ data class Character( R.drawable.ic_selected_kiki_level3, R.drawable.ic_selected_kiki_level3, ), + selectedConcentrateNormalImageResId = R.drawable.ic_selected_kiki_level1, + selectedConcentrateStudyImageResId = R.drawable.ic_selected_kiki_study, + selectedConcentrateExerciseImageResId = R.drawable.ic_selected_kiki_exercise, traitsResIdList = persistentListOf( R.string.kiki_tag_text1, R.string.kiki_tag_text2, @@ -70,6 +76,9 @@ data class Character( R.drawable.ic_selected_bobo_level3, R.drawable.ic_selected_bobo_level3 ), + selectedConcentrateNormalImageResId = R.drawable.ic_selected_bobo_level1, + selectedConcentrateStudyImageResId = R.drawable.ic_selected_bobo_study, + selectedConcentrateExerciseImageResId = R.drawable.ic_selected_bobo_exercise, traitsResIdList = persistentListOf( R.string.bobo_tag_text1, R.string.bobo_tag_text2, @@ -96,6 +105,9 @@ data class Character( R.drawable.ic_selected_nana_level3, R.drawable.ic_selected_nana_level3 ), + selectedConcentrateNormalImageResId = R.drawable.ic_selected_nana_level1, + selectedConcentrateStudyImageResId = R.drawable.ic_selected_nana_study, + selectedConcentrateExerciseImageResId = R.drawable.ic_selected_nana_exercise, traitsResIdList = persistentListOf( R.string.nana_tag_text1, R.string.nana_tag_text2, @@ -122,6 +134,9 @@ data class Character( R.drawable.ic_selected_chacha_level3, R.drawable.ic_selected_chacha_level3 ), + selectedConcentrateNormalImageResId = R.drawable.ic_selected_chacha_level1, + selectedConcentrateStudyImageResId = R.drawable.ic_selected_chacha_study, + selectedConcentrateExerciseImageResId = R.drawable.ic_selected_chacha_exercise, traitsResIdList = persistentListOf( R.string.chacha_tag_text1, R.string.chacha_tag_text2, @@ -148,6 +163,9 @@ data class Character( R.drawable.ic_selected_booboo_level3, R.drawable.ic_selected_booboo_level3 ), + selectedConcentrateNormalImageResId = R.drawable.ic_selected_booboo_level1, + selectedConcentrateStudyImageResId = R.drawable.ic_selected_booboo_study, + selectedConcentrateExerciseImageResId = R.drawable.ic_selected_booboo_exercise, traitsResIdList = persistentListOf( R.string.booboo_tag_text1, R.string.booboo_tag_text2, diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/service/TimerService.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/service/TimerService.kt index f480b68..102f719 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/service/TimerService.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/service/TimerService.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -47,13 +48,39 @@ class TimerService : Service() { val notificationManager: NotificationManager get() = _notificationManager private lateinit var wakeLock: PowerManager.WakeLock + private var remainingTime: Int = 0 + private var isPaused: Boolean = false + + private val _notificationTimerState = MutableStateFlow(NotificationTimerState(0, true, false)) + val notificationTimerState: StateFlow get() = _notificationTimerState + + private fun emitState() { + _notificationTimerState.update { + it.copy( + remainingTime = remainingTime, + isPaused = isPaused, + ) + } + } + + private fun setActionButtonVisible(actionButtonVisible: Boolean) { + _notificationTimerState.update { + it.copy( + actionButtonVisible = actionButtonVisible, + ) + } + } + companion object { const val ACTION_START = "START_TIMER" const val ACTION_STOP = "STOP_TIMER" + const val ACTION_PAUSE = "PAUSE_TIMER" + const val ACTION_RESUME = "RESUME_TIMER" const val INTENT_KEY_TIMER_SECONDS = "TIMER_SECONDS" const val INTENT_KEY_TIMER_AUDIO = "TIMER_AUDIO" const val INTENT_KEY_VIBRATION = "TIMER_VIBRATION" const val INTENT_KEY_VOLUME = "TIMER_VOLUME" + const val INTENT_KEY_ACTION_BUTTON_VISIBLE = "TIMER_ACTION_BUTTON_VISIBLE" const val REQUEST_CODE = 1004 const val NOTIFICATION_CHANNEL_ID = "timer_channel" const val NOTIFICATION_ID = 1 @@ -93,31 +120,63 @@ class TimerService : Service() { val action = intent?.action Log.d("TimerService", "onStartCommand() :: action :: ${action}") - if (action == ACTION_STOP) { - stopSelf() - } else if (action == ACTION_START) { - val seconds = intent.getIntExtra(INTENT_KEY_TIMER_SECONDS, 0) - val vibration = intent.getIntExtra(INTENT_KEY_VIBRATION,3) - val volume = intent.getIntExtra(INTENT_KEY_VOLUME, 3) - audioResPath = intent.getStringExtra(INTENT_KEY_TIMER_AUDIO) ?: "" - startTimer(seconds, vibration, volume) - // 서비스 실행 - startForegroundService() + when (action) { + ACTION_STOP -> stopSelf() + + ACTION_START -> { + val seconds = intent.getIntExtra(INTENT_KEY_TIMER_SECONDS, 0) + val vibration = intent.getIntExtra(INTENT_KEY_VIBRATION, 3) + val volume = intent.getIntExtra(INTENT_KEY_VOLUME, 3) + val actionButtonVisible = intent.getBooleanExtra(INTENT_KEY_ACTION_BUTTON_VISIBLE, false) + setActionButtonVisible(actionButtonVisible) + audioResPath = intent.getStringExtra(INTENT_KEY_TIMER_AUDIO) ?: "" + startTimer(seconds, vibration, volume) + startForegroundService() + } + + ACTION_PAUSE -> { + if (!isPaused) { + isPaused = true + emitState() + updateTimerNotification() + player.pause() + vibrator?.cancel() + } + } + + ACTION_RESUME -> { + if (isPaused) { + isPaused = false + emitState() + updateTimerNotification() + } + } } + return START_STICKY } private fun startTimer(times: Int, vibration: Int, volume: Int) { - var remainingTime = times + remainingTime = times + isPaused = false + emitState() + timerJob?.cancel() timerJob = CoroutineScope(Dispatchers.Main).launch { while (isActive) { - remainingTime-- + if (isPaused) { + delay(200L) + continue + } + delay(1000L) // 1초 대기 + remainingTime-- + emitState() + if (remainingTime > 0) { Log.d("TimerService", "startTimer() :: 현재 숫자 :: ${remainingTime}") - updateTimerNotification(remainingTime) + updateTimerNotification() } else { Log.d("TimerService", "startTimer() :: 타이머 종료") timerJob?.cancel() @@ -210,8 +269,22 @@ class TimerService : Service() { } } + private fun pausePendingIntent(): PendingIntent = + PendingIntent.getService( + this, REQUEST_CODE + 1, + Intent(this, TimerService::class.java).setAction(ACTION_PAUSE), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + private fun resumePendingIntent(): PendingIntent = + PendingIntent.getService( + this, REQUEST_CODE + 2, + Intent(this, TimerService::class.java).setAction(ACTION_RESUME), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + private fun createSilentNotification(): Notification { - return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) + val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) .setContentTitle(getString(R.string.notification_title)) .setContentText(getString(R.string.notification_default_content)) .setPriority(NotificationCompat.PRIORITY_HIGH) // 우선순위 최소 @@ -219,22 +292,37 @@ class TimerService : Service() { .setSilent(true) // 알림 사운드 없음 .setSmallIcon(R.mipmap.ic_app_icon_round) .setOngoing(true) - .build() + + if (notificationTimerState.value.actionButtonVisible) { + notification.addAction(0, getString(R.string.action_pause), pausePendingIntent()) + } + + return notification.build() } - private fun updateTimerNotification(remainingSeconds: Int) { + private fun updateTimerNotification() { val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) .setContentTitle(getString(R.string.notification_title)) - .setContentText(formattedRemainingTime(remainingSeconds)) + .setContentText( + if (isPaused && notificationTimerState.value.actionButtonVisible) getString(R.string.notification_pause_content, formattedRemainingTime(remainingTime)) + else formattedRemainingTime(remainingTime) + ) .setPriority(NotificationCompat.PRIORITY_HIGH) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setSilent(true) .setSmallIcon(R.mipmap.ic_app_icon_round) .setOngoing(true) .setAutoCancel(false) - .build() - notificationManager.notify(NOTIFICATION_ID, notification) + if (notificationTimerState.value.actionButtonVisible) { + if (isPaused) { + notification.addAction(0, getString(R.string.action_resume), resumePendingIntent()) + } else { + notification.addAction(0, getString(R.string.action_pause), pausePendingIntent()) + } + } + + notificationManager.notify(NOTIFICATION_ID, notification.build()) } private fun formattedRemainingTime(remainingSeconds: Int): String { @@ -278,3 +366,9 @@ class TimerService : Service() { } } } + +data class NotificationTimerState( + val remainingTime: Int, + val isPaused: Boolean, + val actionButtonVisible: Boolean, +) diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/service/TimerServiceManager.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/service/TimerServiceManager.kt index a85f3ff..590e81c 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/service/TimerServiceManager.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/service/TimerServiceManager.kt @@ -10,24 +10,39 @@ import kotlinx.coroutines.flow.StateFlow class TimerServiceManager(private val context: Context) { private var service: TimerService? = null - private var serviceConnection: ServiceConnection? = null + private var timerConnection: ServiceConnection? = null + private var overCountConnection: ServiceConnection? = null - fun startTimerService(seconds: Int, audioResPath: String, vibration : Int, volume : Int) { + fun startTimerService(seconds: Int, audioResPath: String, vibration : Int, volume : Int, actionButtonVisible: Boolean) { val intent = Intent(context, TimerService::class.java).apply { action = TimerService.ACTION_START putExtra(TimerService.INTENT_KEY_TIMER_SECONDS, seconds) putExtra(TimerService.INTENT_KEY_TIMER_AUDIO, audioResPath) putExtra(TimerService.INTENT_KEY_VIBRATION, vibration) putExtra(TimerService.INTENT_KEY_VOLUME, volume) + putExtra(TimerService.INTENT_KEY_ACTION_BUTTON_VISIBLE, actionButtonVisible) } context.startService(intent) } + fun pauseTimer() { + val intent = Intent(context, TimerService::class.java) + .setAction(TimerService.ACTION_PAUSE) + context.startService(intent) + } + + fun resumeTimer() { + val intent = Intent(context, TimerService::class.java) + .setAction(TimerService.ACTION_RESUME) + context.startService(intent) + } + fun stopTimerService() { val stopServiceIntent = Intent(context, TimerService::class.java).apply { action = TimerService.ACTION_STOP } - unbindService() + unbindOverCountService() + unbindTimerService() context.stopService(stopServiceIntent) } @@ -42,24 +57,58 @@ class TimerServiceManager(private val context: Context) { return false } - fun bindService(onServiceConnected: (StateFlow) -> Unit) { - if (serviceConnection == null) { - serviceConnection = createServiceConnection(onServiceConnected) + fun bindTimerService(onServiceConnected: (StateFlow) -> Unit) { + if (timerConnection == null) { + timerConnection = createTimerServiceConnection(onServiceConnected) } val intent = Intent(context, TimerService::class.java) - context.bindService(intent, serviceConnection!!, Context.BIND_AUTO_CREATE) + context.bindService(intent, timerConnection!!, Context.BIND_AUTO_CREATE) } - fun unbindService() { - serviceConnection?.let { + fun bindOverCountService(onServiceConnected: (StateFlow) -> Unit) { + if (overCountConnection == null) { + overCountConnection = createOverCountServiceConnection(onServiceConnected) + } + + val intent = Intent(context, TimerService::class.java) + context.bindService(intent, overCountConnection!!, Context.BIND_AUTO_CREATE) + } + + private fun unbindTimerService() { + timerConnection?.let { context.unbindService(it) - serviceConnection = null + timerConnection = null } service = null } - private fun createServiceConnection(onServiceConnected: (StateFlow) -> Unit): ServiceConnection { + fun unbindOverCountService() { + overCountConnection?.let { + context.unbindService(it) + overCountConnection = null + } + overCountConnection = null + } + + private fun createTimerServiceConnection(onServiceConnected: (StateFlow) -> Unit): ServiceConnection { + return object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { + val timerBinder = binder as? TimerService.TimerBinder + service = timerBinder?.getService() + + service?.let { + onServiceConnected(it.notificationTimerState) + } + } + + override fun onServiceDisconnected(name: ComponentName?) { + service = null + } + } + } + + private fun createOverCountServiceConnection(onServiceConnected: (StateFlow) -> Unit): ServiceConnection { return object : ServiceConnection { override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { val timerBinder = binder as? TimerService.TimerBinder diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/alarm/AlarmComponent.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/alarm/AlarmComponent.kt index 3bcd3ca..f921dfa 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/alarm/AlarmComponent.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/alarm/AlarmComponent.kt @@ -86,6 +86,8 @@ private fun AlarmDefaultText() { @Composable fun AlarmBottomSection( modifier: Modifier = Modifier, + finishButtonTextRes: Int, + snoozeButtonTextRes: Int, onClickFinishButton: () -> Unit, onClickSnoozeButton: () -> Unit, isDisableSnoozeButton: Boolean, @@ -99,7 +101,7 @@ fun AlarmBottomSection( ) { GaeBizButton( modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.alarm_finish_timer_btn_text), + text = stringResource(finishButtonTextRes), style = GaeBizTheme.typography.bodySemiBold, containerColor = GaeBizTheme.colors.primaryOrange, contentColor = GaeBizTheme.colors.white, @@ -114,7 +116,7 @@ fun AlarmBottomSection( contentAlignment = Alignment.Center ) { Text( - text = stringResource(R.string.alarm_snooze_timer_btn_text), + text = stringResource(snoozeButtonTextRes), style = GaeBizTheme.typography.bodyMedium, color = GaeBizTheme.colors.gray100, ) @@ -122,4 +124,4 @@ fun AlarmBottomSection( Spacer(Modifier.height(12.dp)) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/alarm/AlarmScreen.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/alarm/AlarmScreen.kt index fe95d1e..122890f 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/alarm/AlarmScreen.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/alarm/AlarmScreen.kt @@ -2,16 +2,18 @@ package com.ggaebiz.ggaebiz.presentation.ui.alarm import android.util.Log import androidx.activity.compose.BackHandler +import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.tooling.preview.Preview +import com.ggaebiz.ggaebiz.R import com.ggaebiz.ggaebiz.presentation.common.extension.collectAsStateWithLifecycle import com.ggaebiz.ggaebiz.presentation.common.extension.collectSideEffectWithLifecycle import com.ggaebiz.ggaebiz.presentation.designsystem.theme.GaeBizTheme import com.ggaebiz.ggaebiz.presentation.designsystem.ui.FullScreen import com.ggaebiz.ggaebiz.presentation.service.TimerServiceManager +import com.ggaebiz.ggaebiz.presentation.ui.setting.TimerMode import org.koin.androidx.compose.koinViewModel import org.koin.compose.getKoin @@ -26,7 +28,7 @@ fun AlarmScreen( DisposableEffect(Unit) { onDispose { - timerServiceManager.unbindService() + timerServiceManager.unbindOverCountService() } } BackHandler(enabled = true) { } @@ -42,7 +44,7 @@ fun AlarmScreen( navigateTimer() } is AlarmSideEffect.GetOverCount -> { - timerServiceManager.bindService { serviceFlow -> + timerServiceManager.bindOverCountService { serviceFlow -> Log.d("TimerService","LaunchedEffect :: ${serviceFlow.value}") viewModel.setTimer(serviceFlow) viewModel.processIntent(AlarmIntent.StartOverCount) @@ -69,13 +71,45 @@ fun AlarmContent( ment = uiState.ment, plusSecond = uiState.plusSeconds ) AlarmBottomSection( - onClickFinishButton = { processIntent(AlarmIntent.ClickFinish) }, - onClickSnoozeButton = { processIntent(AlarmIntent.ClickSnooze) }, + finishButtonTextRes = getFinishButtonTextRes(uiState.timerMode, uiState.isRestAvailable), + snoozeButtonTextRes = getSnoozeButtonTextRes(uiState.timerMode, uiState.isRestAvailable, uiState.snoozeCount), + onClickFinishButton = { finishButtonClickEvent(uiState.timerMode, uiState.isRestAvailable, processIntent) }, + onClickSnoozeButton = { snoozeButtonClickEvent(uiState.snoozeCount, processIntent) }, isDisableSnoozeButton = uiState.disableSnoozeButton ) } } +@StringRes +fun getFinishButtonTextRes(timerMode: TimerMode, isRestAvailable: Boolean): Int = + when { + isRestAvailable -> R.string.alarm_finish_timer_btn_text + timerMode.isConcentrateTimer() -> R.string.alarm_resume_timer_btn_text + else -> R.string.alarm_finish_timer_btn_text + } + +@StringRes +fun getSnoozeButtonTextRes(timerMode: TimerMode, isRestAvailable: Boolean, snoozeCount: Int): Int = + when { + !timerMode.isConcentrateTimer() -> R.string.alarm_snooze_timer_btn_text + snoozeCount >= 2 -> R.string.alarm_finish_timer_btn_text + isRestAvailable -> R.string.alarm_resume_after_10_minutes_timer_btn_text + else -> R.string.alarm_snooze_timer_btn_text + } + +fun finishButtonClickEvent(timerMode: TimerMode, isRestAvailable: Boolean, processIntent: (AlarmIntent) -> Unit) = + when { + isRestAvailable -> processIntent(AlarmIntent.ClickFinish) + timerMode.isConcentrateTimer() -> processIntent(AlarmIntent.ClickResumeConcentrate) + else -> processIntent(AlarmIntent.ClickFinish) + } + +fun snoozeButtonClickEvent(snoozeCount: Int, processIntent: (AlarmIntent) -> Unit) = + when { + snoozeCount >= 2 -> processIntent(AlarmIntent.ClickFinish) + else -> processIntent(AlarmIntent.ClickSnooze) + } + @Preview(showBackground = true) @Composable fun AlarmScreenPreview() { diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/alarm/AlarmViewModel.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/alarm/AlarmViewModel.kt index c4b61ba..73c95cd 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/alarm/AlarmViewModel.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/alarm/AlarmViewModel.kt @@ -2,12 +2,17 @@ package com.ggaebiz.ggaebiz.presentation.ui.alarm import com.ggaebiz.ggaebiz.R import com.ggaebiz.ggaebiz.domain.usecase.GetCharacterIdxUseCase +import com.ggaebiz.ggaebiz.domain.usecase.GetIsRestCompletedUseCase import com.ggaebiz.ggaebiz.domain.usecase.GetSnoozeCountUseCase -import com.ggaebiz.ggaebiz.domain.usecase.GetTimerSettingUseCase +import com.ggaebiz.ggaebiz.domain.usecase.GetCurrentTimerUseCase +import com.ggaebiz.ggaebiz.domain.usecase.GetSettingTimerUseCase +import com.ggaebiz.ggaebiz.domain.usecase.SetIsRestCompletedUseCase import com.ggaebiz.ggaebiz.domain.usecase.SetSnoozeCountUseCase -import com.ggaebiz.ggaebiz.domain.usecase.SetTimerSettingUseCase +import com.ggaebiz.ggaebiz.domain.usecase.SetCurrentTimerUseCase import com.ggaebiz.ggaebiz.presentation.common.base.BaseViewModel import com.ggaebiz.ggaebiz.presentation.common.extension.getCharacterData +import com.ggaebiz.ggaebiz.presentation.ui.setting.RestType +import com.ggaebiz.ggaebiz.presentation.ui.setting.TimerMode import kotlinx.coroutines.delay import kotlinx.coroutines.flow.StateFlow import java.util.Locale @@ -16,14 +21,29 @@ data class AlarmState( val level: Int = 1, val ment: Int = R.string.alarm_ment_kiki_level1_1, val backGroundImgRes: Int = R.drawable.fullpage_kiki_lev_1, + val timerMode: TimerMode = TimerMode.Rest(RestType.NORMAL), val plusSeconds: String = "+ 00:00", var snoozeCount: Int = 0, + val isRestCompleted: Boolean = false, ) { - val disableSnoozeButton: Boolean = snoozeCount >= 2 + companion object { + const val DEFAULT_SNOOZE_MINUTE = 5 + const val DEFAULT_REST_MINUTE = 10 + } + private val isFirstSnoozeOrMaxLevel: Boolean = snoozeCount == 0 && level == 3 + + val isRestAvailable: Boolean = timerMode.isConcentrateTimer() && isRestCompleted.not() + val disableSnoozeButton: Boolean = snoozeCount >= 2 && timerMode.isRestTimer() + + val nextTimerMinute = if (isRestAvailable) DEFAULT_REST_MINUTE else DEFAULT_SNOOZE_MINUTE + + val nextSnoozeCount = if (isRestAvailable) snoozeCount else snoozeCount + 1 + val nextLevel: Int = if (isFirstSnoozeOrMaxLevel || isRestAvailable) level else level + 1 } sealed interface AlarmIntent { data object ClickSnooze : AlarmIntent + data object ClickResumeConcentrate : AlarmIntent data object ClickFinish : AlarmIntent data object StartOverCount : AlarmIntent } @@ -36,10 +56,13 @@ sealed interface AlarmSideEffect { class AlarmViewModel( private val getCharacterIdxUseCase: GetCharacterIdxUseCase, - private val getTimerSettingUseCase: GetTimerSettingUseCase, - private val setTimerSettingUseCase: SetTimerSettingUseCase, + private val getCurrentTimerUseCase: GetCurrentTimerUseCase, + private val setCurrentTimerUseCase: SetCurrentTimerUseCase, + private val getSettingTimerUseCase: GetSettingTimerUseCase, private val getSnoozeCountUseCase: GetSnoozeCountUseCase, private val setSnoozeCountUseCase: SetSnoozeCountUseCase, + private val getIsRestCompletedUseCase: GetIsRestCompletedUseCase, + private val setIsRestCompletedUseCase: SetIsRestCompletedUseCase, ) : BaseViewModel(AlarmState()) { init { @@ -51,6 +74,7 @@ class AlarmViewModel( when (intent) { AlarmIntent.ClickFinish -> finishTimer() AlarmIntent.ClickSnooze -> snoozeTimer() + AlarmIntent.ClickResumeConcentrate -> resumeConcentrateTimer() AlarmIntent.StartOverCount -> startIncreaseSeconds() } } @@ -77,17 +101,20 @@ class AlarmViewModel( private fun getTimerInfo() = launch { val snoozeCount = getSnoozeCountUseCase() val characterIdx = getCharacterIdxUseCase() - val (level, _, _) = getTimerSettingUseCase() - val levelIdx = getTimerSettingUseCase.getLevelIdx() + val (level, _, _, timerMode) = getCurrentTimerUseCase() + val levelIdx = getCurrentTimerUseCase.getLevelIdx() + val isRestCompleted = getIsRestCompletedUseCase() val data = characterIdx.getCharacterData() if (data != null) { updateState { it.copy( - ment = data.mentAudioList[level - 1][levelIdx].ment, + ment = data.getMentAudio(timerMode, level - 1, levelIdx).ment, backGroundImgRes = data.alarmBackgroundImageList[level - 1], + timerMode = timerMode, level = level, - snoozeCount = snoozeCount + snoozeCount = snoozeCount, + isRestCompleted = isRestCompleted, ) } } @@ -99,16 +126,26 @@ class AlarmViewModel( } private fun snoozeTimer() = launch { - val nowLevel = uiState.value.level - val nowSnooze = uiState.value.snoozeCount - val newLevel = if (nowSnooze == 0 && nowLevel == 3) nowLevel else nowLevel + 1 - - setSnoozeCountUseCase.invoke((nowSnooze + 1)) - setTimerSettingUseCase( - level = newLevel, + setSnoozeCountUseCase(uiState.value.nextSnoozeCount) + setIsRestCompletedUseCase(true) + setCurrentTimerUseCase( + level = uiState.value.nextLevel, hour = 0, - minute = 5, - snoozeCount = nowSnooze + 1 + minute = uiState.value.nextTimerMinute, + timerMode = null, + ) + postSideEffect(AlarmSideEffect.ClickSnooze) + } + + private fun resumeConcentrateTimer() = launch { + val (hour, minute) = getSettingTimerUseCase() + setSnoozeCountUseCase(0) + setIsRestCompletedUseCase(false) + setCurrentTimerUseCase( + level = 1, + hour = hour, + minute = minute, + timerMode = uiState.value.timerMode, ) postSideEffect(AlarmSideEffect.ClickSnooze) } diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/setting/SettingScreen.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/setting/SettingScreen.kt index 7a349f9..5474b02 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/setting/SettingScreen.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/setting/SettingScreen.kt @@ -1,5 +1,7 @@ package com.ggaebiz.ggaebiz.presentation.ui.setting +import GaeBizPopupPosition +import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn @@ -7,8 +9,10 @@ import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -37,12 +41,13 @@ import com.ggaebiz.ggaebiz.presentation.common.extension.collectAsStateWithLifec import com.ggaebiz.ggaebiz.presentation.common.extension.collectSideEffectWithLifecycle import com.ggaebiz.ggaebiz.presentation.designsystem.component.button.GaeBizButton import com.ggaebiz.ggaebiz.presentation.designsystem.component.header.GaeBizTextAppBar -import com.ggaebiz.ggaebiz.presentation.designsystem.component.picker.PickerState -import com.ggaebiz.ggaebiz.presentation.designsystem.component.picker.rememberPickerState import com.ggaebiz.ggaebiz.presentation.designsystem.theme.GaeBizTheme -import com.ggaebiz.ggaebiz.presentation.designsystem.ui.GaeBizLevelSlider -import com.ggaebiz.ggaebiz.presentation.designsystem.ui.GaeBizMent +import com.ggaebiz.ggaebiz.presentation.designsystem.ui.CategoryArea import com.ggaebiz.ggaebiz.presentation.designsystem.ui.GaeBizTimePicker +import com.ggaebiz.ggaebiz.presentation.designsystem.ui.LevelItem +import com.ggaebiz.ggaebiz.presentation.designsystem.ui.RestMent +import com.ggaebiz.ggaebiz.presentation.designsystem.ui.SettingSwitch +import com.ggaebiz.ggaebiz.presentation.designsystem.ui.popup.ListPopup import com.ggaebiz.ggaebiz.presentation.model.Character.Companion.CHARACTER_LIST import com.ggaebiz.ggaebiz.presentation.model.Character.Companion.SETTING_MENT_LIST import org.koin.androidx.compose.koinViewModel @@ -61,6 +66,10 @@ fun SettingScreen( } } + BackHandler(enabled = uiState.isLevelPopupVisible) { + viewModel.processIntent(SettingIntent.ClickMentLevel(false)) + } + LaunchedEffect(Unit) { viewModel.processIntent(SettingIntent.EnterScreen) } @@ -78,23 +87,17 @@ fun SettingContent( processIntent: (SettingIntent) -> Unit, onClickBackButton: () -> Unit, ) { - val hourPickerState: PickerState = rememberPickerState(defaultValue = "00") - val minutePickerState: PickerState = rememberPickerState(defaultValue = "15") var buttonEnabled by remember { - mutableStateOf(hourPickerState.selectedItem != "00" && minutePickerState.selectedItem != "00") + mutableStateOf(uiState.selectedHour != "00" && uiState.selectedMinute != "00") } val spacerHeightPx = remember { mutableFloatStateOf(0f) } val density = LocalDensity.current val interactionSource = remember { MutableInteractionSource() } - - LaunchedEffect(hourPickerState.selectedItem, minutePickerState.selectedItem) { - buttonEnabled = - !(hourPickerState.selectedItem == "00" && minutePickerState.selectedItem == "00") + LaunchedEffect(uiState.selectedHour, uiState.selectedMinute) { + buttonEnabled = !(uiState.selectedHour == "00" && uiState.selectedMinute == "00") } - - Column( modifier = Modifier .fillMaxSize() @@ -111,27 +114,46 @@ fun SettingContent( titleRes = R.string.setting_title_text, iconOnClick = { onClickBackButton() }, ) - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(16.dp)) + SettingSwitch( + timerMode = uiState.timerMode, + onToggle = { isRestSelected -> + processIntent(SettingIntent.ClickTimerMode(isRestSelected)) + }, + ) + Spacer(modifier = Modifier.height(36.dp)) Image( painter = painterResource( - CHARACTER_LIST[uiState.selectedCharacterIdx].selectedImageResId[uiState.level - 1] + selectedCharacterImageRes(uiState) ), contentDescription = null, modifier = Modifier.size(125.dp), contentScale = ContentScale.Crop, ) Spacer(modifier = Modifier.height(24.dp)) - GaeBizMent( - text = stringResource(SETTING_MENT_LIST[uiState.level - 1]), - hasBelowArrow = false - ) - Spacer(modifier = Modifier.height(24.dp)) - GaeBizLevelSlider( - selectedLevel = uiState.level, - onValueChange = { selectedLevel -> - processIntent(SettingIntent.SelectLevel(selectedLevel)) - }, - ) + + when (val mode = uiState.timerMode) { + is TimerMode.Rest -> { + RestMent( + text = stringResource(SETTING_MENT_LIST[uiState.level - 1]), + level = uiState.level, + onClick = { + processIntent(SettingIntent.ClickMentLevel(true)) + }, + ) + } + is TimerMode.Concentrate -> { + Spacer(modifier = Modifier.height(12.dp)) + CategoryArea( + selected = mode, + onSelect = { type -> + processIntent(SettingIntent.ClickTimerMode(TimerMode.Concentrate(type))) + }, + modifier = Modifier.padding(horizontal = 20.dp) + ) + } + } + Spacer( modifier = Modifier .weight(1f) @@ -145,8 +167,13 @@ fun SettingContent( .padding(horizontal = 20.dp) ) { GaeBizTimePicker( - hourPickerState = hourPickerState, - minutePickerState = minutePickerState, + selectedHour = uiState.selectedHour, + selectedMinute = uiState.selectedMinute, + maxHour = uiState.maxHour, + maxMinute = uiState.maxMinute, + timerMode = uiState.timerMode, + onHourSelected = { processIntent(SettingIntent.SelectHour(it)) }, + onMinuteSelected = { processIntent(SettingIntent.SelectMinute(it)) }, ) this@Column.AnimatedVisibility( visible = !uiState.isNudgeGuideViewed, @@ -156,7 +183,7 @@ fun SettingContent( SettingNudgePopup( density, spacerHeightPx.floatValue, - { processIntent(SettingIntent.CloseNudgePopUp) }) + ) { processIntent(SettingIntent.CloseNudgePopUp) } } } Spacer(modifier = Modifier.weight(1f)) @@ -169,8 +196,8 @@ fun SettingContent( onClick = { processIntent( SettingIntent.ClickStartButton( - hour = hourPickerState.selectedItem.toInt(), - minute = minutePickerState.selectedItem.toInt(), + hour = uiState.selectedHour.toInt(), + minute = uiState.selectedMinute.toInt(), ), ) }, @@ -178,11 +205,91 @@ fun SettingContent( containerColor = GaeBizTheme.colors.gray800, disabledContentColor = GaeBizTheme.colors.gray400, disabledContainerColor = GaeBizTheme.colors.gray100, - text = stringResource(R.string.start_button_text), + text = if (uiState.timerMode is TimerMode.Rest) { + stringResource(R.string.start_rest_button_text, stringResource(CHARACTER_LIST[uiState.selectedCharacterIdx].nameResId)) + } else { + stringResource(R.string.start_concentrate_button_text, stringResource(CHARACTER_LIST[uiState.selectedCharacterIdx].nameResId)) + }, style = GaeBizTheme.typography.bodySemiBold, ) Spacer(modifier = Modifier.height(12.dp)) } + ChoiceLevelPopup( + uiState.isLevelPopupVisible, + uiState.selectedCharacterIdx, + uiState.level, + ) { selectedLevel -> + processIntent(SettingIntent.SelectLevel(selectedLevel)) + processIntent(SettingIntent.ClickMentLevel(false)) + } +} + +@Composable +fun ChoiceLevelPopup( + visible: Boolean, + selectedCharacterIdx: Int, + selectedLevel: Int, + onClick: (Int) -> Unit, +){ + ListPopup( + visible = visible, + titleText = stringResource(R.string.ment_level_title_text), + subtitleText = stringResource(R.string.ment_level_subtitle_text), + position = GaeBizPopupPosition.Bottom, + itemContent = { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + LevelItem( + modifier = Modifier.weight(1f), + title = stringResource(R.string.ment_level_1_item_text), + isSelected = selectedLevel == 1, + selectedIcon = painterResource(CHARACTER_LIST[selectedCharacterIdx].selectedImageResId[0]), + unSelectedIcon = painterResource(CHARACTER_LIST[selectedCharacterIdx].imageResId[0]), + onClick = { onClick(1) }, + ) + LevelItem( + modifier = Modifier.weight(1f), + title = stringResource(R.string.ment_level_2_item_text), + isSelected = selectedLevel == 2, + selectedIcon = painterResource(CHARACTER_LIST[selectedCharacterIdx].selectedImageResId[1]), + unSelectedIcon = painterResource(CHARACTER_LIST[selectedCharacterIdx].imageResId[1]), + onClick = { onClick(2) }, + ) + LevelItem( + modifier = Modifier.weight(1f), + title = stringResource(R.string.ment_level_3_item_text), + isSelected = selectedLevel == 3, + selectedIcon = painterResource(CHARACTER_LIST[selectedCharacterIdx].selectedImageResId[2]), + unSelectedIcon = painterResource(CHARACTER_LIST[selectedCharacterIdx].imageResId[2]), + onClick = { onClick(3) }, + ) + } + } + ) +} + +private fun selectedCharacterImageRes(uiState: SettingState): Int { + return when { + uiState.timerMode.isRestTimer() -> { + CHARACTER_LIST[uiState.selectedCharacterIdx].selectedImageResId[uiState.level - 1] + } + uiState.timerMode.isConcentrateTimer() -> { + when { + (uiState.timerMode as TimerMode.Concentrate).isNormal() -> { + CHARACTER_LIST[uiState.selectedCharacterIdx].selectedImageResId[uiState.level - 1] + } + (uiState.timerMode as TimerMode.Concentrate).isStudy() -> { + CHARACTER_LIST[uiState.selectedCharacterIdx].selectedConcentrateStudyImageResId + } + (uiState.timerMode as TimerMode.Concentrate).isExercise() -> { + CHARACTER_LIST[uiState.selectedCharacterIdx].selectedConcentrateExerciseImageResId + } + + else -> CHARACTER_LIST[uiState.selectedCharacterIdx].selectedImageResId[uiState.level - 1] + } + } + + else -> CHARACTER_LIST[uiState.selectedCharacterIdx].selectedImageResId[uiState.level - 1] + } } @Preview(showBackground = true) diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/setting/SettingViewModel.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/setting/SettingViewModel.kt index ac7096b..cb929e1 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/setting/SettingViewModel.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/setting/SettingViewModel.kt @@ -2,7 +2,9 @@ package com.ggaebiz.ggaebiz.presentation.ui.setting import com.ggaebiz.ggaebiz.domain.repository.OnboardingRepository import com.ggaebiz.ggaebiz.domain.usecase.GetCharacterIdxUseCase -import com.ggaebiz.ggaebiz.domain.usecase.SetTimerSettingUseCase +import com.ggaebiz.ggaebiz.domain.usecase.SetCurrentTimerUseCase +import com.ggaebiz.ggaebiz.domain.usecase.SetIsRestCompletedUseCase +import com.ggaebiz.ggaebiz.domain.usecase.SetSettingTimerUseCase import com.ggaebiz.ggaebiz.presentation.common.base.BaseViewModel import kotlinx.coroutines.delay @@ -10,6 +12,12 @@ data class SettingState( val selectedCharacterIdx: Int = 0, val level: Int = 1, val isNudgeGuideViewed: Boolean = true, + val selectedHour: String = TimerMode.Rest(RestType.NORMAL).defaultHour, + val selectedMinute: String = TimerMode.Rest(RestType.NORMAL).defaultMinute, + val maxHour: Int = TimerMode.Rest(RestType.NORMAL).maxHour, + val maxMinute: Int = TimerMode.Rest(RestType.NORMAL).maxMinute, + val timerMode: TimerMode = TimerMode.Rest(RestType.NORMAL), + val isLevelPopupVisible: Boolean = false, ) sealed interface SettingSideEffect { @@ -19,13 +27,19 @@ sealed interface SettingSideEffect { sealed interface SettingIntent { data class SelectLevel(val level: Int) : SettingIntent data class ClickStartButton(val hour: Int, val minute: Int) : SettingIntent + data class SelectHour(val hour: String) : SettingIntent + data class SelectMinute(val minute: String) : SettingIntent + data class ClickTimerMode(val timerMode: TimerMode) : SettingIntent + data class ClickMentLevel(val clickMentLevel: Boolean) : SettingIntent data object EnterScreen : SettingIntent data object CloseNudgePopUp : SettingIntent } class SettingViewModel( private val getCharacterIdxUseCase: GetCharacterIdxUseCase, - private val setTimerSettingUseCase: SetTimerSettingUseCase, + private val setCurrentTimerUseCase: SetCurrentTimerUseCase, + private val setSettingTimerUseCase: SetSettingTimerUseCase, + private val setIsRestCompletedUseCase: SetIsRestCompletedUseCase, private val onboardingRepository: OnboardingRepository ) : BaseViewModel(SettingState()) { @@ -42,10 +56,14 @@ class SettingViewModel( when (intent) { is SettingIntent.SelectLevel -> selectLevel(level = intent.level) is SettingIntent.ClickStartButton -> startTimer(intent.hour, intent.minute) - SettingIntent.EnterScreen -> launch{ + is SettingIntent.ClickTimerMode -> switchTimerMode(intent.timerMode) + is SettingIntent.SelectHour -> selectHour(intent.hour) + is SettingIntent.SelectMinute -> selectMinute(intent.minute) + is SettingIntent.ClickMentLevel -> clickMentLevel(intent.clickMentLevel) + SettingIntent.EnterScreen -> launch { if (onboardingRepository.getSettingNudgeGuideViewed()) { updateState { it.copy(isNudgeGuideViewed = true) } - }else{ + } else { delay(200) updateState { it.copy(isNudgeGuideViewed = false) } } @@ -61,10 +79,43 @@ class SettingViewModel( updateState { it.copy(level = level) } } + private fun selectHour(hour: String) = launch { + updateState { it.copy(selectedHour = hour) } + } + + private fun selectMinute(minute: String) = launch { + updateState { it.copy(selectedMinute = minute) } + } + + private fun clickMentLevel(isLevelPopupVisible: Boolean) = launch { + updateState { it.copy(isLevelPopupVisible = isLevelPopupVisible) } + } + private fun startTimer(hour: Int, minute: Int) = launch { - setTimerSettingUseCase( - level = uiState.value.level, hour = hour, minute = minute, snoozeCount = 0 + setCurrentTimerUseCase( + level = if (uiState.value.timerMode.isConcentrateTimer()) 1 else uiState.value.level, + hour = hour, + minute = minute, + timerMode = uiState.value.timerMode, + ) + setSettingTimerUseCase( + hour = hour, + minute = minute, ) + setIsRestCompletedUseCase(false) postSideEffect(SettingSideEffect.NavigateToTimer) } + + private fun switchTimerMode(timerMode: TimerMode) = launch { + updateState { + it.copy( + level = 1, + timerMode = timerMode, + maxHour = timerMode.maxHour, + maxMinute = timerMode.maxMinute, + selectedHour = timerMode.defaultHour, + selectedMinute = timerMode.defaultMinute, + ) + } + } } diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/setting/TimerMode.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/setting/TimerMode.kt new file mode 100644 index 0000000..07f4e14 --- /dev/null +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/setting/TimerMode.kt @@ -0,0 +1,115 @@ +package com.ggaebiz.ggaebiz.presentation.ui.setting + +sealed interface TimerMode { + val maxHour: Int + val maxMinute: Int + val defaultHour: String + val defaultMinute: String + + fun isRestTimer(): Boolean = this is Rest + fun isConcentrateTimer(): Boolean = this is Concentrate + + data class Rest(val type: RestType) : TimerMode { + + override val maxHour: Int + get() = when (type) { + RestType.NORMAL -> 5 + } + + override val maxMinute: Int + get() = when (type) { + RestType.NORMAL -> 59 + } + + override val defaultHour: String + get() = when (type) { + RestType.NORMAL -> "00" + } + + override val defaultMinute: String + get() = when (type) { + RestType.NORMAL -> "15" + } + } + + data class Concentrate(val type: ConcentrateType) : TimerMode { + + fun isNormal(): Boolean = this.type == ConcentrateType.NORMAL + fun isStudy(): Boolean = this.type == ConcentrateType.STUDY + fun isExercise(): Boolean = this.type == ConcentrateType.EXERCISE + + override val maxHour: Int + get() = when (type) { + ConcentrateType.NORMAL -> 5 + ConcentrateType.STUDY -> 5 + ConcentrateType.EXERCISE -> 2 + } + + override val maxMinute: Int + get() = when (type) { + ConcentrateType.NORMAL -> 59 + ConcentrateType.STUDY -> 59 + ConcentrateType.EXERCISE -> 59 + } + + override val defaultHour: String + get() = when (type) { + ConcentrateType.NORMAL -> "00" + ConcentrateType.STUDY -> "02" + ConcentrateType.EXERCISE -> "01" + } + + override val defaultMinute: String + get() = when (type) { + ConcentrateType.NORMAL -> "30" + ConcentrateType.STUDY -> "00" + ConcentrateType.EXERCISE -> "00" + } + } +} + +enum class ConcentrateType { NORMAL, STUDY, EXERCISE } +enum class RestType { NORMAL } + +fun toTimerMode(timerMode: String, concentrateType: String?): TimerMode = + when (timerMode) { + "REST" -> { + val type = when (concentrateType) { + "NORMAL" -> RestType.NORMAL + else -> RestType.NORMAL + } + TimerMode.Rest(type) + } + + "CONCENTRATE" -> { + val type = when (concentrateType) { + "STUDY" -> ConcentrateType.STUDY + "EXERCISE" -> ConcentrateType.EXERCISE + else -> ConcentrateType.NORMAL + } + TimerMode.Concentrate(type) + } + + else -> { + TimerMode.Rest(RestType.NORMAL) + } + } + +fun toTimerModeString(timerMode: TimerMode): Pair = + when (timerMode) { + is TimerMode.Rest -> { + val type = when (timerMode.type) { + RestType.NORMAL -> "NORMAL" + } + "REST" to type + } + + is TimerMode.Concentrate -> { + val type = when (timerMode.type) { + ConcentrateType.STUDY -> "STUDY" + ConcentrateType.EXERCISE -> "EXERCISE" + ConcentrateType.NORMAL -> "NORMAL" + } + "CONCENTRATE" to type + } + } diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/timer/TimerScreen.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/timer/TimerScreen.kt index 503c430..908d9e3 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/timer/TimerScreen.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/timer/TimerScreen.kt @@ -28,6 +28,8 @@ import com.ggaebiz.ggaebiz.presentation.common.extension.collectSideEffectWithLi import com.ggaebiz.ggaebiz.presentation.designsystem.component.header.GaeBizLogoAppBar import com.ggaebiz.ggaebiz.presentation.designsystem.theme.GaeBizTheme import com.ggaebiz.ggaebiz.presentation.designsystem.ui.GaeBizSlideButton +import com.ggaebiz.ggaebiz.presentation.designsystem.ui.TimerActionButton +import com.ggaebiz.ggaebiz.presentation.designsystem.ui.TimerActionType import com.ggaebiz.ggaebiz.presentation.model.Character.Companion.CHARACTER_LIST import com.ggaebiz.ggaebiz.presentation.service.TimerServiceManager import org.koin.androidx.compose.koinViewModel @@ -53,7 +55,8 @@ fun TimerScreen( effect.seconds, effect.audioResPath, effect.vibration, - effect.volume + effect.volume, + effect.actionButtonVisible, ) } @@ -61,6 +64,8 @@ fun TimerScreen( timerServiceManager.stopTimerService() navigateHome() } + is TimerSideEffect.PauseService -> timerServiceManager.pauseTimer() + is TimerSideEffect.ResumeService -> timerServiceManager.resumeTimer() } } @@ -101,6 +106,18 @@ fun TimerContent( screenWidth = LocalConfiguration.current.screenWidthDp.dp ) Spacer(modifier = Modifier.weight(1f)) + TimerActionButton( + type = if (uiState.isPaused) TimerActionType.Resume else TimerActionType.Pause, + visible = uiState.actionButtonVisible, + onClick = { + if (uiState.isPaused) { + processIntent(TimerIntent.ResumeTimer) + } else { + processIntent(TimerIntent.PauseTimer) + } + }, + ) + Spacer(modifier = Modifier.weight(1f)) GaeBizSlideButton( modifier = Modifier .width(260.dp) diff --git a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/timer/TimerViewModel.kt b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/timer/TimerViewModel.kt index bc8382b..25870d6 100644 --- a/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/timer/TimerViewModel.kt +++ b/app/src/main/java/com/ggaebiz/ggaebiz/presentation/ui/timer/TimerViewModel.kt @@ -4,11 +4,14 @@ import com.ggaebiz.ggaebiz.domain.repository.ConfigRepository import com.ggaebiz.ggaebiz.domain.usecase.EndTimerUseCase import com.ggaebiz.ggaebiz.domain.usecase.GetAudioResIdUseCase import com.ggaebiz.ggaebiz.domain.usecase.GetCharacterIdxUseCase -import com.ggaebiz.ggaebiz.domain.usecase.GetTimerSettingUseCase +import com.ggaebiz.ggaebiz.domain.usecase.GetCurrentTimerUseCase +import com.ggaebiz.ggaebiz.domain.usecase.GetIsRestCompletedUseCase import com.ggaebiz.ggaebiz.domain.usecase.SetSnoozeCountUseCase import com.ggaebiz.ggaebiz.presentation.common.base.BaseViewModel import com.ggaebiz.ggaebiz.presentation.common.extension.getCharacterData -import kotlinx.coroutines.delay +import com.ggaebiz.ggaebiz.presentation.service.TimerServiceManager +import com.ggaebiz.ggaebiz.presentation.ui.setting.RestType +import com.ggaebiz.ggaebiz.presentation.ui.setting.TimerMode data class TimerState( val selectedCharacterIdx: Int = 0, @@ -16,26 +19,41 @@ data class TimerState( val levelIdx: Int = 0, val hour: Int = 0, val minute: Int = 30, + val timerMode: TimerMode = TimerMode.Rest(RestType.NORMAL), + val actionButtonVisible: Boolean = false, val remainingSeconds: Int = 0, + val isPaused: Boolean = false, ) sealed interface TimerSideEffect { data object ShowToast : TimerSideEffect - data class StartService(val seconds: Int, val audioResPath: String, val vibration : Int, val volume : Int) : TimerSideEffect + data class StartService( + val seconds: Int, + val audioResPath: String, + val vibration : Int, + val volume : Int, + val actionButtonVisible: Boolean, + ) : TimerSideEffect data object StopService : TimerSideEffect + data object PauseService : TimerSideEffect + data object ResumeService : TimerSideEffect } sealed interface TimerIntent { data object StopTimer : TimerIntent + data object PauseTimer : TimerIntent + data object ResumeTimer : TimerIntent } class TimerViewModel( private val getAudioResIdUseCase: GetAudioResIdUseCase, private val endTimerUseCase: EndTimerUseCase, private val getCharacterIdxUseCase: GetCharacterIdxUseCase, - private val getTimerSettingUseCase: GetTimerSettingUseCase, + private val getCurrentTimerUseCase: GetCurrentTimerUseCase, + private val getIsRestCompletedUseCase: GetIsRestCompletedUseCase, private val setSnoozeCountUseCase: SetSnoozeCountUseCase, - private val configRepository: ConfigRepository + private val configRepository: ConfigRepository, + private val timerServiceManager: TimerServiceManager, ) : BaseViewModel(TimerState()) { init { @@ -45,17 +63,20 @@ class TimerViewModel( fun processIntent(intent: TimerIntent) { when (intent) { is TimerIntent.StopTimer -> stopTimer() + is TimerIntent.PauseTimer -> postSideEffect(TimerSideEffect.PauseService) + is TimerIntent.ResumeTimer -> postSideEffect(TimerSideEffect.ResumeService) } } private fun setTimerSetting() = launch { - val (level, hour, minute) = getTimerSettingUseCase() + val (level, hour, minute, timerMode) = getCurrentTimerUseCase() val selectedCharacterIdx = getCharacterIdxUseCase() - val leveIdx = getTimerSettingUseCase.getLevelIdx() + val leveIdx = getCurrentTimerUseCase.getLevelIdx() val data = selectedCharacterIdx.getCharacterData() - val audioPath = (data?.mentAudioList?.get(level - 1)?.get(leveIdx)?.audioPath) ?: "" + val audioPath = data?.getMentAudio(timerMode, level - 1, leveIdx)?.audioPath ?: "" val settingSeconds = hour * 3600 + minute * 60 + val actionButtonVisible = getIsRestCompletedUseCase().not() && timerMode.isConcentrateTimer() val vibration = if(configRepository.getVibrationStatus()){ configRepository.getVibrationValue() @@ -70,6 +91,8 @@ class TimerViewModel( levelIdx = leveIdx, hour = hour, minute = minute, + timerMode = timerMode, + actionButtonVisible = actionButtonVisible, remainingSeconds = settingSeconds ) } @@ -78,17 +101,10 @@ class TimerViewModel( settingSeconds, audioPath, vibration, - configRepository.getVolumeValue() + configRepository.getVolumeValue(), + actionButtonVisible, )) - startTimeTick() - } - - private fun startTimeTick() = launch { - while (uiState.value.remainingSeconds > 0) { - delay(1000L) - updateState { it.copy(remainingSeconds = uiState.value.remainingSeconds - 1) } - } - endTimer() + bindAndCollectServiceState() } private fun endTimer() = launch { @@ -100,4 +116,23 @@ class TimerViewModel( setSnoozeCountUseCase(0) postSideEffect(TimerSideEffect.StopService) } + + private fun bindAndCollectServiceState() { + timerServiceManager.bindTimerService { flow -> + // flow 수신 + launch { + flow.collect { info -> + updateState { + it.copy( + remainingSeconds = info.remainingTime, + isPaused = info.isPaused + ) + } + if (!info.isPaused && info.remainingTime <= 0) { + endTimer() + } + } + } + } + } } diff --git a/app/src/main/res/drawable/ic_basketball.xml b/app/src/main/res/drawable/ic_basketball.xml new file mode 100644 index 0000000..f977c13 --- /dev/null +++ b/app/src/main/res/drawable/ic_basketball.xml @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 0000000..356e998 --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_pause.xml b/app/src/main/res/drawable/ic_pause.xml new file mode 100644 index 0000000..6b4bbef --- /dev/null +++ b/app/src/main/res/drawable/ic_pause.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_pencil.xml b/app/src/main/res/drawable/ic_pencil.xml new file mode 100644 index 0000000..15477aa --- /dev/null +++ b/app/src/main/res/drawable/ic_pencil.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_resume.xml b/app/src/main/res/drawable/ic_resume.xml new file mode 100644 index 0000000..44a36de --- /dev/null +++ b/app/src/main/res/drawable/ic_resume.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_selected_bobo_exercise.png b/app/src/main/res/drawable/ic_selected_bobo_exercise.png new file mode 100644 index 0000000..96bd33c Binary files /dev/null and b/app/src/main/res/drawable/ic_selected_bobo_exercise.png differ diff --git a/app/src/main/res/drawable/ic_selected_bobo_level1.png b/app/src/main/res/drawable/ic_selected_bobo_level1.png index cc12072..4a4f90a 100644 Binary files a/app/src/main/res/drawable/ic_selected_bobo_level1.png and b/app/src/main/res/drawable/ic_selected_bobo_level1.png differ diff --git a/app/src/main/res/drawable/ic_selected_bobo_level2.png b/app/src/main/res/drawable/ic_selected_bobo_level2.png index 3fb22fe..46c0fba 100644 Binary files a/app/src/main/res/drawable/ic_selected_bobo_level2.png and b/app/src/main/res/drawable/ic_selected_bobo_level2.png differ diff --git a/app/src/main/res/drawable/ic_selected_bobo_level3.png b/app/src/main/res/drawable/ic_selected_bobo_level3.png index 2322723..95d6cb4 100644 Binary files a/app/src/main/res/drawable/ic_selected_bobo_level3.png and b/app/src/main/res/drawable/ic_selected_bobo_level3.png differ diff --git a/app/src/main/res/drawable/ic_selected_bobo_study.png b/app/src/main/res/drawable/ic_selected_bobo_study.png new file mode 100644 index 0000000..a23ae5e Binary files /dev/null and b/app/src/main/res/drawable/ic_selected_bobo_study.png differ diff --git a/app/src/main/res/drawable/ic_selected_booboo_exercise.png b/app/src/main/res/drawable/ic_selected_booboo_exercise.png new file mode 100644 index 0000000..7ecd78b Binary files /dev/null and b/app/src/main/res/drawable/ic_selected_booboo_exercise.png differ diff --git a/app/src/main/res/drawable/ic_selected_booboo_level1.png b/app/src/main/res/drawable/ic_selected_booboo_level1.png index e494ba6..e924faf 100644 Binary files a/app/src/main/res/drawable/ic_selected_booboo_level1.png and b/app/src/main/res/drawable/ic_selected_booboo_level1.png differ diff --git a/app/src/main/res/drawable/ic_selected_booboo_level2.png b/app/src/main/res/drawable/ic_selected_booboo_level2.png index e3c8d20..dc2a1b2 100644 Binary files a/app/src/main/res/drawable/ic_selected_booboo_level2.png and b/app/src/main/res/drawable/ic_selected_booboo_level2.png differ diff --git a/app/src/main/res/drawable/ic_selected_booboo_level3.png b/app/src/main/res/drawable/ic_selected_booboo_level3.png index 368f67a..36f1636 100644 Binary files a/app/src/main/res/drawable/ic_selected_booboo_level3.png and b/app/src/main/res/drawable/ic_selected_booboo_level3.png differ diff --git a/app/src/main/res/drawable/ic_selected_booboo_study.png b/app/src/main/res/drawable/ic_selected_booboo_study.png new file mode 100644 index 0000000..21704bd Binary files /dev/null and b/app/src/main/res/drawable/ic_selected_booboo_study.png differ diff --git a/app/src/main/res/drawable/ic_selected_chacha_exercise.png b/app/src/main/res/drawable/ic_selected_chacha_exercise.png new file mode 100644 index 0000000..52dc3d1 Binary files /dev/null and b/app/src/main/res/drawable/ic_selected_chacha_exercise.png differ diff --git a/app/src/main/res/drawable/ic_selected_chacha_level1.png b/app/src/main/res/drawable/ic_selected_chacha_level1.png index 8deb01d..50113a9 100644 Binary files a/app/src/main/res/drawable/ic_selected_chacha_level1.png and b/app/src/main/res/drawable/ic_selected_chacha_level1.png differ diff --git a/app/src/main/res/drawable/ic_selected_chacha_level2.png b/app/src/main/res/drawable/ic_selected_chacha_level2.png index 3baf177..76d35b5 100644 Binary files a/app/src/main/res/drawable/ic_selected_chacha_level2.png and b/app/src/main/res/drawable/ic_selected_chacha_level2.png differ diff --git a/app/src/main/res/drawable/ic_selected_chacha_level3.png b/app/src/main/res/drawable/ic_selected_chacha_level3.png index e167c94..9e20879 100644 Binary files a/app/src/main/res/drawable/ic_selected_chacha_level3.png and b/app/src/main/res/drawable/ic_selected_chacha_level3.png differ diff --git a/app/src/main/res/drawable/ic_selected_chacha_study.png b/app/src/main/res/drawable/ic_selected_chacha_study.png new file mode 100644 index 0000000..30aa5f5 Binary files /dev/null and b/app/src/main/res/drawable/ic_selected_chacha_study.png differ diff --git a/app/src/main/res/drawable/ic_selected_kiki_exercise.png b/app/src/main/res/drawable/ic_selected_kiki_exercise.png new file mode 100644 index 0000000..83bf47e Binary files /dev/null and b/app/src/main/res/drawable/ic_selected_kiki_exercise.png differ diff --git a/app/src/main/res/drawable/ic_selected_kiki_level1.png b/app/src/main/res/drawable/ic_selected_kiki_level1.png index 73135b2..475623e 100644 Binary files a/app/src/main/res/drawable/ic_selected_kiki_level1.png and b/app/src/main/res/drawable/ic_selected_kiki_level1.png differ diff --git a/app/src/main/res/drawable/ic_selected_kiki_level2.png b/app/src/main/res/drawable/ic_selected_kiki_level2.png index 937724f..0272baa 100644 Binary files a/app/src/main/res/drawable/ic_selected_kiki_level2.png and b/app/src/main/res/drawable/ic_selected_kiki_level2.png differ diff --git a/app/src/main/res/drawable/ic_selected_kiki_level3.png b/app/src/main/res/drawable/ic_selected_kiki_level3.png index 6d8107a..376e98b 100644 Binary files a/app/src/main/res/drawable/ic_selected_kiki_level3.png and b/app/src/main/res/drawable/ic_selected_kiki_level3.png differ diff --git a/app/src/main/res/drawable/ic_selected_kiki_study.png b/app/src/main/res/drawable/ic_selected_kiki_study.png new file mode 100644 index 0000000..e20321c Binary files /dev/null and b/app/src/main/res/drawable/ic_selected_kiki_study.png differ diff --git a/app/src/main/res/drawable/ic_selected_nana_exercise.png b/app/src/main/res/drawable/ic_selected_nana_exercise.png new file mode 100644 index 0000000..e7e45ad Binary files /dev/null and b/app/src/main/res/drawable/ic_selected_nana_exercise.png differ diff --git a/app/src/main/res/drawable/ic_selected_nana_level1.png b/app/src/main/res/drawable/ic_selected_nana_level1.png index a913597..634e1b8 100644 Binary files a/app/src/main/res/drawable/ic_selected_nana_level1.png and b/app/src/main/res/drawable/ic_selected_nana_level1.png differ diff --git a/app/src/main/res/drawable/ic_selected_nana_level2.png b/app/src/main/res/drawable/ic_selected_nana_level2.png index 961baab..d752bc3 100644 Binary files a/app/src/main/res/drawable/ic_selected_nana_level2.png and b/app/src/main/res/drawable/ic_selected_nana_level2.png differ diff --git a/app/src/main/res/drawable/ic_selected_nana_level3.png b/app/src/main/res/drawable/ic_selected_nana_level3.png index a97663c..6b2a989 100644 Binary files a/app/src/main/res/drawable/ic_selected_nana_level3.png and b/app/src/main/res/drawable/ic_selected_nana_level3.png differ diff --git a/app/src/main/res/drawable/ic_selected_nana_study.png b/app/src/main/res/drawable/ic_selected_nana_study.png new file mode 100644 index 0000000..9d8fa2c Binary files /dev/null and b/app/src/main/res/drawable/ic_selected_nana_study.png differ diff --git a/app/src/main/res/drawable/ic_top_bottom_arrow.xml b/app/src/main/res/drawable/ic_top_bottom_arrow.xml new file mode 100644 index 0000000..c7d8a6d --- /dev/null +++ b/app/src/main/res/drawable/ic_top_bottom_arrow.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/raw/bobo_exercise_1.mp3 b/app/src/main/res/raw/bobo_exercise_1.mp3 new file mode 100644 index 0000000..c4b03ba Binary files /dev/null and b/app/src/main/res/raw/bobo_exercise_1.mp3 differ diff --git a/app/src/main/res/raw/bobo_exercise_2.mp3 b/app/src/main/res/raw/bobo_exercise_2.mp3 new file mode 100644 index 0000000..7362854 Binary files /dev/null and b/app/src/main/res/raw/bobo_exercise_2.mp3 differ diff --git a/app/src/main/res/raw/bobo_exercise_3.mp3 b/app/src/main/res/raw/bobo_exercise_3.mp3 new file mode 100644 index 0000000..fb61cb5 Binary files /dev/null and b/app/src/main/res/raw/bobo_exercise_3.mp3 differ diff --git a/app/src/main/res/raw/bobo_focus_1.mp3 b/app/src/main/res/raw/bobo_focus_1.mp3 new file mode 100644 index 0000000..2115c7d Binary files /dev/null and b/app/src/main/res/raw/bobo_focus_1.mp3 differ diff --git a/app/src/main/res/raw/bobo_focus_2.mp3 b/app/src/main/res/raw/bobo_focus_2.mp3 new file mode 100644 index 0000000..ef5fc7e Binary files /dev/null and b/app/src/main/res/raw/bobo_focus_2.mp3 differ diff --git a/app/src/main/res/raw/bobo_focus_3.mp3 b/app/src/main/res/raw/bobo_focus_3.mp3 new file mode 100644 index 0000000..2527816 Binary files /dev/null and b/app/src/main/res/raw/bobo_focus_3.mp3 differ diff --git a/app/src/main/res/raw/bobo_study_1.mp3 b/app/src/main/res/raw/bobo_study_1.mp3 new file mode 100644 index 0000000..0c4a307 Binary files /dev/null and b/app/src/main/res/raw/bobo_study_1.mp3 differ diff --git a/app/src/main/res/raw/bobo_study_2.mp3 b/app/src/main/res/raw/bobo_study_2.mp3 new file mode 100644 index 0000000..e3110cb Binary files /dev/null and b/app/src/main/res/raw/bobo_study_2.mp3 differ diff --git a/app/src/main/res/raw/bobo_study_3.mp3 b/app/src/main/res/raw/bobo_study_3.mp3 new file mode 100644 index 0000000..137b812 Binary files /dev/null and b/app/src/main/res/raw/bobo_study_3.mp3 differ diff --git a/app/src/main/res/raw/booboo_exercise_1.mp3 b/app/src/main/res/raw/booboo_exercise_1.mp3 new file mode 100644 index 0000000..a6eaddd Binary files /dev/null and b/app/src/main/res/raw/booboo_exercise_1.mp3 differ diff --git a/app/src/main/res/raw/booboo_exercise_2.mp3 b/app/src/main/res/raw/booboo_exercise_2.mp3 new file mode 100644 index 0000000..f306816 Binary files /dev/null and b/app/src/main/res/raw/booboo_exercise_2.mp3 differ diff --git a/app/src/main/res/raw/booboo_exercise_3.mp3 b/app/src/main/res/raw/booboo_exercise_3.mp3 new file mode 100644 index 0000000..d26c553 Binary files /dev/null and b/app/src/main/res/raw/booboo_exercise_3.mp3 differ diff --git a/app/src/main/res/raw/booboo_focus_1.mp3 b/app/src/main/res/raw/booboo_focus_1.mp3 new file mode 100644 index 0000000..037c6be Binary files /dev/null and b/app/src/main/res/raw/booboo_focus_1.mp3 differ diff --git a/app/src/main/res/raw/booboo_focus_2.mp3 b/app/src/main/res/raw/booboo_focus_2.mp3 new file mode 100644 index 0000000..80406bf Binary files /dev/null and b/app/src/main/res/raw/booboo_focus_2.mp3 differ diff --git a/app/src/main/res/raw/booboo_focus_3.mp3 b/app/src/main/res/raw/booboo_focus_3.mp3 new file mode 100644 index 0000000..1c4665e Binary files /dev/null and b/app/src/main/res/raw/booboo_focus_3.mp3 differ diff --git a/app/src/main/res/raw/booboo_study_1.mp3 b/app/src/main/res/raw/booboo_study_1.mp3 new file mode 100644 index 0000000..aec7971 Binary files /dev/null and b/app/src/main/res/raw/booboo_study_1.mp3 differ diff --git a/app/src/main/res/raw/booboo_study_2.mp3 b/app/src/main/res/raw/booboo_study_2.mp3 new file mode 100644 index 0000000..91f3e2e Binary files /dev/null and b/app/src/main/res/raw/booboo_study_2.mp3 differ diff --git a/app/src/main/res/raw/booboo_study_3.mp3 b/app/src/main/res/raw/booboo_study_3.mp3 new file mode 100644 index 0000000..54246fa Binary files /dev/null and b/app/src/main/res/raw/booboo_study_3.mp3 differ diff --git a/app/src/main/res/raw/chacha_exercise_1.mp3 b/app/src/main/res/raw/chacha_exercise_1.mp3 new file mode 100644 index 0000000..7525104 Binary files /dev/null and b/app/src/main/res/raw/chacha_exercise_1.mp3 differ diff --git a/app/src/main/res/raw/chacha_exercise_2.mp3 b/app/src/main/res/raw/chacha_exercise_2.mp3 new file mode 100644 index 0000000..a2ac1a6 Binary files /dev/null and b/app/src/main/res/raw/chacha_exercise_2.mp3 differ diff --git a/app/src/main/res/raw/chacha_exercise_3.mp3 b/app/src/main/res/raw/chacha_exercise_3.mp3 new file mode 100644 index 0000000..6b8ec5b Binary files /dev/null and b/app/src/main/res/raw/chacha_exercise_3.mp3 differ diff --git a/app/src/main/res/raw/chacha_focus_1.mp3 b/app/src/main/res/raw/chacha_focus_1.mp3 new file mode 100644 index 0000000..b364ffd Binary files /dev/null and b/app/src/main/res/raw/chacha_focus_1.mp3 differ diff --git a/app/src/main/res/raw/chacha_focus_2.mp3 b/app/src/main/res/raw/chacha_focus_2.mp3 new file mode 100644 index 0000000..b21dce5 Binary files /dev/null and b/app/src/main/res/raw/chacha_focus_2.mp3 differ diff --git a/app/src/main/res/raw/chacha_focus_3.mp3 b/app/src/main/res/raw/chacha_focus_3.mp3 new file mode 100644 index 0000000..e346f71 Binary files /dev/null and b/app/src/main/res/raw/chacha_focus_3.mp3 differ diff --git a/app/src/main/res/raw/chacha_study_1.mp3 b/app/src/main/res/raw/chacha_study_1.mp3 new file mode 100644 index 0000000..46734b8 Binary files /dev/null and b/app/src/main/res/raw/chacha_study_1.mp3 differ diff --git a/app/src/main/res/raw/chacha_study_2.mp3 b/app/src/main/res/raw/chacha_study_2.mp3 new file mode 100644 index 0000000..77f4009 Binary files /dev/null and b/app/src/main/res/raw/chacha_study_2.mp3 differ diff --git a/app/src/main/res/raw/chacha_study_3.mp3 b/app/src/main/res/raw/chacha_study_3.mp3 new file mode 100644 index 0000000..0d4d483 Binary files /dev/null and b/app/src/main/res/raw/chacha_study_3.mp3 differ diff --git a/app/src/main/res/raw/kiki_exercise_1.mp3 b/app/src/main/res/raw/kiki_exercise_1.mp3 new file mode 100644 index 0000000..ff61c29 Binary files /dev/null and b/app/src/main/res/raw/kiki_exercise_1.mp3 differ diff --git a/app/src/main/res/raw/kiki_exercise_2.mp3 b/app/src/main/res/raw/kiki_exercise_2.mp3 new file mode 100644 index 0000000..7f0b6b6 Binary files /dev/null and b/app/src/main/res/raw/kiki_exercise_2.mp3 differ diff --git a/app/src/main/res/raw/kiki_exercise_3.mp3 b/app/src/main/res/raw/kiki_exercise_3.mp3 new file mode 100644 index 0000000..f5e9250 Binary files /dev/null and b/app/src/main/res/raw/kiki_exercise_3.mp3 differ diff --git a/app/src/main/res/raw/kiki_focus_1.mp3 b/app/src/main/res/raw/kiki_focus_1.mp3 new file mode 100644 index 0000000..6d94e2a Binary files /dev/null and b/app/src/main/res/raw/kiki_focus_1.mp3 differ diff --git a/app/src/main/res/raw/kiki_focus_2.mp3 b/app/src/main/res/raw/kiki_focus_2.mp3 new file mode 100644 index 0000000..b911285 Binary files /dev/null and b/app/src/main/res/raw/kiki_focus_2.mp3 differ diff --git a/app/src/main/res/raw/kiki_focus_3.mp3 b/app/src/main/res/raw/kiki_focus_3.mp3 new file mode 100644 index 0000000..3666f19 Binary files /dev/null and b/app/src/main/res/raw/kiki_focus_3.mp3 differ diff --git a/app/src/main/res/raw/kiki_study_1.mp3 b/app/src/main/res/raw/kiki_study_1.mp3 new file mode 100644 index 0000000..a15daf8 Binary files /dev/null and b/app/src/main/res/raw/kiki_study_1.mp3 differ diff --git a/app/src/main/res/raw/kiki_study_2.mp3 b/app/src/main/res/raw/kiki_study_2.mp3 new file mode 100644 index 0000000..7843857 Binary files /dev/null and b/app/src/main/res/raw/kiki_study_2.mp3 differ diff --git a/app/src/main/res/raw/kiki_study_3.mp3 b/app/src/main/res/raw/kiki_study_3.mp3 new file mode 100644 index 0000000..5db2ddb Binary files /dev/null and b/app/src/main/res/raw/kiki_study_3.mp3 differ diff --git a/app/src/main/res/raw/nana_exercise_1.mp3 b/app/src/main/res/raw/nana_exercise_1.mp3 new file mode 100644 index 0000000..bac3855 Binary files /dev/null and b/app/src/main/res/raw/nana_exercise_1.mp3 differ diff --git a/app/src/main/res/raw/nana_exercise_2.mp3 b/app/src/main/res/raw/nana_exercise_2.mp3 new file mode 100644 index 0000000..1e91625 Binary files /dev/null and b/app/src/main/res/raw/nana_exercise_2.mp3 differ diff --git a/app/src/main/res/raw/nana_exercise_3.mp3 b/app/src/main/res/raw/nana_exercise_3.mp3 new file mode 100644 index 0000000..ae2ae69 Binary files /dev/null and b/app/src/main/res/raw/nana_exercise_3.mp3 differ diff --git a/app/src/main/res/raw/nana_focus_1.mp3 b/app/src/main/res/raw/nana_focus_1.mp3 new file mode 100644 index 0000000..cb987ec Binary files /dev/null and b/app/src/main/res/raw/nana_focus_1.mp3 differ diff --git a/app/src/main/res/raw/nana_focus_2.mp3 b/app/src/main/res/raw/nana_focus_2.mp3 new file mode 100644 index 0000000..9bd3dff Binary files /dev/null and b/app/src/main/res/raw/nana_focus_2.mp3 differ diff --git a/app/src/main/res/raw/nana_focus_3.mp3 b/app/src/main/res/raw/nana_focus_3.mp3 new file mode 100644 index 0000000..8797598 Binary files /dev/null and b/app/src/main/res/raw/nana_focus_3.mp3 differ diff --git a/app/src/main/res/raw/nana_study_1.mp3 b/app/src/main/res/raw/nana_study_1.mp3 new file mode 100644 index 0000000..4feba01 Binary files /dev/null and b/app/src/main/res/raw/nana_study_1.mp3 differ diff --git a/app/src/main/res/raw/nana_study_2.mp3 b/app/src/main/res/raw/nana_study_2.mp3 new file mode 100644 index 0000000..98cf647 Binary files /dev/null and b/app/src/main/res/raw/nana_study_2.mp3 differ diff --git a/app/src/main/res/raw/nana_study_3.mp3 b/app/src/main/res/raw/nana_study_3.mp3 new file mode 100644 index 0000000..775661b Binary files /dev/null and b/app/src/main/res/raw/nana_study_3.mp3 differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 01f6620..b2e7a3c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,8 +16,23 @@ 그래서 휴대폰 속으로 들어와\n사람들을 깨우기 시작했어요!\n깨비즈들의 능력을 확인해볼까요? %s랑 타이머 설정하기 - 타이머 시작하기 + %s랑 휴식 시작하기 + %s랑 집중 시작하기 + 휴식 + 집중 + 일반모드 + 공부모드 + 운동모드 + Level.%d + 어떤 맛으로 알려줄까요? + 깨비즈의 쓴소리 강도를 선택해 보세요. + 순한맛 🍼 + 중간맛 🔥 + 매운맛 🌋 + 타이머 끝내기 + 일시정지 + 다시 시작 한 번 더 누르면, 앱을 종료해요! %s가 %d시간 %d분 뒤에 깨워줄게! @@ -67,9 +82,9 @@ 아ㅋㅋ 레알 갓생 살 준비 가보자고. 개큰 휴식~ 달다 달아~ - 순한 맛으로 살짝 깨워줄게!🥛 - 중간 맛🔥 딱 정신 차리기 좋을거야! - 화끈한 매운맛! 안 일어나곤 못 버틸걸?🌋 + 순한 맛으로 살짝 알려줄게! 🥛 + 딱 좋은 중간 맛으로 알려줄게! 🔥 + 화끈한 매운맛으로 알려줄게! 🌋 Slider Animation @@ -81,10 +96,15 @@ 00 타이머 끄기 - 5분 뒤 다시 알림 + 집중 다시 시작하기 + 10분 쉬고 다시 하기 + 5분 미루기 깨비즈 Chill guy는 일을 미루지 않Chill.. 깨비즈 타이머 실행 중 입니다.. + "잠시 멈췄어요. • %1$s + 타이머 일시정지 + 타이머 다시시작 @@ -99,6 +119,15 @@ 너만의 작은 세상에서 탈출 좀 해, 현실을 살아 !! 현실로 컴백 플리즈으~.현실로 컴백 플리즈으~.현실로 컴백 플리즈으~. 조금만 쉰다더니, 그러다 인생도 쉬는 거 아냐? 나 무섭다 진짜아아앙ㅏㅇ 너의 인생 목표가 침대랑 평생 살기면 인정할게. 대단해~! 휴식이랑 결혼해 축의금은 500원 + 삐빅 ! 와~ 나 지금 눈물 난다. 이렇게 집중 하는 사람 드물다~! 멋쟁이 멋쟁이 멋쟁이 + 띵~띵~띵~띵~! 와, 이렇게 성실하면 친구들이 다 부러워하겠다~! + 삐빅! 집중 타임 끝! 삐빅! 집중 타임 끝! 삐빅! 집중 타임 끝! 삐빅! 집중 타임 끝! + 딩동댕~ 공부 타임 종료! 이제 복습 타임 가야지, 안 그러면 싹 다 증발한다? + 공부 루틴 완료! 너 오늘은 진짜 자기계발 유튜버 본체다! + 띵동~ 공부 끝났다!… 아, 아니지. 복습해야지. 복습 끝났다!… 아, 아니지. 또 암기해야지. 아, 아니지. 복습해야지. 복습 끝났다!… 아, 아니지. 또 암기해야지 + 똑딱! 운동 끝! 와, 키키가 인정한다. 너 오늘 근손실이 울고 갔어어어!!!! + 삐이익~! 운동 타임 끝! 너 지금 땀 빼느라 비트코인보다 핫하다~! + 오늘 !! 운동 !! 완료오옭 !! 오늘 !! 운동 !! 완료오옭 !! 오늘 !! 운동 !! 완료오옭 !! 오늘 !! 운동 !! 완료오옭 !! 이제 일어나서 다시 시작하면 더 멋질 거 같은데? 내가 그쪽 성실한 모습에 반한거 알죠? 다 쉬었죠? 당신이 열심히 하는 모습은 또 얼마나 반짝반짝할까? 너무 설렌다. @@ -111,6 +140,15 @@ 아직까지 쉬고 있는거 일부러 나 안달나게 하는거에요? 그렇다면 성공이야. 이렇게 자꾸 미루고 약속을 안 지키면 어떡해요? 응? 나 아직 당신 포기하고 싶지 않아. 하, 내가 이렇게까지 하는데도 안 일어나는 거면 저도 이제 포기할게요.. 안녕..그대.. + 하… 당신 또 해냈네. 집중하기 또 성.공. + 그거 알아요? 방금 당신 또 열심히 집중했어요. 그거 아무나 못하는건데? + 음 ~ 칭찬 타임 ~ 방금 집중하는 모습 너무 빛났어요 ~ 심쿵했잖아? + 우와 ~ 공부까지 잘해버리면 너무 완벽한거 아닌가요~? 당신에게 또 반했다. + 자, 공부 시간 끝났어요 ~ 이리 와요. 내가 칭찬해줄게. + 공부도 너무 잘하네요 ~? 미모와 지성을 다 갖춘 당신은 욕.심.쟁.이. + 운동하는 그대의 땀방울…. 너무 빛나서 보석인줄 알았잖아. 고생했어요~ + 음~ 운동할 때 집중하던 그 눈빛, 너무 인상적이야. 쉬었다가 한세트 더 할까요? + 와~ 운동하는 거 쉽지 않은데~ 끝까지 포기 안 한 당신. 너무 매력적인걸? 오빠야, 꿈나라에서 뭐 하고 있노~? 현실로 도라온나~ 타이머도 맞추고 대단하데? 이제 일어나서 할거 함 해보까?, 생각보다 열심히 사노? @@ -123,6 +161,15 @@ 니 타이머는 왜 맞췄노? 소용도 없으면 끄지,,! 걍 혼자 앵앵 울리게 냅둘끼가? 이불이 니 잡아뭇나?이래 누워만 있으면 어칼라고 그러고 있노. 정신 좀 차리라! 답도 없다, 내 간다, 더 누버있던지는 니 알아 하고, 걍 때려 치아라 + 우와아~ 또 집중하는 중이야? 괜히 방해하고 싶게 만드는 거 알아? 근데 열심히 하는 모습 보면 또 심장이 두근거린단 말이지. + 어머, 진짜 제대로 몰입했네? 나 괜히 칭찬 안 해주고 싶었는데… 어쩔 수 없지, 잘했으니까 오늘은 특별히 인정해줄게. + 또 열심히 하는 거야? 칭찬 스티커 몇 장 붙여줘야 하나? 진짜 귀엽게 왜 이렇게 잘해? + 우와아~ 공부까지 잘한다고? 완전 반칙이지 그고온! 이런 모습 보여주면 나 괜히 질투 난다고. + 나 말고 누구랑 그렇게 사이 좋은거야? 공부랑 그렇게 친한거야? 그래도 오늘은 똑똑이 모드니까 참아줄게. + 조용히 앉아있는 건데 왜 이렇게 귀여워 보이지? 공부까지 귀엽기 있냐고, 반칙이라니까. + 운동까지 이렇게 열심히 한다고? 우와아… 땀 흘리는 거 반짝거리는 거 봐.괜히 설렌다니까. + 힘든데도 끝까지 한다는 거, 진짜 멋있네. 나 또 반해버렸잖아. 칭찬은 아깝지 않다~. + 운동하면서 포기 안 하는 모습, 너무 매력적이라서 짜증날 정도야. 아휴, 또 인정해버렸네. 슬슬 일어날 때가 된거 같은데~~ 할미 타이머도 이렇게 시간 맞춰 일하고 있단다.^^ 얼른 일어나렴 벌써 약속한 시간이 됐단다~ 한 번만 마음먹으면 돼! 후딱 일어나고 나중에 후련하게 쉬렴 @@ -135,6 +182,15 @@ 꼭 그렇게,,,,,,, 이 할미를 또 시간맞춰 깨우게 만들어야만,,,,,, 속이 후련했냐아아악????? (근데 이 톤 못살림 ^_ㅜ) 지금 나 타이머라고 이 할미 말 무시하고 누워 있는 거야? 너 이렇게 누워만 있으면 나중에 할미처럼 허리 휜다? 이 정도면 할머니 타이머가 아니라 타이머 고조 할머니가 와도 안일어나겠다 + 집중하는거 보니까 아주 크게 될 인물이여~ 조금 쉬었다가 할미랑 다시 집중해볼까? + 아이고, 이만큼 집중했으면 잘했다~ 남은 건 내일 할미랑 또 하면 되지! + 벌써 목표한 시간이 다 됐단다~ 내일 할미가 또 같이 집중할테니까 걱정마~ + 아이고 우리 집안에서 박사 나겄다~ 할미 플랜카드 준비한다~? + 너 공부하는 것만봐도 할미 배불러서 밥안먹어도 되겠다~ 기특해~~ + 공부를 이렇게 열심히 하는데, 성적이 오를 수밖에 없지~ 할미가 장담한다! + 숨차다고? 그게 살아있다는 증거여~ 진짜 고생했다~ + 잘했어! 운동의 완성은 식단까지인거 알지? 얼른 할미랑 밥먹자~ + 아까 자세 진짜 좋았다~ 내일 결실의 근육통이 찾아올거여~ 님, 일어나세염 솔직히 지금 하는게 이득이다. 인정? 엥 벌써 시간 다 됨. 그래도 잘 쉬었잖아 한 잔해~ @@ -147,7 +203,15 @@ 님 내일 감당 가능? 오늘 할 일 있다고 하지 않았나? 지금 안 하면 영원히 안할거지? 님 빼고 다 아는 사실임~ 아직도 안 일어난거 실환가여? 또 미루면 후회할 거 뻔하쥬? 이럴거면 알람은 왜 맞추는지 의문이쥬? 전 님 깨우는거 포기 갈깁니다 수고하세여~ 내일의 님이 업보 청산할 거 생각하면 눈물이 핑 도네 - + 여기서 끝날거 아니지? 혹시 긁?? + 이 사람이 왜이리 무리해. 이러다 궁전으로 갈 수도 있어. + 집중 벌써 끝났다고? 한 번 더 할거임? 난 하면 해 + 사람들 여기 보세요 이렇게 공부하다가 맨사 가입할 사람이 있어요~~ + 님 혹시 1등 하려면 이 정도로 만족하면 안됨. 10분 쉬고 다시 가보자고 + 공부한 사람이 있었슨. 한시간 더 달릴 사람도 있었슨. + 개멋진 나를 위해 당장 파워 냉방으로 틀어 + 운동한 나… 제법 멋져… 10분만 쉬고 다시 달리자 + 이걸로 끝낼 거 아니지? 깔끔하게 한 번 더 하는거 어떤데 설정 배터리 최적화 설정 해제