diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e8217229..f23126a4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -72,6 +72,7 @@ dependencies { implementation(project(":data:sleep:impl")) implementation(project(":data:alarm:impl")) implementation(project(":data:graph:impl")) + implementation(project(":data:notification:impl")) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.analytics) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 28c16c03..141b1628 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -69,4 +69,7 @@ -keepclassmembers class * { @com.squareup.moshi.FromJson ; @com.squareup.moshi.ToJson ; -} \ No newline at end of file +} + +-keepattributes Signature +-keepclassmembers class com.titi.app.feature.setting.model.** {*;} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a78ba903..257b2b88 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ android:label="${appName}" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" + android:usesCleartextTraffic="true" android:theme="@style/Theme.TiTi" tools:targetApi="31"> diff --git a/build-logic/src/main/kotlin/com/titi/common/BuildInfo.kt b/build-logic/src/main/kotlin/com/titi/common/BuildInfo.kt index 5335002e..0812cfb8 100644 --- a/build-logic/src/main/kotlin/com/titi/common/BuildInfo.kt +++ b/build-logic/src/main/kotlin/com/titi/common/BuildInfo.kt @@ -9,7 +9,7 @@ object BuildType { object AppConfig { const val APP_ID = "com.titi.app" - const val APP_VERSION_NAME = "1.0.2" - const val APP_VERSION_CODE = 23 + const val APP_VERSION_NAME = "1.0.3" + const val APP_VERSION_CODE = 25 const val APP_NAME = "TiTi" } \ No newline at end of file diff --git a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsStandardDailyGraph.kt b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsStandardDailyGraph.kt index 6d8e8adc..6c1a75e7 100644 --- a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsStandardDailyGraph.kt +++ b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsStandardDailyGraph.kt @@ -51,12 +51,13 @@ fun TdsStandardDailyGraph( modifier = modifier, contentAlignment = Alignment.Center, ) { - val size = maxWidth.coerceAtMost(345.dp) + val size = maxWidth.coerceAtMost(365.dp) OutlinedCard( modifier = Modifier .createCaptureImageModifier(picture = picture) - .size(size), + .size(size) + .padding(10.dp), shape = RoundedCornerShape(size * 0.07), colors = CardDefaults.cardColors(containerColor = TdsColor.BACKGROUND.getColor()), border = BorderStroke( @@ -203,8 +204,8 @@ fun TdsStandardDailyGraph( Box( modifier = Modifier .offset( - x = -size / 2 + 26.dp, - y = -size * 0.38 + 33.dp, + x = -size / 2 + 36.dp, + y = -size * 0.38 + 43.dp, ), ) { TdsToggleIconButton( diff --git a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsTaskProgressDailyGraph.kt b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsTaskProgressDailyGraph.kt index 0a56261d..5901e66a 100644 --- a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsTaskProgressDailyGraph.kt +++ b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsTaskProgressDailyGraph.kt @@ -42,12 +42,13 @@ fun TdsTaskProgressDailyGraph( modifier = modifier, contentAlignment = Alignment.Center, ) { - val size = maxWidth.coerceAtMost(345.dp) + val size = maxWidth.coerceAtMost(365.dp) OutlinedCard( modifier = Modifier .createCaptureImageModifier(picture = picture) - .size(size), + .size(size) + .padding(10.dp), shape = RoundedCornerShape(size * 0.07), colors = CardDefaults.cardColors(containerColor = TdsColor.BACKGROUND.getColor()), border = BorderStroke( @@ -84,8 +85,8 @@ fun TdsTaskProgressDailyGraph( Box( modifier = Modifier .offset( - x = -size / 2 + 26.dp, - y = -size * 0.49 + 26.dp, + x = -size / 2 + 36.dp, + y = -size * 0.49 + 36.dp, ), ) { TdsToggleIconButton( diff --git a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsTimeLineDailyGraph.kt b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsTimeLineDailyGraph.kt index e2d62064..c95fddf2 100644 --- a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsTimeLineDailyGraph.kt +++ b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsTimeLineDailyGraph.kt @@ -48,12 +48,13 @@ fun TdsTimeLineDailyGraph( modifier = modifier, contentAlignment = Alignment.Center, ) { - val size = maxWidth.coerceAtMost(345.dp) + val size = maxWidth.coerceAtMost(365.dp) OutlinedCard( modifier = Modifier .createCaptureImageModifier(picture = picture) - .size(size), + .size(size) + .padding(10.dp), shape = RoundedCornerShape(size * 0.07), colors = CardDefaults.cardColors(containerColor = TdsColor.BACKGROUND.getColor()), border = BorderStroke( @@ -147,8 +148,8 @@ fun TdsTimeLineDailyGraph( Box( modifier = Modifier .offset( - x = -size / 2 + 26.dp, - y = -size * 0.49 + 26.dp, + x = -size / 2 + 36.dp, + y = -size * 0.49 + 36.dp, ), ) { TdsToggleIconButton( diff --git a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsTimeTable.kt b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsTimeTable.kt index cf32d15f..2991953d 100644 --- a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsTimeTable.kt +++ b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsTimeTable.kt @@ -35,7 +35,7 @@ fun TdsTimeTable( mutableStateOf("0") } var fontSize by remember { - mutableStateOf(14.sp) + mutableStateOf(7.sp) } val textStyle = TdsTextStyle .SEMI_BOLD_TEXT_STYLE diff --git a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsTimeTableDailyGraph.kt b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsTimeTableDailyGraph.kt index 7a554fc5..471b7dad 100644 --- a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsTimeTableDailyGraph.kt +++ b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsTimeTableDailyGraph.kt @@ -53,12 +53,13 @@ fun TdsTimeTableDailyGraph( modifier = modifier, contentAlignment = Alignment.Center, ) { - val size = maxWidth.coerceAtMost(345.dp) + val size = maxWidth.coerceAtMost(365.dp) OutlinedCard( modifier = Modifier .createCaptureImageModifier(picture = picture) - .size(size), + .size(size) + .padding(10.dp), shape = RoundedCornerShape(size * 0.07), colors = CardDefaults.cardColors(containerColor = TdsColor.BACKGROUND.getColor()), border = BorderStroke( @@ -216,8 +217,8 @@ fun TdsTimeTableDailyGraph( Box( modifier = Modifier .offset( - x = -size / 2 + 26.dp, - y = -size * 0.38 + 33.dp, + x = -size / 2 + 36.dp, + y = -size * 0.38 + 43.dp, ), ) { TdsToggleIconButton( diff --git a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/navigation/TdsBottomNavigationBar.kt b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/navigation/TdsBottomNavigationBar.kt new file mode 100644 index 00000000..dbedcf05 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/navigation/TdsBottomNavigationBar.kt @@ -0,0 +1,78 @@ +package com.titi.app.core.designsystem.navigation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.systemBarsIgnoringVisibility +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.titi.app.core.designsystem.component.TdsText +import com.titi.app.core.designsystem.theme.TdsColor +import com.titi.app.core.designsystem.theme.TdsTextStyle + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun TdsBottomNavigationBar( + currentTopLevelDestination: TopLevelDestination, + bottomNavigationColor: Long, + onNavigateToDestination: (TopLevelDestination) -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .windowInsetsPadding( + WindowInsets.systemBarsIgnoringVisibility.only( + WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal, + ), + ) + .selectableGroup() + .background(Color(bottomNavigationColor)), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + TopLevelDestination.entries.forEach { destination -> + val selected = currentTopLevelDestination == destination + TdsBottomNavigationBarItem( + selected = selected, + onClick = { onNavigateToDestination(destination) }, + icon = { + Icon( + painter = painterResource(id = destination.iconResourceId), + contentDescription = stringResource(id = destination.titleTextId), + tint = if (selected) { + TdsColor.TEXT.getColor() + } else { + TdsColor.LIGHT_GRAY.getColor() + }, + ) + }, + label = { + TdsText( + text = stringResource(id = destination.titleTextId), + textStyle = TdsTextStyle.SEMI_BOLD_TEXT_STYLE, + fontSize = 16.sp, + color = if (selected) { + TdsColor.TEXT.getColor() + } else { + TdsColor.LIGHT_GRAY.getColor() + }, + ) + }, + ) + } + } +} diff --git a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsNavigationBarItem.kt b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/navigation/TdsBottomNavigationBarItem.kt similarity index 93% rename from core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsNavigationBarItem.kt rename to core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/navigation/TdsBottomNavigationBarItem.kt index 95e2adcb..2a5f7a06 100644 --- a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/component/TdsNavigationBarItem.kt +++ b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/navigation/TdsBottomNavigationBarItem.kt @@ -1,4 +1,4 @@ -package com.titi.app.core.designsystem.component +package com.titi.app.core.designsystem.navigation import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column @@ -16,7 +16,7 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp @Composable -fun RowScope.TdsNavigationBarItem( +fun RowScope.TdsBottomNavigationBarItem( selected: Boolean, onClick: () -> Unit, icon: @Composable () -> Unit, diff --git a/feature/main/src/main/kotlin/com/titi/app/feature/main/navigation/TopLevelDestination.kt b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/navigation/TopLevelDestination.kt similarity index 77% rename from feature/main/src/main/kotlin/com/titi/app/feature/main/navigation/TopLevelDestination.kt rename to core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/navigation/TopLevelDestination.kt index 9ebcf6fe..57618a8d 100644 --- a/feature/main/src/main/kotlin/com/titi/app/feature/main/navigation/TopLevelDestination.kt +++ b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/navigation/TopLevelDestination.kt @@ -1,4 +1,4 @@ -package com.titi.app.feature.main.navigation +package com.titi.app.core.designsystem.navigation import androidx.annotation.DrawableRes import androidx.annotation.StringRes @@ -20,4 +20,8 @@ enum class TopLevelDestination( titleTextId = R.string.bottom_log_text, iconResourceId = R.drawable.log_icon, ), + SETTING( + titleTextId = R.string.bottom_setting_text, + iconResourceId = R.drawable.setting_icon, + ), } diff --git a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/theme/Color.kt b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/theme/Color.kt index ae959416..233ccfa1 100644 --- a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/theme/Color.kt @@ -21,6 +21,7 @@ data class TdsColorsPalette( val textColor: Color = Color.Unspecified, val backgroundColor: Color = Color.Unspecified, val secondaryBackgroundColor: Color = Color.Unspecified, + val groupedBackgroundColor: Color = Color.Unspecified, val switchBackgroundColor: Color = Color.Unspecified, val alertBackgroundColor: Color = Color.Unspecified, val tertiaryBackgroundColor: Color = Color.Unspecified, @@ -52,7 +53,8 @@ val TdsLightColorsPalette = TdsColorsPalette( d12 = Color(0xFF928AEA), textColor = Color(0xFF000000), backgroundColor = Color(0xFFFFFFFF), - secondaryBackgroundColor = Color(0xFFF2F2F7), + secondaryBackgroundColor = Color(0xFFFFFFFF), + groupedBackgroundColor = Color(0xFFF2F2F7), switchBackgroundColor = Color(0xFFE9E9EB), alertBackgroundColor = Color(0xD1EEEEEE), tertiaryBackgroundColor = Color(0xFFFFFFFF), @@ -85,6 +87,7 @@ val TdsDarkColorsPalette = TdsColorsPalette( textColor = Color(0xFFFFFFFF), backgroundColor = Color(0xFF000000), secondaryBackgroundColor = Color(0xFF1C1C1E), + groupedBackgroundColor = Color(0xFF000000), switchBackgroundColor = Color(0xFF39393D), alertBackgroundColor = Color(0xD12B2B2B), tertiaryBackgroundColor = Color(0xFF2C2C2E), @@ -118,6 +121,7 @@ enum class TdsColor { TEXT, BACKGROUND, SECONDARY_BACKGROUND, + GROUPED_BACKGROUND, SWITCH_BACKGROUND, ALERT_BACKGROUND, TERTIARY_BACKGROUND, @@ -154,6 +158,7 @@ enum class TdsColor { TEXT -> TiTiTheme.colors.textColor BACKGROUND -> TiTiTheme.colors.backgroundColor SECONDARY_BACKGROUND -> TiTiTheme.colors.secondaryBackgroundColor + GROUPED_BACKGROUND -> TiTiTheme.colors.groupedBackgroundColor SWITCH_BACKGROUND -> TiTiTheme.colors.switchBackgroundColor ALERT_BACKGROUND -> TiTiTheme.colors.alertBackgroundColor TERTIARY_BACKGROUND -> TiTiTheme.colors.tertiaryBackgroundColor diff --git a/core/designsystem/src/main/res/drawable/add_record_icon.xml b/core/designsystem/src/main/res/drawable/add_record_icon.xml deleted file mode 100644 index caaf5309..00000000 --- a/core/designsystem/src/main/res/drawable/add_record_icon.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - diff --git a/core/designsystem/src/main/res/drawable/edit_record_icon.xml b/core/designsystem/src/main/res/drawable/edit_record_icon.xml new file mode 100644 index 00000000..3de4b7c6 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/edit_record_icon.xml @@ -0,0 +1,14 @@ + + + + diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index c62fe299..92efa1ff 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -3,6 +3,7 @@ Timer Stopwatch Log + Setting 누적 시간 타이머 @@ -24,8 +25,8 @@ 배경 텍스트 해당 색상을 배경색으로 설정하시겠습니까? - 새로운 기록 설정 - %1$s 목표시간 설정 + 목표시간 수정 + %1$s의 목표시간을 수정해요! 타이머 시간 설정 종료예정 : %1$s Task와 Daily를 확인해주세요. diff --git a/data/daily/api/src/main/kotlin/com/titi/app/data/daily/api/DailyRepository.kt b/data/daily/api/src/main/kotlin/com/titi/app/data/daily/api/DailyRepository.kt index e5bd3f46..1d783623 100644 --- a/data/daily/api/src/main/kotlin/com/titi/app/data/daily/api/DailyRepository.kt +++ b/data/daily/api/src/main/kotlin/com/titi/app/data/daily/api/DailyRepository.kt @@ -24,20 +24,7 @@ interface DailyRepository { suspend fun getDailies(startDateTime: String, endDateTime: String): List? - fun getDateDailyFlow( - startDateTime: String = LocalDate - .now() - .minusDays(1) - .atStartOfDay(ZoneOffset.systemDefault()) - .withZoneSameInstant(ZoneOffset.UTC) - .toString(), - endDateTime: String = LocalDate - .now() - .atTime(23, 59, 59) - .atZone(ZoneId.systemDefault()) - .withZoneSameInstant(ZoneOffset.UTC) - .toString(), - ): Flow + fun getLastDailyFlow(): Flow suspend fun getAllDailies(): List? diff --git a/data/daily/impl/src/main/kotlin/com/titi/app/data/daily/impl/local/dao/DailyDao.kt b/data/daily/impl/src/main/kotlin/com/titi/app/data/daily/impl/local/dao/DailyDao.kt index 6f460ea0..599fdfe6 100644 --- a/data/daily/impl/src/main/kotlin/com/titi/app/data/daily/impl/local/dao/DailyDao.kt +++ b/data/daily/impl/src/main/kotlin/com/titi/app/data/daily/impl/local/dao/DailyDao.kt @@ -23,12 +23,8 @@ internal interface DailyDao { ) suspend fun getWeekDaily(startDateTime: String, endDateTime: String): List? - @Query( - "SELECT * FROM dailies " + - " WHERE datetime(day) " + - "BETWEEN datetime(:startDateTime) AND datetime(:endDateTime) ORDER BY id desc LIMIT 1", - ) - fun getDateDailyFlow(startDateTime: String, endDateTime: String): Flow + @Query("SELECT * FROM dailies ORDER BY id desc LIMIT 1") + fun getLastDailyFlow(): Flow @Query("SELECT * FROM dailies") suspend fun getAllDailies(): List? diff --git a/data/daily/impl/src/main/kotlin/com/titi/app/data/daily/impl/repository/DailyRepositoryImpl.kt b/data/daily/impl/src/main/kotlin/com/titi/app/data/daily/impl/repository/DailyRepositoryImpl.kt index 81ca83dc..0ec8d3d1 100644 --- a/data/daily/impl/src/main/kotlin/com/titi/app/data/daily/impl/repository/DailyRepositoryImpl.kt +++ b/data/daily/impl/src/main/kotlin/com/titi/app/data/daily/impl/repository/DailyRepositoryImpl.kt @@ -32,14 +32,8 @@ internal class DailyRepositoryImpl @Inject constructor( )?.map { it.toRepositoryModel() } } - override fun getDateDailyFlow( - startDateTime: String, - endDateTime: String, - ): Flow { - return dailyDao.getDateDailyFlow( - startDateTime = startDateTime, - endDateTime = endDateTime, - ).map { it?.toRepositoryModel() } + override fun getLastDailyFlow(): Flow { + return dailyDao.getLastDailyFlow().map { it?.toRepositoryModel() } } override suspend fun getAllDailies(): List? { diff --git a/data/notification/api/build.gradle.kts b/data/notification/api/build.gradle.kts new file mode 100644 index 00000000..c1dd973b --- /dev/null +++ b/data/notification/api/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("titi.android.library-no-hilt") +} + +android { + namespace = "com.titi.app.data.notification.api" +} diff --git a/data/notification/api/src/main/AndroidManifest.xml b/data/notification/api/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/data/notification/api/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/data/notification/api/src/main/kotlin/com/titi/app/data/notification/api/NotificationRepository.kt b/data/notification/api/src/main/kotlin/com/titi/app/data/notification/api/NotificationRepository.kt new file mode 100644 index 00000000..e41edeb5 --- /dev/null +++ b/data/notification/api/src/main/kotlin/com/titi/app/data/notification/api/NotificationRepository.kt @@ -0,0 +1,12 @@ +package com.titi.app.data.notification.api + +import com.titi.app.data.notification.api.model.NotificationRepositoryModel +import kotlinx.coroutines.flow.Flow + +interface NotificationRepository { + suspend fun setNotification(notificationRepositoryModel: NotificationRepositoryModel) + + fun getNotificationFlow(): Flow + + suspend fun getNotification(): NotificationRepositoryModel +} diff --git a/data/notification/api/src/main/kotlin/com/titi/app/data/notification/api/model/NotificationRepositoryModel.kt b/data/notification/api/src/main/kotlin/com/titi/app/data/notification/api/model/NotificationRepositoryModel.kt new file mode 100644 index 00000000..40eca73f --- /dev/null +++ b/data/notification/api/src/main/kotlin/com/titi/app/data/notification/api/model/NotificationRepositoryModel.kt @@ -0,0 +1,7 @@ +package com.titi.app.data.notification.api.model + +data class NotificationRepositoryModel( + val timerFiveMinutesBeforeTheEnd: Boolean = true, + val timerBeforeTheEnd: Boolean = true, + val stopwatch: Boolean = true, +) diff --git a/data/notification/impl/build.gradle.kts b/data/notification/impl/build.gradle.kts new file mode 100644 index 00000000..c249701a --- /dev/null +++ b/data/notification/impl/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("titi.android.data.local") +} + +android { + namespace = "com.titi.app.data.notification.impl" +} + +dependencies { + implementation(project(":data:notification:api")) +} diff --git a/data/notification/impl/src/main/AndroidManifest.xml b/data/notification/impl/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/data/notification/impl/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/di/DataStoreModule.kt b/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/di/DataStoreModule.kt new file mode 100644 index 00000000..cdd567a0 --- /dev/null +++ b/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/di/DataStoreModule.kt @@ -0,0 +1,19 @@ +package com.titi.app.data.notification.impl.di + +import android.content.Context +import com.titi.app.data.notification.impl.local.NotificationDataStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object DataStoreModule { + @Provides + @Singleton + fun provideNotificationDataStore(@ApplicationContext context: Context) = + NotificationDataStore(context) +} diff --git a/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/di/RepositoryModule.kt b/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/di/RepositoryModule.kt new file mode 100644 index 00000000..2ec2b335 --- /dev/null +++ b/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/di/RepositoryModule.kt @@ -0,0 +1,19 @@ +package com.titi.app.data.notification.impl.di + +import com.titi.app.data.notification.api.NotificationRepository +import com.titi.app.data.notification.impl.repository.NotificationRepositoryImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface RepositoryModule { + @Binds + @Singleton + fun provideNotificationRepository( + notificationRepositoryImpl: NotificationRepositoryImpl, + ): NotificationRepository +} diff --git a/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/local/NotificationDataStore.kt b/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/local/NotificationDataStore.kt new file mode 100644 index 00000000..c620857f --- /dev/null +++ b/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/local/NotificationDataStore.kt @@ -0,0 +1,37 @@ +package com.titi.app.data.notification.impl.local + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.titi.app.core.util.fromJson +import com.titi.app.core.util.readFlowValue +import com.titi.app.core.util.readValue +import com.titi.app.core.util.storeValue +import com.titi.app.core.util.toJson +import com.titi.app.data.notification.impl.local.model.NotificationEntity +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal class NotificationDataStore(context: Context) { + private val dataStore: DataStore = context.dataStore + + suspend fun setNotification(notificationEntity: NotificationEntity) { + dataStore.storeValue(NOTIFICATION_KEY, notificationEntity.toJson()) + } + + fun getNotificationFlow(): Flow = + dataStore.readFlowValue(NOTIFICATION_KEY).map { it?.fromJson() } + + suspend fun getNotification(): NotificationEntity? = + dataStore.readValue(NOTIFICATION_KEY)?.fromJson() + + companion object { + private const val NOTIFICATION_PREF_NAME = "notificationPrefName" + private val NOTIFICATION_KEY = stringPreferencesKey("notificationKey") + + private val Context.dataStore: DataStore + by preferencesDataStore(NOTIFICATION_PREF_NAME) + } +} diff --git a/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/local/model/NotificationEntity.kt b/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/local/model/NotificationEntity.kt new file mode 100644 index 00000000..4a34a759 --- /dev/null +++ b/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/local/model/NotificationEntity.kt @@ -0,0 +1,10 @@ +package com.titi.app.data.notification.impl.local.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class NotificationEntity( + val timerFiveMinutesBeforeTheEnd: Boolean, + val timerBeforeTheEnd: Boolean, + val stopwatch: Boolean, +) diff --git a/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/mapper/LocalToRepositoryMapper.kt b/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/mapper/LocalToRepositoryMapper.kt new file mode 100644 index 00000000..54e4680e --- /dev/null +++ b/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/mapper/LocalToRepositoryMapper.kt @@ -0,0 +1,10 @@ +package com.titi.app.data.notification.impl.mapper + +import com.titi.app.data.notification.api.model.NotificationRepositoryModel +import com.titi.app.data.notification.impl.local.model.NotificationEntity + +internal fun NotificationEntity.toRepositoryModel() = NotificationRepositoryModel( + timerFiveMinutesBeforeTheEnd = timerFiveMinutesBeforeTheEnd, + timerBeforeTheEnd = timerBeforeTheEnd, + stopwatch = stopwatch, +) diff --git a/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/mapper/RepositoryToLocalMapper.kt b/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/mapper/RepositoryToLocalMapper.kt new file mode 100644 index 00000000..7c4902e2 --- /dev/null +++ b/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/mapper/RepositoryToLocalMapper.kt @@ -0,0 +1,10 @@ +package com.titi.app.data.notification.impl.mapper + +import com.titi.app.data.notification.api.model.NotificationRepositoryModel +import com.titi.app.data.notification.impl.local.model.NotificationEntity + +internal fun NotificationRepositoryModel.toLocalModel() = NotificationEntity( + timerFiveMinutesBeforeTheEnd = timerFiveMinutesBeforeTheEnd, + timerBeforeTheEnd = timerBeforeTheEnd, + stopwatch = stopwatch, +) diff --git a/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/repository/NotificationRepositoryImpl.kt b/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/repository/NotificationRepositoryImpl.kt new file mode 100644 index 00000000..bc1852d7 --- /dev/null +++ b/data/notification/impl/src/main/kotlin/com/titi/app/data/notification/impl/repository/NotificationRepositoryImpl.kt @@ -0,0 +1,27 @@ +package com.titi.app.data.notification.impl.repository + +import com.titi.app.data.notification.api.NotificationRepository +import com.titi.app.data.notification.api.model.NotificationRepositoryModel +import com.titi.app.data.notification.impl.local.NotificationDataStore +import com.titi.app.data.notification.impl.mapper.toLocalModel +import com.titi.app.data.notification.impl.mapper.toRepositoryModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal class NotificationRepositoryImpl @Inject constructor( + private val notificationDataStore: NotificationDataStore, +) : NotificationRepository { + override suspend fun setNotification(notificationRepositoryModel: NotificationRepositoryModel) { + notificationDataStore.setNotification(notificationRepositoryModel.toLocalModel()) + } + + override fun getNotificationFlow(): Flow = + notificationDataStore.getNotificationFlow() + .map { it?.toRepositoryModel() ?: NotificationRepositoryModel() } + + override suspend fun getNotification(): NotificationRepositoryModel { + return notificationDataStore.getNotification()?.toRepositoryModel() + ?: NotificationRepositoryModel() + } +} diff --git a/domain/alarm/build.gradle.kts b/domain/alarm/build.gradle.kts index 2cb7b4b4..02e1e7e3 100644 --- a/domain/alarm/build.gradle.kts +++ b/domain/alarm/build.gradle.kts @@ -8,6 +8,7 @@ android { dependencies { implementation(project(":data:alarm:api")) + implementation(project(":data:notification:api")) implementation(libs.threetenabp) } diff --git a/domain/alarm/src/main/kotlin/com/titi/app/domain/alarm/usecase/SetStopWatchAlarmUseCase.kt b/domain/alarm/src/main/kotlin/com/titi/app/domain/alarm/usecase/SetStopWatchAlarmUseCase.kt index 2ae28ec3..b00e41d7 100644 --- a/domain/alarm/src/main/kotlin/com/titi/app/domain/alarm/usecase/SetStopWatchAlarmUseCase.kt +++ b/domain/alarm/src/main/kotlin/com/titi/app/domain/alarm/usecase/SetStopWatchAlarmUseCase.kt @@ -1,6 +1,7 @@ package com.titi.app.domain.alarm.usecase import com.titi.app.data.alarm.api.AlarmRepository +import com.titi.app.data.notification.api.NotificationRepository import com.titi.app.domain.alarm.mapper.toRepositoryModel import com.titi.app.domain.alarm.model.Alarm import com.titi.app.domain.alarm.model.Alarms @@ -9,29 +10,33 @@ import org.threeten.bp.ZonedDateTime class SetStopWatchAlarmUseCase @Inject constructor( private val alarmRepository: AlarmRepository, + private val notificationRepository: NotificationRepository, ) { suspend operator fun invoke(title: String, finishMessage: String, measureTime: Long) { alarmRepository.cancelAlarms() val now = ZonedDateTime.now() val finishTimeRange = (measureTime / ONE_HOUR_SECONDS) + 1..TWENTY_FOUR_HOURS - val alarms = - Alarms( - alarms = finishTimeRange.map { - Alarm( - title = title, - message = "$it$finishMessage", - finishTime = now.plusSeconds( - it * ONE_HOUR_SECONDS - measureTime, - ).toString(), - ) - }, - ) + val alarms = Alarms( + alarms = finishTimeRange.map { + Alarm( + title = title, + message = "$it$finishMessage", + finishTime = now.plusSeconds( + it * ONE_HOUR_SECONDS - measureTime, + ).toString(), + ) + }, + ) - if (alarmRepository.canScheduleExactAlarms()) { - alarmRepository.setExactAlarms(alarms.toRepositoryModel()) - } else { - alarmRepository.addExactAlarms(alarms.toRepositoryModel()) + val notification = notificationRepository.getNotification() + + if (notification.stopwatch) { + if (alarmRepository.canScheduleExactAlarms()) { + alarmRepository.setExactAlarms(alarms.toRepositoryModel()) + } else { + alarmRepository.addExactAlarms(alarms.toRepositoryModel()) + } } } diff --git a/domain/alarm/src/main/kotlin/com/titi/app/domain/alarm/usecase/SetTimerAlarmUseCase.kt b/domain/alarm/src/main/kotlin/com/titi/app/domain/alarm/usecase/SetTimerAlarmUseCase.kt index 2dffac3c..f27497dc 100644 --- a/domain/alarm/src/main/kotlin/com/titi/app/domain/alarm/usecase/SetTimerAlarmUseCase.kt +++ b/domain/alarm/src/main/kotlin/com/titi/app/domain/alarm/usecase/SetTimerAlarmUseCase.kt @@ -1,6 +1,7 @@ package com.titi.app.domain.alarm.usecase import com.titi.app.data.alarm.api.AlarmRepository +import com.titi.app.data.notification.api.NotificationRepository import com.titi.app.domain.alarm.mapper.toRepositoryModel import com.titi.app.domain.alarm.model.Alarm import com.titi.app.domain.alarm.model.Alarms @@ -9,6 +10,7 @@ import org.threeten.bp.ZonedDateTime class SetTimerAlarmUseCase @Inject constructor( private val alarmRepository: AlarmRepository, + private val notificationRepository: NotificationRepository, ) { suspend operator fun invoke( title: String, @@ -26,17 +28,23 @@ class SetTimerAlarmUseCase @Inject constructor( null } + val notification = notificationRepository.getNotification() val alarms = Alarms( alarms = mutableListOf().apply { - add( - Alarm( - title = title, - message = finishMessage, - finishTime = finishTime, - ), - ) + if (notification.timerBeforeTheEnd) { + add( + Alarm( + title = title, + message = finishMessage, + finishTime = finishTime, + ), + ) + } - if (fiveMinutesBeforeFinishTime != null) { + if ( + fiveMinutesBeforeFinishTime != null && + notification.timerFiveMinutesBeforeTheEnd + ) { add( Alarm( title = title, diff --git a/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/AddDailyUseCase.kt b/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/AddDailyUseCase.kt index f1687d3b..835f92d9 100644 --- a/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/AddDailyUseCase.kt +++ b/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/AddDailyUseCase.kt @@ -4,41 +4,11 @@ import com.titi.app.data.daily.api.DailyRepository import com.titi.app.doamin.daily.mapper.toRepositoryModel import com.titi.app.doamin.daily.model.Daily import javax.inject.Inject -import org.threeten.bp.LocalDateTime -import org.threeten.bp.ZoneId -import org.threeten.bp.ZoneOffset -import org.threeten.bp.ZonedDateTime class AddDailyUseCase @Inject constructor( private val dailyRepository: DailyRepository, ) { - suspend operator fun invoke() { - val recentDaily = dailyRepository.getDateDaily() - - val currentDateTime = LocalDateTime.now() - val dailyDayOfMonth = recentDaily?.let { - ZonedDateTime - .parse(it.day) - .withZoneSameInstant(ZoneId.systemDefault()) - .dayOfMonth - } ?: currentDateTime.dayOfMonth - - if ( - recentDaily == null || - (currentDateTime.dayOfMonth != dailyDayOfMonth && currentDateTime.hour >= 6) - ) { - dailyRepository.upsert(Daily().toRepositoryModel()) - } else { - dailyRepository.upsert( - recentDaily.copy( - status = null, - day = ZonedDateTime.now(ZoneOffset.UTC).toString(), - timeline = LongArray(24) { 0 }.toList(), - maxTime = 0, - tasks = null, - taskHistories = null, - ), - ) - } + suspend operator fun invoke(daily: Daily) { + dailyRepository.upsert(daily.toRepositoryModel()) } } diff --git a/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/GetCurrentDailyFlowUseCase.kt b/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/GetLastDailyFlowUseCase.kt similarity index 73% rename from domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/GetCurrentDailyFlowUseCase.kt rename to domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/GetLastDailyFlowUseCase.kt index f0b62f1d..def45b69 100644 --- a/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/GetCurrentDailyFlowUseCase.kt +++ b/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/GetLastDailyFlowUseCase.kt @@ -7,10 +7,10 @@ import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -class GetCurrentDailyFlowUseCase @Inject constructor( +class GetLastDailyFlowUseCase @Inject constructor( private val dailyRepository: DailyRepository, ) { - operator fun invoke(): Flow = dailyRepository.getDateDailyFlow().map { + operator fun invoke(): Flow = dailyRepository.getLastDailyFlow().map { it?.toDomainModel() } } diff --git a/domain/time/src/main/kotlin/com/titi/app/domain/time/model/RecordTimes.kt b/domain/time/src/main/kotlin/com/titi/app/domain/time/model/RecordTimes.kt index 9aacab77..15c9b3d0 100644 --- a/domain/time/src/main/kotlin/com/titi/app/domain/time/model/RecordTimes.kt +++ b/domain/time/src/main/kotlin/com/titi/app/domain/time/model/RecordTimes.kt @@ -13,6 +13,6 @@ data class RecordTimes( val savedSumTime: Long = 0, val savedTimerTime: Long = 3600, val savedStopWatchTime: Long = 0, - val savedGoalTime: Long = 7200, + val savedGoalTime: Long = 21600, val currentTask: CurrentTask? = null, ) : Parcelable diff --git a/domain/time/src/main/kotlin/com/titi/app/domain/time/usecase/UpdateSetGoalTimeUseCase.kt b/domain/time/src/main/kotlin/com/titi/app/domain/time/usecase/UpdateSetGoalTimeUseCase.kt index a0790cd9..2acb4743 100644 --- a/domain/time/src/main/kotlin/com/titi/app/domain/time/usecase/UpdateSetGoalTimeUseCase.kt +++ b/domain/time/src/main/kotlin/com/titi/app/domain/time/usecase/UpdateSetGoalTimeUseCase.kt @@ -14,10 +14,8 @@ class UpdateSetGoalTimeUseCase @Inject constructor( .toRepositoryModel() .copy( setGoalTime = setGoalTime, - savedSumTime = 0, - savedTimerTime = recordTimes.setTimerTime, - savedStopWatchTime = 0, - savedGoalTime = setGoalTime, + savedGoalTime = setGoalTime - + (recordTimes.setGoalTime - recordTimes.savedGoalTime), ), ) } diff --git a/feature/log/src/main/kotlin/com/titi/app/feature/log/navigation/LogNavigation.kt b/feature/log/src/main/kotlin/com/titi/app/feature/log/navigation/LogNavigation.kt index a5abeaaa..e70d2fb0 100644 --- a/feature/log/src/main/kotlin/com/titi/app/feature/log/navigation/LogNavigation.kt +++ b/feature/log/src/main/kotlin/com/titi/app/feature/log/navigation/LogNavigation.kt @@ -4,6 +4,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable +import com.titi.app.core.designsystem.navigation.TopLevelDestination import com.titi.app.feature.log.ui.LogScreen private const val LOG_SCREEN = "log" @@ -13,8 +14,8 @@ fun NavController.navigateToLog(navOptions: NavOptions) { navigate(LOG_ROUTE, navOptions) } -fun NavGraphBuilder.logGraph() { +fun NavGraphBuilder.logGraph(onNavigateToDestination: (TopLevelDestination) -> Unit) { composable(route = LOG_ROUTE) { - LogScreen() + LogScreen(onNavigateToDestination = onNavigateToDestination) } } diff --git a/feature/log/src/main/kotlin/com/titi/app/feature/log/ui/LogScreen.kt b/feature/log/src/main/kotlin/com/titi/app/feature/log/ui/LogScreen.kt index 057402dc..d00df635 100644 --- a/feature/log/src/main/kotlin/com/titi/app/feature/log/ui/LogScreen.kt +++ b/feature/log/src/main/kotlin/com/titi/app/feature/log/ui/LogScreen.kt @@ -2,6 +2,7 @@ package com.titi.app.feature.log.ui import android.content.res.Configuration import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -13,6 +14,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -23,6 +25,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview @@ -32,6 +35,8 @@ import com.airbnb.mvrx.compose.mavericksViewModel import com.titi.app.core.designsystem.R import com.titi.app.core.designsystem.component.TdsIconButton import com.titi.app.core.designsystem.component.TdsTabRow +import com.titi.app.core.designsystem.navigation.TdsBottomNavigationBar +import com.titi.app.core.designsystem.navigation.TopLevelDestination import com.titi.app.core.designsystem.theme.TdsColor import com.titi.app.core.designsystem.theme.TiTiTheme import com.titi.app.feature.log.ui.component.SettingBottomSheet @@ -40,7 +45,10 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable -fun LogScreen(viewModel: LogViewModel = mavericksViewModel()) { +fun LogScreen( + viewModel: LogViewModel = mavericksViewModel(), + onNavigateToDestination: (TopLevelDestination) -> Unit, +) { val scope = rememberCoroutineScope() val orientation = LocalConfiguration.current.orientation @@ -86,116 +94,134 @@ fun LogScreen(viewModel: LogViewModel = mavericksViewModel()) { } } - Column( - modifier = Modifier - .fillMaxSize() - .padding(vertical = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, + val containerColor = if (isSystemInDarkTheme()) { + 0xFF000000 + } else { + 0xFFFFFFFF + } + + Scaffold( + containerColor = Color(containerColor), + bottomBar = { + TdsBottomNavigationBar( + currentTopLevelDestination = TopLevelDestination.LOG, + bottomNavigationColor = containerColor, + onNavigateToDestination = onNavigateToDestination, + ) + }, ) { - Box( + Column( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + .fillMaxSize() + .padding(it) + .padding(vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { - TdsTabRow( + Box( modifier = Modifier - .width(174.dp) - .height(32.dp) - .align(Alignment.Center), - selectedItemIndex = uiState.tabSelectedIndex, - items = listOf("Home", "Daily", "Week"), - onClick = { - viewModel.updateTabSelectedIndex(it) - }, - ) - - if (showSettingButton && orientation == Configuration.ORIENTATION_PORTRAIT) { - TdsIconButton( - modifier = Modifier.align(Alignment.CenterEnd), + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + TdsTabRow( + modifier = Modifier + .width(174.dp) + .height(32.dp) + .align(Alignment.Center), + selectedItemIndex = uiState.tabSelectedIndex, + items = listOf("Home", "Daily", "Week"), onClick = { - showSettingBottomSheet = true + viewModel.updateTabSelectedIndex(it) }, - ) { - Icon( - painter = painterResource(id = R.drawable.setting_icon), - contentDescription = "setting", - tint = TdsColor.TEXT.getColor(), - ) + ) + + if (showSettingButton && orientation == Configuration.ORIENTATION_PORTRAIT) { + TdsIconButton( + modifier = Modifier.align(Alignment.CenterEnd), + onClick = { + showSettingBottomSheet = true + }, + ) { + Icon( + painter = painterResource(id = R.drawable.setting_icon), + contentDescription = "setting", + tint = TdsColor.TEXT.getColor(), + ) + } } } - } - Spacer(modifier = Modifier.height(16.dp)) - - HorizontalPager( - modifier = Modifier.fillMaxSize(), - state = pagerState, - userScrollEnabled = false, - ) { page -> - when (page % 3) { - 0 -> HomeScreen( - tdsColors = uiState.graphColorUiState.graphColors, - totalData = uiState.homeUiState.totalData, - graphGoalTimeUiState = uiState.graphGoalTimeUiState, - homeMonthGraphData = uiState.homeUiState.homeGraphData.homeMonthGraphData, - homeWeekGraphData = uiState.homeUiState.homeGraphData.homeWeekGraphData, - homeDailyGraphData = uiState.homeUiState.homeGraphData.homeDailyGraphData, - ) + Spacer(modifier = Modifier.height(16.dp)) + + HorizontalPager( + modifier = Modifier.fillMaxSize(), + state = pagerState, + userScrollEnabled = false, + ) { page -> + when (page % 3) { + 0 -> HomeScreen( + tdsColors = uiState.graphColorUiState.graphColors, + totalData = uiState.homeUiState.totalData, + graphGoalTimeUiState = uiState.graphGoalTimeUiState, + homeMonthGraphData = uiState.homeUiState.homeGraphData.homeMonthGraphData, + homeWeekGraphData = uiState.homeUiState.homeGraphData.homeWeekGraphData, + homeDailyGraphData = uiState.homeUiState.homeGraphData.homeDailyGraphData, + ) - 1 -> DailyScreen( - currentDate = uiState.dailyUiState.currentDate, - hasDailies = uiState.dailyUiState.hasDailies, - totalTime = uiState.dailyUiState.dailyGraphData.totalTime, - maxTime = uiState.dailyUiState.dailyGraphData.maxTime, - taskData = uiState.dailyUiState.dailyGraphData.taskData, - tdsColors = uiState.graphColorUiState.graphColors, - timeLines = uiState.dailyUiState.dailyGraphData.timeLine, - timeTableData = uiState.dailyUiState.dailyGraphData.tdsTimeTableData, - checkedButtonStates = uiState.dailyUiState.checkedButtonStates, - onClickDate = { - viewModel.updateCurrentDateDaily(it) - }, - onClickGraphColor = { - viewModel.updateGraphColors( - selectedIndex = it, - graphColorUiState = uiState.graphColorUiState, - ) - }, - onCalendarLocalDateChanged = { - viewModel.updateHasDailyAtDailyTab(it) - }, - onCheckedChange = { graph, checked -> - viewModel.updateCheckedState( - page = graph, - checked = checked, - checkedButtonStates = uiState.dailyUiState.checkedButtonStates, - ) - }, - ) + 1 -> DailyScreen( + currentDate = uiState.dailyUiState.currentDate, + hasDailies = uiState.dailyUiState.hasDailies, + totalTime = uiState.dailyUiState.dailyGraphData.totalTime, + maxTime = uiState.dailyUiState.dailyGraphData.maxTime, + taskData = uiState.dailyUiState.dailyGraphData.taskData, + tdsColors = uiState.graphColorUiState.graphColors, + timeLines = uiState.dailyUiState.dailyGraphData.timeLine, + timeTableData = uiState.dailyUiState.dailyGraphData.tdsTimeTableData, + checkedButtonStates = uiState.dailyUiState.checkedButtonStates, + onClickDate = { + viewModel.updateCurrentDateDaily(it) + }, + onClickGraphColor = { + viewModel.updateGraphColors( + selectedIndex = it, + graphColorUiState = uiState.graphColorUiState, + ) + }, + onCalendarLocalDateChanged = { + viewModel.updateHasDailyAtDailyTab(it) + }, + onCheckedChange = { graph, checked -> + viewModel.updateCheckedState( + page = graph, + checked = checked, + checkedButtonStates = uiState.dailyUiState.checkedButtonStates, + ) + }, + ) - 2 -> WeekScreen( - weekInformation = uiState.weekUiState.weekGraphData.weekInformation, - hasDailies = uiState.weekUiState.hasDailies, - totalTime = uiState.weekUiState.weekGraphData.totalWeekTime, - averageTime = uiState.weekUiState.weekGraphData.averageWeekTime, - weekLineChartData = uiState.weekUiState.weekGraphData.weekLineChartData, - tdsColors = uiState.graphColorUiState.graphColors, - topLevelTaskTotal = uiState.weekUiState.weekGraphData.topLevelTaskTotal, - topLevelTaskData = uiState.weekUiState.weekGraphData.topLevelTdsTaskData, - currentDate = uiState.weekUiState.currentDate, - onClickDate = { - viewModel.updateCurrentDateWeek(it) - }, - onClickGraphColor = { - viewModel.updateGraphColors( - selectedIndex = it, - graphColorUiState = uiState.graphColorUiState, - ) - }, - onCalendarLocalDateChanged = { - viewModel.updateHasDailyAtWeekTab(it) - }, - ) + 2 -> WeekScreen( + weekInformation = uiState.weekUiState.weekGraphData.weekInformation, + hasDailies = uiState.weekUiState.hasDailies, + totalTime = uiState.weekUiState.weekGraphData.totalWeekTime, + averageTime = uiState.weekUiState.weekGraphData.averageWeekTime, + weekLineChartData = uiState.weekUiState.weekGraphData.weekLineChartData, + tdsColors = uiState.graphColorUiState.graphColors, + topLevelTaskTotal = uiState.weekUiState.weekGraphData.topLevelTaskTotal, + topLevelTaskData = uiState.weekUiState.weekGraphData.topLevelTdsTaskData, + currentDate = uiState.weekUiState.currentDate, + onClickDate = { + viewModel.updateCurrentDateWeek(it) + }, + onClickGraphColor = { + viewModel.updateGraphColors( + selectedIndex = it, + graphColorUiState = uiState.graphColorUiState, + ) + }, + onCalendarLocalDateChanged = { + viewModel.updateHasDailyAtWeekTab(it) + }, + ) + } } } } @@ -205,6 +231,6 @@ fun LogScreen(viewModel: LogViewModel = mavericksViewModel()) { @Composable private fun LogScreenPreview() { TiTiTheme { - LogScreen() + LogScreen(onNavigateToDestination = {}) } } diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts index d6b9fd69..a7a565cf 100644 --- a/feature/main/build.gradle.kts +++ b/feature/main/build.gradle.kts @@ -12,8 +12,11 @@ dependencies { implementation(project(":domain:daily")) implementation(project(":feature:time")) + implementation(project(":feature:measure")) implementation(project(":feature:log")) implementation(project(":feature:popup")) + implementation(project(":feature:setting")) + implementation(project(":feature:webview")) implementation(libs.androidx.splashscreen) implementation(libs.androidx.material3.window.size) diff --git a/feature/main/src/main/kotlin/com/titi/app/feature/main/navigation/TiTiApp.kt b/feature/main/src/main/kotlin/com/titi/app/feature/main/navigation/TiTiApp.kt new file mode 100644 index 00000000..4cb4b77f --- /dev/null +++ b/feature/main/src/main/kotlin/com/titi/app/feature/main/navigation/TiTiApp.kt @@ -0,0 +1,39 @@ +package com.titi.app.feature.main.navigation + +import android.Manifest +import android.annotation.SuppressLint +import android.os.Build +import android.util.Log +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import com.titi.app.feature.main.model.SplashResultState + +@SuppressLint("UnusedContentLambdaTargetStateParameter") +@Composable +fun TiTiApp(splashResultState: SplashResultState) { + val requestPermissionLauncher = + rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { isGranted: Boolean -> + Log.e("MainActivity", isGranted.toString()) + } + + fun askNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + + LaunchedEffect(Unit) { + askNotificationPermission() + } + + TiTiNavHost( + modifier = Modifier.fillMaxSize(), + splashResultState = splashResultState, + ) +} diff --git a/feature/main/src/main/kotlin/com/titi/app/feature/main/navigation/TiTiNavHost.kt b/feature/main/src/main/kotlin/com/titi/app/feature/main/navigation/TiTiNavHost.kt index cda2d56f..241c2760 100644 --- a/feature/main/src/main/kotlin/com/titi/app/feature/main/navigation/TiTiNavHost.kt +++ b/feature/main/src/main/kotlin/com/titi/app/feature/main/navigation/TiTiNavHost.kt @@ -1,33 +1,50 @@ package com.titi.app.feature.main.navigation import android.content.Intent -import androidx.activity.compose.ManagedActivityResultLauncher -import androidx.activity.result.ActivityResult import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.core.net.toUri +import androidx.navigation.NavController +import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.compose.NavHost +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navOptions +import com.titi.app.core.designsystem.navigation.TopLevelDestination +import com.titi.app.core.util.toJson import com.titi.app.feature.log.navigation.logGraph +import com.titi.app.feature.log.navigation.navigateToLog import com.titi.app.feature.main.model.SplashResultState import com.titi.app.feature.main.model.toFeatureTimeModel -import com.titi.app.feature.main.ui.TiTiAppState +import com.titi.app.feature.measure.navigation.measureGraph +import com.titi.app.feature.measure.navigation.navigateToMeasure import com.titi.app.feature.popup.PopUpActivity import com.titi.app.feature.popup.PopUpActivity.Companion.COLOR_RECORDING_MODE_KEY -import com.titi.app.feature.popup.PopUpActivity.Companion.MEASURE_SPLASH_RESULT_KEY +import com.titi.app.feature.setting.navigation.navigateToFeatures +import com.titi.app.feature.setting.navigation.navigateToSetting +import com.titi.app.feature.setting.navigation.navigateToUpdates +import com.titi.app.feature.setting.navigation.settingGraph import com.titi.app.feature.time.navigation.STOPWATCH_SCREEN +import com.titi.app.feature.time.navigation.TIMER_FINISH_KEY import com.titi.app.feature.time.navigation.TIMER_SCREEN +import com.titi.app.feature.time.navigation.navigateToStopWatch +import com.titi.app.feature.time.navigation.navigateToTimer import com.titi.app.feature.time.navigation.timeGraph +import com.titi.app.feature.webview.navigateToWebView +import com.titi.app.feature.webview.webViewGraph @Composable -fun TiTiNavHost( - splashResultState: SplashResultState, - appState: TiTiAppState, - measuringResult: ManagedActivityResultLauncher, - modifier: Modifier = Modifier, -) { - val navController = appState.navController +fun TiTiNavHost(splashResultState: SplashResultState, modifier: Modifier = Modifier) { + val navController = rememberNavController() val context = LocalContext.current + LaunchedEffect(Unit) { + if (splashResultState.recordTimes.recording) { + navController.navigateToMeasure(splashResultState.toJson()) + } + } + NavHost( modifier = modifier, navController = navController, @@ -49,17 +66,70 @@ fun TiTiNavHost( context.startActivity(intent) }, onNavigateToMeasure = { - val intent = Intent(context, PopUpActivity::class.java).apply { - putExtra( - MEASURE_SPLASH_RESULT_KEY, - it, - ) - } + navController.navigateToMeasure(it) + }, + onNavigateToDestination = { + navController.navigateToTopLevelDestination(it) + }, + ) + + measureGraph( + onFinish = { isFinish -> + navController.previousBackStackEntry + ?.savedStateHandle + ?.set(TIMER_FINISH_KEY, isFinish) + navController.navigateUp() + }, + ) - measuringResult.launch(intent) + logGraph( + onNavigateToDestination = { + navController.navigateToTopLevelDestination(it) }, ) - logGraph() + settingGraph( + onNavigateToFeatures = { navController.navigateToFeatures() }, + onNavigateToUpdates = { navController.navigateToUpdates() }, + onNavigateToPlayStore = { + val intent = Intent( + Intent.ACTION_VIEW, + "https://play.google.com/store/apps/details?id=com.titi.app".toUri(), + ) + + context.startActivity(intent) + }, + onNavigateUp = { navController.navigateUp() }, + onNavigateToWebView = { title, url -> + navController.navigateToWebView( + title = title, + url = url, + ) + }, + onNavigateToDestination = { + navController.navigateToTopLevelDestination(it) + }, + ) + + webViewGraph(onNavigateUp = { navController.navigateUp() }) + } +} + +fun NavController.navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) { + val topLevelNavOptions = + navOptions { + popUpTo(graph.findStartDestination().id) { + saveState = true + } + + launchSingleTop = true + restoreState = true + } + + when (topLevelDestination) { + TopLevelDestination.TIMER -> navigateToTimer(topLevelNavOptions) + TopLevelDestination.STOPWATCH -> navigateToStopWatch(topLevelNavOptions) + TopLevelDestination.LOG -> navigateToLog(topLevelNavOptions) + TopLevelDestination.SETTING -> navigateToSetting(topLevelNavOptions) } } diff --git a/feature/main/src/main/kotlin/com/titi/app/feature/main/ui/TiTiApp.kt b/feature/main/src/main/kotlin/com/titi/app/feature/main/ui/TiTiApp.kt deleted file mode 100644 index 68814bc4..00000000 --- a/feature/main/src/main/kotlin/com/titi/app/feature/main/ui/TiTiApp.kt +++ /dev/null @@ -1,167 +0,0 @@ -package com.titi.app.feature.main.ui - -import android.Manifest -import android.annotation.SuppressLint -import android.content.Intent -import android.os.Build -import android.util.Log -import androidx.activity.compose.ManagedActivityResultLauncher -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.ActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.systemBarsIgnoringVisibility -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.material3.Icon -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavDestination -import androidx.navigation.NavDestination.Companion.hierarchy -import com.titi.app.core.designsystem.component.TdsNavigationBarItem -import com.titi.app.core.designsystem.component.TdsText -import com.titi.app.core.designsystem.theme.TdsColor -import com.titi.app.core.designsystem.theme.TdsTextStyle -import com.titi.app.feature.main.model.SplashResultState -import com.titi.app.feature.main.navigation.TiTiNavHost -import com.titi.app.feature.main.navigation.TopLevelDestination - -@SuppressLint("UnusedContentLambdaTargetStateParameter") -@Composable -fun TiTiApp( - splashResultState: SplashResultState, - appState: TiTiAppState, - measuringResult: ManagedActivityResultLauncher, -) { - val requestPermissionLauncher = - rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission(), - ) { isGranted: Boolean -> - Log.e("MainActivity", isGranted.toString()) - } - - fun askNotificationPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) - } - } - - LaunchedEffect(Unit) { - askNotificationPermission() - } - - val bottomNavigationColor by appState.bottomNavigationColor.collectAsStateWithLifecycle() - - Scaffold( - contentWindowInsets = WindowInsets(0, 0, 0, 0), - containerColor = Color(bottomNavigationColor), - bottomBar = { - if (appState.shouldShowBottomBar) { - TiTiBottomBar( - bottomNavigationColor = bottomNavigationColor, - destinations = appState.topLevelDestinations, - onNavigateToDestination = appState::navigateToTopLevelDestination, - currentDestination = appState.currentDestination, - ) - } - }, - ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .consumeWindowInsets(padding) - .navigationBarsPadding() - .statusBarsPadding(), - ) { - TiTiNavHost( - modifier = Modifier.fillMaxSize(), - appState = appState, - splashResultState = splashResultState, - measuringResult = measuringResult, - ) - } - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -private fun TiTiBottomBar( - bottomNavigationColor: Long, - destinations: List, - onNavigateToDestination: (TopLevelDestination) -> Unit, - currentDestination: NavDestination?, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .windowInsetsPadding( - WindowInsets.systemBarsIgnoringVisibility.only( - WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal, - ), - ) - .selectableGroup() - .background(Color(bottomNavigationColor)), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - destinations.forEach { destination -> - val selected = currentDestination.isTopLevelDestinationInHierarchy(destination) - TdsNavigationBarItem( - selected = selected, - onClick = { onNavigateToDestination(destination) }, - icon = { - Icon( - painter = painterResource(id = destination.iconResourceId), - contentDescription = stringResource(id = destination.titleTextId), - tint = if (selected) { - TdsColor.TEXT.getColor() - } else { - TdsColor.LIGHT_GRAY.getColor() - }, - ) - }, - label = { - TdsText( - text = stringResource(id = destination.titleTextId), - textStyle = TdsTextStyle.SEMI_BOLD_TEXT_STYLE, - fontSize = 16.sp, - color = if (selected) { - TdsColor.TEXT.getColor() - } else { - TdsColor.LIGHT_GRAY.getColor() - }, - ) - }, - ) - } - } -} - -private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) = - this?.hierarchy?.any { - it.route?.contains(destination.name, true) ?: false - } ?: false diff --git a/feature/main/src/main/kotlin/com/titi/app/feature/main/ui/TiTiAppState.kt b/feature/main/src/main/kotlin/com/titi/app/feature/main/ui/TiTiAppState.kt deleted file mode 100644 index bceb18f0..00000000 --- a/feature/main/src/main/kotlin/com/titi/app/feature/main/ui/TiTiAppState.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.titi.app.feature.main.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.navigation.NavDestination -import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.NavHostController -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navOptions -import com.titi.app.domain.color.model.TimeColor -import com.titi.app.domain.color.usecase.GetTimeColorFlowUseCase -import com.titi.app.feature.log.navigation.LOG_ROUTE -import com.titi.app.feature.log.navigation.navigateToLog -import com.titi.app.feature.main.navigation.TopLevelDestination -import com.titi.app.feature.time.navigation.STOPWATCH_ROUTE -import com.titi.app.feature.time.navigation.TIMER_ROUTE -import com.titi.app.feature.time.navigation.navigateToStopWatch -import com.titi.app.feature.time.navigation.navigateToTimer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn - -@Composable -fun rememberNiaAppState( - navController: NavHostController = rememberNavController(), - coroutineScope: CoroutineScope = rememberCoroutineScope(), - isSystemDarkTheme: Boolean, - getTimeColorFlowUseCase: GetTimeColorFlowUseCase, -): TiTiAppState { - return remember(navController) { - TiTiAppState( - navController, - isSystemDarkTheme, - coroutineScope, - getTimeColorFlowUseCase, - ) - } -} - -@Stable -class TiTiAppState( - val navController: NavHostController, - isSystemDarkTheme: Boolean, - coroutineScope: CoroutineScope, - getTimeColorFlowUseCase: GetTimeColorFlowUseCase, -) { - val currentDestination: NavDestination? - @Composable get() = - navController - .currentBackStackEntryAsState().value?.destination - - private val currentDestinationRouteFlow = - navController.currentBackStackEntryFlow.map { it.destination.route } - - private val currentTopLevelDestination: TopLevelDestination? - @Composable get() = - when (currentDestination?.route) { - TIMER_ROUTE -> TopLevelDestination.TIMER - STOPWATCH_ROUTE -> TopLevelDestination.STOPWATCH - LOG_ROUTE -> TopLevelDestination.LOG - else -> null - } - - val topLevelDestinations: List = TopLevelDestination.entries - - private val isTopLevelDestination: Boolean - @Composable get() = currentTopLevelDestination != null - - val shouldShowBottomBar: Boolean - @Composable get() = isTopLevelDestination - - val bottomNavigationColor: StateFlow = - getTimeColorFlowUseCase() - .combine(currentDestinationRouteFlow) { timeColor: TimeColor, route: String? -> - when (route) { - TIMER_ROUTE -> timeColor.timerBackgroundColor - STOPWATCH_ROUTE -> timeColor.stopwatchBackgroundColor - LOG_ROUTE -> if (isSystemDarkTheme) 0xFF000000 else 0xFFFFFFFF - else -> 0xFF000000 - } - } - .stateIn( - scope = coroutineScope, - SharingStarted.WhileSubscribed(), - initialValue = 0xFF000000, - ) - - fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) { - val topLevelNavOptions = - navOptions { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - - launchSingleTop = true - restoreState = true - } - - when (topLevelDestination) { - TopLevelDestination.TIMER -> navController.navigateToTimer(topLevelNavOptions) - TopLevelDestination.STOPWATCH -> navController.navigateToStopWatch(topLevelNavOptions) - TopLevelDestination.LOG -> navController.navigateToLog(topLevelNavOptions) - } - } -} diff --git a/feature/main/src/main/kotlin/com/titi/app/feature/main/ui/main/MainActivity.kt b/feature/main/src/main/kotlin/com/titi/app/feature/main/ui/main/MainActivity.kt index d75fb949..b753b160 100644 --- a/feature/main/src/main/kotlin/com/titi/app/feature/main/ui/main/MainActivity.kt +++ b/feature/main/src/main/kotlin/com/titi/app/feature/main/ui/main/MainActivity.kt @@ -1,16 +1,11 @@ package com.titi.app.feature.main.ui.main -import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle -import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -19,14 +14,9 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.titi.app.core.designsystem.theme.TiTiTheme -import com.titi.app.core.util.toJson import com.titi.app.domain.color.usecase.GetTimeColorFlowUseCase import com.titi.app.feature.main.model.SplashResultState -import com.titi.app.feature.main.ui.TiTiApp -import com.titi.app.feature.main.ui.rememberNiaAppState -import com.titi.app.feature.popup.PopUpActivity -import com.titi.app.feature.popup.PopUpActivity.Companion.MEASURE_SPLASH_RESULT_KEY -import com.titi.app.feature.time.navigation.TIMER_FINISH_KEY +import com.titi.app.feature.main.navigation.TiTiApp import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.flow.filterNotNull @@ -68,46 +58,9 @@ class MainActivity : ComponentActivity() { ) setContent { - val appState = rememberNiaAppState( - getTimeColorFlowUseCase = getTimeColorFlowUseCase, - isSystemDarkTheme = isSystemInDarkTheme(), - ) - - val measuringResult = - rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult(), - ) { result -> - if (result.resultCode == RESULT_OK) { - result.data?.let { data -> - val isFinish = data.getBooleanExtra(TIMER_FINISH_KEY, false) - - appState.navController - .currentBackStackEntry - ?.savedStateHandle - ?.set(TIMER_FINISH_KEY, isFinish) - } - } - } - TiTiTheme { splashResultState?.let { - if (it.recordTimes.recording) { - SideEffect { - val intent = Intent(this, PopUpActivity::class.java).apply { - putExtra( - MEASURE_SPLASH_RESULT_KEY, - it.toJson(), - ) - } - - measuringResult.launch(intent) - } - } - TiTiApp( - splashResultState = it, - appState = appState, - measuringResult = measuringResult, - ) + TiTiApp(splashResultState = it) } } } diff --git a/feature/main/src/main/kotlin/com/titi/app/feature/main/ui/main/MainViewModel.kt b/feature/main/src/main/kotlin/com/titi/app/feature/main/ui/main/MainViewModel.kt index cda736c4..90ddfb61 100644 --- a/feature/main/src/main/kotlin/com/titi/app/feature/main/ui/main/MainViewModel.kt +++ b/feature/main/src/main/kotlin/com/titi/app/feature/main/ui/main/MainViewModel.kt @@ -2,7 +2,7 @@ package com.titi.app.feature.main.ui.main import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.titi.app.doamin.daily.usecase.GetCurrentDailyFlowUseCase +import com.titi.app.doamin.daily.usecase.GetLastDailyFlowUseCase import com.titi.app.domain.color.usecase.GetTimeColorFlowUseCase import com.titi.app.domain.time.usecase.GetRecordTimesFlowUseCase import com.titi.app.feature.main.model.SplashResultState @@ -17,13 +17,13 @@ import kotlinx.coroutines.flow.shareIn class MainViewModel @Inject constructor( getRecordTimesFlowUseCase: GetRecordTimesFlowUseCase, getTimeColorFlowUseCase: GetTimeColorFlowUseCase, - getCurrentDailyFlowUseCase: GetCurrentDailyFlowUseCase, + getLastDailyFlowUseCase: GetLastDailyFlowUseCase, ) : ViewModel() { val splashResultState: SharedFlow = combine( getRecordTimesFlowUseCase(), getTimeColorFlowUseCase(), - getCurrentDailyFlowUseCase(), + getLastDailyFlowUseCase(), ) { recordTimes, timeColor, daily -> SplashResultState( recordTimes = recordTimes, diff --git a/feature/measure/src/main/kotlin/com/titi/app/feature/measure/navigation/MeasureNavigation.kt b/feature/measure/src/main/kotlin/com/titi/app/feature/measure/navigation/MeasureNavigation.kt new file mode 100644 index 00000000..42715b0b --- /dev/null +++ b/feature/measure/src/main/kotlin/com/titi/app/feature/measure/navigation/MeasureNavigation.kt @@ -0,0 +1,32 @@ +package com.titi.app.feature.measure.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.titi.app.feature.measure.ui.MeasuringScreen + +private const val MEASURE_SCREEN = "measure" +const val MEASURE_ARG = "splashResultState" +const val MEASURE_ROUTE = "$MEASURE_SCREEN?$MEASURE_ARG={$MEASURE_ARG}" + +fun NavController.navigateToMeasure(splashResultState: String) { + navigate("$MEASURE_SCREEN?$MEASURE_ARG=$splashResultState") +} + +fun NavGraphBuilder.measureGraph(onFinish: (isFinish: Boolean) -> Unit) { + composable( + route = MEASURE_ROUTE, + arguments = listOf( + navArgument(MEASURE_ARG) { + type = NavType.StringType + }, + ), + ) { + MeasuringScreen( + splashResultState = it.arguments?.getString(MEASURE_ARG, "") ?: "", + onFinish = onFinish, + ) + } +} diff --git a/feature/measure/src/main/kotlin/com/titi/app/feature/measure/ui/MeasuringScreen.kt b/feature/measure/src/main/kotlin/com/titi/app/feature/measure/ui/MeasuringScreen.kt index f6d59375..99f6a15a 100644 --- a/feature/measure/src/main/kotlin/com/titi/app/feature/measure/ui/MeasuringScreen.kt +++ b/feature/measure/src/main/kotlin/com/titi/app/feature/measure/ui/MeasuringScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -204,112 +205,118 @@ private fun MeasuringScreen( ) { val configuration = LocalConfiguration.current - when (configuration.orientation) { - Configuration.ORIENTATION_PORTRAIT -> { - Column( - modifier = Modifier - .fillMaxSize() - .background(Color.Black) - .padding(top = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - TdsIconButton( - modifier = - Modifier - .padding(start = 16.dp) - .align(Alignment.Start), - size = 32.dp, - onClick = onSleepClick, + Scaffold(containerColor = Color.Black) { + when (configuration.orientation) { + Configuration.ORIENTATION_PORTRAIT -> { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.Black) + .padding(it) + .padding(top = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { - Icon( - painter = - if (uiState.isSleepMode) { - painterResource(id = R.drawable.sleep_icon) - } else { - painterResource(id = R.drawable.non_sleep_icon) - }, - contentDescription = "sleepIcon", - tint = Color.White, - ) - } + TdsIconButton( + modifier = Modifier + .padding(start = 16.dp) + .align(Alignment.Start), + size = 32.dp, + onClick = onSleepClick, + ) { + Icon( + painter = if (uiState.isSleepMode) { + painterResource(id = R.drawable.sleep_icon) + } else { + painterResource(id = R.drawable.non_sleep_icon) + }, + contentDescription = "sleepIcon", + tint = Color.White, + ) + } + + Spacer(modifier = Modifier.weight(1f)) - Spacer(modifier = Modifier.weight(1f)) - - TdsText( - modifier = Modifier.padding(vertical = 12.dp), - text = uiState.recordTimes.currentTask?.taskName, - textStyle = TdsTextStyle.NORMAL_TEXT_STYLE, - fontSize = 18.sp, - color = Color.White, - ) - - Spacer(modifier = Modifier.height(50.dp)) - - with(uiState.measuringRecordTimes) { - TdsTimer( - outCircularLineColor = Color(uiState.measuringTimeColor.backgroundColor), - outCircularProgress = outCircularProgress, - inCircularLineTrackColor = Color.White, - inCircularProgress = inCircularProgress, - fontColor = Color.White, - themeColor = Color(uiState.measuringTimeColor.backgroundColor), - recordingMode = uiState.recordTimes.recordingMode, - savedSumTime = savedSumTime, - savedTime = savedTime, - savedGoalTime = savedGoalTime, - finishGoalTime = finishGoalTime, - isTaskTargetTimeOn = isTaskTargetTimeOn, - onClickStopStart = { - onFinishClick() - }, + TdsText( + modifier = Modifier.padding(vertical = 24.dp), + text = uiState.recordTimes.currentTask?.taskName, + textStyle = TdsTextStyle.NORMAL_TEXT_STYLE, + fontSize = 18.sp, + color = Color.White, ) - } - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(50.dp)) + + with(uiState.measuringRecordTimes) { + TdsTimer( + outCircularLineColor = Color( + uiState.measuringTimeColor.backgroundColor, + ), + outCircularProgress = outCircularProgress, + inCircularLineTrackColor = Color.White, + inCircularProgress = inCircularProgress, + fontColor = Color.White, + themeColor = Color(uiState.measuringTimeColor.backgroundColor), + recordingMode = uiState.recordTimes.recordingMode, + savedSumTime = savedSumTime, + savedTime = savedTime, + savedGoalTime = savedGoalTime, + finishGoalTime = finishGoalTime, + isTaskTargetTimeOn = isTaskTargetTimeOn, + onClickStopStart = { + onFinishClick() + }, + ) + } - TdsIconButton( - onClick = onFinishClick, - size = 70.dp, - ) { - Icon( - painter = painterResource(id = R.drawable.stop_record_icon), - contentDescription = "startRecord", - tint = TdsColor.RED.getColor(), - ) - } + Spacer(modifier = Modifier.height(50.dp)) + + TdsIconButton( + onClick = onFinishClick, + size = 70.dp, + ) { + Icon( + painter = painterResource(id = R.drawable.stop_record_icon), + contentDescription = "startRecord", + tint = TdsColor.RED.getColor(), + ) + } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.weight(1f)) - Spacer(modifier = Modifier.height(80.dp)) + Spacer(modifier = Modifier.height(80.dp)) + } } - } - else -> { - Box( - Modifier - .fillMaxSize() - .safeDrawingPadding() - .background(Color.Black), - contentAlignment = Alignment.Center, - ) { - with(uiState.measuringRecordTimes) { - TdsTimer( - outCircularLineColor = Color(uiState.measuringTimeColor.backgroundColor), - outCircularProgress = outCircularProgress, - inCircularLineTrackColor = Color.White, - inCircularProgress = inCircularProgress, - fontColor = Color.White, - themeColor = Color(uiState.measuringTimeColor.backgroundColor), - recordingMode = uiState.recordTimes.recordingMode, - savedSumTime = savedSumTime, - savedTime = savedTime, - savedGoalTime = savedGoalTime, - finishGoalTime = finishGoalTime, - isTaskTargetTimeOn = isTaskTargetTimeOn, - onClickStopStart = { - onFinishClick() - }, - ) + else -> { + Box( + Modifier + .fillMaxSize() + .safeDrawingPadding() + .padding(it) + .background(Color.Black), + contentAlignment = Alignment.Center, + ) { + with(uiState.measuringRecordTimes) { + TdsTimer( + outCircularLineColor = Color( + uiState.measuringTimeColor.backgroundColor, + ), + outCircularProgress = outCircularProgress, + inCircularLineTrackColor = Color.White, + inCircularProgress = inCircularProgress, + fontColor = Color.White, + themeColor = Color(uiState.measuringTimeColor.backgroundColor), + recordingMode = uiState.recordTimes.recordingMode, + savedSumTime = savedSumTime, + savedTime = savedTime, + savedGoalTime = savedGoalTime, + finishGoalTime = finishGoalTime, + isTaskTargetTimeOn = isTaskTargetTimeOn, + onClickStopStart = { + onFinishClick() + }, + ) + } } } } diff --git a/feature/popup/src/main/kotlin/com/titi/app/feature/popup/PopUpActivity.kt b/feature/popup/src/main/kotlin/com/titi/app/feature/popup/PopUpActivity.kt index 69e65bdc..4699a654 100644 --- a/feature/popup/src/main/kotlin/com/titi/app/feature/popup/PopUpActivity.kt +++ b/feature/popup/src/main/kotlin/com/titi/app/feature/popup/PopUpActivity.kt @@ -1,47 +1,28 @@ package com.titi.app.feature.popup -import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import com.titi.app.core.designsystem.theme.TiTiTheme import com.titi.app.feature.color.ui.ColorScreen -import com.titi.app.feature.measure.ui.MeasuringScreen class PopUpActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val recordingMode = intent.getIntExtra(COLOR_RECORDING_MODE_KEY, 0) - val splashResultState = intent.getStringExtra(MEASURE_SPLASH_RESULT_KEY) setContent { TiTiTheme { - when { - recordingMode != 0 -> { - ColorScreen(recordingMode = recordingMode) { - finish() - } - } - - splashResultState != null -> { - MeasuringScreen(splashResultState = splashResultState) { - val resultIntent = Intent().apply { - putExtra("isFinish", it) - } - setResult(RESULT_OK, resultIntent) - finish() - } - } - - else -> finish() - } + ColorScreen( + recordingMode = recordingMode, + onFinish = { finish() }, + ) } } } companion object { const val COLOR_RECORDING_MODE_KEY = "colorRecordingModeKey" - const val MEASURE_SPLASH_RESULT_KEY = "measureSplashResultKey" } } diff --git a/feature/setting/build.gradle.kts b/feature/setting/build.gradle.kts new file mode 100644 index 00000000..1e260fa8 --- /dev/null +++ b/feature/setting/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("titi.android.feature") +} + +android { + namespace = "com.titi.app.feature.setting" +} + +dependencies { + implementation(project(":data:notification:api")) + + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.database) + + implementation(libs.moshi) + implementation(libs.moshi.kotlin) + ksp(libs.moshi.codegen) +} diff --git a/feature/setting/src/androidTest/kotlin/com/titi/app/feature/setting/ExampleInstrumentedTest.kt b/feature/setting/src/androidTest/kotlin/com/titi/app/feature/setting/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..1f19630a --- /dev/null +++ b/feature/setting/src/androidTest/kotlin/com/titi/app/feature/setting/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.titi.app.feature.setting + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.titi.app.feature.setting.test", appContext.packageName) + } +} diff --git a/feature/setting/src/main/AndroidManifest.xml b/feature/setting/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/setting/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/setting/src/main/kotlin/com/titi/app/feature/setting/di/ViewModelModule.kt b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/di/ViewModelModule.kt new file mode 100644 index 00000000..1631bc74 --- /dev/null +++ b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/di/ViewModelModule.kt @@ -0,0 +1,27 @@ +package com.titi.app.feature.setting.di + +import com.airbnb.mvrx.hilt.AssistedViewModelFactory +import com.airbnb.mvrx.hilt.MavericksViewModelComponent +import com.airbnb.mvrx.hilt.ViewModelKey +import com.titi.app.feature.setting.ui.FeaturesListViewModel +import com.titi.app.feature.setting.ui.SettingViewModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.IntoMap + +@Module +@InstallIn(MavericksViewModelComponent::class) +internal interface ViewModelModule { + @Binds + @IntoMap + @ViewModelKey(SettingViewModel::class) + fun settingViewModelFactory(factory: SettingViewModel.Factory): AssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @ViewModelKey(FeaturesListViewModel::class) + fun featuresViewModelFactory( + factory: FeaturesListViewModel.Factory, + ): AssistedViewModelFactory<*, *> +} diff --git a/feature/setting/src/main/kotlin/com/titi/app/feature/setting/mapper/FeatureToRepositoryMapper.kt b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/mapper/FeatureToRepositoryMapper.kt new file mode 100644 index 00000000..393b9d85 --- /dev/null +++ b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/mapper/FeatureToRepositoryMapper.kt @@ -0,0 +1,10 @@ +package com.titi.app.feature.setting.mapper + +import com.titi.app.data.notification.api.model.NotificationRepositoryModel +import com.titi.app.feature.setting.model.SettingUiState + +fun SettingUiState.SwitchState.toRepositoryModel() = NotificationRepositoryModel( + timerFiveMinutesBeforeTheEnd = timerFiveMinutesBeforeTheEnd, + timerBeforeTheEnd = timerBeforeTheEnd, + stopwatch = stopwatch, +) diff --git a/feature/setting/src/main/kotlin/com/titi/app/feature/setting/mapper/RepositoryToFeatureMapper.kt b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/mapper/RepositoryToFeatureMapper.kt new file mode 100644 index 00000000..c261b703 --- /dev/null +++ b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/mapper/RepositoryToFeatureMapper.kt @@ -0,0 +1,10 @@ +package com.titi.app.feature.setting.mapper + +import com.titi.app.data.notification.api.model.NotificationRepositoryModel +import com.titi.app.feature.setting.model.SettingUiState + +fun NotificationRepositoryModel.toFeatureModel() = SettingUiState.SwitchState( + timerFiveMinutesBeforeTheEnd = timerFiveMinutesBeforeTheEnd, + timerBeforeTheEnd = timerBeforeTheEnd, + stopwatch = stopwatch, +) diff --git a/feature/setting/src/main/kotlin/com/titi/app/feature/setting/model/FeaturesUiState.kt b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/model/FeaturesUiState.kt new file mode 100644 index 00000000..6c6d2429 --- /dev/null +++ b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/model/FeaturesUiState.kt @@ -0,0 +1,42 @@ +package com.titi.app.feature.setting.model + +import com.airbnb.mvrx.MavericksState + +data class FeaturesUiState( + val features: List = makeFeatures(), +) : MavericksState { + data class Feature( + val title: String, + val url: String, + ) +} + +internal fun makeFeatures(): List { + return listOf( + FeaturesUiState.Feature( + title = "새로운 기록 설정", + url = "https://www.notion.so/timertiti/2501881bb0ef49c29a1c2cee29b7f48e?pvs=4", + ), + FeaturesUiState.Feature( + title = "Task", + url = "https://www.notion.so/timertiti/Task-5fbd947fe3994ce09dd3d87051861005?pvs=4", + ), + FeaturesUiState.Feature( + title = "Timer", + url = "https://www.notion.so/timertiti/Timer-0083c63a3a464fc69b6c255930690ae8?pvs=4", + ), + FeaturesUiState.Feature( + title = "Stopwatch", + url = + "https://www.notion.so/timertiti/Stopwatch-41984a8ab11444cba79fb94984f799bb?pvs=4", + ), + FeaturesUiState.Feature( + title = "Log", + url = "https://www.notion.so/timertiti/Log-362d4cffb3e74f1686dd4e603fba8496?pvs=4", + ), + FeaturesUiState.Feature( + title = "Daily", + url = "https://www.notion.so/timertiti/Daily-d60dc90f3c104744a74985ea221e5691?pvs=4", + ), + ) +} diff --git a/feature/setting/src/main/kotlin/com/titi/app/feature/setting/model/SettingActions.kt b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/model/SettingActions.kt new file mode 100644 index 00000000..9e4a8890 --- /dev/null +++ b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/model/SettingActions.kt @@ -0,0 +1,18 @@ +package com.titi.app.feature.setting.model + +sealed interface SettingActions { + + sealed interface Navigates : SettingActions { + data object FeaturesList : Navigates + data object PlayStore : Navigates + data object UpdatesList : Navigates + } + + sealed interface Updates : SettingActions { + @JvmInline + value class Switch(val switchState: SettingUiState.SwitchState) : Updates + + @JvmInline + value class Version(val versionState: SettingUiState.VersionState) : Updates + } +} diff --git a/feature/setting/src/main/kotlin/com/titi/app/feature/setting/model/SettingUiState.kt b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/model/SettingUiState.kt new file mode 100644 index 00000000..2885ca44 --- /dev/null +++ b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/model/SettingUiState.kt @@ -0,0 +1,19 @@ +package com.titi.app.feature.setting.model + +import com.airbnb.mvrx.MavericksState + +data class SettingUiState( + val switchState: SwitchState = SwitchState(), + val versionState: VersionState = VersionState(), +) : MavericksState { + data class SwitchState( + val timerFiveMinutesBeforeTheEnd: Boolean = true, + val timerBeforeTheEnd: Boolean = true, + val stopwatch: Boolean = true, + ) + + data class VersionState( + val currentVersion: String = "", + val newVersion: String = "", + ) +} diff --git a/feature/setting/src/main/kotlin/com/titi/app/feature/setting/model/Version.kt b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/model/Version.kt new file mode 100644 index 00000000..0d0edba2 --- /dev/null +++ b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/model/Version.kt @@ -0,0 +1,10 @@ +package com.titi.app.feature.setting.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Version( + val currentVersion: String = "", + val date: String = "", + val description: String = "", +) diff --git a/feature/setting/src/main/kotlin/com/titi/app/feature/setting/navigation/SettingNavigation.kt b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/navigation/SettingNavigation.kt new file mode 100644 index 00000000..cee22fd4 --- /dev/null +++ b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/navigation/SettingNavigation.kt @@ -0,0 +1,65 @@ +package com.titi.app.feature.setting.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.titi.app.core.designsystem.navigation.TopLevelDestination +import com.titi.app.feature.setting.model.SettingActions +import com.titi.app.feature.setting.ui.FeaturesListScreen +import com.titi.app.feature.setting.ui.SettingScreen +import com.titi.app.feature.setting.ui.UpdatesListScreen + +private const val SETTING_SCREEN = "setting" +const val SETTING_ROUTE = SETTING_SCREEN + +private const val FEATURES_SCREEN = "features" +const val FEATURES_ROUTE = FEATURES_SCREEN + +private const val UPDATES_SCREEN = "updates" +const val UPDATES_ROUTE = UPDATES_SCREEN + +fun NavController.navigateToSetting(navOptions: NavOptions) { + navigate(SETTING_ROUTE, navOptions) +} + +fun NavController.navigateToFeatures() { + navigate(FEATURES_ROUTE) +} + +fun NavController.navigateToUpdates() { + navigate(UPDATES_ROUTE) +} + +fun NavGraphBuilder.settingGraph( + onNavigateToFeatures: () -> Unit, + onNavigateToUpdates: () -> Unit, + onNavigateToPlayStore: () -> Unit, + onNavigateUp: () -> Unit, + onNavigateToWebView: (title: String, url: String) -> Unit, + onNavigateToDestination: (TopLevelDestination) -> Unit, +) { + composable(route = SETTING_ROUTE) { + SettingScreen( + handleNavigateActions = { + when (it) { + SettingActions.Navigates.FeaturesList -> onNavigateToFeatures() + SettingActions.Navigates.PlayStore -> onNavigateToPlayStore() + SettingActions.Navigates.UpdatesList -> onNavigateToUpdates() + } + }, + onNavigateToDestination = onNavigateToDestination, + ) + } + + composable(route = FEATURES_ROUTE) { + FeaturesListScreen( + onNavigateUp = onNavigateUp, + onNavigateWebView = onNavigateToWebView, + ) + } + + composable(route = UPDATES_ROUTE) { + UpdatesListScreen(onNavigateUp = onNavigateUp) + } +} diff --git a/feature/setting/src/main/kotlin/com/titi/app/feature/setting/ui/FeaturesListScreen.kt b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/ui/FeaturesListScreen.kt new file mode 100644 index 00000000..52b26591 --- /dev/null +++ b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/ui/FeaturesListScreen.kt @@ -0,0 +1,124 @@ +package com.titi.app.feature.setting.ui + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.airbnb.mvrx.compose.collectAsState +import com.airbnb.mvrx.compose.mavericksViewModel +import com.titi.app.core.designsystem.R +import com.titi.app.core.designsystem.component.TdsIconButton +import com.titi.app.core.designsystem.component.TdsText +import com.titi.app.core.designsystem.theme.TdsColor +import com.titi.app.core.designsystem.theme.TdsTextStyle +import com.titi.app.core.designsystem.theme.TiTiTheme +import com.titi.app.feature.setting.model.FeaturesUiState +import com.titi.app.feature.setting.model.makeFeatures + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FeaturesListScreen( + viewModel: FeaturesListViewModel = mavericksViewModel(), + onNavigateUp: () -> Unit, + onNavigateWebView: (title: String, url: String) -> Unit, +) { + val uiState by viewModel.collectAsState() + + val containerColor = if (isSystemInDarkTheme()) { + 0xFF000000 + } else { + 0xFFF2F2F7 + } + + Scaffold( + containerColor = Color(containerColor), + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = TdsColor.GROUPED_BACKGROUND.getColor(), + ), + navigationIcon = { + TdsIconButton(onClick = onNavigateUp) { + Icon( + painter = painterResource(id = R.drawable.arrow_left_icon), + contentDescription = "back", + tint = TdsColor.TEXT.getColor(), + ) + } + }, + title = { + TdsText( + text = "TiTi 기능들", + textStyle = TdsTextStyle.EXTRA_BOLD_TEXT_STYLE, + fontSize = 24.sp, + color = TdsColor.TEXT, + ) + }, + ) + }, + ) { + FeaturesListScreen( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding() + .padding(it), + uiState = uiState, + onClick = onNavigateWebView, + ) + } +} + +@Composable +fun FeaturesListScreen( + modifier: Modifier = Modifier, + uiState: FeaturesUiState, + onClick: (title: String, url: String) -> Unit, +) { + Column(modifier = modifier) { + uiState.features.forEachIndexed { index, feature -> + ListContent( + title = feature.title, + rightAreaContent = { + Icon( + painter = painterResource(id = R.drawable.arrow_right_icon), + contentDescription = "", + tint = TdsColor.LIGHT_GRAY.getColor(), + ) + }, + onClick = { onClick(feature.title, feature.url) }, + ) + + if (index != uiState.features.size - 1) { + Spacer(modifier = Modifier.height(1.dp)) + } + } + } +} + +@Composable +@Preview +private fun FeaturesListScreenPreview() { + TiTiTheme { + FeaturesListScreen( + modifier = Modifier.fillMaxSize(), + uiState = FeaturesUiState(features = makeFeatures()), + onClick = { title, url -> }, + ) + } +} diff --git a/feature/setting/src/main/kotlin/com/titi/app/feature/setting/ui/FeaturesListViewModel.kt b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/ui/FeaturesListViewModel.kt new file mode 100644 index 00000000..b3ccf8eb --- /dev/null +++ b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/ui/FeaturesListViewModel.kt @@ -0,0 +1,24 @@ +package com.titi.app.feature.setting.ui + +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.hilt.AssistedViewModelFactory +import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory +import com.titi.app.feature.setting.model.FeaturesUiState +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +class FeaturesListViewModel @AssistedInject constructor( + @Assisted initialState: FeaturesUiState, +) : MavericksViewModel(initialState) { + + @AssistedFactory + interface Factory : AssistedViewModelFactory { + override fun create(state: FeaturesUiState): FeaturesListViewModel + } + + companion object : + MavericksViewModelFactory + by hiltMavericksViewModelFactory() +} diff --git a/feature/setting/src/main/kotlin/com/titi/app/feature/setting/ui/SettingScreen.kt b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/ui/SettingScreen.kt new file mode 100644 index 00000000..e3d4276d --- /dev/null +++ b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/ui/SettingScreen.kt @@ -0,0 +1,398 @@ +package com.titi.app.feature.setting.ui + +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +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 +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.airbnb.mvrx.compose.collectAsState +import com.airbnb.mvrx.compose.mavericksViewModel +import com.google.firebase.database.DataSnapshot +import com.google.firebase.database.DatabaseError +import com.google.firebase.database.FirebaseDatabase +import com.google.firebase.database.ValueEventListener +import com.google.firebase.database.getValue +import com.titi.app.core.designsystem.R +import com.titi.app.core.designsystem.component.TdsText +import com.titi.app.core.designsystem.navigation.TdsBottomNavigationBar +import com.titi.app.core.designsystem.navigation.TopLevelDestination +import com.titi.app.core.designsystem.theme.TdsColor +import com.titi.app.core.designsystem.theme.TdsTextStyle +import com.titi.app.core.designsystem.theme.TiTiTheme +import com.titi.app.feature.setting.model.SettingActions +import com.titi.app.feature.setting.model.SettingUiState +import com.titi.app.feature.setting.model.Version + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingScreen( + viewModel: SettingViewModel = mavericksViewModel(), + handleNavigateActions: (SettingActions.Navigates) -> Unit, + onNavigateToDestination: (TopLevelDestination) -> Unit, +) { + val uiState by viewModel.collectAsState() + + val firebaseDatabase = FirebaseDatabase.getInstance() + val databaseReference = firebaseDatabase.getReference("versions") + + val context = LocalContext.current + + LaunchedEffect(Unit) { + databaseReference.addValueEventListener( + object : ValueEventListener { + override fun onDataChange(snapshot: DataSnapshot) { + val latestVersion = snapshot + .children + .lastOrNull() + ?.getValue() + ?.currentVersion + + val currentVersion = context + .packageManager + .getPackageInfo(context.packageName, 0) + .versionName + + latestVersion?.let { safeLatestVersion -> + viewModel.handleUpdateActions( + SettingActions.Updates.Version( + uiState.versionState.copy( + newVersion = safeLatestVersion, + currentVersion = currentVersion, + ), + ), + ) + } + } + + override fun onCancelled(error: DatabaseError) { + Log.e("SettingScreen", error.message) + } + }, + ) + } + + val containerColor = if (isSystemInDarkTheme()) { + 0xFF000000 + } else { + 0xFFF2F2F7 + } + + Scaffold( + containerColor = Color(containerColor), + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = TdsColor.GROUPED_BACKGROUND.getColor(), + ), + title = { + TdsText( + text = "Setting", + textStyle = TdsTextStyle.EXTRA_BOLD_TEXT_STYLE, + fontSize = 24.sp, + color = TdsColor.TEXT, + ) + }, + ) + }, + bottomBar = { + TdsBottomNavigationBar( + currentTopLevelDestination = TopLevelDestination.SETTING, + bottomNavigationColor = containerColor, + onNavigateToDestination = onNavigateToDestination, + ) + }, + ) { + SettingScreen( + modifier = Modifier + .padding(it) + .safeDrawingPadding(), + uiState = uiState, + onSettingActions = { settingActions -> + when (settingActions) { + is SettingActions.Navigates -> handleNavigateActions(settingActions) + + is SettingActions.Updates -> viewModel.handleUpdateActions(settingActions) + } + }, + ) + } +} + +@Composable +private fun SettingScreen( + modifier: Modifier, + uiState: SettingUiState, + onSettingActions: (SettingActions) -> Unit, +) { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .fillMaxSize() + .background(color = TdsColor.GROUPED_BACKGROUND.getColor()) + .then(modifier) + .verticalScroll(scrollState), + ) { + SettingServiceSection(onSettingActions = onSettingActions) + + Spacer(modifier = Modifier.height(35.dp)) + + SettingNotificationSection( + switchState = uiState.switchState, + onSettingActions = onSettingActions, + ) + + Spacer(modifier = Modifier.height(35.dp)) + + SettingVersionSection( + versionState = uiState.versionState, + onSettingActions = onSettingActions, + ) + + Spacer(modifier = Modifier.height(35.dp)) + } +} + +@Composable +private fun SettingServiceSection(onSettingActions: (SettingActions) -> Unit) { + TdsText( + modifier = Modifier.padding(start = 16.dp), + text = "서비스", + textStyle = TdsTextStyle.SEMI_BOLD_TEXT_STYLE, + fontSize = 14.sp, + color = TdsColor.TEXT, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + ListContent( + title = "TiTi 기능들", + rightAreaContent = { + Icon( + painter = painterResource(id = R.drawable.arrow_right_icon), + contentDescription = "", + tint = TdsColor.LIGHT_GRAY.getColor(), + ) + }, + onClick = { + onSettingActions(SettingActions.Navigates.FeaturesList) + }, + ) +} + +@Composable +private fun SettingNotificationSection( + switchState: SettingUiState.SwitchState, + onSettingActions: (SettingActions) -> Unit, +) { + TdsText( + modifier = Modifier.padding(start = 16.dp), + text = "알림", + textStyle = TdsTextStyle.SEMI_BOLD_TEXT_STYLE, + fontSize = 14.sp, + color = TdsColor.TEXT, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + ListContent( + title = "타이머", + description = "종료 5분전 알림", + rightAreaContent = { + Switch( + checked = switchState.timerFiveMinutesBeforeTheEnd, + onCheckedChange = { + onSettingActions( + SettingActions.Updates.Switch( + switchState = switchState.copy( + timerFiveMinutesBeforeTheEnd = + !switchState.timerFiveMinutesBeforeTheEnd, + ), + ), + ) + }, + ) + }, + ) + + Spacer(modifier = Modifier.height(1.dp)) + + ListContent( + title = "타이머", + description = "종료 알림", + rightAreaContent = { + Switch( + checked = switchState.timerBeforeTheEnd, + onCheckedChange = { + onSettingActions( + SettingActions.Updates.Switch( + switchState = switchState.copy( + timerBeforeTheEnd = !switchState.timerBeforeTheEnd, + ), + ), + ) + }, + ) + }, + ) + + Spacer(modifier = Modifier.height(1.dp)) + + ListContent( + title = "스톱워치", + description = "1시간단위 경과시 알림", + rightAreaContent = { + Switch( + checked = switchState.stopwatch, + onCheckedChange = { + onSettingActions( + SettingActions.Updates.Switch( + switchState = switchState.copy( + stopwatch = !switchState.stopwatch, + ), + ), + ) + }, + ) + }, + ) +} + +@Composable +private fun SettingVersionSection( + versionState: SettingUiState.VersionState, + onSettingActions: (SettingActions) -> Unit, +) { + TdsText( + modifier = Modifier.padding(start = 16.dp), + text = "버전 및 업데이트 내역", + textStyle = TdsTextStyle.SEMI_BOLD_TEXT_STYLE, + fontSize = 14.sp, + color = TdsColor.TEXT, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + ListContent( + title = "버전 정보", + description = "최신버전: ${versionState.newVersion}", + rightAreaContent = { + TdsText( + text = versionState.currentVersion, + textStyle = TdsTextStyle.SEMI_BOLD_TEXT_STYLE, + color = TdsColor.LIGHT_GRAY, + fontSize = 14.sp, + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Icon( + painter = painterResource(id = R.drawable.arrow_right_icon), + contentDescription = "", + tint = TdsColor.LIGHT_GRAY.getColor(), + ) + }, + onClick = { + onSettingActions(SettingActions.Navigates.PlayStore) + }, + ) + + Spacer(modifier = Modifier.height(1.dp)) + + ListContent( + title = "업데이트 내역", + rightAreaContent = { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(id = R.drawable.arrow_right_icon), + contentDescription = "", + tint = TdsColor.LIGHT_GRAY.getColor(), + ) + } + }, + onClick = { + onSettingActions(SettingActions.Navigates.UpdatesList) + }, + ) +} + +@Composable +internal fun ListContent( + title: String, + description: String? = null, + rightAreaContent: @Composable () -> Unit, + onClick: (() -> Unit)? = null, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(TdsColor.SECONDARY_BACKGROUND.getColor()) + .clickable { onClick?.invoke() } + .padding( + horizontal = 16.dp, + vertical = 14.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + TdsText( + text = title, + textStyle = TdsTextStyle.SEMI_BOLD_TEXT_STYLE, + fontSize = 17.sp, + color = TdsColor.TEXT, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + description?.let { + TdsText( + text = it, + textStyle = TdsTextStyle.SEMI_BOLD_TEXT_STYLE, + fontSize = 11.sp, + color = TdsColor.LIGHT_GRAY, + ) + } + } + + rightAreaContent() + } +} + +@PreviewLightDark +@Composable +private fun SettingScreenPreview() { + TiTiTheme { + SettingScreen( + modifier = Modifier, + uiState = SettingUiState(), + onSettingActions = {}, + ) + } +} diff --git a/feature/setting/src/main/kotlin/com/titi/app/feature/setting/ui/SettingViewModel.kt b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/ui/SettingViewModel.kt new file mode 100644 index 00000000..d0b399f1 --- /dev/null +++ b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/ui/SettingViewModel.kt @@ -0,0 +1,65 @@ +package com.titi.app.feature.setting.ui + +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.hilt.AssistedViewModelFactory +import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory +import com.titi.app.data.notification.api.NotificationRepository +import com.titi.app.feature.setting.mapper.toFeatureModel +import com.titi.app.feature.setting.mapper.toRepositoryModel +import com.titi.app.feature.setting.model.SettingActions +import com.titi.app.feature.setting.model.SettingUiState +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +class SettingViewModel @AssistedInject constructor( + @Assisted initialState: SettingUiState, + private val notificationRepository: NotificationRepository, +) : MavericksViewModel(initialState) { + + init { + viewModelScope.launch { + notificationRepository.getNotificationFlow() + .collectLatest { + updateSwitch(it.toFeatureModel()) + } + } + } + + fun handleUpdateActions(updateActions: SettingActions.Updates) { + when (updateActions) { + is SettingActions.Updates.Switch -> { + viewModelScope.launch { + notificationRepository + .setNotification(updateActions.switchState.toRepositoryModel()) + } + } + + is SettingActions.Updates.Version -> updateVersion(updateActions.versionState) + } + } + + private fun updateSwitch(switchState: SettingUiState.SwitchState) { + setState { + copy(switchState = switchState) + } + } + + private fun updateVersion(versionState: SettingUiState.VersionState) { + setState { + copy(versionState = versionState) + } + } + + @AssistedFactory + interface Factory : AssistedViewModelFactory { + override fun create(state: SettingUiState): SettingViewModel + } + + companion object : + MavericksViewModelFactory + by hiltMavericksViewModelFactory() +} diff --git a/feature/setting/src/main/kotlin/com/titi/app/feature/setting/ui/UpdatesListScreen.kt b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/ui/UpdatesListScreen.kt new file mode 100644 index 00000000..278da84e --- /dev/null +++ b/feature/setting/src/main/kotlin/com/titi/app/feature/setting/ui/UpdatesListScreen.kt @@ -0,0 +1,187 @@ +package com.titi.app.feature.setting.ui + +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +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 +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.firebase.database.DataSnapshot +import com.google.firebase.database.DatabaseError +import com.google.firebase.database.FirebaseDatabase +import com.google.firebase.database.ValueEventListener +import com.google.firebase.database.getValue +import com.titi.app.core.designsystem.R +import com.titi.app.core.designsystem.component.TdsIconButton +import com.titi.app.core.designsystem.component.TdsText +import com.titi.app.core.designsystem.theme.TdsColor +import com.titi.app.core.designsystem.theme.TdsTextStyle +import com.titi.app.feature.setting.model.Version + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UpdatesListScreen(onNavigateUp: () -> Unit) { + val firebaseDatabase = FirebaseDatabase.getInstance() + val databaseReference = firebaseDatabase.getReference("versions") + + val updates = remember { mutableStateListOf() } + + LaunchedEffect(Unit) { + databaseReference.addValueEventListener( + object : ValueEventListener { + override fun onDataChange(snapshot: DataSnapshot) { + for (data in snapshot.children) { + data.getValue()?.let { + updates.add(it) + } + } + } + + override fun onCancelled(error: DatabaseError) { + Log.e("UpdateListScreen", error.message) + } + }, + ) + } + + val containerColor = if (isSystemInDarkTheme()) { + 0xFF000000 + } else { + 0xFFF2F2F7 + } + + Scaffold( + containerColor = Color(containerColor), + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = TdsColor.GROUPED_BACKGROUND.getColor(), + ), + navigationIcon = { + TdsIconButton(onClick = onNavigateUp) { + Icon( + painter = painterResource(id = R.drawable.arrow_left_icon), + contentDescription = "back", + tint = TdsColor.TEXT.getColor(), + ) + } + }, + title = { + TdsText( + text = "업데이트 내역", + textStyle = TdsTextStyle.EXTRA_BOLD_TEXT_STYLE, + fontSize = 24.sp, + color = TdsColor.TEXT, + ) + }, + ) + }, + ) { + UpdateListScreen( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding() + .padding(it), + updates = updates.reversed(), + ) + } +} + +@Composable +private fun UpdateListScreen(modifier: Modifier = Modifier, updates: List) { + LazyColumn(modifier) { + items(updates) { + VersionContent(version = it) + } + } +} + +@Composable +private fun VersionContent(version: Version) { + Column { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + TdsText( + text = "ver ${version.currentVersion}", + textStyle = TdsTextStyle.SEMI_BOLD_TEXT_STYLE, + color = TdsColor.TEXT, + fontSize = 16.sp, + ) + + TdsText( + text = version.date, + textStyle = TdsTextStyle.NORMAL_TEXT_STYLE, + color = TdsColor.LIGHT_GRAY, + fontSize = 16.sp, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + TdsText( + modifier = Modifier + .fillMaxWidth() + .background(color = TdsColor.SECONDARY_BACKGROUND.getColor()) + .padding( + horizontal = 8.dp, + vertical = 12.dp, + ), + text = version.description, + textStyle = TdsTextStyle.NORMAL_TEXT_STYLE, + color = TdsColor.LIGHT_GRAY, + fontSize = 16.sp, + ) + + Spacer(modifier = Modifier.height(8.dp)) + } +} + +@Preview +@Composable +private fun UpdatesListScreenPreview() { + UpdateListScreen( + modifier = Modifier.fillMaxSize(), + updates = listOf( + Version( + currentVersion = "repudiandae", + date = "utinam", + description = "magnis", + ), + Version( + currentVersion = "repudiandae", + date = "utinam", + description = "magnis", + ), + Version( + currentVersion = "repudiandae", + date = "utinam", + description = "magnis", + ), + ), + ) +} diff --git a/feature/setting/src/test/kotlin/com/titi/app/feature/setting/ExampleUnitTest.kt b/feature/setting/src/test/kotlin/com/titi/app/feature/setting/ExampleUnitTest.kt new file mode 100644 index 00000000..ef0f6baa --- /dev/null +++ b/feature/setting/src/test/kotlin/com/titi/app/feature/setting/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.titi.app.feature.setting + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/feature/time/src/main/kotlin/com/titi/app/feature/time/content/ColorSelectContent.kt b/feature/time/src/main/kotlin/com/titi/app/feature/time/component/ColorSelectComponent.kt similarity index 97% rename from feature/time/src/main/kotlin/com/titi/app/feature/time/content/ColorSelectContent.kt rename to feature/time/src/main/kotlin/com/titi/app/feature/time/component/ColorSelectComponent.kt index 0078814b..bdb84ede 100644 --- a/feature/time/src/main/kotlin/com/titi/app/feature/time/content/ColorSelectContent.kt +++ b/feature/time/src/main/kotlin/com/titi/app/feature/time/component/ColorSelectComponent.kt @@ -1,4 +1,4 @@ -package com.titi.app.feature.time.content +package com.titi.app.feature.time.component import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -32,7 +32,7 @@ import com.titi.app.core.designsystem.theme.TdsTextStyle import com.titi.app.core.designsystem.theme.TiTiTheme @Composable -fun ColorSelectContent( +fun ColorSelectComponent( backgroundColor: Color, textColor: Color, onClickBackgroundColor: () -> Unit, @@ -129,9 +129,9 @@ fun ColorSelectContent( @Preview @Composable -private fun ColorSelectContentPreview() { +private fun ColorSelectComponentPreview() { TiTiTheme { - ColorSelectContent( + ColorSelectComponent( backgroundColor = Color.Blue, textColor = Color.Black, onClickBackgroundColor = {}, diff --git a/feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeButtonContent.kt b/feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeButtonComponent.kt similarity index 84% rename from feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeButtonContent.kt rename to feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeButtonComponent.kt index 6bc7b032..9969d389 100644 --- a/feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeButtonContent.kt +++ b/feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeButtonComponent.kt @@ -1,4 +1,4 @@ -package com.titi.app.feature.time.content +package com.titi.app.feature.time.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -14,14 +14,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import com.titi.app.core.designsystem.R import com.titi.app.core.designsystem.component.TdsIconButton -import com.titi.app.core.designsystem.theme.TdsColor @Composable -fun TimeButtonContent( +fun TimeButtonComponent( recordingMode: Int, - isDailyAfter6AM: Boolean, tintColor: Color, - onClickAddDaily: () -> Unit, + onClickGoalTimeEdit: () -> Unit, onClickStartRecord: () -> Unit, onClickSettingTimer: (() -> Unit)? = null, onClickResetStopwatch: (() -> Unit)? = null, @@ -32,17 +30,14 @@ fun TimeButtonContent( horizontalArrangement = Arrangement.Center, ) { TdsIconButton( - onClick = onClickAddDaily, + onClick = onClickGoalTimeEdit, size = 50.dp, ) { Icon( - painter = painterResource(id = R.drawable.add_record_icon), + painter = + painterResource(id = R.drawable.edit_record_icon), contentDescription = "addRecord", - tint = if (isDailyAfter6AM) { - tintColor - } else { - TdsColor.RED.getColor() - }, + tint = tintColor, ) } diff --git a/feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeCheckDailyDialog.kt b/feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeCheckTaskDialog.kt similarity index 79% rename from feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeCheckDailyDialog.kt rename to feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeCheckTaskDialog.kt index f3390513..88fd5bd0 100644 --- a/feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeCheckDailyDialog.kt +++ b/feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeCheckTaskDialog.kt @@ -1,4 +1,4 @@ -package com.titi.app.feature.time.content +package com.titi.app.feature.time.component import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -11,10 +11,10 @@ import com.titi.app.core.designsystem.component.TdsDialog import com.titi.app.core.designsystem.model.TdsDialogInfo @Composable -fun TimeCheckDailyDialog(title: String, onShowDialog: (Boolean) -> Unit) { +fun TimeCheckTaskDialog(onShowDialog: (Boolean) -> Unit) { TdsDialog( tdsDialogInfo = TdsDialogInfo.Alert( - title = title, + title = stringResource(id = R.string.task_check_title), confirmText = stringResource(id = R.string.Ok), ), onShowDialog = onShowDialog, diff --git a/feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeColorDialog.kt b/feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeColorDialog.kt similarity index 94% rename from feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeColorDialog.kt rename to feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeColorDialog.kt index 49af82b2..475fe06f 100644 --- a/feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeColorDialog.kt +++ b/feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeColorDialog.kt @@ -1,4 +1,4 @@ -package com.titi.app.feature.time.content +package com.titi.app.feature.time.component import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -32,7 +32,7 @@ fun TimeColorDialog( ), onShowDialog = onShowDialog, ) { - ColorSelectContent( + ColorSelectComponent( backgroundColor = backgroundColor, textColor = textColor, onClickBackgroundColor = { diff --git a/feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeDailyDialog.kt b/feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeGoalTimeEditDialog.kt similarity index 89% rename from feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeDailyDialog.kt rename to feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeGoalTimeEditDialog.kt index f98b81dc..28734f50 100644 --- a/feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeDailyDialog.kt +++ b/feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeGoalTimeEditDialog.kt @@ -1,4 +1,4 @@ -package com.titi.app.feature.time.content +package com.titi.app.feature.time.component import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable @@ -17,7 +17,7 @@ import com.titi.app.core.designsystem.model.TdsTime import com.titi.app.core.util.getTimeToLong @Composable -fun TimeDailyDialog( +fun TimeGoalTimeEditDialog( todayDate: String, currentTime: TdsTime, onPositive: (Long) -> Unit, @@ -29,8 +29,8 @@ fun TimeDailyDialog( TdsDialog( tdsDialogInfo = TdsDialogInfo.Confirm( - title = stringResource(R.string.add_daily_title), - message = stringResource(R.string.add_daily_message, todayDate), + title = stringResource(R.string.edit_daily_title), + message = stringResource(R.string.edit_daily_message, todayDate), positiveText = stringResource(id = R.string.Ok), negativeText = stringResource(id = R.string.Cancel), onPositive = { diff --git a/feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeHeaderContent.kt b/feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeHeaderComponent.kt similarity index 81% rename from feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeHeaderContent.kt rename to feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeHeaderComponent.kt index a1334748..f990ff75 100644 --- a/feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeHeaderContent.kt +++ b/feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeHeaderComponent.kt @@ -1,4 +1,4 @@ -package com.titi.app.feature.time.content +package com.titi.app.feature.time.component import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth @@ -14,16 +14,10 @@ import androidx.compose.ui.unit.sp import com.titi.app.core.designsystem.R import com.titi.app.core.designsystem.component.TdsIconButton import com.titi.app.core.designsystem.component.TdsText -import com.titi.app.core.designsystem.theme.TdsColor import com.titi.app.core.designsystem.theme.TdsTextStyle @Composable -fun TimeHeaderContent( - todayDate: String, - isDailyAfter6AM: Boolean, - textColor: Color, - onClickColor: () -> Unit, -) { +fun TimeHeaderComponent(todayDate: String, textColor: Color, onClickColor: () -> Unit) { Box( modifier = Modifier .fillMaxWidth() @@ -34,7 +28,7 @@ fun TimeHeaderContent( text = todayDate, textStyle = TdsTextStyle.SEMI_BOLD_TEXT_STYLE, fontSize = 16.sp, - color = if (isDailyAfter6AM) textColor else TdsColor.RED.getColor(), + color = textColor, ) TdsIconButton( diff --git a/feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeTaskContent.kt b/feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeTaskComponent.kt similarity index 96% rename from feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeTaskContent.kt rename to feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeTaskComponent.kt index 5590438b..da22b2b6 100644 --- a/feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeTaskContent.kt +++ b/feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeTaskComponent.kt @@ -1,4 +1,4 @@ -package com.titi.app.feature.time.content +package com.titi.app.feature.time.component import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.PaddingValues @@ -15,7 +15,7 @@ import com.titi.app.core.designsystem.theme.TdsColor import com.titi.app.core.designsystem.theme.TdsTextStyle @Composable -fun TimeTaskContent( +fun TimeTaskComponent( isSetTask: Boolean, textColor: Color, taskName: String, diff --git a/feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeTimerDialog.kt b/feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeTimerDialog.kt similarity index 98% rename from feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeTimerDialog.kt rename to feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeTimerDialog.kt index f481bce6..2eb2f4e4 100644 --- a/feature/time/src/main/kotlin/com/titi/app/feature/time/content/TimeTimerDialog.kt +++ b/feature/time/src/main/kotlin/com/titi/app/feature/time/component/TimeTimerDialog.kt @@ -1,4 +1,4 @@ -package com.titi.app.feature.time.content +package com.titi.app.feature.time.component import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable diff --git a/feature/time/src/main/kotlin/com/titi/app/feature/time/model/StopWatchUiState.kt b/feature/time/src/main/kotlin/com/titi/app/feature/time/model/StopWatchUiState.kt index 9ecff11e..91713512 100644 --- a/feature/time/src/main/kotlin/com/titi/app/feature/time/model/StopWatchUiState.kt +++ b/feature/time/src/main/kotlin/com/titi/app/feature/time/model/StopWatchUiState.kt @@ -4,7 +4,6 @@ import android.os.Bundle import com.airbnb.mvrx.MavericksState import com.titi.app.core.util.addTimeToNow import com.titi.app.core.util.getTodayDate -import com.titi.app.core.util.isAfterSixAM import com.titi.app.doamin.daily.model.Daily import com.titi.app.domain.color.model.TimeColor import com.titi.app.domain.time.model.RecordTimes @@ -21,12 +20,10 @@ data class StopWatchUiState( daily = getSplashResultStateFromArgs(args).daily, ) - val isDailyAfter6AM: Boolean = isAfterSixAM(daily?.day) val isSetTask: Boolean = recordTimes.currentTask != null val taskName: String = recordTimes.currentTask?.taskName ?: "" val stopWatchColor = timeColor.toUiModel() val stopWatchRecordTimes = recordTimes.toUiModel(daily) - val isEnableStartRecording: Boolean = isDailyAfter6AM && isSetTask } data class StopWatchColor( diff --git a/feature/time/src/main/kotlin/com/titi/app/feature/time/model/TimerUiState.kt b/feature/time/src/main/kotlin/com/titi/app/feature/time/model/TimerUiState.kt index 07f84864..2b796ebe 100644 --- a/feature/time/src/main/kotlin/com/titi/app/feature/time/model/TimerUiState.kt +++ b/feature/time/src/main/kotlin/com/titi/app/feature/time/model/TimerUiState.kt @@ -6,7 +6,6 @@ import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.MavericksState import com.titi.app.core.util.addTimeToNow import com.titi.app.core.util.getTodayDate -import com.titi.app.core.util.isAfterSixAM import com.titi.app.doamin.daily.model.Daily import com.titi.app.domain.color.model.TimeColor import com.titi.app.domain.time.model.RecordTimes @@ -23,12 +22,10 @@ data class TimerUiState( daily = getSplashResultStateFromArgs(args).daily, ) - val isDailyAfter6AM: Boolean = isAfterSixAM(daily?.day) val isSetTask: Boolean = recordTimes.currentTask != null val taskName: String = recordTimes.currentTask?.taskName ?: "" val timerColor = timeColor.toUiModel() val timerRecordTimes = recordTimes.toUiModel(daily) - val isEnableStartRecording: Boolean = isDailyAfter6AM && isSetTask } fun getSplashResultStateFromArgs(args: Bundle): SplashResultState = diff --git a/feature/time/src/main/kotlin/com/titi/app/feature/time/navigation/TimeNavigation.kt b/feature/time/src/main/kotlin/com/titi/app/feature/time/navigation/TimeNavigation.kt index 9399a0c8..04b6bcce 100644 --- a/feature/time/src/main/kotlin/com/titi/app/feature/time/navigation/TimeNavigation.kt +++ b/feature/time/src/main/kotlin/com/titi/app/feature/time/navigation/TimeNavigation.kt @@ -6,6 +6,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable +import com.titi.app.core.designsystem.navigation.TopLevelDestination import com.titi.app.feature.time.model.SplashResultState import com.titi.app.feature.time.ui.stopwatch.StopWatchScreen import com.titi.app.feature.time.ui.timer.TimerScreen @@ -29,6 +30,7 @@ fun NavGraphBuilder.timeGraph( splashResultState: SplashResultState, onNavigateToColor: (Int) -> Unit, onNavigateToMeasure: (String) -> Unit, + onNavigateToDestination: (TopLevelDestination) -> Unit, ) { composable(route = TIMER_ROUTE) { backStackEntry -> val isFinish by backStackEntry @@ -44,6 +46,7 @@ fun NavGraphBuilder.timeGraph( }, onNavigateToColor = { onNavigateToColor(1) }, onNavigateToMeasure = onNavigateToMeasure, + onNavigateToDestination = onNavigateToDestination, ) } @@ -52,6 +55,7 @@ fun NavGraphBuilder.timeGraph( splashResultState = splashResultState, onNavigateToColor = { onNavigateToColor(2) }, onNavigateToMeasure = onNavigateToMeasure, + onNavigateToDestination = onNavigateToDestination, ) } } diff --git a/feature/time/src/main/kotlin/com/titi/app/feature/time/ui/stopwatch/StopWatchScreen.kt b/feature/time/src/main/kotlin/com/titi/app/feature/time/ui/stopwatch/StopWatchScreen.kt index 6c6daed7..cae72517 100644 --- a/feature/time/src/main/kotlin/com/titi/app/feature/time/ui/stopwatch/StopWatchScreen.kt +++ b/feature/time/src/main/kotlin/com/titi/app/feature/time/ui/stopwatch/StopWatchScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -18,32 +19,30 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.airbnb.mvrx.asMavericksArgs import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel -import com.titi.app.core.designsystem.R import com.titi.app.core.designsystem.component.TdsTimer import com.titi.app.core.designsystem.extension.getTdsTime -import com.titi.app.core.util.toJson -import com.titi.app.feature.time.content.TimeButtonContent -import com.titi.app.feature.time.content.TimeCheckDailyDialog -import com.titi.app.feature.time.content.TimeColorDialog -import com.titi.app.feature.time.content.TimeDailyDialog -import com.titi.app.feature.time.content.TimeHeaderContent -import com.titi.app.feature.time.content.TimeTaskContent +import com.titi.app.core.designsystem.navigation.TdsBottomNavigationBar +import com.titi.app.core.designsystem.navigation.TopLevelDestination +import com.titi.app.feature.time.component.TimeButtonComponent +import com.titi.app.feature.time.component.TimeCheckTaskDialog +import com.titi.app.feature.time.component.TimeColorDialog +import com.titi.app.feature.time.component.TimeGoalTimeEditDialog +import com.titi.app.feature.time.component.TimeHeaderComponent +import com.titi.app.feature.time.component.TimeTaskComponent import com.titi.app.feature.time.model.SplashResultState import com.titi.app.feature.time.model.StopWatchUiState import com.titi.app.feature.time.ui.task.TaskBottomSheet -import org.threeten.bp.ZoneOffset -import org.threeten.bp.ZonedDateTime @Composable fun StopWatchScreen( splashResultState: SplashResultState, onNavigateToColor: () -> Unit, onNavigateToMeasure: (String) -> Unit, + onNavigateToDestination: (TopLevelDestination) -> Unit, ) { val viewModel: StopWatchViewModel = mavericksViewModel( argsFactory = { @@ -59,8 +58,8 @@ fun StopWatchScreen( var showTaskBottomSheet by remember { mutableStateOf(false) } var showSelectColorDialog by remember { mutableStateOf(false) } - var showAddDailyDialog by remember { mutableStateOf(false) } - var showCheckTaskDailyDialog by remember { mutableStateOf(false) } + var showGoalTimeEditDialog by remember { mutableStateOf(false) } + var showCheckTaskDialog by remember { mutableStateOf(false) } if (showTaskBottomSheet) { TaskBottomSheet( @@ -90,36 +89,28 @@ fun StopWatchScreen( ) } - if (showAddDailyDialog) { - TimeDailyDialog( + if (showGoalTimeEditDialog) { + TimeGoalTimeEditDialog( todayDate = uiState.todayDate, currentTime = uiState.recordTimes.setGoalTime.getTdsTime(), - onPositive = { - if (it > 0) { + onPositive = { goalTime -> + if (goalTime > 0) { viewModel.updateSetGoalTime( uiState.recordTimes, - it, + goalTime, ) - viewModel.addDaily() } }, onShowDialog = { - showAddDailyDialog = it + showGoalTimeEditDialog = it }, ) } - if (showCheckTaskDailyDialog) { - TimeCheckDailyDialog( - title = if (!uiState.isSetTask && !uiState.isDailyAfter6AM) { - stringResource(id = R.string.daily_task_check_title) - } else if (!uiState.isSetTask) { - stringResource(id = R.string.task_check_title) - } else { - stringResource(id = R.string.daily_check_title) - }, + if (showCheckTaskDialog) { + TimeCheckTaskDialog( onShowDialog = { - showCheckTaskDailyDialog = it + showCheckTaskDialog = it }, ) } @@ -138,34 +129,25 @@ fun StopWatchScreen( onClickTask = { showTaskBottomSheet = true }, - onClickAddDaily = { - showAddDailyDialog = true + onClickGoalTimeEdit = { + showGoalTimeEditDialog = true }, onClickStartRecord = { - if (uiState.isEnableStartRecording) { - val updateRecordTimes = - uiState.recordTimes.copy( - recording = true, - recordStartAt = ZonedDateTime.now(ZoneOffset.UTC).toString(), - ) - - viewModel.updateMeasuringState(updateRecordTimes) - - val splashResultStateString = - SplashResultState( - recordTimes = updateRecordTimes, - timeColor = uiState.timeColor, - daily = uiState.daily, - ).toJson() - + if (uiState.isSetTask) { + val splashResultStateString = viewModel.startRecording( + recordTimes = uiState.recordTimes, + daily = uiState.daily, + timeColor = uiState.timeColor, + ) onNavigateToMeasure(splashResultStateString) } else { - showCheckTaskDailyDialog = true + showGoalTimeEditDialog = true } }, onClickResetStopWatch = { viewModel.updateSavedStopWatchTime(uiState.recordTimes) }, + onNavigateToDestination = onNavigateToDestination, ) } @@ -175,104 +157,120 @@ private fun StopWatchScreen( textColor: Color, onClickColor: () -> Unit, onClickTask: () -> Unit, - onClickAddDaily: () -> Unit, + onClickGoalTimeEdit: () -> Unit, onClickStartRecord: () -> Unit, onClickResetStopWatch: () -> Unit, + onNavigateToDestination: (TopLevelDestination) -> Unit, ) { val configuration = LocalConfiguration.current - when (configuration.orientation) { - Configuration.ORIENTATION_PORTRAIT -> { - Column( - modifier = Modifier - .fillMaxSize() - .padding(top = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - TimeHeaderContent( - todayDate = uiState.todayDate, - isDailyAfter6AM = uiState.isDailyAfter6AM, - textColor = textColor, - onClickColor = onClickColor, - ) - - Spacer(modifier = Modifier.weight(1f)) - - TimeTaskContent( - isSetTask = uiState.isSetTask, - textColor = textColor, - taskName = uiState.taskName, - onClickTask = onClickTask, + Scaffold( + containerColor = Color(uiState.timeColor.stopwatchBackgroundColor), + bottomBar = { + if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { + TdsBottomNavigationBar( + currentTopLevelDestination = TopLevelDestination.STOPWATCH, + bottomNavigationColor = uiState.timeColor.stopwatchBackgroundColor, + onNavigateToDestination = onNavigateToDestination, ) + } + }, + ) { + when (configuration.orientation) { + Configuration.ORIENTATION_PORTRAIT -> { + Column( + modifier = Modifier + .fillMaxSize() + .padding(it) + .padding(top = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + TimeHeaderComponent( + todayDate = uiState.todayDate, + textColor = textColor, + onClickColor = onClickColor, + ) - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.weight(1f)) - with(uiState.stopWatchRecordTimes) { - TdsTimer( - outCircularLineColor = textColor, - outCircularProgress = outCircularProgress, - inCircularLineTrackColor = if (uiState.stopWatchColor.isTextColorBlack) { - Color.White - } else { - Color(0x8C000000) - }, - inCircularProgress = inCircularProgress, - fontColor = textColor, - recordingMode = 2, - savedSumTime = savedSumTime, - savedTime = savedTime, - savedGoalTime = savedGoalTime, - finishGoalTime = finishGoalTime, - isTaskTargetTimeOn = isTaskTargetTimeOn, - onClickStopStart = { - onClickStartRecord() - }, + TimeTaskComponent( + isSetTask = uiState.isSetTask, + textColor = textColor, + taskName = uiState.taskName, + onClickTask = onClickTask, ) - } - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(50.dp)) - TimeButtonContent( - recordingMode = 2, - isDailyAfter6AM = uiState.isDailyAfter6AM, - tintColor = textColor, - onClickAddDaily = onClickAddDaily, - onClickStartRecord = onClickStartRecord, - onClickResetStopwatch = onClickResetStopWatch, - ) + with(uiState.stopWatchRecordTimes) { + TdsTimer( + outCircularLineColor = textColor, + outCircularProgress = outCircularProgress, + inCircularLineTrackColor = + if (uiState.stopWatchColor.isTextColorBlack) { + Color.White + } else { + Color(0x8C000000) + }, + inCircularProgress = inCircularProgress, + fontColor = textColor, + recordingMode = 2, + savedSumTime = savedSumTime, + savedTime = savedTime, + savedGoalTime = savedGoalTime, + finishGoalTime = finishGoalTime, + isTaskTargetTimeOn = isTaskTargetTimeOn, + onClickStopStart = { + onClickStartRecord() + }, + ) + } - Spacer(modifier = Modifier.weight(1f)) - } - } + Spacer(modifier = Modifier.height(50.dp)) - else -> { - Box( - Modifier - .fillMaxSize() - .safeDrawingPadding(), - contentAlignment = Alignment.Center, - ) { - with(uiState.stopWatchRecordTimes) { - TdsTimer( - outCircularLineColor = textColor, - outCircularProgress = outCircularProgress, - inCircularLineTrackColor = if (uiState.stopWatchColor.isTextColorBlack) { - Color.White - } else { - Color(0x8C000000) - }, - inCircularProgress = inCircularProgress, - fontColor = textColor, + TimeButtonComponent( recordingMode = 2, - savedSumTime = savedSumTime, - savedTime = savedTime, - savedGoalTime = savedGoalTime, - finishGoalTime = finishGoalTime, - isTaskTargetTimeOn = isTaskTargetTimeOn, - onClickStopStart = { - onClickStartRecord() - }, + tintColor = textColor, + onClickGoalTimeEdit = onClickGoalTimeEdit, + onClickStartRecord = onClickStartRecord, + onClickResetStopwatch = onClickResetStopWatch, ) + + Spacer(modifier = Modifier.weight(1f)) + } + } + + else -> { + Box( + Modifier + .fillMaxSize() + .safeDrawingPadding() + .padding(it), + contentAlignment = Alignment.Center, + ) { + with(uiState.stopWatchRecordTimes) { + TdsTimer( + outCircularLineColor = textColor, + outCircularProgress = outCircularProgress, + inCircularLineTrackColor = + if (uiState.stopWatchColor.isTextColorBlack) { + Color.White + } else { + Color(0x8C000000) + }, + inCircularProgress = inCircularProgress, + fontColor = textColor, + recordingMode = 2, + savedSumTime = savedSumTime, + savedTime = savedTime, + savedGoalTime = savedGoalTime, + finishGoalTime = finishGoalTime, + isTaskTargetTimeOn = isTaskTargetTimeOn, + onClickStopStart = { + onClickStartRecord() + }, + ) + } } } } diff --git a/feature/time/src/main/kotlin/com/titi/app/feature/time/ui/stopwatch/StopWatchViewModel.kt b/feature/time/src/main/kotlin/com/titi/app/feature/time/ui/stopwatch/StopWatchViewModel.kt index 405f626a..778fb796 100644 --- a/feature/time/src/main/kotlin/com/titi/app/feature/time/ui/stopwatch/StopWatchViewModel.kt +++ b/feature/time/src/main/kotlin/com/titi/app/feature/time/ui/stopwatch/StopWatchViewModel.kt @@ -5,8 +5,12 @@ import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.hilt.AssistedViewModelFactory import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory +import com.titi.app.core.util.isAfterSixAM +import com.titi.app.core.util.toJson +import com.titi.app.doamin.daily.model.Daily import com.titi.app.doamin.daily.usecase.AddDailyUseCase -import com.titi.app.doamin.daily.usecase.GetCurrentDailyFlowUseCase +import com.titi.app.doamin.daily.usecase.GetLastDailyFlowUseCase +import com.titi.app.domain.color.model.TimeColor import com.titi.app.domain.color.usecase.GetTimeColorFlowUseCase import com.titi.app.domain.color.usecase.UpdateColorUseCase import com.titi.app.domain.time.model.RecordTimes @@ -15,6 +19,7 @@ import com.titi.app.domain.time.usecase.UpdateMeasuringStateUseCase import com.titi.app.domain.time.usecase.UpdateRecordingModeUseCase import com.titi.app.domain.time.usecase.UpdateSavedStopWatchTimeUseCase import com.titi.app.domain.time.usecase.UpdateSetGoalTimeUseCase +import com.titi.app.feature.time.model.SplashResultState import com.titi.app.feature.time.model.StopWatchColor import com.titi.app.feature.time.model.StopWatchUiState import dagger.assisted.Assisted @@ -22,12 +27,14 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.catch import kotlinx.coroutines.launch +import org.threeten.bp.ZoneOffset +import org.threeten.bp.ZonedDateTime class StopWatchViewModel @AssistedInject constructor( @Assisted initialState: StopWatchUiState, getRecordTimesFlowUseCase: GetRecordTimesFlowUseCase, getTimeColorFlowUseCase: GetTimeColorFlowUseCase, - getCurrentDailyFlowUseCase: GetCurrentDailyFlowUseCase, + getLastDailyFlowUseCase: GetLastDailyFlowUseCase, private val updateRecordingModeUseCase: UpdateRecordingModeUseCase, private val updateColorUseCase: UpdateColorUseCase, private val updateSetGoalTimeUseCase: UpdateSetGoalTimeUseCase, @@ -50,7 +57,7 @@ class StopWatchViewModel @AssistedInject constructor( copy(timeColor = it) } - getCurrentDailyFlowUseCase().catch { + getLastDailyFlowUseCase().catch { Log.e("TimeViewModel", it.message.toString()) }.setOnEach { copy(daily = it) @@ -97,16 +104,46 @@ class StopWatchViewModel @AssistedInject constructor( } } - fun addDaily() { - viewModelScope.launch { - addDailyUseCase() + fun startRecording(recordTimes: RecordTimes, daily: Daily?, timeColor: TimeColor): String { + val updateRecordTimes = if (isAfterSixAM(daily?.day)) { + if (recordTimes.savedTimerTime <= 0) { + recordTimes.copy( + recording = true, + recordStartAt = ZonedDateTime.now(ZoneOffset.UTC).toString(), + savedTimerTime = recordTimes.setTimerTime, + ) + } else { + recordTimes.copy( + recording = true, + recordStartAt = ZonedDateTime.now(ZoneOffset.UTC).toString(), + ) + } + } else { + recordTimes.copy( + recording = true, + recordStartAt = ZonedDateTime.now(ZoneOffset.UTC).toString(), + savedSumTime = 0, + savedTimerTime = recordTimes.setTimerTime, + savedStopWatchTime = 0, + ) + } + + val updateDaily = if (daily != null && isAfterSixAM(daily.day)) { + daily + } else { + Daily() } - } - fun updateMeasuringState(recordTimes: RecordTimes) { viewModelScope.launch { - updateMeasuringStateUseCase(recordTimes) + updateMeasuringStateUseCase(updateRecordTimes) + addDailyUseCase(updateDaily) } + + return SplashResultState( + recordTimes = updateRecordTimes, + daily = updateDaily, + timeColor = timeColor, + ).toJson() } fun updateSavedStopWatchTime(recordTimes: RecordTimes) { diff --git a/feature/time/src/main/kotlin/com/titi/app/feature/time/ui/timer/TimerScreen.kt b/feature/time/src/main/kotlin/com/titi/app/feature/time/ui/timer/TimerScreen.kt index 2a6361bc..01967a49 100644 --- a/feature/time/src/main/kotlin/com/titi/app/feature/time/ui/timer/TimerScreen.kt +++ b/feature/time/src/main/kotlin/com/titi/app/feature/time/ui/timer/TimerScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -19,27 +20,24 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.airbnb.mvrx.asMavericksArgs import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel -import com.titi.app.core.designsystem.R import com.titi.app.core.designsystem.component.TdsTimer import com.titi.app.core.designsystem.extension.getTdsTime -import com.titi.app.core.util.toJson -import com.titi.app.feature.time.content.TimeButtonContent -import com.titi.app.feature.time.content.TimeCheckDailyDialog -import com.titi.app.feature.time.content.TimeColorDialog -import com.titi.app.feature.time.content.TimeDailyDialog -import com.titi.app.feature.time.content.TimeHeaderContent -import com.titi.app.feature.time.content.TimeTaskContent -import com.titi.app.feature.time.content.TimeTimerDialog +import com.titi.app.core.designsystem.navigation.TdsBottomNavigationBar +import com.titi.app.core.designsystem.navigation.TopLevelDestination +import com.titi.app.feature.time.component.TimeButtonComponent +import com.titi.app.feature.time.component.TimeCheckTaskDialog +import com.titi.app.feature.time.component.TimeColorDialog +import com.titi.app.feature.time.component.TimeGoalTimeEditDialog +import com.titi.app.feature.time.component.TimeHeaderComponent +import com.titi.app.feature.time.component.TimeTaskComponent +import com.titi.app.feature.time.component.TimeTimerDialog import com.titi.app.feature.time.model.SplashResultState import com.titi.app.feature.time.model.TimerUiState import com.titi.app.feature.time.ui.task.TaskBottomSheet -import org.threeten.bp.ZoneOffset -import org.threeten.bp.ZonedDateTime @Composable fun TimerScreen( @@ -48,6 +46,7 @@ fun TimerScreen( onChangeFinishStateFalse: () -> Unit, onNavigateToColor: () -> Unit, onNavigateToMeasure: (String) -> Unit, + onNavigateToDestination: (TopLevelDestination) -> Unit, ) { val viewModel: TimerViewModel = mavericksViewModel( argsFactory = { @@ -63,8 +62,8 @@ fun TimerScreen( var showTaskBottomSheet by remember { mutableStateOf(false) } var showSelectColorDialog by remember { mutableStateOf(false) } - var showAddDailyDialog by remember { mutableStateOf(false) } - var showCheckTaskDailyDialog by remember { mutableStateOf(false) } + var showGoalTimeEditDialog by remember { mutableStateOf(false) } + var showCheckTaskDialog by remember { mutableStateOf(false) } var showUpdateTimerDialog by remember { mutableStateOf(false) } if (showTaskBottomSheet) { @@ -97,37 +96,30 @@ fun TimerScreen( ) } - if (showAddDailyDialog) { - TimeDailyDialog( + if (showGoalTimeEditDialog) { + TimeGoalTimeEditDialog( todayDate = uiState.todayDate, currentTime = uiState.recordTimes.setGoalTime.getTdsTime(), - onPositive = { - if (it > 0) { + onPositive = { goalTime -> + if (goalTime > 0) { viewModel.updateSetGoalTime( uiState.recordTimes, - it, + goalTime, ) - viewModel.addDaily() + onChangeFinishStateFalse() } }, onShowDialog = { - showAddDailyDialog = it + showGoalTimeEditDialog = it }, ) } - if (showCheckTaskDailyDialog) { - TimeCheckDailyDialog( - title = if (!uiState.isSetTask && !uiState.isDailyAfter6AM) { - stringResource(id = R.string.daily_task_check_title) - } else if (!uiState.isSetTask) { - stringResource(id = R.string.task_check_title) - } else { - stringResource(id = R.string.daily_check_title) - }, + if (showCheckTaskDialog) { + TimeCheckTaskDialog( onShowDialog = { - showCheckTaskDailyDialog = it + showCheckTaskDialog = it }, ) } @@ -164,44 +156,25 @@ fun TimerScreen( onClickTask = { showTaskBottomSheet = true }, - onClickAddDaily = { - showAddDailyDialog = true + onClickGoalTimeEdit = { + showGoalTimeEditDialog = true }, onClickStartRecord = { - if (uiState.isEnableStartRecording) { - val updateRecordTimes = - with(uiState.recordTimes) { - if (savedTimerTime <= 0) { - copy( - recording = true, - recordStartAt = ZonedDateTime.now(ZoneOffset.UTC).toString(), - savedTimerTime = setTimerTime, - ) - } else { - copy( - recording = true, - recordStartAt = ZonedDateTime.now(ZoneOffset.UTC).toString(), - ) - } - } - - viewModel.updateMeasuringState(updateRecordTimes) - - val splashResultStateString = - SplashResultState( - recordTimes = updateRecordTimes, - timeColor = uiState.timeColor, - daily = uiState.daily, - ).toJson() - + if (uiState.isSetTask) { + val splashResultStateString = viewModel.startRecording( + recordTimes = uiState.recordTimes, + daily = uiState.daily, + timeColor = uiState.timeColor, + ) onNavigateToMeasure(splashResultStateString) } else { - showCheckTaskDailyDialog = true + showCheckTaskDialog = true } }, onClickSettingTimer = { showUpdateTimerDialog = true }, + onNavigateToDestination = onNavigateToDestination, ) } @@ -212,107 +185,121 @@ private fun TimerScreen( textColor: Color, onClickColor: () -> Unit, onClickTask: () -> Unit, - onClickAddDaily: () -> Unit, + onClickGoalTimeEdit: () -> Unit, onClickStartRecord: () -> Unit, onClickSettingTimer: () -> Unit, + onNavigateToDestination: (TopLevelDestination) -> Unit, ) { val configuration = LocalConfiguration.current - when (configuration.orientation) { - Configuration.ORIENTATION_PORTRAIT -> { - Column( - modifier = Modifier - .fillMaxSize() - .padding(top = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - TimeHeaderContent( - todayDate = uiState.todayDate, - isDailyAfter6AM = uiState.isDailyAfter6AM, - textColor = textColor, - onClickColor = onClickColor, - ) - - Spacer(modifier = Modifier.weight(1f)) - - TimeTaskContent( - isSetTask = uiState.isSetTask, - textColor = textColor, - taskName = uiState.taskName, - onClickTask = onClickTask, + Scaffold( + containerColor = Color(uiState.timeColor.timerBackgroundColor), + bottomBar = { + if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { + TdsBottomNavigationBar( + currentTopLevelDestination = TopLevelDestination.TIMER, + bottomNavigationColor = uiState.timeColor.timerBackgroundColor, + onNavigateToDestination = onNavigateToDestination, ) + } + }, + ) { + when (configuration.orientation) { + Configuration.ORIENTATION_PORTRAIT -> { + Column( + modifier = Modifier + .fillMaxSize() + .padding(it) + .padding(top = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + TimeHeaderComponent( + todayDate = uiState.todayDate, + textColor = textColor, + onClickColor = onClickColor, + ) - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.weight(1f)) - with(uiState.timerRecordTimes) { - TdsTimer( - isFinish = isFinish, - outCircularLineColor = textColor, - outCircularProgress = outCircularProgress, - inCircularLineTrackColor = if (uiState.timerColor.isTextColorBlack) { - Color.White - } else { - Color(0x8C000000) - }, - inCircularProgress = inCircularProgress, - fontColor = textColor, - recordingMode = 1, - savedSumTime = savedSumTime, - savedTime = savedTime, - savedGoalTime = savedGoalTime, - finishGoalTime = finishGoalTime, - isTaskTargetTimeOn = isTaskTargetTimeOn, - onClickStopStart = { - onClickStartRecord() - }, + TimeTaskComponent( + isSetTask = uiState.isSetTask, + textColor = textColor, + taskName = uiState.taskName, + onClickTask = onClickTask, ) - } - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(50.dp)) - TimeButtonContent( - recordingMode = 1, - isDailyAfter6AM = uiState.isDailyAfter6AM, - tintColor = textColor, - onClickAddDaily = onClickAddDaily, - onClickStartRecord = onClickStartRecord, - onClickSettingTimer = onClickSettingTimer, - ) + with(uiState.timerRecordTimes) { + TdsTimer( + isFinish = isFinish, + outCircularLineColor = textColor, + outCircularProgress = outCircularProgress, + inCircularLineTrackColor = if (uiState.timerColor.isTextColorBlack) { + Color.White + } else { + Color(0x8C000000) + }, + inCircularProgress = inCircularProgress, + fontColor = textColor, + recordingMode = 1, + savedSumTime = savedSumTime, + savedTime = savedTime, + savedGoalTime = savedGoalTime, + finishGoalTime = finishGoalTime, + isTaskTargetTimeOn = isTaskTargetTimeOn, + onClickStopStart = { + onClickStartRecord() + }, + ) + } - Spacer(modifier = Modifier.weight(1f)) - } - } + Spacer(modifier = Modifier.height(50.dp)) - else -> { - Box( - Modifier - .fillMaxSize() - .safeDrawingPadding(), - contentAlignment = Alignment.Center, - ) { - with(uiState.timerRecordTimes) { - TdsTimer( - isFinish = isFinish, - outCircularLineColor = textColor, - outCircularProgress = outCircularProgress, - inCircularLineTrackColor = if (uiState.timerColor.isTextColorBlack) { - Color.White - } else { - Color(0x8C000000) - }, - inCircularProgress = inCircularProgress, - fontColor = textColor, + TimeButtonComponent( recordingMode = 1, - savedSumTime = savedSumTime, - savedTime = savedTime, - savedGoalTime = savedGoalTime, - finishGoalTime = finishGoalTime, - isTaskTargetTimeOn = isTaskTargetTimeOn, - onClickStopStart = { - onClickStartRecord() - }, + tintColor = textColor, + onClickGoalTimeEdit = onClickGoalTimeEdit, + onClickStartRecord = onClickStartRecord, + onClickSettingTimer = onClickSettingTimer, ) + + Spacer(modifier = Modifier.weight(1f)) + } + } + + else -> { + Box( + Modifier + .fillMaxSize() + .safeDrawingPadding() + .padding(it), + contentAlignment = Alignment.Center, + ) { + with(uiState.timerRecordTimes) { + TdsTimer( + isFinish = isFinish, + outCircularLineColor = textColor, + outCircularProgress = outCircularProgress, + inCircularLineTrackColor = if (uiState.timerColor.isTextColorBlack) { + Color.White + } else { + Color(0x8C000000) + }, + inCircularProgress = inCircularProgress, + fontColor = textColor, + recordingMode = 1, + savedSumTime = savedSumTime, + savedTime = savedTime, + savedGoalTime = savedGoalTime, + finishGoalTime = finishGoalTime, + isTaskTargetTimeOn = isTaskTargetTimeOn, + onClickStopStart = { + onClickStartRecord() + }, + ) + } } } } diff --git a/feature/time/src/main/kotlin/com/titi/app/feature/time/ui/timer/TimerViewModel.kt b/feature/time/src/main/kotlin/com/titi/app/feature/time/ui/timer/TimerViewModel.kt index e5c0c097..9c4e7aed 100644 --- a/feature/time/src/main/kotlin/com/titi/app/feature/time/ui/timer/TimerViewModel.kt +++ b/feature/time/src/main/kotlin/com/titi/app/feature/time/ui/timer/TimerViewModel.kt @@ -5,8 +5,12 @@ import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.hilt.AssistedViewModelFactory import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory +import com.titi.app.core.util.isAfterSixAM +import com.titi.app.core.util.toJson +import com.titi.app.doamin.daily.model.Daily import com.titi.app.doamin.daily.usecase.AddDailyUseCase -import com.titi.app.doamin.daily.usecase.GetCurrentDailyFlowUseCase +import com.titi.app.doamin.daily.usecase.GetLastDailyFlowUseCase +import com.titi.app.domain.color.model.TimeColor import com.titi.app.domain.color.usecase.GetTimeColorFlowUseCase import com.titi.app.domain.color.usecase.UpdateColorUseCase import com.titi.app.domain.time.model.RecordTimes @@ -15,6 +19,7 @@ import com.titi.app.domain.time.usecase.UpdateMeasuringStateUseCase import com.titi.app.domain.time.usecase.UpdateRecordingModeUseCase import com.titi.app.domain.time.usecase.UpdateSetGoalTimeUseCase import com.titi.app.domain.time.usecase.UpdateSetTimerTimeUseCase +import com.titi.app.feature.time.model.SplashResultState import com.titi.app.feature.time.model.TimerColor import com.titi.app.feature.time.model.TimerUiState import dagger.assisted.Assisted @@ -22,12 +27,14 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.catch import kotlinx.coroutines.launch +import org.threeten.bp.ZoneOffset +import org.threeten.bp.ZonedDateTime class TimerViewModel @AssistedInject constructor( @Assisted initialState: TimerUiState, getRecordTimesFlowUseCase: GetRecordTimesFlowUseCase, getTimeColorFlowUseCase: GetTimeColorFlowUseCase, - getCurrentDailyFlowUseCase: GetCurrentDailyFlowUseCase, + getLastDailyFlowUseCase: GetLastDailyFlowUseCase, private val updateRecordingModeUseCase: UpdateRecordingModeUseCase, private val updateColorUseCase: UpdateColorUseCase, private val updateSetGoalTimeUseCase: UpdateSetGoalTimeUseCase, @@ -48,7 +55,7 @@ class TimerViewModel @AssistedInject constructor( copy(timeColor = it) } - getCurrentDailyFlowUseCase().catch { + getLastDailyFlowUseCase().catch { Log.e("TimeViewModel", it.message.toString()) }.setOnEach { copy(daily = it) @@ -97,16 +104,47 @@ class TimerViewModel @AssistedInject constructor( } } - fun addDaily() { - viewModelScope.launch { - addDailyUseCase() + fun startRecording(recordTimes: RecordTimes, daily: Daily?, timeColor: TimeColor): String { + val updateRecordTimes = if (isAfterSixAM(daily?.day)) { + if (recordTimes.savedTimerTime <= 0) { + recordTimes.copy( + recording = true, + recordStartAt = ZonedDateTime.now(ZoneOffset.UTC).toString(), + savedTimerTime = recordTimes.setTimerTime, + ) + } else { + recordTimes.copy( + recording = true, + recordStartAt = ZonedDateTime.now(ZoneOffset.UTC).toString(), + ) + } + } else { + recordTimes.copy( + recording = true, + recordStartAt = ZonedDateTime.now(ZoneOffset.UTC).toString(), + savedSumTime = 0, + savedTimerTime = recordTimes.setTimerTime, + savedStopWatchTime = 0, + savedGoalTime = recordTimes.setGoalTime, + ) + } + + val updateDaily = if (daily != null && isAfterSixAM(daily.day)) { + daily + } else { + Daily() } - } - fun updateMeasuringState(recordTimes: RecordTimes) { viewModelScope.launch { - updateMeasuringStateUseCase(recordTimes) + updateMeasuringStateUseCase(updateRecordTimes) + addDailyUseCase(updateDaily) } + + return SplashResultState( + recordTimes = updateRecordTimes, + daily = updateDaily, + timeColor = timeColor, + ).toJson() } fun updateSetTimerTime(recordTimes: RecordTimes, timerTime: Long) { diff --git a/feature/webview/build.gradle.kts b/feature/webview/build.gradle.kts new file mode 100644 index 00000000..6f8ab32b --- /dev/null +++ b/feature/webview/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("titi.android.feature") +} + +android { + namespace = "com.titi.app.feature.webview" +} + +dependencies { +} diff --git a/feature/webview/src/main/AndroidManifest.xml b/feature/webview/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/webview/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/webview/src/main/kotlin/com/titi/app/feature/webview/WebViewNavigation.kt b/feature/webview/src/main/kotlin/com/titi/app/feature/webview/WebViewNavigation.kt new file mode 100644 index 00000000..477bcd39 --- /dev/null +++ b/feature/webview/src/main/kotlin/com/titi/app/feature/webview/WebViewNavigation.kt @@ -0,0 +1,37 @@ +package com.titi.app.feature.webview + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument + +private const val WEBVIEW_SCREEN = "webview" +const val WEBVIEW_TITLE_ARG = "title" +const val WEBVIEW_URL_ARG = "url" +const val WEBVIEW_ROUTE = + "$WEBVIEW_SCREEN?$WEBVIEW_TITLE_ARG={$WEBVIEW_TITLE_ARG}&$WEBVIEW_URL_ARG={$WEBVIEW_URL_ARG}" + +fun NavController.navigateToWebView(title: String, url: String) { + navigate("$WEBVIEW_SCREEN?$WEBVIEW_TITLE_ARG=$title&$WEBVIEW_URL_ARG=$url") +} + +fun NavGraphBuilder.webViewGraph(onNavigateUp: () -> Unit) { + composable( + route = WEBVIEW_ROUTE, + arguments = listOf( + navArgument(WEBVIEW_TITLE_ARG) { + type = NavType.StringType + }, + navArgument(WEBVIEW_URL_ARG) { + type = NavType.StringType + }, + ), + ) { + WebViewScreen( + title = it.arguments?.getString(WEBVIEW_TITLE_ARG) ?: "", + url = it.arguments?.getString(WEBVIEW_URL_ARG) ?: "", + onNavigateUp = onNavigateUp, + ) + } +} diff --git a/feature/webview/src/main/kotlin/com/titi/app/feature/webview/WebViewScreen.kt b/feature/webview/src/main/kotlin/com/titi/app/feature/webview/WebViewScreen.kt new file mode 100644 index 00000000..861f502a --- /dev/null +++ b/feature/webview/src/main/kotlin/com/titi/app/feature/webview/WebViewScreen.kt @@ -0,0 +1,92 @@ +package com.titi.app.feature.webview + +import android.view.ViewGroup +import android.webkit.WebChromeClient +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import com.titi.app.core.designsystem.R +import com.titi.app.core.designsystem.component.TdsIconButton +import com.titi.app.core.designsystem.component.TdsText +import com.titi.app.core.designsystem.theme.TdsColor +import com.titi.app.core.designsystem.theme.TdsTextStyle + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WebViewScreen(title: String, url: String, onNavigateUp: () -> Unit) { + Scaffold( + containerColor = Color.White, + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = TdsColor.GROUPED_BACKGROUND.getColor(), + ), + navigationIcon = { + TdsIconButton(onClick = onNavigateUp) { + Icon( + painter = painterResource(id = R.drawable.arrow_left_icon), + contentDescription = "back", + tint = TdsColor.TEXT.getColor(), + ) + } + }, + title = { + TdsText( + text = title, + textStyle = TdsTextStyle.EXTRA_BOLD_TEXT_STYLE, + fontSize = 24.sp, + color = TdsColor.TEXT, + ) + }, + ) + }, + ) { + WebViewScreen( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding() + .padding(it), + url = url, + ) + } +} + +@Composable +private fun WebViewScreen(modifier: Modifier, url: String) { + AndroidView( + modifier = modifier, + factory = { context -> + WebView(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + webViewClient = WebViewClient() + webChromeClient = WebChromeClient() + settings.apply { + javaScriptEnabled = true + domStorageEnabled = true + loadWithOverviewMode = true + useWideViewPort = true + } + } + }, + update = { webView -> + webView.loadUrl(url) + }, + ) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c89aab3..1370c2b6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -117,6 +117,7 @@ androidx-junit-ktx = { group = "androidx.test.ext", name = "junit-ktx", version. firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" } firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" } +firebase-database = { group = "com.google.firebase", name = "firebase-database-ktx" } calendar = { group = "com.kizitonwose.calendar", name = "compose", version.ref = "calendar" } diff --git a/release-note.txt b/release-note.txt index 527e29ed..7eec99f6 100644 --- a/release-note.txt +++ b/release-note.txt @@ -1,3 +1,4 @@ -TiTi android dev 1.0.2(23) -- 타이머, 스탑워치 디자인 수정 -- 그래프 디자인 수정 \ No newline at end of file +TiTi android dev 1.0.3(25) +- 세팅 화면 +- 데일리 추가, 수정 로직 변경 +- 측정화면 activity -> screen \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 93be0a03..7911b62d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -46,3 +46,7 @@ include(":feature:popup") include(":feature:log") include(":data:graph:impl") include(":data:graph:api") +include(":feature:setting") +include(":data:notification:api") +include(":data:notification:impl") +include(":feature:webview")