diff --git a/build-logic/src/main/kotlin/titi.android.feature.gradle.kts b/build-logic/src/main/kotlin/titi.android.feature.gradle.kts index 09739425..2f5425de 100644 --- a/build-logic/src/main/kotlin/titi.android.feature.gradle.kts +++ b/build-logic/src/main/kotlin/titi.android.feature.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(project(":core:ui")) implementation(project(":core:util")) implementation(project(":core:designsystem")) + implementation(project(":tds")) implementation(libs.findLibrary("androidx.compose.navigation").get()) implementation(libs.findLibrary("androidx.hilt.navigation.compose").get()) diff --git a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/theme/Theme.kt index 36e6fcbd..d8f0051a 100644 --- a/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/theme/Theme.kt +++ b/core/designsystem/src/main/kotlin/com/titi/app/core/designsystem/theme/Theme.kt @@ -7,15 +7,13 @@ import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density -val LocalTiTiColors = - staticCompositionLocalOf { - TdsColorsPalette() - } +val LocalTiTiColors = staticCompositionLocalOf { + TdsColorsPalette() +} -val LocalTiTiTypography = - staticCompositionLocalOf { - TdsTypography() - } +val LocalTiTiTypography = staticCompositionLocalOf { + TdsTypography() +} object TiTiTheme { val colors: TdsColorsPalette @@ -33,12 +31,11 @@ fun TiTiTheme( tdsTypography: TdsTypography = TiTiTheme.textStyle, content: @Composable () -> Unit, ) { - val tdsColorsPalette = - if (darkTheme) { - TdsDarkColorsPalette - } else { - TdsLightColorsPalette - } + val tdsColorsPalette = if (darkTheme) { + TdsDarkColorsPalette + } else { + TdsLightColorsPalette + } CompositionLocalProvider( LocalTiTiColors provides tdsColorsPalette, diff --git a/core/ui/src/main/kotlin/com/titi/app/core/ui/ScreenUtil.kt b/core/ui/src/main/kotlin/com/titi/app/core/ui/ScreenUtil.kt new file mode 100644 index 00000000..4291fe31 --- /dev/null +++ b/core/ui/src/main/kotlin/com/titi/app/core/ui/ScreenUtil.kt @@ -0,0 +1,9 @@ +package com.titi.app.core.ui + +import android.content.res.Configuration + +fun Configuration.isTablet(): Boolean { + val screenLayout = this.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK + return screenLayout == Configuration.SCREENLAYOUT_SIZE_LARGE || + screenLayout == Configuration.SCREENLAYOUT_SIZE_XLARGE +} diff --git a/core/util/src/main/kotlin/com/titi/app/core/util/JavaTimeUtil.kt b/core/util/src/main/kotlin/com/titi/app/core/util/JavaTimeUtil.kt index 3f9ab1bc..96866fb2 100644 --- a/core/util/src/main/kotlin/com/titi/app/core/util/JavaTimeUtil.kt +++ b/core/util/src/main/kotlin/com/titi/app/core/util/JavaTimeUtil.kt @@ -1,7 +1,6 @@ package com.titi.app.core.util import java.time.DayOfWeek -import java.time.Duration import java.time.LocalDate import java.time.LocalDateTime import java.time.ZoneId @@ -47,25 +46,48 @@ fun LocalDateTime.toOnlyTime(): String { return this.format(formatter) } -fun addTimeLine( - startTime: ZonedDateTime, - endTime: ZonedDateTime, - timeLine: List, -): List { - var current = startTime - val updateTimeLine = timeLine.toMutableList() +fun getDailyDayWithHour(hour: Int): Pair { + val currentDateTime = LocalDateTime.now() - while (current.isBefore(endTime)) { - val diffSeconds = if (current.hour == endTime.hour) { - Duration.between(current, endTime).seconds - } else { - val nextTime = current.plusHours(1).withMinute(0).withSecond(0) - Duration.between(current, nextTime).seconds - } + return if (currentDateTime.hour < hour) { + val startDateTime = currentDateTime + .minusDays(1) + .withHour(hour) + .withMinute(0) + .withSecond(0) + .withNano(0) + .atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC) + .toString() + val endDateTime = currentDateTime + .withHour(hour) + .withMinute(0) + .withSecond(0) + .withNano(0) + .atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC) + .toString() - updateTimeLine[current.hour] += diffSeconds - current = current.plusHours(1).withMinute(0).withSecond(0) - } + startDateTime to endDateTime + } else { + val startDateTime = currentDateTime + .withHour(hour) + .withMinute(0) + .withSecond(0) + .withNano(0) + .atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC) + .toString() + val endDateTime = currentDateTime + .plusDays(1) + .withHour(hour) + .withMinute(0) + .withSecond(0) + .withNano(0) + .atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC) + .toString() - return updateTimeLine.toList() + startDateTime to endDateTime + } } diff --git a/core/util/src/main/kotlin/com/titi/app/core/util/StringExtensions.kt b/core/util/src/main/kotlin/com/titi/app/core/util/StringExtensions.kt new file mode 100644 index 00000000..3d7a7750 --- /dev/null +++ b/core/util/src/main/kotlin/com/titi/app/core/util/StringExtensions.kt @@ -0,0 +1,11 @@ +package com.titi.app.core.util + +import java.time.ZoneId +import java.time.ZonedDateTime + +fun String.isAfterH(hour: Int): Boolean { + val inputDateTime = ZonedDateTime.parse(this).withZoneSameInstant(ZoneId.systemDefault()) + val currentDateTime = ZonedDateTime.now() + + return inputDateTime.dayOfMonth != currentDateTime.dayOfMonth && currentDateTime.hour >= hour +} diff --git a/core/util/src/main/kotlin/com/titi/app/core/util/TimeUtil.kt b/core/util/src/main/kotlin/com/titi/app/core/util/TimeUtil.kt index 89a1b325..9c516749 100644 --- a/core/util/src/main/kotlin/com/titi/app/core/util/TimeUtil.kt +++ b/core/util/src/main/kotlin/com/titi/app/core/util/TimeUtil.kt @@ -13,11 +13,6 @@ fun String.parseZoneDateTime(): String { return inputDateTime.format(DateTimeFormatter.ofPattern("uuuu.MM.dd")) } -fun getTodayDate(): String { - val now = ZonedDateTime.now() - return now.format(DateTimeFormatter.ofPattern("uuuu.MM.dd")) -} - fun addTimeToNow(time: Long): String { val now = ZonedDateTime.now() val interval = Duration.ofSeconds(time) @@ -31,23 +26,6 @@ fun getTimeToLong(hour: String, minutes: String, seconds: String): Long { return hourLong * 3600 + minutesLong * 60 + secondsLong } -fun isAfterSixAM(dateTime: String?): Boolean { - return if (dateTime.isNullOrBlank()) { - false - } else { - val inputDateTime = - ZonedDateTime.parse(dateTime).withZoneSameInstant(ZoneId.systemDefault()) - val currentDateTime = ZonedDateTime.now() - - if (inputDateTime.dayOfMonth == currentDateTime.dayOfMonth) { - true - } else { - val plusDays = inputDateTime.plusDays(1).dayOfMonth - currentDateTime.hour <= 6 && plusDays == currentDateTime.dayOfMonth - } - } -} - fun getMeasureTime(dateTime: String): Long { val inputDateTime = ZonedDateTime.parse(dateTime) val currentDateTime = ZonedDateTime.now(ZoneOffset.UTC) 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 76839aa4..a67e3b22 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 @@ -39,8 +39,6 @@ interface DailyRepository { suspend fun getDailies(startDateTime: String, endDateTime: String): List? - fun getLastDailyFlow(): Flow - suspend fun getAllDailies(): List? suspend fun upsert(dailyRepositoryModel: DailyRepositoryModel) 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 8457322a..06e4e1f9 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 @@ -12,14 +12,14 @@ internal interface DailyDao { @Query( "SELECT * FROM dailies " + " WHERE datetime(day) " + - "BETWEEN datetime(:startDateTime) AND datetime(:endDateTime)", + "BETWEEN datetime(:startDateTime) AND datetime(:endDateTime) LIMIT 1", ) suspend fun getDateDaily(startDateTime: String, endDateTime: String): DailyEntity? @Query( "SELECT * FROM dailies " + " WHERE datetime(day) " + - "BETWEEN datetime(:startDateTime) AND datetime(:endDateTime)", + "BETWEEN datetime(:startDateTime) AND datetime(:endDateTime) LIMIT 1", ) fun getDateDailyFlow(startDateTime: String, endDateTime: String): Flow @@ -28,10 +28,7 @@ internal interface DailyDao { " WHERE datetime(day) " + "BETWEEN datetime(:startDateTime) AND datetime(:endDateTime)", ) - suspend fun getWeekDaily(startDateTime: String, endDateTime: String): List? - - @Query("SELECT * FROM dailies ORDER BY id desc LIMIT 1") - fun getLastDailyFlow(): Flow + suspend fun getDailies(startDateTime: String, endDateTime: String): List? @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 efb7065f..e1464ddc 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 @@ -36,16 +36,12 @@ internal class DailyRepositoryImpl @Inject constructor( startDateTime: String, endDateTime: String, ): List? { - return dailyDao.getWeekDaily( + return dailyDao.getDailies( startDateTime = startDateTime, endDateTime = endDateTime, )?.map { it.toRepositoryModel() } } - override fun getLastDailyFlow(): Flow { - return dailyDao.getLastDailyFlow().map { it?.toRepositoryModel() } - } - override suspend fun getAllDailies(): List? { return dailyDao.getAllDailies()?.map { it.toRepositoryModel() } } diff --git a/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/model/Daily.kt b/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/model/Daily.kt index 5134f781..9425142c 100644 --- a/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/model/Daily.kt +++ b/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/model/Daily.kt @@ -2,17 +2,17 @@ package com.titi.app.doamin.daily.model import android.os.Parcelable import com.titi.app.core.util.addTimeLine +import com.titi.app.core.util.getDailyDayWithHour import kotlin.math.max import kotlinx.parcelize.Parcelize import org.threeten.bp.Duration -import org.threeten.bp.ZoneOffset import org.threeten.bp.ZonedDateTime @Parcelize data class Daily( val id: Long = 0, val status: String? = null, - val day: String = ZonedDateTime.now(ZoneOffset.UTC).toString(), + val day: String = getDailyDayWithHour(6).first, val timeLine: List = LongArray(24) { 0 }.toList(), val maxTime: Long = 0, val tasks: Map? = null, diff --git a/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/AddMeasureTimeAtDailyUseCase.kt b/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/AddMeasureTimeAtDailyUseCase.kt index ba00a800..494479b5 100644 --- a/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/AddMeasureTimeAtDailyUseCase.kt +++ b/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/AddMeasureTimeAtDailyUseCase.kt @@ -1,44 +1,39 @@ package com.titi.app.doamin.daily.usecase +import com.titi.app.core.util.getDailyDayWithHour import com.titi.app.data.daily.api.DailyRepository import com.titi.app.doamin.daily.mapper.toDomainModel import com.titi.app.doamin.daily.mapper.toRepositoryModel +import com.titi.app.doamin.daily.model.Daily import com.titi.app.doamin.daily.model.TaskHistory import com.titi.app.doamin.daily.model.toUpdateDaily -import java.time.LocalDate -import java.time.ZoneId -import java.time.ZoneOffset import javax.inject.Inject class AddMeasureTimeAtDailyUseCase @Inject constructor( private val dailyRepository: DailyRepository, ) { suspend operator fun invoke(taskName: String, startTime: String, endTime: String) { - val recentDaily = dailyRepository - .getDateDaily( - startDateTime = LocalDate.now() - .atStartOfDay(ZoneId.systemDefault()) - .withZoneSameInstant(ZoneOffset.UTC) - .toString(), - )?.toDomainModel() + val timePair = getDailyDayWithHour(6) + val recentDaily = dailyRepository.getDateDaily( + startDateTime = timePair.first, + endDateTime = timePair.second, + )?.toDomainModel() ?: Daily() - recentDaily?.let { daily -> - val taskHistory = TaskHistory( - startDate = startTime, - endDate = endTime, - ) + val taskHistory = TaskHistory( + startDate = startTime, + endDate = endTime, + ) - val updateTaskHistories = daily.taskHistories?.toMutableMap()?.apply { - this[taskName] = this[taskName] - ?.toMutableList() - ?.apply { add(taskHistory) } - ?: listOf(taskHistory) - }?.toMap() - ?: mapOf(taskName to listOf(taskHistory)) + val updateTaskHistories = recentDaily.taskHistories?.toMutableMap()?.apply { + this[taskName] = this[taskName] + ?.toMutableList() + ?.apply { add(taskHistory) } + ?: listOf(taskHistory) + }?.toMap() + ?: mapOf(taskName to listOf(taskHistory)) - dailyRepository.upsert( - daily.toUpdateDaily(updateTaskHistories).toRepositoryModel(), - ) - } + dailyRepository.upsert( + recentDaily.toUpdateDaily(updateTaskHistories).toRepositoryModel(), + ) } } diff --git a/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/GetLastDailyFlowUseCase.kt b/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/GetLastDailyFlowUseCase.kt deleted file mode 100644 index def45b69..00000000 --- a/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/GetLastDailyFlowUseCase.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.titi.app.doamin.daily.usecase - -import com.titi.app.data.daily.api.DailyRepository -import com.titi.app.doamin.daily.mapper.toDomainModel -import com.titi.app.doamin.daily.model.Daily -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -class GetLastDailyFlowUseCase @Inject constructor( - private val dailyRepository: DailyRepository, -) { - operator fun invoke(): Flow = dailyRepository.getLastDailyFlow().map { - it?.toDomainModel() - } -} diff --git a/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/GetTodayDailyFlowUseCase.kt b/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/GetTodayDailyFlowUseCase.kt new file mode 100644 index 00000000..09e78cdb --- /dev/null +++ b/domain/daily/src/main/kotlin/com/titi/app/doamin/daily/usecase/GetTodayDailyFlowUseCase.kt @@ -0,0 +1,22 @@ +package com.titi.app.doamin.daily.usecase + +import com.titi.app.core.util.getDailyDayWithHour +import com.titi.app.data.daily.api.DailyRepository +import com.titi.app.doamin.daily.mapper.toDomainModel +import com.titi.app.doamin.daily.model.Daily +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetTodayDailyFlowUseCase @Inject constructor( + private val dailyRepository: DailyRepository, +) { + operator fun invoke(): Flow { + val timePair = getDailyDayWithHour(6) + + return dailyRepository.getDateDailyFlow( + startDateTime = timePair.first, + endDateTime = timePair.second, + ).map { it?.toDomainModel() ?: Daily() } + } +} diff --git a/domain/time/src/main/kotlin/com/titi/app/domain/time/usecase/UpdateMeasuringStateUseCase.kt b/domain/time/src/main/kotlin/com/titi/app/domain/time/usecase/UpdateRecordTimesUseCase.kt similarity index 89% rename from domain/time/src/main/kotlin/com/titi/app/domain/time/usecase/UpdateMeasuringStateUseCase.kt rename to domain/time/src/main/kotlin/com/titi/app/domain/time/usecase/UpdateRecordTimesUseCase.kt index 9e94a2c4..ad358613 100644 --- a/domain/time/src/main/kotlin/com/titi/app/domain/time/usecase/UpdateMeasuringStateUseCase.kt +++ b/domain/time/src/main/kotlin/com/titi/app/domain/time/usecase/UpdateRecordTimesUseCase.kt @@ -5,7 +5,7 @@ import com.titi.app.domain.time.mapper.toRepositoryModel import com.titi.app.domain.time.model.RecordTimes import javax.inject.Inject -class UpdateMeasuringStateUseCase @Inject constructor( +class UpdateRecordTimesUseCase @Inject constructor( private val recordTimesRepository: RecordTimesRepository, ) { suspend operator fun invoke(recordTimes: RecordTimes) { diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts index ac087263..4905560a 100644 --- a/feature/main/build.gradle.kts +++ b/feature/main/build.gradle.kts @@ -21,4 +21,5 @@ dependencies { implementation(libs.androidx.splashscreen) implementation(libs.androidx.material3.window.size) + implementation(libs.lottie) } diff --git a/feature/main/src/main/kotlin/com/titi/app/feature/main/model/SplashResultState.kt b/feature/main/src/main/kotlin/com/titi/app/feature/main/model/SplashResultState.kt index dd3f6c71..ee198f02 100644 --- a/feature/main/src/main/kotlin/com/titi/app/feature/main/model/SplashResultState.kt +++ b/feature/main/src/main/kotlin/com/titi/app/feature/main/model/SplashResultState.kt @@ -7,7 +7,7 @@ import com.titi.app.domain.time.model.RecordTimes data class SplashResultState( val recordTimes: RecordTimes = RecordTimes(), val timeColor: TimeColor = TimeColor(), - val daily: Daily? = null, + val daily: Daily = Daily(), ) fun SplashResultState.toFeatureTimeModel() = com.titi.app.feature.time.model.SplashResultState( 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 index 4cb4b77f..267af2e7 100644 --- 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 @@ -6,11 +6,30 @@ import android.os.Build import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import com.titi.app.core.ui.isTablet import com.titi.app.feature.main.model.SplashResultState +import com.titi.app.tds.R +import com.titi.app.tds.component.TtdsSnackbarHost +import com.titi.app.tds.component.TtdsSnackbarHostState +import kotlinx.coroutines.launch @SuppressLint("UnusedContentLambdaTargetStateParameter") @Composable @@ -22,6 +41,10 @@ fun TiTiApp(splashResultState: SplashResultState) { Log.e("MainActivity", isGranted.toString()) } + val scope = rememberCoroutineScope() + val snackbarHostState = remember { TtdsSnackbarHostState() } + val configuration = LocalConfiguration.current + fun askNotificationPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) @@ -32,8 +55,40 @@ fun TiTiApp(splashResultState: SplashResultState) { askNotificationPermission() } - TiTiNavHost( - modifier = Modifier.fillMaxSize(), - splashResultState = splashResultState, - ) + Box(modifier = Modifier.fillMaxSize()) { + TiTiNavHost( + modifier = Modifier.fillMaxSize(), + splashResultState = splashResultState, + onShowResetDailySnackBar = { date -> + scope.launch { + snackbarHostState.showSnackbar( + startIcon = { + val composition by rememberLottieComposition( + LottieCompositionSpec.RawRes(R.raw.reset_daily_lottie), + ) + val progress by animateLottieCompositionAsState(composition) + + LottieAnimation( + modifier = Modifier + .size(22.dp) + .padding(4.dp), + composition = composition, + progress = { progress }, + ) + }, + emphasizedMessage = date, + message = "기록 시작!", + targetDpFromTop = if (configuration.isTablet()) 80.dp else 40.dp, + ) + } + }, + ) + + TtdsSnackbarHost( + modifier = Modifier + .fillMaxWidth() + .statusBarsPadding(), + hostState = snackbarHostState, + ) + } } 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 33a5270a..c30fa699 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 @@ -37,7 +37,11 @@ import com.titi.app.feature.webview.navigateToWebView import com.titi.app.feature.webview.webViewGraph @Composable -fun TiTiNavHost(splashResultState: SplashResultState, modifier: Modifier = Modifier) { +fun TiTiNavHost( + modifier: Modifier = Modifier, + splashResultState: SplashResultState, + onShowResetDailySnackBar: (String) -> Unit, +) { val navController = rememberNavController() val context = LocalContext.current @@ -73,6 +77,7 @@ fun TiTiNavHost(splashResultState: SplashResultState, modifier: Modifier = Modif onNavigateToDestination = { navController.navigateToTopLevelDestination(it) }, + onShowResetDailySnackBar = onShowResetDailySnackBar, ) measureGraph( 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 90ddfb61..8b7099e8 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.GetLastDailyFlowUseCase +import com.titi.app.doamin.daily.usecase.GetTodayDailyFlowUseCase 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, - getLastDailyFlowUseCase: GetLastDailyFlowUseCase, + getTodayDailyFlowUseCase: GetTodayDailyFlowUseCase, ) : ViewModel() { val splashResultState: SharedFlow = combine( getRecordTimesFlowUseCase(), getTimeColorFlowUseCase(), - getLastDailyFlowUseCase(), + getTodayDailyFlowUseCase(), ) { recordTimes, timeColor, daily -> SplashResultState( recordTimes = recordTimes, diff --git a/feature/measure/src/main/kotlin/com/titi/app/feature/measure/model/MeasuringUiState.kt b/feature/measure/src/main/kotlin/com/titi/app/feature/measure/model/MeasuringUiState.kt index 7ea9b988..0565ba20 100644 --- a/feature/measure/src/main/kotlin/com/titi/app/feature/measure/model/MeasuringUiState.kt +++ b/feature/measure/src/main/kotlin/com/titi/app/feature/measure/model/MeasuringUiState.kt @@ -41,7 +41,7 @@ data class MeasuringUiState( splashResultState.timeColor.toMeasuringTimeColor( recordTimes.recordingMode, ) - val daily: Daily? get() = splashResultState.daily + val daily: Daily get() = splashResultState.daily } fun getSplashResultStateFromArgs(args: Bundle): SplashResultState = @@ -77,7 +77,7 @@ fun TimeColor.toMeasuringTimeColor(recordingMode: Int) = MeasuringTimeColor( fun RecordTimes.toMeasuringRecordTimes( isSleepMode: Boolean, measureTime: Long, - daily: Daily?, + daily: Daily, ): MeasuringRecordTimes { val calculateSumTime = savedSumTime + measureTime val calculateSavedSumTime = @@ -104,43 +104,38 @@ fun RecordTimes.toMeasuringRecordTimes( calculateTime } - val calculateGoalTime = - currentTask?.let { - if (it.isTaskTargetTimeOn) { - it.taskTargetTime - (daily?.tasks?.get(it.taskName) ?: 0) - measureTime - } else { - savedGoalTime - measureTime - } - } ?: (savedGoalTime - measureTime) - val calculateSavedGoalTime = - if (isSleepMode) { - calculateGoalTime - calculateGoalTime % 60 + val calculateGoalTime = currentTask?.let { + if (it.isTaskTargetTimeOn) { + it.taskTargetTime - (daily.tasks?.get(it.taskName) ?: 0) - measureTime } else { - calculateGoalTime + savedGoalTime - measureTime } + } ?: (savedGoalTime - measureTime) + val calculateSavedGoalTime = if (isSleepMode) { + calculateGoalTime - calculateGoalTime % 60 + } else { + calculateGoalTime + } val finishGoalTime = addTimeToNow(calculateSavedGoalTime) - val outCircularDividend = - if (recordingMode == 1) { - setTimerTime - calculateSavedTime - } else { - calculateSavedTime - } - val outCircularDivisor = - if (recordingMode == 1) { - setTimerTime.toFloat() - } else { - 3600f - } + val outCircularDividend = if (recordingMode == 1) { + setTimerTime - calculateSavedTime + } else { + calculateSavedTime + } + val outCircularDivisor = if (recordingMode == 1) { + setTimerTime.toFloat() + } else { + 3600f + } val outCircularProgress = outCircularDividend / outCircularDivisor - val inCircularDivisor = - if (currentTask?.isTaskTargetTimeOn == true) { - currentTask?.taskTargetTime?.toFloat() ?: 0f - } else { - setGoalTime.toFloat() - } + val inCircularDivisor = if (currentTask?.isTaskTargetTimeOn == true) { + currentTask?.taskTargetTime?.toFloat() ?: 0f + } else { + setGoalTime.toFloat() + } val inCircularProgress = calculateSavedSumTime / inCircularDivisor return MeasuringRecordTimes( diff --git a/feature/measure/src/main/kotlin/com/titi/app/feature/measure/model/SplashResultState.kt b/feature/measure/src/main/kotlin/com/titi/app/feature/measure/model/SplashResultState.kt index 802acfe1..69fd65ef 100644 --- a/feature/measure/src/main/kotlin/com/titi/app/feature/measure/model/SplashResultState.kt +++ b/feature/measure/src/main/kotlin/com/titi/app/feature/measure/model/SplashResultState.kt @@ -10,5 +10,5 @@ import kotlinx.parcelize.Parcelize data class SplashResultState( val recordTimes: RecordTimes = RecordTimes(), val timeColor: TimeColor = TimeColor(), - val daily: Daily? = null, + val daily: Daily = Daily(), ) : Parcelable diff --git a/feature/time/src/main/kotlin/com/titi/app/feature/time/model/SplashResultState.kt b/feature/time/src/main/kotlin/com/titi/app/feature/time/model/SplashResultState.kt index 09f0937e..26ed9ea1 100644 --- a/feature/time/src/main/kotlin/com/titi/app/feature/time/model/SplashResultState.kt +++ b/feature/time/src/main/kotlin/com/titi/app/feature/time/model/SplashResultState.kt @@ -10,5 +10,5 @@ import kotlinx.parcelize.Parcelize data class SplashResultState( val recordTimes: RecordTimes = RecordTimes(), val timeColor: TimeColor = TimeColor(), - val daily: Daily? = null, + val daily: Daily = Daily(), ) : Parcelable 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 013cd526..a812e941 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 @@ -3,8 +3,6 @@ package com.titi.app.feature.time.model 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.parseZoneDateTime import com.titi.app.doamin.daily.model.Daily import com.titi.app.domain.color.model.TimeColor import com.titi.app.domain.time.model.RecordTimes @@ -12,7 +10,9 @@ import com.titi.app.domain.time.model.RecordTimes data class StopWatchUiState( val recordTimes: RecordTimes, val timeColor: TimeColor, - val daily: Daily?, + val daily: Daily, + val splashResultStateString: String? = null, + val showResetDailySnackBar: Boolean = false, ) : MavericksState { constructor(args: Bundle) : this( recordTimes = getSplashResultStateFromArgs(args).recordTimes, @@ -20,7 +20,6 @@ data class StopWatchUiState( daily = getSplashResultStateFromArgs(args).daily, ) - val todayDate: String = daily?.day?.parseZoneDateTime() ?: getTodayDate() val isSetTask: Boolean = recordTimes.currentTask != null val taskName: String = recordTimes.currentTask?.taskName ?: "" val stopWatchColor = timeColor.toUiModel() @@ -47,22 +46,21 @@ data class StopWatchRecordTimes( val isTaskTargetTimeOn: Boolean, ) -private fun RecordTimes.toUiModel(daily: Daily?): StopWatchRecordTimes { - val goalTime = - currentTask?.let { - if (it.isTaskTargetTimeOn) { - it.taskTargetTime - (daily?.tasks?.get(it.taskName) ?: 0) - } else { - savedGoalTime - } - } ?: savedGoalTime +private fun RecordTimes.toUiModel(daily: Daily): StopWatchRecordTimes { + val goalTime = currentTask?.let { + if (it.isTaskTargetTimeOn) { + it.taskTargetTime - (daily.tasks?.get(it.taskName) ?: 0) + } else { + savedGoalTime + } + } ?: savedGoalTime return StopWatchRecordTimes( outCircularProgress = savedStopWatchTime / 3600f, inCircularProgress = currentTask?.let { if (it.isTaskTargetTimeOn) { - val taskTime = daily?.tasks?.get(it.taskName) ?: 0 + val taskTime = daily.tasks?.get(it.taskName) ?: 0 taskTime / it.taskTargetTime.toFloat() } else { savedSumTime / setGoalTime.toFloat() 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 7d745352..fdcf5d27 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 @@ -5,8 +5,6 @@ import android.os.Bundle 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.parseZoneDateTime import com.titi.app.doamin.daily.model.Daily import com.titi.app.domain.color.model.TimeColor import com.titi.app.domain.time.model.RecordTimes @@ -14,7 +12,9 @@ import com.titi.app.domain.time.model.RecordTimes data class TimerUiState( val recordTimes: RecordTimes, val timeColor: TimeColor, - val daily: Daily?, + val daily: Daily, + val splashResultStateString: String? = null, + val showResetDailySnackBar: Boolean = false, ) : MavericksState { constructor(args: Bundle) : this( recordTimes = getSplashResultStateFromArgs(args).recordTimes, @@ -22,7 +22,6 @@ data class TimerUiState( daily = getSplashResultStateFromArgs(args).daily, ) - val todayDate: String = daily?.day?.parseZoneDateTime() ?: getTodayDate() val isSetTask: Boolean = recordTimes.currentTask != null val taskName: String = recordTimes.currentTask?.taskName ?: "" val timerColor = timeColor.toUiModel() @@ -59,22 +58,20 @@ data class TimerRecordTimes( val isTaskTargetTimeOn: Boolean, ) -private fun RecordTimes.toUiModel(daily: Daily?): TimerRecordTimes { - val goalTime = - currentTask?.let { - if (it.isTaskTargetTimeOn) { - it.taskTargetTime - (daily?.tasks?.get(it.taskName) ?: 0) - } else { - savedGoalTime - } - } ?: savedGoalTime +private fun RecordTimes.toUiModel(daily: Daily): TimerRecordTimes { + val goalTime = currentTask?.let { + if (it.isTaskTargetTimeOn) { + it.taskTargetTime - (daily.tasks?.get(it.taskName) ?: 0) + } else { + savedGoalTime + } + } ?: savedGoalTime return TimerRecordTimes( outCircularProgress = (setTimerTime - savedTimerTime) / setTimerTime.toFloat(), - inCircularProgress = - currentTask?.let { + inCircularProgress = currentTask?.let { if (it.isTaskTargetTimeOn) { - val taskTime = daily?.tasks?.get(it.taskName) ?: 0 + val taskTime = daily.tasks?.get(it.taskName) ?: 0 taskTime / it.taskTargetTime.toFloat() } else { savedSumTime / setGoalTime.toFloat() 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 04b6bcce..99f1ec2b 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 @@ -31,6 +31,7 @@ fun NavGraphBuilder.timeGraph( onNavigateToColor: (Int) -> Unit, onNavigateToMeasure: (String) -> Unit, onNavigateToDestination: (TopLevelDestination) -> Unit, + onShowResetDailySnackBar: (String) -> Unit, ) { composable(route = TIMER_ROUTE) { backStackEntry -> val isFinish by backStackEntry @@ -47,6 +48,7 @@ fun NavGraphBuilder.timeGraph( onNavigateToColor = { onNavigateToColor(1) }, onNavigateToMeasure = onNavigateToMeasure, onNavigateToDestination = onNavigateToDestination, + onShowResetDailySnackBar = onShowResetDailySnackBar, ) } @@ -56,6 +58,7 @@ fun NavGraphBuilder.timeGraph( onNavigateToColor = { onNavigateToColor(2) }, onNavigateToMeasure = onNavigateToMeasure, onNavigateToDestination = onNavigateToDestination, + onShowResetDailySnackBar = onShowResetDailySnackBar, ) } } 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 cae72517..739ab515 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 @@ -27,6 +27,7 @@ import com.titi.app.core.designsystem.component.TdsTimer import com.titi.app.core.designsystem.extension.getTdsTime import com.titi.app.core.designsystem.navigation.TdsBottomNavigationBar import com.titi.app.core.designsystem.navigation.TopLevelDestination +import com.titi.app.core.util.parseZoneDateTime import com.titi.app.feature.time.component.TimeButtonComponent import com.titi.app.feature.time.component.TimeCheckTaskDialog import com.titi.app.feature.time.component.TimeColorDialog @@ -43,19 +44,14 @@ fun StopWatchScreen( onNavigateToColor: () -> Unit, onNavigateToMeasure: (String) -> Unit, onNavigateToDestination: (TopLevelDestination) -> Unit, + onShowResetDailySnackBar: (String) -> Unit, ) { val viewModel: StopWatchViewModel = mavericksViewModel( argsFactory = { splashResultState.asMavericksArgs() }, ) - - LaunchedEffect(Unit) { - viewModel.updateRecordingMode() - } - val uiState by viewModel.collectAsState() - var showTaskBottomSheet by remember { mutableStateOf(false) } var showSelectColorDialog by remember { mutableStateOf(false) } var showGoalTimeEditDialog by remember { mutableStateOf(false) } @@ -91,14 +87,11 @@ fun StopWatchScreen( if (showGoalTimeEditDialog) { TimeGoalTimeEditDialog( - todayDate = uiState.todayDate, + todayDate = uiState.daily.day.parseZoneDateTime(), currentTime = uiState.recordTimes.setGoalTime.getTdsTime(), onPositive = { goalTime -> if (goalTime > 0) { - viewModel.updateSetGoalTime( - uiState.recordTimes, - goalTime, - ) + viewModel.updateSetGoalTime(goalTime) } }, onShowDialog = { @@ -115,6 +108,25 @@ fun StopWatchScreen( ) } + LaunchedEffect(Unit) { + viewModel.init() + viewModel.updateDailyRecordTimesAfterH() + } + + LaunchedEffect(uiState.showResetDailySnackBar) { + if (uiState.showResetDailySnackBar) { + onShowResetDailySnackBar(uiState.daily.day.parseZoneDateTime().substring(5)) + viewModel.initShowResetDailySnackBar() + } + } + + LaunchedEffect(uiState.splashResultStateString) { + uiState.splashResultStateString?.let { + onNavigateToMeasure(it) + viewModel.initSplashResultStateString() + } + } + StopWatchScreen( uiState = uiState, textColor = if (uiState.stopWatchColor.isTextColorBlack) { @@ -134,18 +146,13 @@ fun StopWatchScreen( }, onClickStartRecord = { if (uiState.isSetTask) { - val splashResultStateString = viewModel.startRecording( - recordTimes = uiState.recordTimes, - daily = uiState.daily, - timeColor = uiState.timeColor, - ) - onNavigateToMeasure(splashResultStateString) + viewModel.startRecording() } else { showGoalTimeEditDialog = true } }, onClickResetStopWatch = { - viewModel.updateSavedStopWatchTime(uiState.recordTimes) + viewModel.updateSavedStopWatchTime() }, onNavigateToDestination = onNavigateToDestination, ) @@ -186,7 +193,7 @@ private fun StopWatchScreen( horizontalAlignment = Alignment.CenterHorizontally, ) { TimeHeaderComponent( - todayDate = uiState.todayDate, + todayDate = uiState.daily.day.parseZoneDateTime(), textColor = textColor, onClickColor = onClickColor, ) 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 bb767be7..7db8de59 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,17 +5,14 @@ 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.isAfterH import com.titi.app.core.util.toJson import com.titi.app.doamin.daily.model.Daily -import com.titi.app.doamin.daily.usecase.GetLastDailyFlowUseCase -import com.titi.app.doamin.daily.usecase.UpsertDailyUseCase -import com.titi.app.domain.color.model.TimeColor +import com.titi.app.doamin.daily.usecase.GetTodayDailyFlowUseCase import com.titi.app.domain.color.usecase.GetTimeColorFlowUseCase import com.titi.app.domain.color.usecase.UpdateColorUseCase -import com.titi.app.domain.time.model.RecordTimes import com.titi.app.domain.time.usecase.GetRecordTimesFlowUseCase -import com.titi.app.domain.time.usecase.UpdateMeasuringStateUseCase +import com.titi.app.domain.time.usecase.UpdateRecordTimesUseCase import com.titi.app.domain.time.usecase.UpdateRecordingModeUseCase import com.titi.app.domain.time.usecase.UpdateSavedStopWatchTimeUseCase import com.titi.app.domain.time.usecase.UpdateSetGoalTimeUseCase @@ -26,25 +23,26 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.distinctUntilChanged 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, - getLastDailyFlowUseCase: GetLastDailyFlowUseCase, + private val getRecordTimesFlowUseCase: GetRecordTimesFlowUseCase, + private val getTimeColorFlowUseCase: GetTimeColorFlowUseCase, + private val getTodayDailyFlowUseCase: GetTodayDailyFlowUseCase, private val updateRecordingModeUseCase: UpdateRecordingModeUseCase, private val updateColorUseCase: UpdateColorUseCase, private val updateSetGoalTimeUseCase: UpdateSetGoalTimeUseCase, - private val upsertDailyUseCase: UpsertDailyUseCase, - private val updateMeasuringStateUseCase: UpdateMeasuringStateUseCase, + private val updateRecordTimesUseCase: UpdateRecordTimesUseCase, private val updateSavedStopWatchTimeUseCase: UpdateSavedStopWatchTimeUseCase, ) : MavericksViewModel(initialState) { + private lateinit var prevStopWatchColor: StopWatchColor - init { + fun init() { getRecordTimesFlowUseCase().catch { Log.e("TimeViewModel", it.message.toString()) }.setOnEach { @@ -57,16 +55,41 @@ class StopWatchViewModel @AssistedInject constructor( copy(timeColor = it) } - getLastDailyFlowUseCase().catch { - Log.e("TimeViewModel", it.message.toString()) - }.setOnEach { - copy(daily = it) - } + getTodayDailyFlowUseCase() + .distinctUntilChanged() + .catch { + Log.e("TimeViewModel", it.message.toString()) + }.setOnEach { + copy(daily = it) + } } - fun updateRecordingMode() { - viewModelScope.launch { - updateRecordingModeUseCase(2) + fun updateDailyRecordTimesAfterH() { + withState { + if (it.daily.day.isAfterH(6)) { + viewModelScope.launch { + updateRecordTimesUseCase( + recordTimes = it.recordTimes.copy( + recordingMode = 2, + savedSumTime = 0, + savedTimerTime = it.recordTimes.setTimerTime, + savedStopWatchTime = 0, + savedGoalTime = it.recordTimes.setGoalTime, + ), + ) + } + + setState { + copy( + daily = Daily(), + showResetDailySnackBar = true, + ) + } + } else { + viewModelScope.launch { + updateRecordingModeUseCase(2) + } + } } } @@ -95,60 +118,66 @@ class StopWatchViewModel @AssistedInject constructor( prevStopWatchColor = stopWatchColor } - fun updateSetGoalTime(recordTimes: RecordTimes, setGoalTime: Long) { - viewModelScope.launch { - updateSetGoalTimeUseCase( - recordTimes, - setGoalTime, - ) + fun updateSetGoalTime(setGoalTime: Long) { + withState { + viewModelScope.launch { + updateSetGoalTimeUseCase( + it.recordTimes, + setGoalTime, + ) + } } } - fun startRecording(recordTimes: RecordTimes, daily: Daily?, timeColor: TimeColor): String { - val updateRecordTimes = if (isAfterSixAM(daily?.day)) { - if (recordTimes.savedTimerTime <= 0) { - recordTimes.copy( + fun startRecording() { + withState { + val isAfterH = it.daily.day.isAfterH(6) + val updatePair = if (isAfterH) { + it.recordTimes.copy( recording = true, recordStartAt = ZonedDateTime.now(ZoneOffset.UTC).toString(), - savedTimerTime = recordTimes.setTimerTime, - ) + savedSumTime = 0, + savedTimerTime = it.recordTimes.setTimerTime, + savedStopWatchTime = 0, + savedGoalTime = it.recordTimes.setGoalTime, + ) to Daily() } else { - recordTimes.copy( + it.recordTimes.copy( recording = true, recordStartAt = ZonedDateTime.now(ZoneOffset.UTC).toString(), + ) to it.daily + } + + setState { + copy( + splashResultStateString = SplashResultState( + recordTimes = updatePair.first, + daily = updatePair.second, + timeColor = it.timeColor, + ).toJson(), + showResetDailySnackBar = isAfterH, ) } - } 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 updateSavedStopWatchTime() { + withState { + viewModelScope.launch { + updateSavedStopWatchTimeUseCase(it.recordTimes) + } } + } - viewModelScope.launch { - updateMeasuringStateUseCase(updateRecordTimes) - upsertDailyUseCase(updateDaily) + fun initSplashResultStateString() { + setState { + copy(splashResultStateString = null) } - - return SplashResultState( - recordTimes = updateRecordTimes, - daily = updateDaily, - timeColor = timeColor, - ).toJson() } - fun updateSavedStopWatchTime(recordTimes: RecordTimes) { - viewModelScope.launch { - updateSavedStopWatchTimeUseCase(recordTimes) + fun initShowResetDailySnackBar() { + setState { + copy(showResetDailySnackBar = false) } } 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 01967a49..2171539f 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 @@ -28,6 +28,7 @@ import com.titi.app.core.designsystem.component.TdsTimer import com.titi.app.core.designsystem.extension.getTdsTime import com.titi.app.core.designsystem.navigation.TdsBottomNavigationBar import com.titi.app.core.designsystem.navigation.TopLevelDestination +import com.titi.app.core.util.parseZoneDateTime import com.titi.app.feature.time.component.TimeButtonComponent import com.titi.app.feature.time.component.TimeCheckTaskDialog import com.titi.app.feature.time.component.TimeColorDialog @@ -47,19 +48,14 @@ fun TimerScreen( onNavigateToColor: () -> Unit, onNavigateToMeasure: (String) -> Unit, onNavigateToDestination: (TopLevelDestination) -> Unit, + onShowResetDailySnackBar: (String) -> Unit, ) { val viewModel: TimerViewModel = mavericksViewModel( argsFactory = { splashResultState.asMavericksArgs() }, ) - - LaunchedEffect(Unit) { - viewModel.updateRecordingMode() - } - val uiState by viewModel.collectAsState() - var showTaskBottomSheet by remember { mutableStateOf(false) } var showSelectColorDialog by remember { mutableStateOf(false) } var showGoalTimeEditDialog by remember { mutableStateOf(false) } @@ -98,14 +94,11 @@ fun TimerScreen( if (showGoalTimeEditDialog) { TimeGoalTimeEditDialog( - todayDate = uiState.todayDate, + todayDate = uiState.daily.day.parseZoneDateTime(), currentTime = uiState.recordTimes.setGoalTime.getTdsTime(), onPositive = { goalTime -> if (goalTime > 0) { - viewModel.updateSetGoalTime( - uiState.recordTimes, - goalTime, - ) + viewModel.updateSetGoalTime(goalTime) onChangeFinishStateFalse() } @@ -128,10 +121,7 @@ fun TimerScreen( TimeTimerDialog( onPositive = { if (it > 0) { - viewModel.updateSetTimerTime( - uiState.recordTimes, - it, - ) + viewModel.updateSetTimerTime(it) onChangeFinishStateFalse() } }, @@ -141,6 +131,25 @@ fun TimerScreen( ) } + LaunchedEffect(Unit) { + viewModel.init() + viewModel.updateDailyRecordTimesAfterH() + } + + LaunchedEffect(uiState.showResetDailySnackBar) { + if (uiState.showResetDailySnackBar) { + onShowResetDailySnackBar(uiState.daily.day.parseZoneDateTime().substring(5)) + viewModel.initShowResetDailySnackBar() + } + } + + LaunchedEffect(uiState.splashResultStateString) { + uiState.splashResultStateString?.let { + onNavigateToMeasure(it) + viewModel.initSplashResultStateString() + } + } + TimerScreen( uiState = uiState, isFinish = isFinish, @@ -161,12 +170,7 @@ fun TimerScreen( }, onClickStartRecord = { if (uiState.isSetTask) { - val splashResultStateString = viewModel.startRecording( - recordTimes = uiState.recordTimes, - daily = uiState.daily, - timeColor = uiState.timeColor, - ) - onNavigateToMeasure(splashResultStateString) + viewModel.startRecording() } else { showCheckTaskDialog = true } @@ -215,7 +219,7 @@ private fun TimerScreen( verticalArrangement = Arrangement.Center, ) { TimeHeaderComponent( - todayDate = uiState.todayDate, + todayDate = uiState.daily.day.parseZoneDateTime(), textColor = textColor, onClickColor = onClickColor, ) 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 e058381b..ec5d53c9 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,17 +5,14 @@ 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.isAfterH import com.titi.app.core.util.toJson import com.titi.app.doamin.daily.model.Daily -import com.titi.app.doamin.daily.usecase.GetLastDailyFlowUseCase -import com.titi.app.doamin.daily.usecase.UpsertDailyUseCase -import com.titi.app.domain.color.model.TimeColor +import com.titi.app.doamin.daily.usecase.GetTodayDailyFlowUseCase import com.titi.app.domain.color.usecase.GetTimeColorFlowUseCase import com.titi.app.domain.color.usecase.UpdateColorUseCase -import com.titi.app.domain.time.model.RecordTimes import com.titi.app.domain.time.usecase.GetRecordTimesFlowUseCase -import com.titi.app.domain.time.usecase.UpdateMeasuringStateUseCase +import com.titi.app.domain.time.usecase.UpdateRecordTimesUseCase import com.titi.app.domain.time.usecase.UpdateRecordingModeUseCase import com.titi.app.domain.time.usecase.UpdateSetGoalTimeUseCase import com.titi.app.domain.time.usecase.UpdateSetTimerTimeUseCase @@ -26,23 +23,26 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.distinctUntilChanged 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, - getLastDailyFlowUseCase: GetLastDailyFlowUseCase, + private val getRecordTimesFlowUseCase: GetRecordTimesFlowUseCase, + private val getTimeColorFlowUseCase: GetTimeColorFlowUseCase, + private val getTodayDailyFlowUseCase: GetTodayDailyFlowUseCase, private val updateRecordingModeUseCase: UpdateRecordingModeUseCase, private val updateColorUseCase: UpdateColorUseCase, private val updateSetGoalTimeUseCase: UpdateSetGoalTimeUseCase, - private val upsertDailyUseCase: UpsertDailyUseCase, - private val updateMeasuringStateUseCase: UpdateMeasuringStateUseCase, + private val updateRecordTimesUseCase: UpdateRecordTimesUseCase, private val updateSetTimerTimeUseCase: UpdateSetTimerTimeUseCase, ) : MavericksViewModel(initialState) { - init { + + private lateinit var prevTimerColor: TimerColor + + fun init() { getRecordTimesFlowUseCase().catch { Log.e("TimeViewModel", it.message.toString()) }.setOnEach { @@ -55,18 +55,42 @@ class TimerViewModel @AssistedInject constructor( copy(timeColor = it) } - getLastDailyFlowUseCase().catch { - Log.e("TimeViewModel", it.message.toString()) - }.setOnEach { - copy(daily = it) - } + getTodayDailyFlowUseCase() + .distinctUntilChanged() + .catch { + Log.e("TimeViewModel", it.message.toString()) + } + .setOnEach { + copy(daily = it) + } } - private lateinit var prevTimerColor: TimerColor - - fun updateRecordingMode() { - viewModelScope.launch { - updateRecordingModeUseCase(1) + fun updateDailyRecordTimesAfterH() { + withState { + if (it.daily.day.isAfterH(6)) { + viewModelScope.launch { + updateRecordTimesUseCase( + recordTimes = it.recordTimes.copy( + recordingMode = 1, + savedSumTime = 0, + savedTimerTime = it.recordTimes.setTimerTime, + savedStopWatchTime = 0, + savedGoalTime = it.recordTimes.setGoalTime, + ), + ) + } + + setState { + copy( + daily = Daily(), + showResetDailySnackBar = true, + ) + } + } else { + viewModelScope.launch { + updateRecordingModeUseCase(1) + } + } } } @@ -95,64 +119,71 @@ class TimerViewModel @AssistedInject constructor( prevTimerColor = timerColor } - fun updateSetGoalTime(recordTimes: RecordTimes, setGoalTime: Long) { - viewModelScope.launch { - updateSetGoalTimeUseCase( - recordTimes, - setGoalTime, - ) + fun updateSetGoalTime(setGoalTime: Long) { + withState { + viewModelScope.launch { + updateSetGoalTimeUseCase( + it.recordTimes, + setGoalTime, + ) + } } } - fun startRecording(recordTimes: RecordTimes, daily: Daily?, timeColor: TimeColor): String { - val updateRecordTimes = if (isAfterSixAM(daily?.day)) { - if (recordTimes.savedTimerTime <= 0) { - recordTimes.copy( + fun startRecording() { + withState { + val isAfterH = it.daily.day.isAfterH(6) + val updatePair = if (isAfterH) { + it.recordTimes.copy( recording = true, recordStartAt = ZonedDateTime.now(ZoneOffset.UTC).toString(), - savedTimerTime = recordTimes.setTimerTime, - ) + savedSumTime = 0, + savedTimerTime = it.recordTimes.setTimerTime, + savedStopWatchTime = 0, + savedGoalTime = it.recordTimes.setGoalTime, + ) to Daily() } else { - recordTimes.copy( + it.recordTimes.copy( recording = true, recordStartAt = ZonedDateTime.now(ZoneOffset.UTC).toString(), + ) to it.daily + } + + setState { + copy( + recordTimes = updatePair.first, + daily = updatePair.second, + splashResultStateString = SplashResultState( + recordTimes = updatePair.first, + daily = updatePair.second, + timeColor = it.timeColor, + ).toJson(), + showResetDailySnackBar = isAfterH, ) } - } 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 updateSetTimerTime(timerTime: Long) { + withState { + viewModelScope.launch { + updateSetTimerTimeUseCase( + it.recordTimes, + timerTime, + ) + } } + } - viewModelScope.launch { - updateMeasuringStateUseCase(updateRecordTimes) - upsertDailyUseCase(updateDaily) + fun initSplashResultStateString() { + setState { + copy(splashResultStateString = null) } - - return SplashResultState( - recordTimes = updateRecordTimes, - daily = updateDaily, - timeColor = timeColor, - ).toJson() } - fun updateSetTimerTime(recordTimes: RecordTimes, timerTime: Long) { - viewModelScope.launch { - updateSetTimerTimeUseCase( - recordTimes, - timerTime, - ) + fun initShowResetDailySnackBar() { + setState { + copy(showResetDailySnackBar = false) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ac70d180..e2a7e512 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,6 +50,7 @@ ktlint = "12.1.0" calendar = "2.5.0-beta01" picker = "1.0.3" +lottie = "6.4.1" [libraries] android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" } @@ -122,6 +123,7 @@ firebase-database = { group = "com.google.firebase", name = "firebase-database-k calendar = { group = "com.kizitonwose.calendar", name = "compose", version.ref = "calendar" } picker = { group = "com.chargemap.compose", name = "numberpicker", version.ref = "picker" } +lottie = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottie" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 1fc6245f..0399c87f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -51,3 +51,4 @@ include(":data:notification:api") include(":data:notification:impl") include(":feature:webview") include(":feature:edit") +include(":tds") diff --git a/tds/build.gradle.kts b/tds/build.gradle.kts new file mode 100644 index 00000000..89392000 --- /dev/null +++ b/tds/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("titi.android.compose") + id("titi.android.library") +} + +android { + namespace = "com.titi.app.tds" +} diff --git a/tds/src/androidTest/kotlin/com/titi/app/tds/ExampleInstrumentedTest.kt b/tds/src/androidTest/kotlin/com/titi/app/tds/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..a4f971ee --- /dev/null +++ b/tds/src/androidTest/kotlin/com/titi/app/tds/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.titi.app.tds + +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.tds.test", appContext.packageName) + } +} diff --git a/tds/src/main/AndroidManifest.xml b/tds/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/tds/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/tds/src/main/kotlin/com/titi/app/tds/component/TdsIcon.kt b/tds/src/main/kotlin/com/titi/app/tds/component/TdsIcon.kt new file mode 100644 index 00000000..a8b65534 --- /dev/null +++ b/tds/src/main/kotlin/com/titi/app/tds/component/TdsIcon.kt @@ -0,0 +1,39 @@ +package com.titi.app.tds.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.titi.app.tds.R +import com.titi.app.tds.theme.TtdsColor +import com.titi.app.tds.theme.TtdsTheme + +@Composable +fun TtdsSmallIcon( + modifier: Modifier = Modifier, + tint: TtdsColor = TtdsColor.PRIMARY, + @DrawableRes icon: Int, +) { + Icon( + modifier = Modifier + .size(22.dp) + .padding(4.dp) + .then(modifier), + painter = painterResource(id = icon), + tint = tint.getColor(), + contentDescription = null, + ) +} + +@Preview +@Composable +private fun TtdsSmallIconPreview() { + TtdsTheme { + TtdsSmallIcon(icon = R.drawable.reset_daily_icon) + } +} diff --git a/tds/src/main/kotlin/com/titi/app/tds/component/TtdsSnackBarHost.kt b/tds/src/main/kotlin/com/titi/app/tds/component/TtdsSnackBarHost.kt new file mode 100644 index 00000000..82094319 --- /dev/null +++ b/tds/src/main/kotlin/com/titi/app/tds/component/TtdsSnackBarHost.kt @@ -0,0 +1,338 @@ +package com.titi.app.tds.component + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.RecomposeScope +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.currentRecomposeScope +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.AccessibilityManager +import androidx.compose.ui.platform.LocalAccessibilityManager +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.dismiss +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.coroutines.resume +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.delay +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +@Stable +class TtdsSnackbarHostState { + + private val mutex = Mutex() + + var currentSnackbarData by mutableStateOf(null) + private set + + suspend fun showSnackbar( + startIcon: (@Composable () -> Unit)? = null, + emphasizedMessage: String? = null, + message: String, + targetDpFromTop: Dp = 40.dp, + actionLabel: Boolean = false, + withDismissAction: Boolean = false, + duration: TtdsSnackbarDuration = if (actionLabel) { + TtdsSnackbarDuration.Indefinite + } else { + TtdsSnackbarDuration.Short + }, + ): TtdsSnackbarResult = showSnackbar( + TtdsSnackbarVisualsImpl( + startIcon = startIcon, + emphasizedMessage = emphasizedMessage, + message = message, + targetDpFromTop = targetDpFromTop, + actionLabel = actionLabel, + withDismissAction = withDismissAction, + duration = duration, + ), + ) + + private suspend fun showSnackbar(visuals: TtdsSnackbarVisuals): TtdsSnackbarResult = + mutex.withLock { + try { + return suspendCancellableCoroutine { continuation -> + currentSnackbarData = TtdsSnackbarDataImpl(visuals, continuation) + } + } finally { + currentSnackbarData = null + } + } + + private class TtdsSnackbarVisualsImpl( + override val startIcon: (@Composable () -> Unit)?, + override val emphasizedMessage: String?, + override val message: String, + override val targetDpFromTop: Dp, + override val actionLabel: Boolean, + override val withDismissAction: Boolean, + override val duration: TtdsSnackbarDuration, + ) : TtdsSnackbarVisuals { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as TtdsSnackbarVisualsImpl + + if (startIcon != other.startIcon) return false + if (emphasizedMessage != other.emphasizedMessage) return false + if (message != other.message) return false + if (targetDpFromTop != other.targetDpFromTop) return false + if (actionLabel != other.actionLabel) return false + if (withDismissAction != other.withDismissAction) return false + if (duration != other.duration) return false + + return true + } + + override fun hashCode(): Int { + var result = startIcon.hashCode() + result = 31 * result + emphasizedMessage.hashCode() + result = 31 * result + message.hashCode() + result = 31 * result + targetDpFromTop.hashCode() + result = 31 * result + actionLabel.hashCode() + result = 31 * result + withDismissAction.hashCode() + result = 31 * result + duration.hashCode() + return result + } + } + + private class TtdsSnackbarDataImpl( + override val visuals: TtdsSnackbarVisuals, + private val continuation: CancellableContinuation, + ) : TtdsSnackbarData { + + override fun performAction() { + if (continuation.isActive) continuation.resume(TtdsSnackbarResult.ActionPerformed) + } + + override fun dismiss() { + if (continuation.isActive) continuation.resume(TtdsSnackbarResult.Dismissed) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as TtdsSnackbarDataImpl + + if (visuals != other.visuals) return false + if (continuation != other.continuation) return false + + return true + } + + override fun hashCode(): Int { + var result = visuals.hashCode() + result = 31 * result + continuation.hashCode() + return result + } + } +} + +@Composable +fun TtdsSnackbarHost( + modifier: Modifier = Modifier, + hostState: TtdsSnackbarHostState, + snackbar: @Composable (TtdsSnackbarData) -> Unit = { TtdsSnackbar(it) }, +) { + val currentSnackbarData = hostState.currentSnackbarData + val accessibilityManager = LocalAccessibilityManager.current + LaunchedEffect(currentSnackbarData) { + if (currentSnackbarData != null) { + val duration = currentSnackbarData.visuals.duration.toMillis( + currentSnackbarData.visuals.actionLabel, + accessibilityManager, + ) + delay(duration) + currentSnackbarData.dismiss() + } + } + TtdsSlideInSlideOutVertically( + current = hostState.currentSnackbarData, + modifier = modifier, + content = snackbar, + ) +} + +@Stable +interface TtdsSnackbarVisuals { + val startIcon: (@Composable () -> Unit)? + val emphasizedMessage: String? + val message: String + val targetDpFromTop: Dp + val actionLabel: Boolean + val withDismissAction: Boolean + val duration: TtdsSnackbarDuration +} + +@Stable +interface TtdsSnackbarData { + val visuals: TtdsSnackbarVisuals + + fun performAction() + + fun dismiss() +} + +enum class TtdsSnackbarResult { + Dismissed, + ActionPerformed, +} + +enum class TtdsSnackbarDuration { + Short, + Long, + Indefinite, +} + +internal fun TtdsSnackbarDuration.toMillis( + hasAction: Boolean, + accessibilityManager: AccessibilityManager?, +): Long { + val original = when (this) { + TtdsSnackbarDuration.Indefinite -> Long.MAX_VALUE + TtdsSnackbarDuration.Long -> 10000L + TtdsSnackbarDuration.Short -> 4000L + } + if (accessibilityManager == null) { + return original + } + return accessibilityManager.calculateRecommendedTimeoutMillis( + original, + containsIcons = true, + containsText = true, + containsControls = hasAction, + ) +} + +@Composable +private fun TtdsSlideInSlideOutVertically( + modifier: Modifier = Modifier, + current: TtdsSnackbarData?, + content: @Composable (TtdsSnackbarData) -> Unit, +) { + val state = remember { TtdsSlideInSlideOutVerticallyState() } + if (current != state.current) { + state.current = current + val keys = state.items.map { it.key }.toMutableList() + if (!keys.contains(current)) { + keys.add(current) + } + state.items.clear() + keys.filterNotNull().mapTo(state.items) { key -> + TtdsSlideInSlideOutVerticallyAnimationItem(key) { children -> + val isVisible = key == current + val duration = if (isVisible) { + TTDS_SNACKBAR_SLIDE_IN_VERTICALLY_MILLIS + } else { + TTDS_SNACKBAR_SLIDE_OUT_VERTICALLY_MILLIS + } + val delay = TTDS_SNACKBAR_SLIDE_OUT_VERTICALLY_MILLIS + + TTDS_SNACKBAR_IN_BETWEEN_DELAY_MILLIS + val animationDelay = if (isVisible && keys.filterNotNull().size != 1) delay else 0 + + val offsetY = animatedOffsetY( + animation = tween( + easing = LinearEasing, + delayMillis = animationDelay, + durationMillis = duration, + ), + visible = isVisible, + endDp = current?.visuals?.targetDpFromTop ?: 40.dp, + onAnimationFinish = { + if (key != state.current) { + state.items.removeAll { it.key == key } + state.scope?.invalidate() + } + }, + ) + + Box( + Modifier + .offset(y = offsetY.value.dp) + .semantics { + liveRegion = LiveRegionMode.Polite + dismiss { + key.dismiss() + true + } + }, + ) { + children() + } + } + } + } + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + state.scope = currentRecomposeScope + state.items.forEach { (item, opacity) -> + key(item) { + opacity { + content(item!!) + } + } + } + } +} + +private class TtdsSlideInSlideOutVerticallyState { + var current: Any? = Any() + var items = mutableListOf>() + var scope: RecomposeScope? = null +} + +private data class TtdsSlideInSlideOutVerticallyAnimationItem( + val key: T, + val transition: TtdsSlideInSlideOutVerticallyTransition, +) + +private typealias TtdsSlideInSlideOutVerticallyTransition = + @Composable (content: @Composable () -> Unit) -> Unit + +@Composable +private fun animatedOffsetY( + animation: AnimationSpec, + visible: Boolean, + endDp: Dp, + onAnimationFinish: () -> Unit = {}, +): State { + val density = LocalDensity.current + val targetY = with(density) { endDp.toPx() } + val offsetY = remember { Animatable(if (!visible) targetY else -targetY) } + LaunchedEffect(visible) { + offsetY.animateTo( + if (visible) targetY else -targetY, + animationSpec = animation, + ) + onAnimationFinish() + } + + return offsetY.asState() +} + +private const val TTDS_SNACKBAR_SLIDE_IN_VERTICALLY_MILLIS = 150 +private const val TTDS_SNACKBAR_SLIDE_OUT_VERTICALLY_MILLIS = 75 +private const val TTDS_SNACKBAR_IN_BETWEEN_DELAY_MILLIS = 0 diff --git a/tds/src/main/kotlin/com/titi/app/tds/component/TtdsSnackbar.kt b/tds/src/main/kotlin/com/titi/app/tds/component/TtdsSnackbar.kt new file mode 100644 index 00000000..0c804d20 --- /dev/null +++ b/tds/src/main/kotlin/com/titi/app/tds/component/TtdsSnackbar.kt @@ -0,0 +1,106 @@ +package com.titi.app.tds.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.titi.app.tds.R +import com.titi.app.tds.theme.TtdsColor +import com.titi.app.tds.theme.TtdsTextStyle +import com.titi.app.tds.theme.TtdsTheme + +@Composable +fun TtdsSnackbar(snackbarData: TtdsSnackbarData) { + TtdsSnackbar( + startIcon = snackbarData.visuals.startIcon, + emphasizedMessage = snackbarData.visuals.emphasizedMessage, + message = snackbarData.visuals.message, + ) +} + +@Composable +fun TtdsSnackbar( + startIcon: (@Composable () -> Unit)? = null, + emphasizedMessage: String?, + message: String, +) { + TtdsTheme { + Row( + modifier = Modifier + .shadow( + elevation = 3.dp, + shape = RoundedCornerShape(160.dp), + spotColor = Color.Black.copy(alpha = 0.12f), + ) + .background( + color = Color.White, + shape = RoundedCornerShape(160.dp), + ) + .padding( + vertical = 8.dp, + horizontal = 14.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + if (startIcon != null) { + startIcon() + } + + TtdsSnackbarMessage( + emphasizedMessage = emphasizedMessage, + message = message, + ) + } + } +} + +@Composable +private fun TtdsSnackbarMessage( + modifier: Modifier = Modifier, + emphasizedMessage: String?, + message: String, +) { + TtdsText( + modifier = modifier, + text = buildAnnotatedString { + withStyle( + SpanStyle( + color = TtdsColor.PRIMARY.getColor(), + fontWeight = FontWeight.SemiBold, + ), + ) { + append("$emphasizedMessage ") + } + append(message) + }, + textStyle = TtdsTextStyle.MEDIUM_TEXT_STYLE, + color = TtdsColor.TEXT, + fontSize = 14.sp, + ) +} + +@Preview +@Composable +private fun TtdsSnackbarMessagePreview() { + TtdsTheme { + TtdsSnackbar( + startIcon = { + TtdsSmallIcon(icon = R.drawable.reset_daily_icon) + }, + emphasizedMessage = "안녕하세요", + message = "반갑습니다", + ) + } +} diff --git a/tds/src/main/kotlin/com/titi/app/tds/component/TtdsText.kt b/tds/src/main/kotlin/com/titi/app/tds/component/TtdsText.kt new file mode 100644 index 00000000..9a817907 --- /dev/null +++ b/tds/src/main/kotlin/com/titi/app/tds/component/TtdsText.kt @@ -0,0 +1,66 @@ +package com.titi.app.tds.component + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.TextUnit +import com.titi.app.tds.theme.TtdsColor +import com.titi.app.tds.theme.TtdsTextStyle + +@Composable +fun TtdsText( + modifier: Modifier = Modifier, + text: String, + textStyle: TtdsTextStyle, + fontSize: TextUnit, + color: TtdsColor, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + overflow: TextOverflow = TextOverflow.Clip, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, +) { + Text( + modifier = modifier, + text = text, + color = color.getColor(), + style = textStyle.getTextStyle(), + fontSize = fontSize, + textAlign = textAlign, + overflow = overflow, + maxLines = maxLines, + minLines = minLines, + textDecoration = textDecoration, + ) +} + +@Composable +fun TtdsText( + modifier: Modifier = Modifier, + text: AnnotatedString, + textStyle: TtdsTextStyle, + fontSize: TextUnit, + color: TtdsColor, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + overflow: TextOverflow = TextOverflow.Clip, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, +) { + Text( + modifier = modifier, + text = text, + color = color.getColor(), + style = textStyle.getTextStyle(), + fontSize = fontSize, + textAlign = textAlign, + overflow = overflow, + maxLines = maxLines, + minLines = minLines, + textDecoration = textDecoration, + ) +} diff --git a/tds/src/main/kotlin/com/titi/app/tds/theme/Color.kt b/tds/src/main/kotlin/com/titi/app/tds/theme/Color.kt new file mode 100644 index 00000000..a03564b9 --- /dev/null +++ b/tds/src/main/kotlin/com/titi/app/tds/theme/Color.kt @@ -0,0 +1,33 @@ +package com.titi.app.tds.theme + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +@Immutable +data class TtdsColorsPalette( + val primary: Color = Color.Unspecified, + val text: Color = Color.Unspecified, +) + +val TtdsLightColorsPalette = TtdsColorsPalette( + primary = Color(0xFF007AFF), + text = Color(0xFF222222), +) + +val TtdsDarkColorsPalette = TtdsColorsPalette( + primary = Color(0xFF0A84FF), + text = Color(0xFF222222), +) + +enum class TtdsColor { + PRIMARY, + TEXT, + ; + + @Composable + fun getColor() = when (this) { + PRIMARY -> TtdsTheme.colors.primary + TEXT -> TtdsTheme.colors.text + } +} diff --git a/tds/src/main/kotlin/com/titi/app/tds/theme/Theme.kt b/tds/src/main/kotlin/com/titi/app/tds/theme/Theme.kt new file mode 100644 index 00000000..2484b87f --- /dev/null +++ b/tds/src/main/kotlin/com/titi/app/tds/theme/Theme.kt @@ -0,0 +1,47 @@ +package com.titi.app.tds.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density + +val LocalTtdsColors = staticCompositionLocalOf { + TtdsColorsPalette() +} + +val LocalTtdsTypography = staticCompositionLocalOf { + TtdsTypography() +} + +object TtdsTheme { + val colors: TtdsColorsPalette + @Composable + get() = LocalTtdsColors.current + + val textStyle: TtdsTypography + @Composable + get() = LocalTtdsTypography.current +} + +@Composable +fun TtdsTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + ttdsTypography: TtdsTypography = TtdsTheme.textStyle, + content: @Composable () -> Unit, +) { + val ttdsColorsPalette = if (darkTheme) { + TtdsDarkColorsPalette + } else { + TtdsLightColorsPalette + } + + CompositionLocalProvider( + LocalTtdsColors provides ttdsColorsPalette, + LocalTtdsTypography provides ttdsTypography, + LocalDensity provides Density(LocalDensity.current.density, 1f), + ) { + content() + } +} diff --git a/tds/src/main/kotlin/com/titi/app/tds/theme/Typography.kt b/tds/src/main/kotlin/com/titi/app/tds/theme/Typography.kt new file mode 100644 index 00000000..9b8a6c83 --- /dev/null +++ b/tds/src/main/kotlin/com/titi/app/tds/theme/Typography.kt @@ -0,0 +1,88 @@ +package com.titi.app.tds.theme + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import com.titi.app.tds.R + +val pretendardFontFamily = + FontFamily( + Font(R.font.pretendard_thin, FontWeight.Thin), + Font(R.font.pretendard_extra_light, FontWeight.ExtraLight), + Font(R.font.pretendard_light, FontWeight.Light), + Font(R.font.pretendard_regular, FontWeight.Normal), + Font(R.font.pretendard_medium, FontWeight.Medium), + Font(R.font.pretendard_semi_bold, FontWeight.SemiBold), + Font(R.font.pretendard_bold, FontWeight.Bold), + Font(R.font.pretendard_extra_bold, FontWeight.ExtraBold), + Font(R.font.pretendard_black, FontWeight.Black), + ) + +@Immutable +data class TtdsTypography( + val thinTextStyle: TextStyle = TextStyle( + fontFamily = pretendardFontFamily, + fontWeight = FontWeight.Thin, + ), + val extraLightTextStyle: TextStyle = TextStyle( + fontFamily = pretendardFontFamily, + fontWeight = FontWeight.ExtraLight, + ), + val lightTextStyle: TextStyle = TextStyle( + fontFamily = pretendardFontFamily, + fontWeight = FontWeight.Light, + ), + val normalTextStyle: TextStyle = TextStyle( + fontFamily = pretendardFontFamily, + fontWeight = FontWeight.Normal, + ), + val mediumTextStyle: TextStyle = TextStyle( + fontFamily = pretendardFontFamily, + fontWeight = FontWeight.Medium, + ), + val semiBoldTextStyle: TextStyle = TextStyle( + fontFamily = pretendardFontFamily, + fontWeight = FontWeight.SemiBold, + ), + val boldTextStyle: TextStyle = TextStyle( + fontFamily = pretendardFontFamily, + fontWeight = FontWeight.Bold, + ), + val extraBoldTextStyle: TextStyle = TextStyle( + fontFamily = pretendardFontFamily, + fontWeight = FontWeight.ExtraBold, + ), + val blackTextStyle: TextStyle = TextStyle( + fontFamily = pretendardFontFamily, + fontWeight = FontWeight.Black, + ), +) + +enum class TtdsTextStyle { + THIN_TEXT_STYLE, + EXTRA_LIGHT_TEXT_STYLE, + LIGHT_TEXT_STYLE, + NORMAL_TEXT_STYLE, + MEDIUM_TEXT_STYLE, + SEMI_BOLD_TEXT_STYLE, + BOLD_TEXT_STYLE, + EXTRA_BOLD_TEXT_STYLE, + BLACK_TEXT_STYLE, + ; + + @Composable + fun getTextStyle() = when (this) { + THIN_TEXT_STYLE -> TtdsTheme.textStyle.thinTextStyle + EXTRA_LIGHT_TEXT_STYLE -> TtdsTheme.textStyle.extraLightTextStyle + LIGHT_TEXT_STYLE -> TtdsTheme.textStyle.lightTextStyle + NORMAL_TEXT_STYLE -> TtdsTheme.textStyle.normalTextStyle + MEDIUM_TEXT_STYLE -> TtdsTheme.textStyle.mediumTextStyle + SEMI_BOLD_TEXT_STYLE -> TtdsTheme.textStyle.semiBoldTextStyle + BOLD_TEXT_STYLE -> TtdsTheme.textStyle.boldTextStyle + EXTRA_BOLD_TEXT_STYLE -> TtdsTheme.textStyle.extraBoldTextStyle + BLACK_TEXT_STYLE -> TtdsTheme.textStyle.blackTextStyle + } +} diff --git a/tds/src/main/res/drawable/reset_daily_icon.xml b/tds/src/main/res/drawable/reset_daily_icon.xml new file mode 100644 index 00000000..17d8390e --- /dev/null +++ b/tds/src/main/res/drawable/reset_daily_icon.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + diff --git a/tds/src/main/res/font/pretendard_black.otf b/tds/src/main/res/font/pretendard_black.otf new file mode 100644 index 00000000..a0d849e7 Binary files /dev/null and b/tds/src/main/res/font/pretendard_black.otf differ diff --git a/tds/src/main/res/font/pretendard_bold.otf b/tds/src/main/res/font/pretendard_bold.otf new file mode 100644 index 00000000..8e5e30a2 Binary files /dev/null and b/tds/src/main/res/font/pretendard_bold.otf differ diff --git a/tds/src/main/res/font/pretendard_extra_bold.otf b/tds/src/main/res/font/pretendard_extra_bold.otf new file mode 100644 index 00000000..388f3ca4 Binary files /dev/null and b/tds/src/main/res/font/pretendard_extra_bold.otf differ diff --git a/tds/src/main/res/font/pretendard_extra_light.otf b/tds/src/main/res/font/pretendard_extra_light.otf new file mode 100644 index 00000000..40c8b69c Binary files /dev/null and b/tds/src/main/res/font/pretendard_extra_light.otf differ diff --git a/tds/src/main/res/font/pretendard_light.otf b/tds/src/main/res/font/pretendard_light.otf new file mode 100644 index 00000000..228679e9 Binary files /dev/null and b/tds/src/main/res/font/pretendard_light.otf differ diff --git a/tds/src/main/res/font/pretendard_medium.otf b/tds/src/main/res/font/pretendard_medium.otf new file mode 100644 index 00000000..05750698 Binary files /dev/null and b/tds/src/main/res/font/pretendard_medium.otf differ diff --git a/tds/src/main/res/font/pretendard_regular.otf b/tds/src/main/res/font/pretendard_regular.otf new file mode 100644 index 00000000..08bf4cfc Binary files /dev/null and b/tds/src/main/res/font/pretendard_regular.otf differ diff --git a/tds/src/main/res/font/pretendard_semi_bold.otf b/tds/src/main/res/font/pretendard_semi_bold.otf new file mode 100644 index 00000000..e7e36abc Binary files /dev/null and b/tds/src/main/res/font/pretendard_semi_bold.otf differ diff --git a/tds/src/main/res/font/pretendard_thin.otf b/tds/src/main/res/font/pretendard_thin.otf new file mode 100644 index 00000000..77e792d7 Binary files /dev/null and b/tds/src/main/res/font/pretendard_thin.otf differ diff --git a/tds/src/main/res/raw/reset_daily_lottie.json b/tds/src/main/res/raw/reset_daily_lottie.json new file mode 100644 index 00000000..c0b13941 --- /dev/null +++ b/tds/src/main/res/raw/reset_daily_lottie.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":60,"w":500,"h":500,"nm":"system-regular-162-update","ddd":0,"assets":[{"id":"comp_1","nm":"hover-update","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,249.958,0],"ix":2,"l":2},"a":{"a":0,"k":[250.004,249.958,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.023,-0.334],[-0.019,-0.181],[-0.063,-0.357],[-0.03,-0.147],[-2.089,-2.192],[-0.099,-0.098],[-0.037,-0.036],[0,0],[-3.942,0],[-3.066,3.141],[6.186,6.038],[0,0],[0,0],[8.644,0],[0,-8.643],[0,0],[0,-0.033]],"o":[[0.013,0.183],[0.038,0.36],[0.026,0.149],[0.571,2.78],[0.096,0.101],[0.037,0.036],[0,0],[3.043,2.971],[4.068,0],[6.037,-6.185],[0,0],[0,0],[0,-8.643],[-8.644,0],[0,0],[0,0.033],[0.002,0.335]],"v":[[-46.845,12.997],[-46.812,13.547],[-46.649,14.62],[-46.578,15.068],[-42.57,22.684],[-42.286,22.99],[-42.182,23.103],[20.319,84.116],[31.25,88.567],[42.45,83.849],[42.182,61.718],[-15.6,5.312],[-15.6,-72.917],[-31.25,-88.567],[-46.901,-72.917],[-46.901,11.896],[-46.896,11.995]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.039,0.518,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-162-update').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[276.045,255.211],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,249.958,0],"ix":2,"l":2},"a":{"a":0,"k":[250.004,249.958,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.023,-0.334],[-0.019,-0.181],[-0.063,-0.357],[-0.03,-0.147],[-2.089,-2.192],[-0.099,-0.098],[-0.037,-0.036],[0,0],[-3.942,0],[-3.066,3.141],[6.186,6.038],[0,0],[0,0],[8.644,0],[0,-8.643],[0,0],[0,-0.033]],"o":[[0.013,0.183],[0.038,0.36],[0.026,0.149],[0.571,2.78],[0.096,0.101],[0.037,0.036],[0,0],[3.043,2.971],[4.068,0],[6.037,-6.185],[0,0],[0,0],[0,-8.643],[-8.644,0],[0,0],[0,0.033],[0.002,0.335]],"v":[[-46.845,12.997],[-46.812,13.547],[-46.649,14.62],[-46.578,15.068],[-42.57,22.684],[-42.286,22.99],[-42.182,23.103],[20.319,84.116],[31.25,88.567],[42.45,83.849],[42.182,61.718],[-15.6,5.312],[-15.6,-72.917],[-31.25,-88.567],[-46.901,-72.917],[-46.901,11.896],[-46.896,11.995]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.039,0.518,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-162-update').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[276.045,255.211],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":60,"s":[360]}],"ix":10},"p":{"a":0,"k":[250.004,249.958,0],"ix":2,"l":2},"a":{"a":0,"k":[250.004,249.958,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-0.001,8.643],[0,0],[8.643,0],[0,0],[0.001,-8.643],[0,0],[7.58,7.955],[50.075,1.21],[36.264,-34.553],[1.21,-50.075],[-34.553,-36.265],[-49.554,0.005],[-36.238,34.527],[-7.36,38.559],[8.49,1.621],[1.62,-8.49],[23.752,-22.631],[59.422,62.365],[-1.009,41.717],[-30.212,28.785],[-41.745,-1.001],[-28.785,-30.212],[-5.57,-8.872],[0,0],[0,-8.643],[-8.644,0]],"o":[[8.644,0],[0,0],[0,-8.643],[-0.001,0],[-8.643,0],[0,0],[-6.046,-9.081],[-34.552,-36.265],[-50.054,-1.209],[-36.264,34.553],[-1.211,50.075],[36.8,38.623],[46.493,-0.004],[28.509,-27.163],[1.621,-8.49],[-8.493,-1.62],[-6.129,32.106],[-62.367,59.424],[-28.785,-30.211],[1.008,-41.717],[30.211,-28.785],[41.717,1.008],[7.293,7.654],[0,0],[-8.644,0],[0,8.643],[0,0]],"v":[[171.873,-51.553],[187.524,-67.202],[187.527,-161.414],[171.877,-177.064],[171.876,-177.064],[156.226,-161.415],[156.224,-103.69],[135.765,-129.313],[4.532,-187.426],[-129.358,-135.721],[-187.47,-4.486],[-135.765,129.403],[0.043,187.482],[129.358,135.811],[184.186,35.349],[171.748,17.042],[153.442,29.48],[107.766,113.149],[-113.105,107.811],[-156.179,-3.73],[-107.766,-113.06],[3.775,-156.135],[113.105,-107.722],[132.439,-82.853],[82.872,-82.853],[67.222,-67.203],[82.872,-51.553]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.039,0.518,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-162-update').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250.004,249.958],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":628,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.407],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.425],"y":[0]},"t":20,"s":[23]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":45,"s":[-5]},{"t":60,"s":[0]}],"ix":10},"p":{"a":0,"k":[244.795,267.116,0],"ix":2,"l":2},"a":{"a":0,"k":[244.795,267.116,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[31.25,30.506],[-31.25,-30.506]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.039,0.518,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-162-update').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[276.045,297.622],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.176],"y":[0]},"t":42,"s":[374]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":53,"s":[355]},{"t":60,"s":[360]}],"ix":10},"p":{"a":0,"k":[244.795,267.108,0],"ix":2,"l":2},"a":{"a":0,"k":[244.795,267.108,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-42.407],[0,42.407]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.039,0.518,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-162-update').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[244.795,224.701],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[0.039,0.518,1],"ix":1}}]}],"ip":0,"op":201,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-update","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":70,"st":0,"bm":0}],"markers":[{"tm":0,"cm":"default:hover-update","dr":60}],"props":{}} \ No newline at end of file diff --git a/tds/src/test/kotlin/com/titi/app/tds/ExampleUnitTest.kt b/tds/src/test/kotlin/com/titi/app/tds/ExampleUnitTest.kt new file mode 100644 index 00000000..0de62894 --- /dev/null +++ b/tds/src/test/kotlin/com/titi/app/tds/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.titi.app.tds + +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) + } +}