diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml new file mode 100644 index 0000000..c1b0a6c --- /dev/null +++ b/.github/workflows/android-ci.yml @@ -0,0 +1,69 @@ +name: Android CI + +on: + push: + branches: + - main + - 'feature/**' + pull_request: + branches: + - '*' + +jobs: + build-and-test: + runs-on: macos-latest + env: + ANDROID_SDK_ROOT: /Users/runner/Library/Android/sdk + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*','**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 17 + + - name: Install Android SDK + uses: android-actions/setup-android@v2 + with: + api-level: 33 + build-tools: "33.0.2" + components: | + platform-tools + emulator + tools + platforms;android-33 + system-images;android-33;google_apis;x86_64 + + - name: Accept Android SDK licenses + run: yes | sdkmanager --licenses + + - name: Create and start emulator + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 33 + target: google_apis + arch: x86_64 + force-avd-creation: true + emulator-options: -no-window -no-boot-anim -gpu swiftshader_indirect + disable-animations: true + emulator-boot-timeout: 180 + + - name: Build debug APK, run unit & instrumentation tests + run: | + ./gradlew clean assembleDebug \ + testDebugUnitTest \ + connectedDebugAndroidTest \ + --stacktrace diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 700f2cb..89e0263 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -46,6 +46,7 @@ android { dependencies { testImplementation(libs.mockk) + testImplementation(libs.jupiter.junit.jupiter) androidTestImplementation(libs.ui.test.junit4) debugImplementation(libs.ui.test.manifest) implementation(libs.androidx.lifecycle.process) diff --git a/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt new file mode 100644 index 0000000..09d5c37 --- /dev/null +++ b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt @@ -0,0 +1,107 @@ +package org.hse.smartcalendar.ui.screens + +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.datetime.LocalTime +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.store.StatisticsStore +import org.hse.smartcalendar.ui.screens.model.AchievementType +import org.hse.smartcalendar.ui.theme.SmartCalendarTheme +import org.hse.smartcalendar.utility.TimeUtils +import org.hse.smartcalendar.utility.fromMinutesOfDay +import org.hse.smartcalendar.utility.rememberNavigation +import org.hse.smartcalendar.view.model.AbstractListViewModel +import org.hse.smartcalendar.view.model.StatisticsViewModel +import org.hse.smartcalendar.view.model.StatisticsManager +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.UUID +@RunWith(AndroidJUnit4::class) +class AchievementsScreenTest { + private lateinit var firstTask: DailyTask + private lateinit var secondTask: DailyTask + @get:Rule + val composeTestRule = createComposeRule() + @Before + fun initTasks(){ + StatisticsStore.uploader={} + firstTask = DailyTask( + title = "first", + id = UUID.randomUUID(), + isComplete = false, + type = DailyTaskType.COMMON, + creationTime = TimeUtils.Companion.getCurrentDateTime(), + description = "", + start = LocalTime.Companion.fromMinutesOfDay(0), + end = LocalTime.Companion.fromMinutesOfDay(300), + date = TimeUtils.Companion.getCurrentDateTime().date, + ) + secondTask = DailyTask( + title = "first", + id = UUID.randomUUID(), + isComplete = false, + type = DailyTaskType.WORK, + creationTime = TimeUtils.Companion.getCurrentDateTime(), + description = "", + start = LocalTime.Companion.fromMinutesOfDay(300), + end = LocalTime.Companion.fromMinutesOfDay(1440-1), + date = TimeUtils.Companion.getCurrentDateTime().date, + ) + } + fun assertAchievementData(type: AchievementType, text: String){ + composeTestRule + .onNodeWithTag(type.testTag) + .onChildren() + .filter(hasText(text)) + .onFirst() + .assertExists() + } + @Test + fun achievementsShowsStreak() { + val statisticsViewModel = StatisticsViewModel() + val listViewModel = AbstractListViewModel(StatisticsManager(statisticsViewModel)) +//нужно потестить каждый элемент:Planning everything - без заданий 0, +// с заданием 5ч 5/10, c 24ч 24 часа + composeTestRule.setContent { + SmartCalendarTheme { + AchievementsScreen( + navigation = rememberNavigation(), + openDrawer = {}, + statisticsModel = statisticsViewModel + ) + } + } + + composeTestRule.onNodeWithText("Eternal Flame").assertIsDisplayed() + listViewModel.addDailyTask(firstTask) + composeTestRule.runOnIdle { + } + assertAchievementData(AchievementType.PlanToday, "5/10") + assertAchievementData(AchievementType.CommonSpend, "0/10") + listViewModel.changeTaskCompletion(firstTask, true) + assert(statisticsViewModel.uiState.value.total.Common.toMinutes().toInt() == firstTask.getMinutesLength()) + composeTestRule.runOnIdle {} + assertAchievementData(AchievementType.CommonSpend, "5/10") + assertAchievementData(AchievementType.Streak, "1/5") + listViewModel.addDailyTask(secondTask) + composeTestRule.runOnIdle {} + assertAchievementData(AchievementType.PlanToday, "24/24") + assertAchievementData(AchievementType.Streak, "0/5") + } +} +// assertAchievementData(AchievementType.CommonSpend, "0/10") +// assertAchievementData(AchievementType.PlanToday, "5/10") +// listViewModel.changeTaskCompletion(firstTask, true) +// assertAchievementData(AchievementType.CommonSpend, "5/10") +// listViewModel.addDailyTask(secondTask) +// composeTestRule.runOnIdle {} +// assertAchievementData(AchievementType.PlanToday, "24/24") +// listViewModel.changeTaskCompletion(secondTask, true) +// composeTestRule.runOnIdle {} +// assertAchievementData(AchievementType.WorkWeek, (24/7).toInt().toString()+"/${AchievementType.WorkWeek.levels[0]}") +// } +//} diff --git a/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievmentsCardTest.kt b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievmentsCardTest.kt new file mode 100644 index 0000000..954bc49 --- /dev/null +++ b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievmentsCardTest.kt @@ -0,0 +1,54 @@ +package org.hse.smartcalendar.ui.screens + +import androidx.activity.ComponentActivity +import androidx.compose.ui.semantics.ProgressBarRangeInfo +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.junit4.createComposeRule +import org.hse.smartcalendar.ui.screens.model.AchievementType +import org.junit.Rule +import org.junit.Test + +class AchievementCardTest { + val type = AchievementType.PlanToday + val tag = type.testTag + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun nullProgress() { + composeTestRule.setContent {//cannot setContent twice + AchievementCard(data = type, parameter = 0L) + } + composeTestRule.onNode( + hasText("Plan ${type.levels[0]} hours of your time today") + .and(hasParent(hasTestTag(tag))) + ).assertExists() + } + @Test fun mediumProgress() { + composeTestRule.setContent { + AchievementCard(data = type, parameter = 6L) + } + composeTestRule.onNodeWithTag("${tag}_progress") + .assertIsDisplayed() + .assertRangeInfoEquals(ProgressBarRangeInfo(current = 0.6f, range = 0f..1f)) + composeTestRule.onNode( + hasText("Plan ${type.levels[1]} hours of your time today") + .and(hasParent(hasTestTag(tag))) + ).assertExists() + } + @Test + fun maxProgress(){ + val max = type.levels.last() + composeTestRule.setContent { + AchievementCard(data = type, parameter = max) + } + composeTestRule.onNodeWithTag("${tag}_progress") + .assertIsDisplayed() + .assertRangeInfoEquals(ProgressBarRangeInfo(current = 1.0f, range = 0f..1f)) + composeTestRule.onNode( + hasText("You have max level, you achieve:${type.description(max)}") + .and(hasParent(hasTestTag(tag))) + ).assertExists() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/activity/DailyTasksListActivity.kt b/app/src/main/java/org/hse/smartcalendar/activity/DailyTasksListActivity.kt index dfc7df6..a6e4fdf 100644 --- a/app/src/main/java/org/hse/smartcalendar/activity/DailyTasksListActivity.kt +++ b/app/src/main/java/org/hse/smartcalendar/activity/DailyTasksListActivity.kt @@ -11,8 +11,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import org.hse.smartcalendar.data.WorkManagerHolder -import org.hse.smartcalendar.notification.ReminderViewModel -import org.hse.smartcalendar.notification.ReminderViewModelFactory +import org.hse.smartcalendar.view.model.ReminderViewModel +import org.hse.smartcalendar.view.model.ReminderViewModelFactory import org.hse.smartcalendar.ui.screens.StatisticsScreen import org.hse.smartcalendar.ui.task.DailyTasksList import org.hse.smartcalendar.ui.task.TaskEditWindow @@ -74,7 +74,9 @@ fun ListNavigation( listViewModel.removeDailyTask(task) }, onCancel = {}, - taskEditViewModel = taskEditViewModel, navController = navController + taskEditViewModel = taskEditViewModel, + navController = navController, + reminderModel = reminderModel ) } composable(route = Screens.SETTINGS.route) { diff --git a/app/src/main/java/org/hse/smartcalendar/activity/MainActivity.kt b/app/src/main/java/org/hse/smartcalendar/activity/MainActivity.kt index 85a28c0..0256135 100644 --- a/app/src/main/java/org/hse/smartcalendar/activity/MainActivity.kt +++ b/app/src/main/java/org/hse/smartcalendar/activity/MainActivity.kt @@ -42,7 +42,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WorkManagerHolder.init(this) - val statisticsModel: StatisticsViewModel = StatisticsViewModel() + val statisticsModel = StatisticsViewModel() val listModel = ListViewModel(StatisticsManager(statisticsModel)) val editModel = TaskEditViewModel(listModel) enableEdgeToEdge() diff --git a/app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt b/app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt index 87bd8fc..1ef64ae 100644 --- a/app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt +++ b/app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt @@ -29,7 +29,7 @@ class DailySchedule (val date : LocalDate = Clock.System.now() fun setCompletionById(id: UUID, status: Boolean): Boolean { dailyTasksList.forEach { task -> - if (task.getId() == id && task.isComplete()!=status) { + if (task.getId() == id && task.isComplete()!=status) {//т.е. у нас complete поэтому не меняем task.setCompletion(status) return true } diff --git a/app/src/main/java/org/hse/smartcalendar/data/TotalTaskTypes.kt b/app/src/main/java/org/hse/smartcalendar/data/TotalTaskTypes.kt index b138201..8c5f9a4 100644 --- a/app/src/main/java/org/hse/smartcalendar/data/TotalTaskTypes.kt +++ b/app/src/main/java/org/hse/smartcalendar/data/TotalTaskTypes.kt @@ -1,7 +1,7 @@ package org.hse.smartcalendar.data -import org.hse.smartcalendar.utility.TimePeriod -import org.hse.smartcalendar.view.model.AbstractStatisticsViewModel.Companion.getPercent +import org.hse.smartcalendar.view.model.state.TimePeriod +import org.hse.smartcalendar.view.model.StatisticsViewModel.Companion.getPercent class TotalTimeTaskTypes(common: Long, work: Long, study: Long, fitness: Long){ val All: TimePeriod = TimePeriod(work+study+common+fitness) @@ -19,6 +19,14 @@ class TotalTimeTaskTypes(common: Long, work: Long, study: Long, fitness: Long){ private set var WorkPercent: Float = getPercent(work, totalMinutes) private set + fun getPercentByType(type: DailyTaskType): Float { + return when (type) { + DailyTaskType.COMMON -> CommonPercent + DailyTaskType.FITNESS -> FitnessPercent + DailyTaskType.WORK -> WorkPercent + DailyTaskType.STUDIES -> StudyPercent + } + } fun completeTask(task: DailyTask, isComplete: Boolean){ val taskMinutesLength = task.getMinutesLength().toLong() when(isComplete){ @@ -28,21 +36,18 @@ class TotalTimeTaskTypes(common: Long, work: Long, study: Long, fitness: Long){ false -> totalMinutes-=taskMinutesLength } All.addMinutes(taskMinutesLength, isComplete) - when(task.getDailyTaskType()){ - DailyTaskType.COMMON -> {Common.addMinutes(taskMinutesLength, isComplete) - CommonPercent=getPercent(Common.toMinutes(), totalMinutes)} - DailyTaskType.FITNESS -> { - Fitness.addMinutes(taskMinutesLength, isComplete) - FitnessPercent=getPercent(Fitness.toMinutes(), totalMinutes) - } - DailyTaskType.WORK -> { - Work.addMinutes(taskMinutesLength, isComplete) - WorkPercent=getPercent(Work.toMinutes(), totalMinutes) - } - DailyTaskType.STUDIES -> { - Study.addMinutes(taskMinutesLength, isComplete) - StudyPercent=getPercent(Study.toMinutes(), totalMinutes) - } + when (task.getDailyTaskType()) { + DailyTaskType.COMMON -> Common.addMinutes(taskMinutesLength, isComplete) + DailyTaskType.FITNESS -> Fitness.addMinutes(taskMinutesLength, isComplete) + DailyTaskType.WORK -> Work.addMinutes(taskMinutesLength, isComplete) + DailyTaskType.STUDIES -> Study.addMinutes(taskMinutesLength, isComplete) } + recalculatePercents() + } + private fun recalculatePercents() { + StudyPercent = getPercent(Study.toMinutes(), totalMinutes) + CommonPercent = getPercent(Common.toMinutes(), totalMinutes) + FitnessPercent = getPercent(Fitness.toMinutes(), totalMinutes) + WorkPercent = getPercent(Work.toMinutes(), totalMinutes) } } \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/data/store/StatisticsStore.kt b/app/src/main/java/org/hse/smartcalendar/data/store/StatisticsStore.kt new file mode 100644 index 0000000..23cb014 --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/data/store/StatisticsStore.kt @@ -0,0 +1,144 @@ +package org.hse.smartcalendar.store + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.workDataOf +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.minus +import kotlinx.datetime.toLocalDateTime +import kotlinx.serialization.json.Json +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.TotalTimeTaskTypes +import org.hse.smartcalendar.data.User +import org.hse.smartcalendar.data.WorkManagerHolder +import org.hse.smartcalendar.network.ApiClient +import org.hse.smartcalendar.network.NetworkResponse +import org.hse.smartcalendar.network.StatisticsDTO +import org.hse.smartcalendar.repository.StatisticsRepository +import org.hse.smartcalendar.utility.TimeUtils +import org.hse.smartcalendar.view.model.state.AverageDayTimeVars +import org.hse.smartcalendar.view.model.state.StatisticsCalculator +import org.hse.smartcalendar.view.model.state.TodayTimeVars +import org.hse.smartcalendar.view.model.state.WeekTime +import org.hse.smartcalendar.work.StatisticsUploadWorker + +object StatisticsStore { + private val repository = StatisticsRepository(ApiClient.statisticsApiService) + var totalTime by mutableStateOf(TotalTimeTaskTypes(0,0,0,0)) + private set + var averageDayTime by mutableStateOf(AverageDayTimeVars(firstDay = LocalDate(1970,1,1), totalWorkMinutes = 0)) + private set + var todayTime by mutableStateOf(TodayTimeVars(0,0)) + private set + var weekTime by mutableStateOf(WeekTime(0)) + private set + var calculator = StatisticsCalculator() + private set + fun clear() { + totalTime = TotalTimeTaskTypes(0, 0, 0, 0) + averageDayTime = AverageDayTimeVars(firstDay = LocalDate(1970, 1, 1), totalWorkMinutes = 0) + todayTime = TodayTimeVars(0, 0) + weekTime = WeekTime(0) + calculator = StatisticsCalculator() + } + var uploader: () -> Unit = { uploadStatistics() } + private fun updateNextDay(oldDate: LocalDate){ + val currentDate = TimeUtils.getCurrentDateTime().date + if (oldDate == currentDate){ + return + } + val schedule = User.getSchedule() + val todayTasks = schedule + .getOrCreateDailySchedule(currentDate) + .getDailyTaskList() + val todayPlanned = todayTasks.sumOf { it.getMinutesLength().toLong() } + val todayCompleted = todayTasks + .filter { it.isComplete() } + .sumOf { it.getMinutesLength().toLong() } + todayTime = TodayTimeVars(planned = todayPlanned, completed = todayCompleted) + var weekSum = 0L + for (offset in 0L..6L) { + val date = currentDate.minus(offset, DateTimeUnit.DAY) + val tasks = schedule + .getOrCreateDailySchedule(date) + .getDailyTaskList() + weekSum += tasks + .filter { it.isComplete() } + .sumOf { it.getMinutesLength().toLong() } + } + weekTime = WeekTime(weekSum) + } + suspend fun init(): NetworkResponse = withContext(Dispatchers.IO) { + when(val resp = repository.getStatistics()) { + is NetworkResponse.Success -> { + val d = resp.data + totalTime = d.totalTime.toVMTotalTime() + averageDayTime = AverageDayTimeVars.fromAverageDayDTO(d.averageDayTime) + todayTime = TodayTimeVars.fromTodayTimeDTO(d.todayTime) + weekTime = WeekTime(d.weekTime) + calculator.init(d) + val oldDate = d.jsonDate + .toLocalDateTime(TimeUtils.getCurrentTimezone()).date + updateNextDay(oldDate) + resp + } + else -> resp + } + } + private fun createOrDeleteTask(task: DailyTask, isCreate: Boolean){ + if (task.isComplete() && isCreate==false){ + changeTaskCompletion(task, false) + } + if (task.belongsCurrentDay()){ + todayTime.Planned.plusMinutes(task.getMinutesLength(), isCreate) + } + } + + fun createOrDeleteTask(task: DailyTask, dateTasks: List, isAdd: Boolean) { + createOrDeleteTask(task, isAdd) + calculator.addOrDeleteTask( + StatisticsCalculator.AddOrDeleteRequest + (date = task.getTaskDate(), dateTasks = dateTasks)) + uploader() + } + + fun changeTaskCompletion(task: DailyTask, isComplete: Boolean, isUploadStats: Boolean=true) { + if (task.belongsCurrentDay()) todayTime = todayTime.apply { Completed.plusMinutes(task.getMinutesLength(), isComplete) } + if (task.belongsCurrentWeek()) weekTime = weekTime.apply { All.addMinutes(task.getMinutesLength().toLong(), isComplete) } + totalTime = totalTime.apply { completeTask(task, isComplete) } + averageDayTime = averageDayTime.apply { update(totalTime.totalMinutes) } + calculator.changeTaskCompletion(task) + if (isUploadStats) {uploader()} + } + fun changeTask(task: DailyTask, newTask: DailyTask){ + if (task.isComplete()){ + changeTaskCompletion(task, false, isUploadStats = false) + changeTaskCompletion(newTask, true, isUploadStats = false) + } + createOrDeleteTask(task, false) + createOrDeleteTask(newTask, true) + uploader() + } + private fun uploadStatistics() { + val workManager = WorkManagerHolder.getInstance() + val statsDTO = StatisticsDTO.fromStore() + val json = Json.encodeToString(StatisticsDTO.serializer(), statsDTO) + + val workRequest = OneTimeWorkRequestBuilder() + .setInputData(workDataOf("statistics_json" to json)) + .build() + + workManager.enqueueUniqueWork( + "upload_stats", + ExistingWorkPolicy.REPLACE, + workRequest + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt b/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt index b60b724..083c964 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt @@ -1,10 +1,13 @@ package org.hse.smartcalendar.network import DailyTaskTypeAdapter +import InstantTimeAdapter import LocalDateAdapter import LocalDateTimeAdapter import LocalTimeAdapter +import OffsetDateTimeAdapter import com.google.gson.GsonBuilder +import kotlinx.datetime.Instant import kotlinx.datetime.LocalTime import okhttp3.Interceptor import okhttp3.OkHttpClient @@ -13,6 +16,7 @@ import okhttp3.logging.HttpLoggingInterceptor import org.hse.smartcalendar.data.DailyTaskType import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.time.OffsetDateTime object ApiClient { private const val SERVER_BASE_URL = "http://10.0.2.2:8080/" @@ -26,6 +30,8 @@ object ApiClient { .registerTypeAdapter(LocalTime::class.java, LocalTimeAdapter()) .registerTypeAdapter(kotlinx.datetime.LocalDateTime::class.java, LocalDateTimeAdapter()) .registerTypeAdapter(DailyTaskType::class.java, DailyTaskTypeAdapter()) + .registerTypeAdapter(OffsetDateTime::class.java, OffsetDateTimeAdapter()) + .registerTypeAdapter(Instant::class.java, InstantTimeAdapter()) .create() private val retrofit = Retrofit.Builder() .baseUrl(SERVER_BASE_URL) @@ -41,6 +47,9 @@ object ApiClient { val statisticsApiService: StatisticsApiInterface by lazy { retrofit.create(StatisticsApiInterface::class.java) } + val audioApiService : AudioApiInterface by lazy { + retrofit.create(AudioApiInterface::class.java) + } } class AuthInterceptor(private val tokenProvider: () -> String?) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { diff --git a/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt b/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt index caa33e9..1179f75 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt @@ -1,5 +1,6 @@ package org.hse.smartcalendar.network +import okhttp3.MultipartBody import okhttp3.ResponseBody import retrofit2.Response import retrofit2.http.Body @@ -9,6 +10,8 @@ import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Path +import retrofit2.http.Multipart +import retrofit2.http.Part import java.util.UUID interface AuthApiInterface { @@ -72,4 +75,12 @@ interface StatisticsApiInterface { @GET("api/statistics/total-time-task-types") suspend fun getGlobalTaskTypeStatistics(): Response -} \ No newline at end of file +} + +interface AudioApiInterface { + @Multipart + @POST("api/audio/process") + suspend fun processAudio( + @Part file: MultipartBody.Part + ): Response> +} diff --git a/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt b/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt index ee2d21e..5d3ad9a 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt @@ -6,7 +6,9 @@ import kotlinx.datetime.LocalTime import kotlinx.serialization.Serializable import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.utility.TimeUtils import java.util.UUID +import androidx.compose.runtime.MutableState data class RegisterResponse ( val id: Long? = null, @@ -59,3 +61,57 @@ data class TaskResponse( ) } } +@Serializable +data class ChatTaskResponse( + val id: String, + val title: String, + val description: String, + val start: LocalDateTime?, + val end: LocalDateTime?, + val date: String, + val type: String?, + val creationTime: LocalDateTime?, + val complete: Boolean? +) { + fun toDailyTask(): DailyTask { + val task = DailyTask( + id = UUID.fromString(id), + title = title, + description = description, + start = start?.time ?: LocalTime(0, 0), + end = end?.time ?: LocalTime(0, 0), + date = (start ?: end?: creationTime?: TimeUtils.getCurrentDateTime()).date , + type = DailyTaskType.valueOf(type?.uppercase() ?: "COMMON"), + creationTime = creationTime?: TimeUtils.getCurrentDateTime(), + isComplete = complete == true, + ) + return task + } + private fun applyToState( + change: T?, + state: MutableState + ){ + if (change !=null){ + state.value = change + } + } + fun applyToUiStates( + taskTitle: MutableState, + taskDescription: MutableState, + taskType: MutableState, + startTime: MutableState, + endTime: MutableState, + isErrorInRecorder: MutableState + ) { + isErrorInRecorder.value = false + + taskTitle.value = this.title + taskDescription.value = this.description + val newType = type?.let { DailyTaskType.valueOf(it.uppercase()) } + applyToState(newType, taskType) + val newStart = start?.time?.toSecondOfDay()?.div(60) + applyToState(newStart, startTime) + val newEnd = start?.time?.toSecondOfDay()?.div(60) + applyToState(newEnd, endTime) + } +} diff --git a/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt b/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt index f450ecd..37f2963 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt @@ -4,6 +4,7 @@ import kotlinx.datetime.LocalDateTime import kotlinx.serialization.Serializable import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.utility.TimeUtils data class LoginRequest( val username: String, @@ -12,7 +13,8 @@ data class LoginRequest( data class RegisterRequest( val username: String, val email: String, - val password: String + val password: String, + val firstDay:String = TimeUtils.getCurrentDateTime().date.toString() ) data class ChangeCredentialsRequest( val currentUsername: String, diff --git a/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt b/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt index dede84f..5930c27 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt @@ -1,20 +1,27 @@ package org.hse.smartcalendar.network +import kotlinx.datetime.Clock +import kotlinx.datetime.Clock.System +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.datetime.toInstant import kotlinx.serialization.Serializable import org.hse.smartcalendar.data.TotalTimeTaskTypes -import org.hse.smartcalendar.view.model.StatisticsViewModel +import org.hse.smartcalendar.store.StatisticsStore +import org.hse.smartcalendar.utility.TimeUtils + @Serializable data class StatisticsDTO( val totalTime: TotalTime, val weekTime: Long, val todayTime: TodayTime, val continuesSuccessDays: ContinuesSuccessDays, - val averageDayTime: AverageDayTime + val averageDayTime: AverageDayTime, + val jsonDate: Instant ) { companion object { - fun fromViewModel(viewModel: StatisticsViewModel): StatisticsDTO { - val totalTime = viewModel.getTotalTimeActivityTypes() - + fun fromStore(): StatisticsDTO { + val totalTime: TotalTimeTaskTypes = StatisticsStore.totalTime return StatisticsDTO( totalTime = TotalTime( common = totalTime.Common.time.inWholeMinutes, @@ -22,19 +29,20 @@ data class StatisticsDTO( study = totalTime.Study.time.inWholeMinutes, fitness = totalTime.Fitness.time.inWholeMinutes ), - weekTime = viewModel.getWeekWorkTime().time.inWholeMinutes, + weekTime = StatisticsStore.weekTime.All.time.inWholeMinutes, todayTime = TodayTime( - planned = viewModel.getTodayPlannedTime().time.inWholeMinutes, - completed = viewModel.getTodayCompletedTime().time.inWholeMinutes + planned = StatisticsStore.todayTime.Planned.time.inWholeMinutes, + completed = StatisticsStore.todayTime.Completed.time.inWholeMinutes ), continuesSuccessDays = ContinuesSuccessDays( - record = viewModel.getRecordContiniusSuccessDays().amount.toLong(), - now = viewModel.getTodayContinusSuccessDays().amount.toLong() + record = StatisticsStore.calculator.getRecordContinuesSuccessDays().toLong(), + now = StatisticsStore.calculator.getTodayContinuesSuccessDays().toLong() ), averageDayTime = AverageDayTime( - totalWorkMinutes = viewModel.getTotalWorkTime().time.inWholeMinutes, - totalDays = 7 - ) + totalWorkMinutes = StatisticsStore.averageDayTime.All.time.inWholeMinutes, + firstDay = StatisticsStore.averageDayTime.firstDay + ), + jsonDate = System.now() ) } } @@ -68,5 +76,5 @@ data class ContinuesSuccessDays( @Serializable data class AverageDayTime( val totalWorkMinutes: Long, - val totalDays: Long + val firstDay: LocalDate ) \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/network/TypeAdapter.kt b/app/src/main/java/org/hse/smartcalendar/network/TypeAdapter.kt index c2fd578..9a2af02 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/TypeAdapter.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/TypeAdapter.kt @@ -1,10 +1,12 @@ import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter +import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailyTaskType +import java.time.OffsetDateTime class LocalTimeAdapter : TypeAdapter() { override fun write(out: JsonWriter, value: LocalTime) { @@ -43,3 +45,19 @@ class DailyTaskTypeAdapter : TypeAdapter() { return DailyTaskType.fromString(reader.nextString().uppercase()) } } +class OffsetDateTimeAdapter: TypeAdapter() { + override fun write(out: JsonWriter, value: OffsetDateTime) { + out.value(value.toString()) + } + override fun read(reader: JsonReader): OffsetDateTime { + return OffsetDateTime.parse(reader.nextString()) + } +} +class InstantTimeAdapter: TypeAdapter() { + override fun write(out: JsonWriter, value: Instant) { + out.value(value.toString()) // ISO-8601 with Z + } + override fun read(reader: JsonReader): Instant { + return Instant.parse(reader.nextString()) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/repository/AudioRepository.kt b/app/src/main/java/org/hse/smartcalendar/repository/AudioRepository.kt new file mode 100644 index 0000000..b0e2d31 --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/repository/AudioRepository.kt @@ -0,0 +1,70 @@ +package org.hse.smartcalendar.repository + +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import org.hse.smartcalendar.data.User +import org.hse.smartcalendar.network.AudioApiInterface +import org.hse.smartcalendar.network.ChatTaskResponse +import org.hse.smartcalendar.network.NetworkResponse +import org.hse.smartcalendar.data.DailyTask +import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.RequestBody.Companion.asRequestBody +import org.hse.smartcalendar.store.StatisticsStore + +class AudioRepository(private val api: AudioApiInterface) : BaseRepository() { + + suspend fun sendAudioAndAddTasks(audioFile: File): NetworkResponse> = withContext(Dispatchers.IO) { + return@withContext try { + val requestFile = audioFile.asRequestBody("audio/*".toMediaType()) + val part = MultipartBody.Part.createFormData("file", audioFile.name, requestFile) + + when (val resp = sendAudio(part)) { + is NetworkResponse.Success -> { + val newTasks: List = resp.data.map { it.toDailyTask() } + val addedTasks: ArrayList = ArrayList() + val schedule = User.getSchedule() + newTasks.forEach { task ->{ + val dailySchedule = schedule + .getOrCreateDailySchedule(task.getTaskDate()) + if (dailySchedule + .addDailyTask(task)){ + addedTasks.add(task) + } + } + } + NetworkResponse.Success(addedTasks) + } + is NetworkResponse.Error -> NetworkResponse.Error(resp.message) + is NetworkResponse.NetworkError -> NetworkResponse.NetworkError(resp.exceptionMessage) + is NetworkResponse.Loading -> NetworkResponse.Loading + } + } catch (e: Exception) { + NetworkResponse.NetworkError(e.localizedMessage ?: "Unknown error") + } + } + suspend fun sendAudioGetResponse(audioFile: File): NetworkResponse = withContext(Dispatchers.IO) { + return@withContext try { + val requestFile = audioFile.asRequestBody("audio/*".toMediaType()) + val part = MultipartBody.Part.createFormData("file", audioFile.name, requestFile) + when (val resp = sendAudio(part)) { + is NetworkResponse.Success -> { + if (resp.data.isEmpty()){ + NetworkResponse.Error("response empty") + } + NetworkResponse.Success(resp.data[0]) + } + is NetworkResponse.Error -> NetworkResponse.Error(resp.message) + is NetworkResponse.NetworkError -> NetworkResponse.NetworkError(resp.exceptionMessage) + is NetworkResponse.Loading -> NetworkResponse.Loading + } + } catch (e: Exception) { + NetworkResponse.NetworkError(e.localizedMessage ?: "Unknown error") + } + } + + suspend fun sendAudio(filePart: MultipartBody.Part): NetworkResponse> { + return withSupplierRequest { api.processAudio(filePart) } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/repository/StatisticsRepository.kt b/app/src/main/java/org/hse/smartcalendar/repository/StatisticsRepository.kt index ecac771..3b6a7e9 100644 --- a/app/src/main/java/org/hse/smartcalendar/repository/StatisticsRepository.kt +++ b/app/src/main/java/org/hse/smartcalendar/repository/StatisticsRepository.kt @@ -7,11 +7,6 @@ import org.hse.smartcalendar.network.StatisticsDTO import org.hse.smartcalendar.view.model.StatisticsViewModel class StatisticsRepository(private val api: StatisticsApiInterface): BaseRepository() { - suspend fun updateStatistics(viewModel: StatisticsViewModel): NetworkResponse { - return withIdRequest { id -> - api.updateUserStatistics(id, StatisticsDTO.fromViewModel(viewModel)) - } - } suspend fun updateStatistics(statisticsRequest: StatisticsDTO): NetworkResponse { return withIdRequest { id -> api.updateUserStatistics(id, statisticsRequest) diff --git a/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt b/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt index 2676126..785255b 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt @@ -19,8 +19,8 @@ import kotlinx.coroutines.launch import org.hse.smartcalendar.AuthScreen import org.hse.smartcalendar.AuthType import org.hse.smartcalendar.AuthViewModel -import org.hse.smartcalendar.notification.ReminderViewModel -import org.hse.smartcalendar.notification.ReminderViewModelFactory +import org.hse.smartcalendar.view.model.ReminderViewModel +import org.hse.smartcalendar.view.model.ReminderViewModelFactory import org.hse.smartcalendar.ui.screens.AchievementsScreen import org.hse.smartcalendar.ui.screens.ChangeLogin import org.hse.smartcalendar.ui.screens.ChangePassword @@ -30,12 +30,10 @@ import org.hse.smartcalendar.ui.screens.SettingsScreen import org.hse.smartcalendar.ui.screens.StatisticsScreen import org.hse.smartcalendar.ui.task.DailyTasksList import org.hse.smartcalendar.ui.task.TaskEditWindow -import org.hse.smartcalendar.utility.AppDrawer import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.Screens import org.hse.smartcalendar.utility.rememberNavigation import org.hse.smartcalendar.view.model.ListViewModel -import org.hse.smartcalendar.view.model.SettingsViewModel import org.hse.smartcalendar.view.model.StatisticsViewModel import org.hse.smartcalendar.view.model.TaskEditViewModel @@ -82,7 +80,6 @@ fun App( @Composable fun NestedNavigator(navigation: Navigation, authModel: AuthViewModel,openDrawer: ()-> Unit, statisticsModel: StatisticsViewModel, listModel: ListViewModel, editModel: TaskEditViewModel){ - var settingsViewModel: SettingsViewModel = viewModel() val reminderModel: ReminderViewModel = viewModel(factory = ReminderViewModelFactory( LocalContext.current.applicationContext as Application )) @@ -105,7 +102,7 @@ fun NestedNavigator(navigation: Navigation, authModel: AuthViewModel,openDrawer: AuthScreen(navigation, authModel, AuthType.Login) } composable(Screens.LOADING.route) { - LoadingScreen(navigation, statisticsModel) + LoadingScreen(navigation, statisticsModel, listModel) } } @@ -143,12 +140,14 @@ fun NestedNavigator(navigation: Navigation, authModel: AuthViewModel,openDrawer: } composable(Screens.EDIT_TASK.route) { TaskEditWindow( - onSave = {}, + onSave = {task->reminderModel.scheduleReminder(task)}, onDelete = { task -> listModel.removeDailyTask(task) - + reminderModel.cancelReminder(task) }, onCancel = {}, - taskEditViewModel = editModel, navController = navigation.navController + taskEditViewModel = editModel, + reminderModel = reminderModel, + navController = navigation.navController ) } } diff --git a/app/src/main/java/org/hse/smartcalendar/utility/AppDriver.kt b/app/src/main/java/org/hse/smartcalendar/ui/navigation/AppDriver.kt similarity index 94% rename from app/src/main/java/org/hse/smartcalendar/utility/AppDriver.kt rename to app/src/main/java/org/hse/smartcalendar/ui/navigation/AppDriver.kt index 2a82af9..b698324 100644 --- a/app/src/main/java/org/hse/smartcalendar/utility/AppDriver.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/navigation/AppDriver.kt @@ -1,4 +1,4 @@ -package org.hse.smartcalendar.utility +package org.hse.smartcalendar.ui.navigation import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -18,6 +18,9 @@ import org.hse.smartcalendar.ui.elements.Finance import org.hse.smartcalendar.ui.elements.Follow_the_signs import org.hse.smartcalendar.ui.elements.Medal import org.hse.smartcalendar.ui.theme.SmartCalendarTheme +import org.hse.smartcalendar.utility.Navigation +import org.hse.smartcalendar.utility.Screens +import org.hse.smartcalendar.utility.rememberNavigation @Composable diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt index 37d2586..04ef3d5 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt @@ -17,12 +17,14 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight @@ -33,6 +35,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import org.hse.smartcalendar.R import org.hse.smartcalendar.ui.navigation.App import org.hse.smartcalendar.ui.navigation.TopButton +import org.hse.smartcalendar.ui.screens.model.AchievementType import org.hse.smartcalendar.ui.theme.DarkBlue import org.hse.smartcalendar.ui.theme.DarkRed import org.hse.smartcalendar.ui.theme.Graphite @@ -40,60 +43,17 @@ import org.hse.smartcalendar.ui.theme.Purple import org.hse.smartcalendar.ui.theme.SmartCalendarTheme import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.Screens +import org.hse.smartcalendar.view.model.StatisticsViewModel import org.hse.smartcalendar.view.model.ListViewModel import org.hse.smartcalendar.view.model.StatisticsManager -import org.hse.smartcalendar.view.model.StatisticsViewModel import org.hse.smartcalendar.view.model.TaskEditViewModel -import kotlin.time.DurationUnit - +import androidx.compose.runtime.getValue @Composable fun AchievementsScreen(navigation: Navigation, openDrawer: (()->Unit)?=null, statisticsModel: StatisticsViewModel) { - val fire = AchievementData( - "Eternal Flame", - R.drawable.fire, - { i -> "Reach a $i day streak" }, - statisticsModel.getRecordContiniusSuccessDays().amount.toLong(), - listOf(5, 10, 20, 50, 100) - ) - val plan = AchievementData( - "Planning everything", - R.drawable.writing_hand, - { i -> "Plan $i hours of your time today" }, - statisticsModel.getTodayPlannedTime().toMinutes() / 60, - listOf(5, 10, 20, 24) - ) - val common = AchievementData( - "Types are boring", - R.drawable.yawning_face, - { i -> "Spend $i hours with common tasks" }, - statisticsModel.getTotalTimeActivityTypes().Common.time.toLong(DurationUnit.HOURS), - listOf(10, 20, 50, 100, 1000) - ) - val taskByTask = AchievementData( - "Hour by Hour", - R.drawable.tasks_complete, - { i -> "Work a total of $i hours" }, - statisticsModel.getTotalWorkTime().toMinutes()/60, - listOf(10, 20, 50, 100, 1000) - ) - val automatic = AchievementData( - "Mechanical Focus", - R.drawable.robot, - { i -> "Work a total of $i hours last week" }, - statisticsModel.getWeekWorkTime().toMinutes()/60/7, - listOf(4, 6, 8, 10) - ) - val totallyBalanced = AchievementData( - "Totally balanced", - R.drawable.balance_scale, - { i -> "Have $i types of task in day" }, - statisticsModel.getTypesInDay(), - listOf(4) - ) - val itemsData = - listOf(fire, plan, common, taskByTask, automatic, totallyBalanced) + val uiState by statisticsModel.uiState.collectAsState() + val itemsData = AchievementType.entries.toTypedArray() Scaffold( topBar = { TopButton(openDrawer, navigation, "Achievements") } ) {paddingValues-> @@ -102,7 +62,11 @@ fun AchievementsScreen(navigation: Navigation, .padding(paddingValues), content = { items(itemsData.size) { index -> - AchievementCard(itemsData[index]) + val parameter = itemsData[index].parameterProvider(uiState) + AchievementCard( + itemsData[index], + parameter + ) } } ) @@ -111,23 +75,25 @@ fun AchievementsScreen(navigation: Navigation, } @Composable -fun AchievementCard(data: AchievementData) { +fun AchievementCard(data: AchievementType, + parameter: Long) { val colors: List = listOf(colorResource(R.color.bronze), Color.LightGray, Color.Yellow, colorResource(R.color.emerald), Color.Cyan, Color.Cyan) val backColors: List = listOf(DarkBlue, Graphite, Purple, Color.DarkGray, DarkRed, DarkRed) var level = 0 var description = data.description val lastLevel = data.levels.takeLast(1)[0] - if (lastLevel <= data.parameter) { + if (lastLevel <= parameter) { description = { long -> "You have max level, you achieve:" + data.description(lastLevel) } level = data.levels.size-1 } else { - while (data.levels[level] <= data.parameter) { + while (data.levels[level] <= parameter) { level++ } } Row( Modifier .padding(32.dp) + .testTag(data.testTag) ) { Box(modifier = Modifier.weight(0.3f) .wrapContentSize(align = Alignment.Center) @@ -165,31 +131,26 @@ fun AchievementCard(data: AchievementData) { fontWeight = FontWeight.SemiBold ) Text( - data.parameter.toString() + "/" + data.levels[level].toString(), + parameter.toString() + "/" + data.levels[level].toString(), fontSize = 14.sp, fontWeight = FontWeight.Light ) } LinearProgressIndicator( - progress = { data.parameter.toFloat() / data.levels[level] }, + progress = {parameter.toFloat() / data.levels[level] }, color = Color.Yellow, trackColor = Color.LightGray, modifier = Modifier .padding(8.dp) .align(Alignment.Start) + .testTag("${data.testTag}_progress") ) Text(description(data.levels.get(level))) } } } -data class AchievementData( - val title: String, - val iconId: Int, val description: (Long) -> String, - val parameter: Long, - val levels: List -) @Preview @Composable @@ -213,13 +174,8 @@ fun AchievementsScreenPreview() { fun AchievementElementPreview() { SmartCalendarTheme { AchievementCard( - AchievementData( - title = "Eternal Flame", - iconId = R.drawable.fire, - description = { i -> "Reach a $i day streak" }, - parameter = 5, - levels = listOf(5, 10, 20, 50, 100) - ) + AchievementType.Streak, + parameter = 5 ) } } \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/AuthScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/AuthScreen.kt index 165dcfb..7191f32 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/AuthScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/AuthScreen.kt @@ -51,14 +51,6 @@ fun AuthScreen(navigation: Navigation, viewModel: AuthViewModel, authType:AuthTy verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - Button( - onClick = { - navigation.navigateToMainApp(Screens.CALENDAR.route) - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Calendar") - } TextField( value = username, onValueChange = { username = it }, @@ -106,7 +98,7 @@ fun AuthScreen(navigation: Navigation, viewModel: AuthViewModel, authType:AuthTy CircularProgressIndicator() } is NetworkResponse.Success -> { - Text("Login successful! Token: ${state.data.token}") + Text("Login successful!") navigation.navigateTo(Screens.LOADING.route) } is NetworkResponse.Error -> { diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/LoadingScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/LoadingScreen.kt index bf74262..6bb77fa 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/LoadingScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/LoadingScreen.kt @@ -22,9 +22,9 @@ import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.Screens import org.hse.smartcalendar.view.model.InitViewModel import org.hse.smartcalendar.view.model.StatisticsViewModel - +import org.hse.smartcalendar.view.model.ListViewModel @Composable -fun LoadingScreen(navigation: Navigation, statisticsVM: StatisticsViewModel){ +fun LoadingScreen(navigation: Navigation, statisticsVM: StatisticsViewModel, listModel: ListViewModel){ val initVM: InitViewModel = viewModel()//гарантирует 1 модель на Composable val initState by initVM.initResult.observeAsState() val statisticsState by statisticsVM.initResult.observeAsState() @@ -59,6 +59,7 @@ fun LoadingScreen(navigation: Navigation, statisticsVM: StatisticsViewModel){ initState is NetworkResponse.Success && statisticsState is NetworkResponse.Success-> { LaunchedEffect(statisticsState) { delay(1000) + listModel.loadDailyTasks() navigation.navigateToMainApp(Screens.CALENDAR.route) } } diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/SettingsScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/SettingsScreen.kt index 7863f3b..f29b1f4 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/SettingsScreen.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import org.hse.smartcalendar.AuthViewModel -import org.hse.smartcalendar.notification.ReminderViewModel +import org.hse.smartcalendar.view.model.ReminderViewModel import org.hse.smartcalendar.ui.elements.Password import org.hse.smartcalendar.ui.elements.Person import org.hse.smartcalendar.ui.elements.Reminder diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt index ccec651..185a346 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt @@ -25,13 +25,17 @@ import org.hse.smartcalendar.ui.elements.ChartModel import org.hse.smartcalendar.ui.elements.CircleColored import org.hse.smartcalendar.ui.navigation.TopButton import org.hse.smartcalendar.ui.theme.SmartCalendarTheme +import androidx.compose.runtime.collectAsState import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.rememberNavigation -import org.hse.smartcalendar.view.model.AbstractStatisticsViewModel.Companion.toPercent +import org.hse.smartcalendar.view.model.StatisticsViewModel.Companion.toPercent import org.hse.smartcalendar.view.model.StatisticsViewModel +import androidx.compose.runtime.getValue +import org.hse.smartcalendar.view.model.state.DaysAmount @Composable fun StatisticsScreen(navigation: Navigation, openMenu: () -> Unit, statisticsModel: StatisticsViewModel) { + val uiState by statisticsModel.uiState.collectAsState() fun safeDelete(dividend: Long, divisor: Long): Float { return if (divisor == 0L || dividend == 0L) 0f else dividend.toFloat() / divisor } @@ -47,22 +51,22 @@ fun StatisticsScreen(navigation: Navigation, openMenu: () -> Unit, statisticsMod ) { Row { SafeProgressBox( - dividend = statisticsModel.getTodayCompletedTime().time.inWholeMinutes, - divisor = statisticsModel.getTodayPlannedTime().time.inWholeMinutes, + dividend = {uiState.today.Completed.time.inWholeMinutes}, + divisor = {uiState.today.Planned.time.inWholeMinutes}, label = "Completed/Planned", color = Color.Blue, modifier = Modifier.weight(1f / 3f) ) SafeProgressBox( - dividend = statisticsModel.getTodayCompletedTime().time.inWholeMinutes, - divisor = statisticsModel.getAverageDailyTime().time.inWholeMinutes, + dividend = {uiState.today.Completed.time.inWholeMinutes}, + divisor = {uiState.averageDay.All.time.inWholeMinutes}, label = "Completed/Average", color = Color.Green, modifier = Modifier.weight(1f / 3f) ) SafeProgressBox( - dividend = statisticsModel.getTodayContinusSuccessDays().amount.toLong(), - divisor = statisticsModel.getRecordContiniusSuccessDays().amount.toLong(), + dividend = {uiState.calculable.continuesCurrent.toLong()}, + divisor = {uiState.calculable.continuesTotal.toLong()}, label = "Days in a row, when completed all the tasks", color = Color.Red, modifier = Modifier.weight(1f / 3f) @@ -72,43 +76,43 @@ fun StatisticsScreen(navigation: Navigation, openMenu: () -> Unit, statisticsMod Row { Box(modifier = Modifier.weight(0.5f), contentAlignment = Alignment.Center){ Column { - Text("Average work time:") + Text("Average activity time:") Text("Today planned work time:") - Text("Total work time:") + Text("Total activity time:") Text("Maximum days in a row when all tasks are completed:") } } Box(modifier = Modifier.weight(0.5f)){ Column { - Text(statisticsModel.getAverageDailyTime().toFullString()) - Text(statisticsModel.getTodayPlannedTime().toFullString()) - Text(statisticsModel.getTotalWorkTime().toPrettyString()) - Text(statisticsModel.getRecordContiniusSuccessDays().toPrettyString()) + Text(uiState.averageDay.All.toFullString()) + Text(uiState.today.Planned.toFullString()) + Text(uiState.total.All.toPrettyString()) + Text(DaysAmount(uiState.calculable.continuesTotal).toPrettyString()) } } } } - ActivityTypesDataDisplay(typesModel = statisticsModel.getTotalTimeActivityTypes(), + ActivityTypesDataDisplay(typesModel = {uiState.total}, columnHeight = 8.dp); } } } @Composable fun ActivityTypesDataDisplay(modifier: Modifier=Modifier, - typesModel: TotalTimeTaskTypes, + typesModel: ()->TotalTimeTaskTypes, columnHeight: Dp ){ val charts = listOf( - ChartModel(value = typesModel.StudyPercent, color = DailyTaskType.STUDIES.color), - ChartModel(value = typesModel.FitnessPercent, color = DailyTaskType.FITNESS.color), - ChartModel(value = typesModel.CommonPercent, color = DailyTaskType.COMMON.color), - ChartModel(value = typesModel.WorkPercent, color = DailyTaskType.WORK.color), + ChartModel(value = typesModel().StudyPercent, color = DailyTaskType.STUDIES.color), + ChartModel(value = typesModel().FitnessPercent, color = DailyTaskType.FITNESS.color), + ChartModel(value = typesModel().CommonPercent, color = DailyTaskType.COMMON.color), + ChartModel(value = typesModel().WorkPercent, color = DailyTaskType.WORK.color), ) - val tableData = mapOf("Work" to Pair(typesModel.WorkPercent, DailyTaskType.WORK.color), + val tableData = mapOf("Work" to Pair(typesModel().WorkPercent, DailyTaskType.WORK.color), "Study" to - Pair(typesModel.StudyPercent, DailyTaskType.STUDIES.color), - "Fitness" to Pair(typesModel.FitnessPercent, DailyTaskType.FITNESS.color), - "Common" to Pair(typesModel.CommonPercent, DailyTaskType.COMMON.color)) + Pair(typesModel().StudyPercent, DailyTaskType.STUDIES.color), + "Fitness" to Pair(typesModel().FitnessPercent, DailyTaskType.FITNESS.color), + "Common" to Pair(typesModel().CommonPercent, DailyTaskType.COMMON.color)) Column(verticalArrangement = Arrangement.Center) { ChartCirclePie(modifier.align(Alignment.CenterHorizontally), charts) tableData.forEach { @@ -129,19 +133,19 @@ fun ActivityTypesDataDisplay(modifier: Modifier=Modifier, } @Composable fun SafeProgressBox( - dividend: Long, - divisor: Long, + dividend: ()->Long, + divisor: ()->Long, label: String, color: Color, modifier: Modifier = Modifier ) { Box(modifier = modifier, contentAlignment = Alignment.Center) { - if (dividend == 0L) { + if (divisor() == 0L) { Box(modifier = modifier.then(Modifier.size(100.dp)), contentAlignment = Alignment.Center) { Text("No data") } } else { - fun progress(): Float = dividend.toFloat() / divisor + fun progress(): Float = dividend().toFloat() / divisor() ProgressCircleWithText( progress = { progress() }, text = label, diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt new file mode 100644 index 0000000..3240b9d --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt @@ -0,0 +1,65 @@ +package org.hse.smartcalendar.ui.screens.model + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Composable +import org.hse.smartcalendar.R +import org.hse.smartcalendar.view.model.StatisticsViewModel +import org.hse.smartcalendar.view.model.state.StatisticsUiState + +enum class AchievementType( + val title: String, + @DrawableRes val iconId: Int, + val description: (Long) -> String, + val levels: List, + val parameterProvider: (StatisticsUiState) -> Long,//читает изменения State + val testTag: String +) { + Streak( + title = "Eternal Flame", + iconId = R.drawable.fire, + description = { i -> "Reach a $i day streak" }, + levels = listOf(5, 10, 20, 50, 100), + parameterProvider = { uiState->uiState.calculable.continuesCurrent.toLong() }, + testTag = "Fire" + ), + PlanToday( + title = "Planning everything", + iconId = R.drawable.writing_hand, + description = { i -> "Plan $i hours of your time today" }, + levels = listOf(5, 10, 20, 24), + parameterProvider = { uiState->(uiState.today.Planned.time.inWholeMinutes + 1) / 60 }, + testTag = "PlanToday" + ), + CommonSpend( + title = "Types are boring", + iconId = R.drawable.yawning_face, + description = { i -> "Spend $i hours with common tasks" }, + levels = listOf(10, 20, 50, 100, 1000), + parameterProvider = { uiState->uiState.total.Common.time.inWholeHours }, + testTag = "CommonSpend" + ), + WorkTotal( + title = "Hour by Hour", + iconId = R.drawable.tasks_complete, + description = { i -> "Work a total of $i hours" }, + levels = listOf(10, 20, 50, 100, 1000), + parameterProvider = { uiState->uiState.total.All.time.inWholeHours }, + testTag = "WorkTotal" + ), + WorkWeek( + title = "Mechanical Focus", + iconId = R.drawable.robot, + description = { i -> "Work a total of $i hours last week" }, + levels = listOf(4, 6, 8, 10), + parameterProvider = { uiState->uiState.week.All.time.inWholeHours / 7 }, + testTag = "WorkWeek" + ), + TypesToday( + title = "Totally balanced", + iconId = R.drawable.balance_scale, + description = { i -> "Have $i types of task in day" }, + levels = listOf(4), + parameterProvider = { uiState->uiState.calculable.typesInDay.toLong() }, + testTag = "TypesToday" + ); +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt index 87614a2..fc1b949 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt @@ -19,8 +19,10 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -41,6 +43,8 @@ import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailySchedule import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.network.ChatTaskResponse +import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.ui.elements.AudioRecorderButton import org.hse.smartcalendar.ui.theme.SmartCalendarTheme import org.hse.smartcalendar.utility.fromMinutesOfDay @@ -55,7 +59,7 @@ fun BottomSheet( isBottomSheetVisible: MutableState, sheetState: SheetState, onDismiss: () -> Unit, - onRecordStop: () -> DailyTask? = { null }, + onRecordStop: () -> ChatTaskResponse? = { null }, audioFile: MutableState, taskTitle: MutableState, taskType: MutableState, @@ -71,9 +75,13 @@ fun BottomSheet( val isNestedTask = rememberSaveable { mutableStateOf(false) } val fstFiledHasFormatError = rememberSaveable { mutableStateOf(false) } val sndFiledHasFormatError = rememberSaveable { mutableStateOf(false) } - + val actionResult by viewModel.actionResult.observeAsState() val isErrorInRecorder = rememberSaveable { mutableStateOf(false) } - + LaunchedEffect(isBottomSheetVisible.value) { + if (isBottomSheetVisible.value){ + isErrorInRecorder.value = false + } + } if (isBottomSheetVisible.value) { ModalBottomSheet( onDismissRequest = { @@ -154,9 +162,14 @@ fun BottomSheet( Spacer(modifier = Modifier.padding(12.dp)) Box(Modifier.align(Alignment.CenterHorizontally)) { + val errorMsg = when (actionResult) { + is NetworkResponse.Error -> (actionResult as NetworkResponse.Error).message + is NetworkResponse.NetworkError -> (actionResult as NetworkResponse.NetworkError).exceptionMessage + else -> "Unknown error" + } if (isErrorInRecorder.value) { Text( - text = "Error in Audio", + text = "Error in Audio:$errorMsg", color = MaterialTheme.colorScheme.error, fontSize = 12.sp, modifier = Modifier.padding(top = 4.dp) @@ -180,13 +193,13 @@ fun BottomSheet( onStop = { val task = onRecordStop() if (task != null) { - isErrorInRecorder.value = false - taskTitle.value = task.getDailyTaskTitle() - taskDescription.value = task.getDailyTaskDescription() - taskType.value = task.getDailyTaskType() - startTime.value = - LocalTime.toMinutesOfDay(task.getDailyTaskStartTime()) - endTime.value = LocalTime.toMinutesOfDay(task.getDailyTaskEndTime()) + task.applyToUiStates( + taskTitle = taskTitle, + taskDescription = taskDescription, + taskType = taskType, + isErrorInRecorder = isErrorInRecorder, + startTime = startTime, + endTime = endTime) } else { isErrorInRecorder.value = true } diff --git a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskEditWindow.kt b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskEditWindow.kt index fd782a0..7c1d67b 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskEditWindow.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskEditWindow.kt @@ -1,6 +1,7 @@ package org.hse.smartcalendar.ui.task import android.annotation.SuppressLint +import android.app.Application import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -29,6 +30,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.wear.compose.material3.Text import kotlinx.datetime.LocalTime @@ -37,6 +39,8 @@ import org.hse.smartcalendar.utility.Screens import org.hse.smartcalendar.utility.fromMinutesOfDay import org.hse.smartcalendar.utility.toMinutesOfDay import org.hse.smartcalendar.view.model.ListViewModel +import org.hse.smartcalendar.view.model.ReminderViewModel +import org.hse.smartcalendar.view.model.ReminderViewModelFactory import org.hse.smartcalendar.view.model.StatisticsManager import org.hse.smartcalendar.view.model.StatisticsViewModel import org.hse.smartcalendar.view.model.TaskEditViewModel @@ -45,10 +49,11 @@ import org.hse.smartcalendar.view.model.TaskEditViewModel @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun TaskEditWindow( - onSave: () -> Unit, + onSave: (DailyTask) -> Unit, onCancel: () -> Unit, onDelete: (DailyTask) -> Unit, taskEditViewModel: TaskEditViewModel, + reminderModel: ReminderViewModel, navController: NavController ) { val taskState = taskEditViewModel.getTask() @@ -78,12 +83,12 @@ fun TaskEditWindow( ) taskEditViewModel.changes.setDailyTaskEndTime(LocalTime.fromMinutesOfDay(endTime.intValue)) taskEditViewModel.changes.setDailyTaskType(taskType.value) - taskEditViewModel.updateInnerTask( + val result = taskEditViewModel.updateInnerTask( isEmptyTitle, isConflictInTimeField, - isNestedTask + isNestedTask, + reminderModel ) - onSave() if (!isEmptyTitle.value && !isConflictInTimeField.value && !isNestedTask.value) { navController.navigate(Screens.CALENDAR.route) } @@ -168,6 +173,9 @@ fun TaskEditWindow( @Preview @Composable fun TaskEditWindowPreview() { + val reminderModel: ReminderViewModel = viewModel(factory = ReminderViewModelFactory( + LocalContext.current.applicationContext as Application + )) TaskEditWindow( onSave = { }, onCancel = { }, @@ -175,6 +183,7 @@ fun TaskEditWindowPreview() { taskEditViewModel = TaskEditViewModel( listViewModel = ListViewModel(StatisticsManager(StatisticsViewModel())) ), + reminderModel = reminderModel, navController = NavController( LocalContext.current ) diff --git a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt index ec86193..c37474d 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt @@ -59,8 +59,8 @@ import kotlinx.datetime.format.char import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType import org.hse.smartcalendar.network.NetworkResponse -import org.hse.smartcalendar.notification.ReminderViewModel -import org.hse.smartcalendar.notification.ReminderViewModelFactory +import org.hse.smartcalendar.view.model.ReminderViewModel +import org.hse.smartcalendar.view.model.ReminderViewModelFactory import org.hse.smartcalendar.ui.navigation.TopButton import org.hse.smartcalendar.ui.theme.SmartCalendarTheme import org.hse.smartcalendar.utility.Navigation @@ -124,6 +124,8 @@ fun DailyTasksList( DailyTaskCard( it, onCompletionChange = { + if (!it.isComplete()){reminderModel.cancelReminder(it)} + else {reminderModel.scheduleReminder(it)} viewModel.changeTaskCompletion(it, !it.isComplete()) }, taskEditViewModel = taskEditViewModel, @@ -301,7 +303,8 @@ fun DailyTaskListPreview() { onCancel = { navController.popBackStack() }, taskEditViewModel = taskEditViewModel, navController = navController, - onDelete = { } + onDelete = { }, + reminderModel = reminderModel ) } } diff --git a/app/src/main/java/org/hse/smartcalendar/utility/ListViewUtility.kt b/app/src/main/java/org/hse/smartcalendar/utility/ListViewUtility.kt index 991ac0e..7590a3c 100644 --- a/app/src/main/java/org/hse/smartcalendar/utility/ListViewUtility.kt +++ b/app/src/main/java/org/hse/smartcalendar/utility/ListViewUtility.kt @@ -11,8 +11,9 @@ fun editHandler( isEmptyTitle: MutableState, isConflictInTimeField: MutableState, isNestedTask: MutableState, - statsUpdateOldToNewTask: (DailyTask, DailyTask)-> Unit -) { + statsUpdateOldToNewTask: (DailyTask, DailyTask)-> Unit, + reminderUpdate: (DailyTask)->Unit +): Boolean { isEmptyTitle.value = false isNestedTask.value = false isConflictInTimeField.value = false @@ -24,5 +25,8 @@ fun editHandler( if (!isEmptyTitle.value && !isConflictInTimeField.value && !isNestedTask.value) { oldTask.updateDailyTask(newTask) statsUpdateOldToNewTask(oldTask, newTask) + reminderUpdate(newTask) + return true } + return false } diff --git a/app/src/main/java/org/hse/smartcalendar/utility/TimePeriodUtils.kt b/app/src/main/java/org/hse/smartcalendar/utility/TimePeriodUtils.kt index 72b4dac..5622510 100644 --- a/app/src/main/java/org/hse/smartcalendar/utility/TimePeriodUtils.kt +++ b/app/src/main/java/org/hse/smartcalendar/utility/TimePeriodUtils.kt @@ -1,17 +1,10 @@ package org.hse.smartcalendar.utility -import androidx.compose.runtime.mutableStateOf import kotlinx.datetime.Clock import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone -import kotlinx.datetime.toDateTimePeriod import kotlinx.datetime.toLocalDateTime -import org.hse.smartcalendar.utility.TimeUtils.Companion.numberToWord -import kotlin.time.Duration -import kotlin.time.Duration.Companion.minutes -import kotlin.time.DurationUnit -import kotlin.time.toDuration class TimeUtils { @@ -19,6 +12,9 @@ class TimeUtils { fun getCurrentDateTime(): LocalDateTime { return Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()); } + fun getCurrentTimezone(): TimeZone{ + return TimeZone.currentSystemDefault() + } fun numberToWord(amount: Int, item: String): String { if (amount != 0) { @@ -38,80 +34,3 @@ class TimeUtils { } } -//every class in Kotlin is final by default, so inheritance from LocalTime not ok -class TimePeriod(minute: Long) { - private var _time = mutableStateOf(Duration.ZERO) - val time: Duration get() = _time.value - - init { - fromMinutes(minute) - } - - fun fromMinutes(minute: Long) { - _time.value += minute.minutes - } - - fun toMinutes(): Long { - return time.inWholeMinutes - } - - fun addMinutes(minutes: Long, sign: Boolean) { - when (sign) { - true -> _time.value += minutes.toDuration(DurationUnit.MINUTES) - false -> _time.value -= minutes.toDuration(DurationUnit.MINUTES) - } - } - - fun toPrettyString(): String { - val stringBuilder = StringBuilder() - val dataTime = time.toDateTimePeriod() - stringBuilder.append(numberToWord(dataTime.years, "year")) - stringBuilder.append(numberToWord(dataTime.days, "day")) - stringBuilder.append(numberToWord(dataTime.hours, "hour")) - stringBuilder.append(numberToWord(dataTime.minutes, "minute")) - return if (stringBuilder.toString() != "") stringBuilder.toString() else "0 minute" - } -} - -class DaysAmount(initialAmount: Int) { - private var _amount = mutableStateOf(initialAmount) - val amount: Int get() = _amount.value - - init { - _amount.value = amount - } - - fun toPrettyString(): String { - return if (amount != 1) "$amount days" else "1 day" - } -} - -class DayPeriod(minute: Long) { - private var _time = mutableStateOf(Duration.ZERO) - val time: Duration get() = _time.value - fun toMinutes(): Long { - return time.inWholeMinutes - } - - init { - fromMinutes(minute) - } - - fun fromMinutes(minute: Long) { - _time.value = minute.toDuration(DurationUnit.MINUTES) - } - - fun plusMinutes(minute: Int, sign: Boolean) { - when (sign) { - true -> _time.value += minute.toDuration(DurationUnit.MINUTES); - false -> _time.value -= minute.toDuration(DurationUnit.MINUTES); - } - } - - fun toFullString(): String { - var stringBuilder: StringBuilder = StringBuilder() - stringBuilder.append(numberToWord(time.toDateTimePeriod().hours, "hour")) - stringBuilder.append(numberToWord(time.toDateTimePeriod().minutes, "minute")) - return if (stringBuilder.toString() != "") stringBuilder.toString() else "0 minute" - } -} diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt index 7348c3f..f1b9831 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt @@ -1,18 +1,21 @@ package org.hse.smartcalendar.view.model import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.workDataOf +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalTime import kotlinx.datetime.TimeZone import kotlinx.datetime.minus import kotlinx.datetime.plus @@ -23,10 +26,12 @@ import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskAction import org.hse.smartcalendar.data.User import org.hse.smartcalendar.data.WorkManagerHolder +import org.hse.smartcalendar.network.ApiClient +import org.hse.smartcalendar.network.ChatTaskResponse import org.hse.smartcalendar.network.NetworkResponse -import org.hse.smartcalendar.notification.TaskApiWorker +import org.hse.smartcalendar.repository.AudioRepository +import org.hse.smartcalendar.work.TaskApiWorker import java.io.File -import java.util.concurrent.TimeUnit open class AbstractListViewModel(val statisticsManager: StatisticsManager) : ViewModel() { var _actionResult = MutableLiveData>() @@ -34,19 +39,19 @@ open class AbstractListViewModel(val statisticsManager: StatisticsManager) : Vie fun getScreenDate(): LocalDate{ return dailyTaskSchedule.date } - private var dailyTaskSchedule: DailySchedule + private lateinit var dailyTaskSchedule: DailySchedule private var dailyScheduleDate = mutableStateOf( Clock.System.now() .toLocalDateTime(TimeZone.currentSystemDefault()).date ) - var dailyTaskList: SnapshotStateList - private val user: User = User + val dailyTaskList: SnapshotStateList = mutableStateListOf() + protected val user: User = User init { - val date: LocalDate = - Clock.System.now() - .toLocalDateTime(TimeZone.currentSystemDefault()).date - dailyTaskSchedule = user.getSchedule().getOrCreateDailySchedule(date) - dailyTaskList = SnapshotStateList(dailyTaskSchedule.getDailyTaskList()) + loadDailyTasks() + } + fun loadDailyTasks(){ + dailyTaskSchedule = user.getSchedule().getOrCreateDailySchedule(dailyScheduleDate.value) + dailyTaskList.addAll(dailyTaskSchedule.getDailyTaskList()) } open fun scheduleTaskRequest(task: DailyTask, action: DailyTaskAction.Type) { } @@ -60,7 +65,7 @@ open class AbstractListViewModel(val statisticsManager: StatisticsManager) : Vie dailyTaskList.sortBy { task -> task.getDailyTaskStartTime() } - statisticsManager.addDailyTask(newTask) + statisticsManager.addDailyTask(newTask, dailyTaskList) scheduleTaskRequest(newTask, DailyTaskAction.Type.ADD) } @@ -69,7 +74,7 @@ open class AbstractListViewModel(val statisticsManager: StatisticsManager) : Vie // TODO } else { dailyTaskList.remove(task) - statisticsManager.removeDailyTask(task) + statisticsManager.removeDailyTask(task, dailyTaskList) scheduleTaskRequest(task, DailyTaskAction.Type.DELETE) } } @@ -139,13 +144,12 @@ open class AbstractListViewModel(val statisticsManager: StatisticsManager) : Vie } class ListViewModel(statisticsManager: StatisticsManager) : AbstractListViewModel(statisticsManager) { private val workManager = WorkManagerHolder.getInstance() - + private val audioRepo = AudioRepository(ApiClient.audioApiService) override fun scheduleTaskRequest(task: DailyTask, action: DailyTaskAction.Type) { val taskJson = Json.encodeToString(DailyTaskAction.serializer(), DailyTaskAction(task = task, type = action)) val workRequest = OneTimeWorkRequestBuilder() .setInputData(workDataOf(DailyTaskAction.jsonName to taskJson)) - .setInitialDelay(10, TimeUnit.SECONDS) .build() workManager.enqueueUniqueWork( "task_${task.getId()}", @@ -156,17 +160,20 @@ class ListViewModel(statisticsManager: StatisticsManager) : AbstractListViewMode fun sendAudio( audioFile: MutableState, description: AudioDescription, - ): DailyTask? { - // TODO Надо написать отправку файла и обработку ответа. - Thread.sleep(1000) - val task: DailyTask = DailyTask( - title = "TODO", - description = "TODO", - start = LocalTime(0, 0), - end = LocalTime(0, 0), - date = DailyTask.defaultDate - ) - return task + ): ChatTaskResponse? { + val file = audioFile.value + var response: ChatTaskResponse? = null + if (file!=null) { + viewModelScope.launch { + _actionResult.value = NetworkResponse.Loading + val result = audioRepo.sendAudioGetResponse(file) + if (result is NetworkResponse.Success){ + response = result.data + } + _actionResult.value = result + } + } + return response } class NestedTask(val nestedTask: DailyTask) : diff --git a/app/src/main/java/org/hse/smartcalendar/notification/ReminderViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/ReminderViewModel.kt similarity index 74% rename from app/src/main/java/org/hse/smartcalendar/notification/ReminderViewModel.kt rename to app/src/main/java/org/hse/smartcalendar/view/model/ReminderViewModel.kt index 01c7b2e..1300a32 100644 --- a/app/src/main/java/org/hse/smartcalendar/notification/ReminderViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/ReminderViewModel.kt @@ -1,8 +1,9 @@ -package org.hse.smartcalendar.notification +package org.hse.smartcalendar.view.model import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.workDataOf @@ -14,6 +15,7 @@ import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.utility.prettyPrint import org.hse.smartcalendar.utility.toEpochMilliseconds +import org.hse.smartcalendar.work.ReminderWorker import java.util.concurrent.TimeUnit import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -35,8 +37,6 @@ class ReminderViewModel(application: Application): ViewModel() { internal fun scheduleReminder( task: DailyTask): Boolean { - //ReminderVm ff62295 - //разные ссылки на VM if (!isReminders.value){ return false } @@ -60,17 +60,24 @@ class ReminderViewModel(application: Application): ViewModel() { } myWorkRequestBuilder.setInputData( workDataOf( - ReminderWorker.TYPE_KEY to task.getDailyTaskType().toString().lowercase(), - ReminderWorker.BEFORE_KEY to realMinutesBefore, - ReminderWorker.TITLE_KEY to task.getDailyTaskTitle(), - ReminderWorker.MESSAGE_KEY to task.getDailyTaskDescription(), - ReminderWorker.START_KEY to LocalTime.prettyPrint(task.getDailyTaskStartTime()), - ReminderWorker.END_KEY to LocalTime.prettyPrint(task.getDailyTaskEndTime()), + ReminderWorker.Companion.TYPE_KEY to task.getDailyTaskType().toString().lowercase(), + ReminderWorker.Companion.BEFORE_KEY to realMinutesBefore, + ReminderWorker.Companion.TITLE_KEY to task.getDailyTaskTitle(), + ReminderWorker.Companion.MESSAGE_KEY to task.getDailyTaskDescription(), + ReminderWorker.Companion.START_KEY to LocalTime.prettyPrint(task.getDailyTaskStartTime()), + ReminderWorker.Companion.END_KEY to LocalTime.prettyPrint(task.getDailyTaskEndTime()), ) ) - workManager.enqueue(myWorkRequestBuilder.build()) + workManager.enqueueUniqueWork( + "reminder${task.getId()}", + ExistingWorkPolicy.REPLACE, + myWorkRequestBuilder.build()) return true } + internal fun cancelReminder(task: DailyTask) { + val workName = "reminder${task.getId()}" + workManager.cancelUniqueWork(workName) + } } class ReminderViewModelFactory(private val application: Application) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/SettingsViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/SettingsViewModel.kt deleted file mode 100644 index 009f1aa..0000000 --- a/app/src/main/java/org/hse/smartcalendar/view/model/SettingsViewModel.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.hse.smartcalendar.view.model - -//import dagger.hilt.android.lifecycle.HiltViewModel -//import jakarta.inject.Inject -import androidx.lifecycle.ViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow - -class SettingsViewModel constructor( -) : ViewModel() { - private val _isReminders: MutableStateFlow = MutableStateFlow(true) - var isReminders = _isReminders.asStateFlow() - fun switchReminders(){ - _isReminders.value = _isReminders.value.not() - } - //SettingsVM 46d3d -} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsManager.kt b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsManager.kt index c3f52a1..627ff3d 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsManager.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsManager.kt @@ -5,15 +5,20 @@ import org.hse.smartcalendar.data.DailyTask /** * Класс, передающий изменения ListVM и TaskEditVM в StatisticsVM */ -class StatisticsManager(private val viewModel: AbstractStatisticsViewModel){ +class StatisticsManager(private val viewModel: StatisticsViewModel){ fun changeTaskCompletion(task: DailyTask, isCompleted: Boolean){ viewModel.changeTaskCompletion(task, isCompleted) } - fun removeDailyTask(task: DailyTask){ - viewModel.createOrDeleteTask(task, false) + fun removeDailyTask(task: DailyTask, dailyTaskList: List){ + viewModel.createOrDeleteTask(dailyTaskList = dailyTaskList, + task = task, + isCreate = false) } - fun addDailyTask(task: DailyTask){ - viewModel.createOrDeleteTask(task, true) + fun addDailyTask(task: DailyTask, dailyTaskList: List){ + viewModel.createOrDeleteTask( + dailyTaskList= dailyTaskList, + task = task, + isCreate = true) } /** diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt index 4e52957..29fba10 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt @@ -4,33 +4,22 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.workDataOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json import org.hse.smartcalendar.data.DailyTask -import org.hse.smartcalendar.data.TotalTimeTaskTypes -import org.hse.smartcalendar.data.WorkManagerHolder -import org.hse.smartcalendar.network.ApiClient -import org.hse.smartcalendar.network.AverageDayTime -import org.hse.smartcalendar.network.ContinuesSuccessDays import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.network.StatisticsDTO -import org.hse.smartcalendar.network.TodayTime -import org.hse.smartcalendar.notification.StatisticsUploadWorker -import org.hse.smartcalendar.repository.StatisticsRepository -import org.hse.smartcalendar.utility.DayPeriod -import org.hse.smartcalendar.utility.DaysAmount -import org.hse.smartcalendar.utility.TimePeriod -import java.util.concurrent.TimeUnit -import kotlin.math.max +import org.hse.smartcalendar.store.StatisticsStore +import org.hse.smartcalendar.view.model.state.StatisticsUiState import kotlin.math.roundToInt -open class AbstractStatisticsViewModel():ViewModel() { - private val statisticsRepo: StatisticsRepository = StatisticsRepository(ApiClient.statisticsApiService) +open class StatisticsViewModel():ViewModel() { var _initResult = MutableLiveData>() val initResult:LiveData> = _initResult + private val _uiState = MutableStateFlow(StatisticsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() companion object { fun getPercent(part: Long, all: Long): Float { if (all == 0L) return 25.0f @@ -43,143 +32,37 @@ open class AbstractStatisticsViewModel():ViewModel() { return (part * 1000).roundToInt().toFloat() / 10 } } - private class WeekTime(all: Long){ - val All: TimePeriod = TimePeriod(all) - } - private class TodayTimeVars(planned: Long, completed: Long){ - val Planned: DayPeriod = DayPeriod(planned) - var Completed: DayPeriod = DayPeriod(completed) - companion object{ - fun fromTodayTimeDTO(todayTimeDTO: TodayTime): TodayTimeVars{ - return TodayTimeVars(planned = todayTimeDTO.planned, - completed = todayTimeDTO.completed) - } - } - } - private class ContinuesSuccessDaysVars(record: Int, now: Int){ - val Record: DaysAmount = DaysAmount(record) - val Now: DaysAmount = DaysAmount(now) - companion object{ - fun fromContinuesSuccessDTO(continuesSuccessDTO: ContinuesSuccessDays): ContinuesSuccessDaysVars{ - return ContinuesSuccessDaysVars(record = continuesSuccessDTO.record.toInt(), - now = continuesSuccessDTO.now.toInt()) - } - } - } - private class AverageDayTimeVars(totalWorkMinutes: Long, val totalDays: Long){ - var All: DayPeriod = DayPeriod(totalWorkMinutes/totalDays) - fun update(totalTimeMinutes: Long){ - All = DayPeriod(totalTimeMinutes/totalDays) - } - companion object{ - fun fromAverageDayDTO(averageDayTimeDTO: AverageDayTime): AverageDayTimeVars{ - return AverageDayTimeVars(totalWorkMinutes = averageDayTimeDTO.totalWorkMinutes, - totalDays = max(averageDayTimeDTO.totalDays, 1) - ) - } - } + fun updateStatistics(){ + _uiState.value = StatisticsUiState( + total = StatisticsStore.totalTime, + week = StatisticsStore.weekTime, + averageDay = StatisticsStore.averageDayTime, + today = StatisticsStore.todayTime, + calculable = StatisticsStore.calculator.stats.value + ) } - private var ContiniusSuccessDays: ContinuesSuccessDaysVars = ContinuesSuccessDaysVars(0, 0) - private var TotalTime: TotalTimeTaskTypes = TotalTimeTaskTypes(0, 0, 0, 0) - private var weekTime = WeekTime(0) - private var AverageDayTime: AverageDayTimeVars = AverageDayTimeVars(totalDays = 1, totalWorkMinutes = 0) - private var TodayTime: TodayTimeVars = TodayTimeVars(0, 0) fun init(){ viewModelScope.launch { _initResult.value = NetworkResponse.Loading - val response = statisticsRepo.getStatistics() + val response = StatisticsStore.init() if (response is NetworkResponse.Success){ - val data = response.data - TotalTime = data.totalTime.toVMTotalTime() - AverageDayTime = AverageDayTimeVars.fromAverageDayDTO(data.averageDayTime) - weekTime = WeekTime(data.weekTime) - ContiniusSuccessDays = ContinuesSuccessDaysVars.fromContinuesSuccessDTO(data.continuesSuccessDays) - TodayTime = TodayTimeVars.fromTodayTimeDTO(data.todayTime) + updateStatistics() } _initResult.value = response } } - open fun uploadStatistics(){ - } - fun createOrDeleteTask(task: DailyTask, isCreate: Boolean, isUploadStats: Boolean =true){ - if (task.isComplete() && isCreate==false){ - changeTaskCompletion(task, false) - } - if (task.belongsCurrentDay()){ - TodayTime.Planned.plusMinutes(task.getMinutesLength(), isCreate) - } - if (isUploadStats) {uploadStatistics()} - } - - fun changeTaskCompletion(task: DailyTask, isComplete: Boolean, isUploadStats: Boolean =true){//когда таска запатчена - val taskMinutesLength = task.getMinutesLength() - if (task.belongsCurrentDay()) { - TodayTime.Completed.plusMinutes(taskMinutesLength, isComplete) - } - if (task.belongsCurrentWeek()){ - weekTime.All.addMinutes(taskMinutesLength.toLong(), isComplete) - } - TotalTime.completeTask(task, isComplete) - AverageDayTime.update(TotalTime.totalMinutes) - if (isUploadStats) {uploadStatistics()} - } - - fun changeTask(task: DailyTask, newTask: DailyTask){ - if (task.isComplete()){ - changeTaskCompletion(task, false, isUploadStats = false) - changeTaskCompletion(newTask, true, isUploadStats = false) - } - createOrDeleteTask(task, false, isUploadStats = false) - createOrDeleteTask(newTask, true, isUploadStats = false) - uploadStatistics() - } - - fun getTotalWorkTime():TimePeriod{ - return TotalTime.All - } - - fun getTodayPlannedTime(): DayPeriod{ - return TodayTime.Planned - } - fun getAverageDailyTime():DayPeriod{ - return AverageDayTime.All + fun createOrDeleteTask(task: DailyTask, dailyTaskList: List, isCreate: Boolean) { + StatisticsStore.createOrDeleteTask(task, dailyTaskList, isCreate) + updateStatistics() } - fun getTodayCompletedTime():DayPeriod{ - return TodayTime.Completed + fun changeTaskCompletion(task: DailyTask, isComplete: Boolean, isUploadStats: Boolean = true) { + StatisticsStore.changeTaskCompletion(task, isComplete, isUploadStats) + updateStatistics() } - fun getRecordContiniusSuccessDays():DaysAmount{ - return ContiniusSuccessDays.Record - } - fun getTodayContinusSuccessDays():DaysAmount{ - return ContiniusSuccessDays.Now - } - fun getTotalTimeActivityTypes():TotalTimeTaskTypes{ - return TotalTimeTaskTypes(TotalTime.Common.time.inWholeMinutes, TotalTime.Work.time.inWholeMinutes, TotalTime.Study.time.inWholeMinutes, TotalTime.Fitness.time.inWholeMinutes) - } - fun getWeekWorkTime(): TimePeriod{ - return weekTime.All - } - fun getTypesInDay(): Long{ - return 2 - } -} -class StatisticsViewModel(): AbstractStatisticsViewModel(){ - override fun uploadStatistics() { - val workManager = WorkManagerHolder.getInstance() - val statsDTO = StatisticsDTO.fromViewModel(this) - val json = Json.encodeToString(StatisticsDTO.serializer(), statsDTO) - - val workRequest = OneTimeWorkRequestBuilder() - .setInputData(workDataOf("statistics_json" to json)) - .setInitialDelay(10, TimeUnit.SECONDS) - .build() - - workManager.enqueueUniqueWork( - "upload_stats", - ExistingWorkPolicy.REPLACE, - workRequest - ) + fun changeTask(task: DailyTask, newTask: DailyTask) { + StatisticsStore.changeTask(task, newTask) + updateStatistics() } } \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/TaskEditViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/TaskEditViewModel.kt index 51687ed..2b90f0b 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/TaskEditViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/TaskEditViewModel.kt @@ -33,8 +33,9 @@ class TaskEditViewModel( isEmptyTitle: MutableState, isConflictInTimeField: MutableState, isNestedTask: MutableState, - ) { - editHandler( + reminderViewModel: ReminderViewModel + ): Boolean { + return editHandler( oldTask = task, newTask = changes, viewModel = listViewModel, @@ -44,7 +45,8 @@ class TaskEditViewModel( statsUpdateOldToNewTask = { oldTask, newTask-> listViewModel.statisticsManager.updateDailyTask(oldTask=oldTask, newTask = newTask) listViewModel.scheduleTaskRequest(newTask, DailyTaskAction.Type.EDIT) - } + }, + reminderUpdate = {task ->reminderViewModel.scheduleReminder(task)} ) } } \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsCalculator.kt b/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsCalculator.kt new file mode 100644 index 0000000..f902577 --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsCalculator.kt @@ -0,0 +1,90 @@ +package org.hse.smartcalendar.view.model.state + +import androidx.compose.runtime.mutableStateOf +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.daysUntil +import kotlinx.datetime.minus +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.User +import androidx.compose.runtime.State +import org.hse.smartcalendar.network.StatisticsDTO +import org.hse.smartcalendar.utility.TimeUtils +data class StatisticsCalculableData( + val typesInDay: Int, + val continuesCurrent: Int, + val continuesTotal: Int +) +class StatisticsCalculator { + private val _stats = mutableStateOf(StatisticsCalculableData(0,0,0)) + val stats: State = _stats + private fun recalculateTypes(dayTasks: List ){ + val state = stats.value + val dayTypes = dayTasks + .map { it.getDailyTaskType() } + .toSet() + .size + _stats.value = StatisticsCalculableData(dayTypes, + continuesTotal = state.continuesTotal, + continuesCurrent = state.continuesCurrent) + } + private fun recalculateDays(){ + val currentDate = TimeUtils.Companion.getCurrentDateTime().date + var lastSuccessDate = currentDate + val mainSchedule = User.getSchedule() + val isSuccess: (LocalDate)->Boolean = { date -> + val tasks = mainSchedule + .getOrCreateDailySchedule(date) + .getDailyTaskList() + .map { it.isComplete() } + !tasks.isEmpty()&&tasks.all{it} + } + while (isSuccess(lastSuccessDate)){ + lastSuccessDate = lastSuccessDate.minus(1, DateTimeUnit.DAY) + } + val continuesCurrent = lastSuccessDate.daysUntil(currentDate) + val continuesTotal = maxOf(stats.value.continuesTotal, continuesCurrent) + _stats.value = StatisticsCalculableData( + typesInDay = stats.value.typesInDay, + continuesTotal = continuesTotal, + continuesCurrent = continuesCurrent + ) + } + private fun recalculateDays(changedDate: LocalDate){ + val currentDate = TimeUtils.Companion.getCurrentDateTime().date + if (changedDate>currentDate + || changedDate.daysUntil(currentDate)>stats.value.continuesCurrent){ + return + } + recalculateDays() + } + data class AddOrDeleteRequest(val dateTasks: List, val date: LocalDate) + fun addOrDeleteTask(request: AddOrDeleteRequest){ + if (request.date== TimeUtils.getCurrentDateTime().date) { + recalculateTypes(request.dateTasks) + } + recalculateDays(request.date) + } + fun changeTaskCompletion(task: DailyTask){ + recalculateDays(task.getTaskDate()) + } + fun init(statisticsDTO: StatisticsDTO){ + _stats.value = StatisticsCalculableData(typesInDay = 0, + continuesTotal = statisticsDTO.continuesSuccessDays.record.toInt(), + continuesCurrent = 0) + recalculateDays() + val currentDate = TimeUtils.Companion.getCurrentDateTime().date + val tasks = User.getSchedule().getOrCreateDailySchedule(currentDate) + .getDailyTaskList() + recalculateTypes(tasks) + } + fun getCurrentDayTypes(): Int{ + return stats.value.typesInDay + } + fun getTodayContinuesSuccessDays(): Int{ + return stats.value.continuesCurrent + } + fun getRecordContinuesSuccessDays(): Int{ + return stats.value.continuesTotal + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt b/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt new file mode 100644 index 0000000..d3dbae4 --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt @@ -0,0 +1,44 @@ +package org.hse.smartcalendar.view.model.state + +import kotlinx.datetime.LocalDate +import kotlinx.datetime.daysUntil +import org.hse.smartcalendar.data.TotalTimeTaskTypes +import org.hse.smartcalendar.network.AverageDayTime +import org.hse.smartcalendar.network.TodayTime +import org.hse.smartcalendar.utility.TimeUtils + +data class StatisticsUiState( + val total: TotalTimeTaskTypes = TotalTimeTaskTypes(0,0,0,0), + val week: WeekTime = WeekTime(0), + val averageDay: AverageDayTimeVars = AverageDayTimeVars(firstDay = LocalDate(1970,1,1), totalWorkMinutes = 0), + val today: TodayTimeVars = TodayTimeVars(0,0), + val calculable: StatisticsCalculableData = StatisticsCalculableData(0, 0, 0) +) + +class TodayTimeVars(planned: Long, completed: Long){ + val Planned: DayPeriod = DayPeriod(planned) + var Completed: DayPeriod = DayPeriod(completed) + companion object{ + fun fromTodayTimeDTO(todayTimeDTO: TodayTime): TodayTimeVars{ + return TodayTimeVars(planned = todayTimeDTO.planned, + completed = todayTimeDTO.completed) + } + } +} +class AverageDayTimeVars(totalWorkMinutes: Long, val firstDay: LocalDate){ + private val dayLength = firstDay.daysUntil(TimeUtils.getCurrentDateTime().date)+1 + var All: DayPeriod = DayPeriod(totalWorkMinutes/dayLength) + fun update(totalTimeMinutes: Long){ + All = DayPeriod(totalTimeMinutes/dayLength) + } + companion object{ + fun fromAverageDayDTO(averageDayTimeDTO: AverageDayTime): AverageDayTimeVars{ + return AverageDayTimeVars(totalWorkMinutes = averageDayTimeDTO.totalWorkMinutes, + firstDay = averageDayTimeDTO.firstDay + ) + } + } +} +class WeekTime(all: Long){ + val All: TimePeriod = TimePeriod(all) +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/state/TimeStateClasses.kt b/app/src/main/java/org/hse/smartcalendar/view/model/state/TimeStateClasses.kt new file mode 100644 index 0000000..a14ea3b --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/view/model/state/TimeStateClasses.kt @@ -0,0 +1,89 @@ +package org.hse.smartcalendar.view.model.state + +import androidx.compose.runtime.mutableStateOf +import kotlinx.datetime.toDateTimePeriod +import org.hse.smartcalendar.utility.TimeUtils +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +class TimePeriod(minute: Long) { + private var _time = mutableStateOf(Duration.Companion.ZERO) + val time: Duration get() = _time.value + + init { + fromMinutes(minute) + } + + fun fromMinutes(minute: Long) { + _time.value += minute.minutes + } + + fun toMinutes(): Long { + return time.inWholeMinutes + } + + fun addMinutes(minutes: Long, sign: Boolean) { + when (sign) { + true -> _time.value += minutes.toDuration(DurationUnit.MINUTES) + false -> _time.value -= minutes.toDuration(DurationUnit.MINUTES) + } + } + + fun toPrettyString(): String { + val stringBuilder = StringBuilder() + val dataTime = time.toDateTimePeriod() + stringBuilder.append(TimeUtils.Companion.numberToWord(dataTime.years, "year")) + stringBuilder.append(TimeUtils.Companion.numberToWord(dataTime.days, "day")) + stringBuilder.append(TimeUtils.Companion.numberToWord(dataTime.hours, "hour")) + stringBuilder.append(TimeUtils.Companion.numberToWord(dataTime.minutes, "minute")) + return if (stringBuilder.toString() != "") stringBuilder.toString() else "0 minute" + } +} + +data class DaysAmount(val amount: Int) { + fun toPrettyString(): String { + return if (amount != 1) "$amount days" else "1 day" + } +} + +class DayPeriod(minute: Long) { + private var _time = mutableStateOf(Duration.Companion.ZERO) + val time: Duration get() = _time.value + fun toMinutes(): Long { + return time.inWholeMinutes + } + + init { + fromMinutes(minute) + } + + fun fromMinutes(minute: Long) { + _time.value = minute.toDuration(DurationUnit.MINUTES) + } + + fun plusMinutes(minute: Int, sign: Boolean) { + when (sign) { + true -> _time.value += minute.toDuration(DurationUnit.MINUTES); + false -> _time.value -= minute.toDuration(DurationUnit.MINUTES); + } + } + + fun toFullString(): String { + var stringBuilder: StringBuilder = StringBuilder() + stringBuilder.append( + TimeUtils.Companion.numberToWord( + time.toDateTimePeriod().hours, + "hour" + ) + ) + stringBuilder.append( + TimeUtils.Companion.numberToWord( + time.toDateTimePeriod().minutes, + "minute" + ) + ) + return if (stringBuilder.toString() != "") stringBuilder.toString() else "0 minute" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/notification/ReminderWorker.kt b/app/src/main/java/org/hse/smartcalendar/work/ReminderWorker.kt similarity index 98% rename from app/src/main/java/org/hse/smartcalendar/notification/ReminderWorker.kt rename to app/src/main/java/org/hse/smartcalendar/work/ReminderWorker.kt index cbf3578..038d4ab 100644 --- a/app/src/main/java/org/hse/smartcalendar/notification/ReminderWorker.kt +++ b/app/src/main/java/org/hse/smartcalendar/work/ReminderWorker.kt @@ -1,4 +1,4 @@ -package org.hse.smartcalendar.notification +package org.hse.smartcalendar.work import android.annotation.SuppressLint import android.app.PendingIntent diff --git a/app/src/main/java/org/hse/smartcalendar/notification/StatisticsUploadWorker.kt b/app/src/main/java/org/hse/smartcalendar/work/StatisticsUploadWorker.kt similarity index 95% rename from app/src/main/java/org/hse/smartcalendar/notification/StatisticsUploadWorker.kt rename to app/src/main/java/org/hse/smartcalendar/work/StatisticsUploadWorker.kt index 1e9de03..36b5aa7 100644 --- a/app/src/main/java/org/hse/smartcalendar/notification/StatisticsUploadWorker.kt +++ b/app/src/main/java/org/hse/smartcalendar/work/StatisticsUploadWorker.kt @@ -1,4 +1,4 @@ -package org.hse.smartcalendar.notification +package org.hse.smartcalendar.work import android.content.Context import androidx.work.CoroutineWorker diff --git a/app/src/main/java/org/hse/smartcalendar/notification/TaskApiWorker.kt b/app/src/main/java/org/hse/smartcalendar/work/TaskApiWorker.kt similarity index 96% rename from app/src/main/java/org/hse/smartcalendar/notification/TaskApiWorker.kt rename to app/src/main/java/org/hse/smartcalendar/work/TaskApiWorker.kt index e1f0ec3..57ccc82 100644 --- a/app/src/main/java/org/hse/smartcalendar/notification/TaskApiWorker.kt +++ b/app/src/main/java/org/hse/smartcalendar/work/TaskApiWorker.kt @@ -1,4 +1,4 @@ -package org.hse.smartcalendar.notification +package org.hse.smartcalendar.work import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters diff --git a/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt b/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt index 54f1aad..d4bde24 100644 --- a/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt +++ b/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt @@ -1,41 +1,37 @@ package org.hse.smartcalendar.view.model -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import io.mockk.every -import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain -import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType import org.hse.smartcalendar.data.User -import org.hse.smartcalendar.data.WorkManagerHolder +import org.hse.smartcalendar.store.StatisticsStore import org.hse.smartcalendar.utility.TimeUtils -import org.hse.smartcalendar.utility.fromMinutesOfDay +import org.hse.smartcalendar.view.model.state.CalcState import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance -import java.util.UUID import kotlin.test.assertEquals @TestInstance(TestInstance.Lifecycle.PER_CLASS) @OptIn(ExperimentalCoroutinesApi::class) class StatisticsTest { private val testDispatcher = StandardTestDispatcher() - private lateinit var statisticsViewModel: AbstractStatisticsViewModel + private lateinit var statisticsViewModel: StatisticsViewModel private lateinit var listViewModel: AbstractListViewModel - private lateinit var firstTask: DailyTask - private lateinit var secondTask: DailyTask + private lateinit var todayWorkTask: DailyTask + private lateinit var todayCommonTask: DailyTask private lateinit var tomorrowTask: DailyTask private lateinit var weekFitnessTask: DailyTask + private lateinit var yesterdayTask: DailyTask fun addTaskInDay(task: DailyTask){ listViewModel.changeDailyTaskSchedule(task.getTaskDate()) @@ -52,89 +48,106 @@ class StatisticsTest { listViewModel.removeDailyTask(task) listViewModel.changeDailyTaskSchedule(TimeUtils.getCurrentDateTime().date) } + fun assertCalculatorState(calcState: CalcState){ + Assertions.assertEquals(calcState.currentStreak, statisticsViewModel.uiState.value.calculable.continuesCurrent) + Assertions.assertEquals(calcState.maxStreak, statisticsViewModel.uiState.value.calculable.continuesTotal) + Assertions.assertEquals(calcState.types, statisticsViewModel.uiState.value.calculable.typesInDay) + } + fun setTasks(){ + todayWorkTask = TaskProvider.TodayWorkTask.provide() + todayCommonTask = TaskProvider.TodayCommonTask.provide() + tomorrowTask = TaskProvider.TomorrowTask.provide() + weekFitnessTask = TaskProvider.WeekFitnessTask.provide() + yesterdayTask = TaskProvider.YesterdayTask.provide() + } + fun assertPercent(type: DailyTaskType, percent: Float){ + assertEquals( + percent, + statisticsViewModel.uiState.value.total.getPercentByType(type), + absoluteTolerance = 0.1f) + } @BeforeAll - fun setUp(){ + fun setUp(){//Global Init, ONE call before ALL tests Dispatchers.setMain(testDispatcher) - statisticsViewModel = AbstractStatisticsViewModel() - listViewModel = AbstractListViewModel(StatisticsManager(statisticsViewModel)) - firstTask = DailyTask( - title = "first", - id = UUID.randomUUID(), - isComplete = false, - type = DailyTaskType.WORK, - creationTime = TimeUtils.Companion.getCurrentDateTime(), - description = "", - start = LocalTime.Companion.fromMinutesOfDay(10), - end = LocalTime.Companion.fromMinutesOfDay(30), - date = TimeUtils.Companion.getCurrentDateTime().date, - ) - secondTask = DailyTask.fromTime( - start = LocalTime.Companion.fromMinutesOfDay(30), - end = LocalTime.Companion.fromMinutesOfDay(50), - date = TimeUtils.Companion.getCurrentDateTime().date) - tomorrowTask = DailyTask.fromTime( - start = LocalTime.Companion.fromMinutesOfDay(30), - end = LocalTime.Companion.fromMinutesOfDay(50), - date = TimeUtils.addDaysToNowDate(1) - ) - weekFitnessTask = DailyTask.fromTimeAndType( - start = LocalTime.Companion.fromMinutesOfDay(30), - end = LocalTime.Companion.fromMinutesOfDay(50), - date = TimeUtils.addDaysToNowDate(-6), - type = DailyTaskType.FITNESS - ) + StatisticsStore.uploader = {} } - @AfterAll + @AfterAll//Global Clear, ONE call before ALL tests fun tearDown() { Dispatchers.resetMain() } + @BeforeEach//Init, call before EACH test + fun init(){ + statisticsViewModel = StatisticsViewModel() + listViewModel = AbstractListViewModel(StatisticsManager(statisticsViewModel)) + setTasks() + } + @AfterEach//Clear, call after EACH test + fun clear(){ + User.clearSchedule() + StatisticsStore.clear() + } + /** + * check state in BeforeAll in WithPreAddedTasks: Add 4 tasks + */ @Nested inner class EmptyState { @Test fun addTaskTest(){ - listViewModel.addDailyTask(firstTask) + listViewModel.addDailyTask(todayWorkTask) assertEquals( - firstTask.getMinutesLength().toLong(), - statisticsViewModel.getTodayPlannedTime().time.inWholeMinutes, + this@StatisticsTest.todayWorkTask.getMinutesLength().toLong(), + statisticsViewModel.uiState.value.today.Planned.time.inWholeMinutes, ) - listViewModel.addDailyTask(secondTask) + listViewModel.addDailyTask(todayCommonTask) assertEquals( - (firstTask.getMinutesLength() + secondTask.getMinutesLength()).toLong(), - statisticsViewModel.getTodayPlannedTime().time.inWholeMinutes, + (todayWorkTask.getMinutesLength() + todayCommonTask.getMinutesLength()).toLong(), + statisticsViewModel.uiState.value.today.Planned.time.inWholeMinutes, ) addTaskInDay(tomorrowTask) assertEquals( - (firstTask.getMinutesLength() + secondTask.getMinutesLength()).toLong(), - statisticsViewModel.getTodayPlannedTime().time.inWholeMinutes, + (todayWorkTask.getMinutesLength() + todayCommonTask.getMinutesLength()).toLong(), + statisticsViewModel.uiState.value.today.Planned.time.inWholeMinutes, ) addTaskInDay(weekFitnessTask) assertEquals( - (firstTask.getMinutesLength() + secondTask.getMinutesLength()).toLong(), - statisticsViewModel.getTodayPlannedTime().time.inWholeMinutes, + (todayWorkTask.getMinutesLength() + todayCommonTask.getMinutesLength()).toLong(), + statisticsViewModel.uiState.value.today.Planned.time.inWholeMinutes, ) } + @Test + fun calculatorAddTest(){ + listViewModel.addDailyTask(todayWorkTask) + assertCalculatorState(CalcState(types = 1, currentStreak = 0, maxStreak = 0)) + listViewModel.addDailyTask(todayCommonTask) + assertCalculatorState(CalcState(types = 2, currentStreak = 0, maxStreak = 0)) + addTaskInDay(tomorrowTask) + assertCalculatorState(CalcState(types = 2, currentStreak = 0, maxStreak = 0)) + addTaskInDay(weekFitnessTask) + assertCalculatorState(CalcState(types = 2, currentStreak = 0, maxStreak = 0)) + listViewModel.removeDailyTask(todayCommonTask) + assertCalculatorState(CalcState(types = 1, currentStreak = 0, maxStreak = 0)) + } } + + /** + * до каждого теста добавлены 4 задания + */ @Nested inner class WithPreAddedTasks{ @BeforeEach fun addTasks(){ - //User - синглтон - User.clearSchedule() - //нам нужен чистый listViewModel перед каждым тестом - statisticsViewModel = AbstractStatisticsViewModel() - listViewModel = AbstractListViewModel(StatisticsManager(statisticsViewModel)) - listViewModel.addDailyTask(firstTask) - listViewModel.addDailyTask(secondTask) + listViewModel.addDailyTask(todayWorkTask) + listViewModel.addDailyTask(todayCommonTask) addTaskInDay(tomorrowTask) addTaskInDay(weekFitnessTask) } @Test fun deleteTaskTest(){ - listViewModel.removeDailyTask(firstTask) + listViewModel.removeDailyTask(todayWorkTask) assertEquals( - (secondTask.getMinutesLength()).toLong(), - statisticsViewModel.getTodayPlannedTime().time.inWholeMinutes + (todayCommonTask.getMinutesLength()).toLong(), + statisticsViewModel.uiState.value.today.Planned.time.inWholeMinutes ) } fun assertTaskTimeEquals(task: DailyTask, parameter: Long){ @@ -159,52 +172,52 @@ class StatisticsTest { } @Test fun completeTaskTest(){ - listViewModel.changeTaskCompletion(firstTask, true) + listViewModel.changeTaskCompletion(todayWorkTask, true) assertTaskTimeEquals( - firstTask, - listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes, - statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes, - statisticsViewModel.getTotalWorkTime().time.inWholeMinutes, - statisticsViewModel.getTotalTimeActivityTypes().Work.time.inWholeMinutes) + todayWorkTask, + listOf(statisticsViewModel.uiState.value.week.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.today.Completed.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.Work.time.inWholeMinutes) ) assertEquals( 100.0f, - statisticsViewModel.getTotalTimeActivityTypes().WorkPercent + statisticsViewModel.uiState.value.total.WorkPercent ) - listViewModel.changeTaskCompletion(firstTask, true) + listViewModel.changeTaskCompletion(todayWorkTask, true) assertTaskTimeEquals( - firstTask, - listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes, - statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes, - statisticsViewModel.getTotalWorkTime().time.inWholeMinutes, - statisticsViewModel.getTotalTimeActivityTypes().Work.time.inWholeMinutes) + todayWorkTask, + listOf(statisticsViewModel.uiState.value.week.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.today.Completed.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.Work.time.inWholeMinutes) ) assertEquals( 100.0f, - statisticsViewModel.getTotalTimeActivityTypes().WorkPercent + statisticsViewModel.uiState.value.total.WorkPercent ) - listViewModel.changeTaskCompletion(secondTask, true) + listViewModel.changeTaskCompletion(todayCommonTask, true) assertTaskTimeEquals( - listOf(firstTask, secondTask), - listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes, - statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes, - statisticsViewModel.getTotalWorkTime().time.inWholeMinutes) + listOf(todayWorkTask, todayCommonTask), + listOf(statisticsViewModel.uiState.value.week.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.today.Completed.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.All.time.inWholeMinutes) ) assertTaskTimeEquals( - firstTask, - statisticsViewModel.getTotalTimeActivityTypes().Work.time.inWholeMinutes + todayWorkTask, + statisticsViewModel.uiState.value.total.Work.time.inWholeMinutes ) assertTaskTimeEquals( - secondTask, - statisticsViewModel.getTotalTimeActivityTypes().Common.time.inWholeMinutes + todayCommonTask, + statisticsViewModel.uiState.value.total.Common.time.inWholeMinutes ) - listViewModel.changeTaskCompletion(secondTask, false) + listViewModel.changeTaskCompletion(todayCommonTask, false) assertTaskTimeEquals( - firstTask, - listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes, - statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes, - statisticsViewModel.getTotalWorkTime().time.inWholeMinutes, - statisticsViewModel.getTotalTimeActivityTypes().Work.time.inWholeMinutes) + todayWorkTask, + listOf(statisticsViewModel.uiState.value.week.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.today.Completed.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.Work.time.inWholeMinutes) ) } @Test @@ -212,27 +225,27 @@ class StatisticsTest { changeTaskInDay(tomorrowTask, true) assertTaskTimeEquals( listOf(), - listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes, - statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes, - statisticsViewModel.getTotalTimeActivityTypes().Work.time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.week.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.today.Completed.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.Work.time.inWholeMinutes) ) assertTaskTimeEquals( listOf(tomorrowTask), - listOf(statisticsViewModel.getTotalWorkTime().time.inWholeMinutes, - statisticsViewModel.getTotalTimeActivityTypes().Common.time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.total.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.Common.time.inWholeMinutes) ) changeTaskInDay(weekFitnessTask, true) assertTaskTimeEquals( weekFitnessTask, - listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes, - statisticsViewModel.getTotalTimeActivityTypes().Fitness.time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.week.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.Fitness.time.inWholeMinutes) ) assertTaskTimeEquals( listOf(tomorrowTask, weekFitnessTask), - listOf(statisticsViewModel.getTotalWorkTime().time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.total.All.time.inWholeMinutes) ) assertEquals(50.0f, - statisticsViewModel.getTotalTimeActivityTypes().FitnessPercent) + statisticsViewModel.uiState.value.total.FitnessPercent) } /** @@ -242,42 +255,73 @@ class StatisticsTest { fun integrityTest(){ changeTaskInDay(tomorrowTask, true) changeTaskInDay(weekFitnessTask, true) - changeTaskInDay(firstTask, true) + changeTaskInDay(todayWorkTask, true) changeTaskInDay(weekFitnessTask, false) changeTaskInDay(weekFitnessTask, true) assertTaskTimeEquals( - listOf(weekFitnessTask, firstTask), - listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes) + listOf(weekFitnessTask, todayWorkTask), + listOf(statisticsViewModel.uiState.value.week.All.time.inWholeMinutes) ) assertEquals(100.0f/3, - statisticsViewModel.getTotalTimeActivityTypes().FitnessPercent, + statisticsViewModel.uiState.value.total.FitnessPercent, absoluteTolerance = 0.1f) - listViewModel.removeDailyTask(firstTask) + listViewModel.removeDailyTask(todayWorkTask) assertTaskTimeEquals( listOf(), - listOf(statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.today.Completed.time.inWholeMinutes) ) assertTaskTimeEquals( listOf(weekFitnessTask), - listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.week.All.time.inWholeMinutes) ) assertTaskTimeEquals( listOf(weekFitnessTask, tomorrowTask), - listOf(statisticsViewModel.getTotalWorkTime().time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.total.All.time.inWholeMinutes) ) changeTaskInDay(tomorrowTask, false) assertTaskTimeEquals( listOf(weekFitnessTask), - listOf(statisticsViewModel.getTotalWorkTime().time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.total.All.time.inWholeMinutes) ) removeTaskInDay(tomorrowTask) assertTaskTimeEquals( listOf(weekFitnessTask), - listOf(statisticsViewModel.getTotalWorkTime().time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.total.All.time.inWholeMinutes) ) - assertEquals(100.0f, - statisticsViewModel.getTotalTimeActivityTypes().FitnessPercent, - absoluteTolerance = 0.1f) + assertPercent(DailyTaskType.FITNESS, 100.0f) + } + @Test + fun percentTest(){ + changeTaskInDay(weekFitnessTask, true) + assertPercent(DailyTaskType.FITNESS, 100.0f) + changeTaskInDay(todayWorkTask, true) + assertPercent(DailyTaskType.WORK, 50.0f) + changeTaskInDay(todayWorkTask, false) + assertPercent(DailyTaskType.FITNESS, 100.0f) + changeTaskInDay(todayWorkTask, true) + assertPercent(DailyTaskType.FITNESS, 50.0f) + removeTaskInDay(todayWorkTask) + assertPercent(DailyTaskType.FITNESS, 100.0f) + } + + /** + * check what listVM changes apply to statsCalculator + */ + @Test + fun calculateTest(){ + assertCalculatorState(CalcState(types = 2, currentStreak = 0, maxStreak = 0)) + changeTaskInDay(tomorrowTask, true) + assertEquals(0, statisticsViewModel.uiState.value.calculable.continuesCurrent) + changeTaskInDay(todayWorkTask, true) + assertEquals(0, statisticsViewModel.uiState.value.calculable.continuesCurrent) + changeTaskInDay(todayCommonTask, true) + assertCalculatorState(CalcState(types = 2, currentStreak = 1, maxStreak = 1)) + addTaskInDay(yesterdayTask) + changeTaskInDay(yesterdayTask, true) + assertCalculatorState(CalcState(types = 2, currentStreak = 2, maxStreak = 2)) + removeTaskInDay(todayWorkTask) + removeTaskInDay(todayCommonTask) + assertCalculatorState(CalcState(types = 0, currentStreak = 0, maxStreak = 2)) } } } diff --git a/app/src/test/kotlin/org/hse/smartcalendar/view/model/TaskProvider.kt b/app/src/test/kotlin/org/hse/smartcalendar/view/model/TaskProvider.kt new file mode 100644 index 0000000..5cd2ade --- /dev/null +++ b/app/src/test/kotlin/org/hse/smartcalendar/view/model/TaskProvider.kt @@ -0,0 +1,67 @@ +package org.hse.smartcalendar.view.model + +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.utility.TimeUtils +import kotlinx.datetime.LocalTime +import org.hse.smartcalendar.utility.fromMinutesOfDay +import java.util.UUID + +/** + * enum - singleton, if instead of provide store values like TaskProvider(task:DailyTask) + * after it change in tests(task = TaskProvider.TodayWorkTask; task.setCompletion(true)) + * it change in enum + */ +enum class TaskProvider(val provide:()-> DailyTask) { + TodayWorkTask({ + DailyTask( + title = "first", + id = UUID.randomUUID(), + isComplete = false, + type = DailyTaskType.WORK, + creationTime = TimeUtils.getCurrentDateTime(), + description = "", + start = LocalTime.fromMinutesOfDay(10), + end = LocalTime.fromMinutesOfDay(30), + date = TimeUtils.getCurrentDateTime().date + ) + }), + + TodayCommonTask({ + DailyTask.fromTime( + start = LocalTime.fromMinutesOfDay(30), + end = LocalTime.fromMinutesOfDay(50), + date = TimeUtils.getCurrentDateTime().date + ) + }), + + TomorrowTask({ + DailyTask.fromTime( + start = LocalTime.fromMinutesOfDay(30), + end = LocalTime.fromMinutesOfDay(50), + date = TimeUtils.addDaysToNowDate(1) + ) + }), + YesterdayTask({ + DailyTask.fromTime( + start = LocalTime.fromMinutesOfDay(30), + end = LocalTime.fromMinutesOfDay(50), + date = TimeUtils.addDaysToNowDate(-1) + ) + }), + TwoDaysAgoTask({ + DailyTask.fromTime( + start = LocalTime.fromMinutesOfDay(30), + end = LocalTime.fromMinutesOfDay(50), + date = TimeUtils.addDaysToNowDate(-2) + ) + }), + WeekFitnessTask({ + DailyTask.fromTimeAndType( + start = LocalTime.fromMinutesOfDay(30), + end = LocalTime.fromMinutesOfDay(50), + date = TimeUtils.addDaysToNowDate(-6), + type = DailyTaskType.FITNESS + ) + }); +} \ No newline at end of file diff --git a/app/src/test/kotlin/org/hse/smartcalendar/view/model/state/StatisticsCalculatorTest.kt b/app/src/test/kotlin/org/hse/smartcalendar/view/model/state/StatisticsCalculatorTest.kt new file mode 100644 index 0000000..098478f --- /dev/null +++ b/app/src/test/kotlin/org/hse/smartcalendar/view/model/state/StatisticsCalculatorTest.kt @@ -0,0 +1,111 @@ +package org.hse.smartcalendar.view.model.state + +import org.hse.smartcalendar.data.DailySchedule +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.MainSchedule +import org.hse.smartcalendar.data.User +import org.hse.smartcalendar.view.model.TaskProvider +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions + +data class CalcState(val types: Int, val currentStreak: Int, val maxStreak: Int) + +class StatisticsCalculatorTest { + private lateinit var firstTodayTask: DailyTask + private lateinit var secondTask: DailyTask + private lateinit var yesterdayTask: DailyTask + private lateinit var twoDaysAgoTask: DailyTask + private lateinit var tomorrowTask: DailyTask + private lateinit var statisticsCalculator: StatisticsCalculator + private lateinit var mainSchedule: MainSchedule + @Before + fun initTask(){ + firstTodayTask = TaskProvider.TodayWorkTask.provide() + secondTask = TaskProvider.TodayCommonTask.provide() + yesterdayTask = TaskProvider.YesterdayTask.provide() + twoDaysAgoTask = TaskProvider.TwoDaysAgoTask.provide() + tomorrowTask = TaskProvider.TomorrowTask.provide() + statisticsCalculator = StatisticsCalculator() + mainSchedule = User.getSchedule() + } + @After + fun clearTask(){ + User.clearSchedule() + } + fun assertCalculatorState(calcState: CalcState){ + Assertions.assertEquals(calcState.currentStreak, statisticsCalculator.stats.value.continuesCurrent) + Assertions.assertEquals(calcState.maxStreak, statisticsCalculator.stats.value.continuesTotal) + Assertions.assertEquals(calcState.types, statisticsCalculator.stats.value.typesInDay) + } + fun addOrDeleteTask(dailySchedule: DailySchedule, task: DailyTask){ + statisticsCalculator.addOrDeleteTask( + StatisticsCalculator.AddOrDeleteRequest + (date = task.getTaskDate(), dateTasks = dailySchedule.getDailyTaskList())) + } + /** + * test types & streak in day + */ + @Test + fun oneDay(){ + val dailySchedule= mainSchedule.getOrCreateDailySchedule(firstTodayTask.getTaskDate()) + dailySchedule.addDailyTask(firstTodayTask) + addOrDeleteTask(dailySchedule, firstTodayTask) + assertCalculatorState(CalcState(types = 1, currentStreak = 0, maxStreak = 0)) + firstTodayTask.setCompletion(true) + statisticsCalculator.changeTaskCompletion(firstTodayTask) + assertCalculatorState(CalcState(types = 1, currentStreak = 1, maxStreak = 1)) + dailySchedule.addDailyTask(secondTask) + addOrDeleteTask(dailySchedule, secondTask) + assertCalculatorState(CalcState(types = 2, currentStreak = 0, maxStreak = 1)) + secondTask.setCompletion(true) + statisticsCalculator.changeTaskCompletion(secondTask) + assertCalculatorState(CalcState(types = 2, currentStreak = 1, maxStreak = 1)) + secondTask.setCompletion(false) + statisticsCalculator.changeTaskCompletion(secondTask) + assertCalculatorState(CalcState(types = 2, currentStreak = 0, maxStreak = 1)) + dailySchedule.removeDailyTask(firstTodayTask) + addOrDeleteTask(dailySchedule, firstTodayTask) + assertCalculatorState(CalcState(types = 1, currentStreak = 0, maxStreak = 1)) + } + + fun addTask(dailySchedule: DailySchedule, task: DailyTask){ + dailySchedule.addDailyTask(task) + addOrDeleteTask(dailySchedule, task) + } + fun setCompletion(task: DailyTask, isComplete: Boolean){ + task.setCompletion(isComplete) + statisticsCalculator.changeTaskCompletion(task) + } + /** + * test streak in period twoDaysAgo-tomorrow + */ + @Test + fun manyDay(){ + val todaySchedule= mainSchedule.getOrCreateDailySchedule(firstTodayTask.getTaskDate()) + val yesterdaySchedule = mainSchedule.getOrCreateDailySchedule(yesterdayTask.getTaskDate()) + val twoDaysAgoSchedule = mainSchedule.getOrCreateDailySchedule(twoDaysAgoTask.getTaskDate()) + val tomorrowSchedule = mainSchedule.getOrCreateDailySchedule(tomorrowTask.getTaskDate()) + //add task for every schedule + addTask(todaySchedule, firstTodayTask) + setCompletion(firstTodayTask, true) + addTask(twoDaysAgoSchedule, twoDaysAgoTask) + setCompletion(twoDaysAgoTask, true) + assertCalculatorState(CalcState(types = 1, currentStreak = 1, maxStreak = 1)) + addTask(yesterdaySchedule, yesterdayTask) + setCompletion(yesterdayTask, true) + assertCalculatorState(CalcState(types = 1, currentStreak = 3, maxStreak = 3)) + addTask(tomorrowSchedule, tomorrowTask) + setCompletion(tomorrowTask, true) + assertCalculatorState(CalcState(types = 1, currentStreak = 3, maxStreak = 3)) + //next set some to false + setCompletion(yesterdayTask, false) + assertCalculatorState(CalcState(types = 1, currentStreak = 1, maxStreak = 3)) + setCompletion(firstTodayTask, true) + assertCalculatorState(CalcState(types = 1, currentStreak = 1, maxStreak = 3)) + todaySchedule.removeDailyTask(firstTodayTask) + addOrDeleteTask(todaySchedule, firstTodayTask) + assertCalculatorState(CalcState(types = 0, currentStreak = 0, maxStreak = 3)) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d8db73a..c088385 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,7 @@ runtimeLivedata = "1.8.2" uiTestJunit4 = "" uiTestManifest = "" workRuntimeKtx = "2.10.1" +junitJupiterVersion = "5.8.1" [libraries] @@ -96,6 +97,7 @@ retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "converte retrofit-v2110 = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "uiTestJunit4" } ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "uiTestManifest" } +jupiter-junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junitJupiterVersion" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }