diff --git a/app/build.gradle.kts b/app/build.gradle.kts index eefd4c3c..84908843 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -62,6 +62,7 @@ android { buildTypes { release { signingConfig = signingConfigs.getByName("production") + baselineProfile.automaticGenerationDuringBuild = true } } } @@ -90,6 +91,7 @@ dependencies { implementation(projects.feature.postponeTask) implementation(libs.fragment) + implementation(libs.compose.activity) implementation(libs.compose.ui) implementation(libs.compose.material3) implementation(libs.compose.material3.windowsize) @@ -102,7 +104,6 @@ dependencies { implementation(libs.viewmodel) implementation(libs.hilt) implementation(libs.profileinstaller) - "baselineProfile"(project(":baselineprofile")) kapt(libs.hilt.compiler) implementation(libs.hilt.work) implementation(libs.hilt.navigation.compose) @@ -125,6 +126,14 @@ dependencies { testImplementation(libs.compose.ui.test) androidTestImplementation(projects.core.testing) + + baselineProfile(projects.benchmarks) +} + +baselineProfile { + // Don't build on every iteration of a full assemble. + // Instead enable generation directly for the release build variant. + automaticGenerationDuringBuild = false } class RoomSchemaArgProvider( diff --git a/app/config/detekt/detekt.yml b/app/config/detekt/detekt.yml index 041bb849..ec8e8fe2 100644 --- a/app/config/detekt/detekt.yml +++ b/app/config/detekt/detekt.yml @@ -283,7 +283,7 @@ exceptions: ThrowingNewInstanceOfSameException: active: true TooGenericExceptionCaught: - active: true + active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] exceptionNames: - 'ArrayIndexOutOfBoundsException' @@ -625,6 +625,7 @@ style: ignoreEnums: false ignoreRanges: false ignoreExtensionFunctions: true + ignoreAnnotated: ['Preview'] MandatoryBracesLoops: active: false MaxChainedCallsOnSameLine: diff --git a/app/src/main/java/com/costular/atomtasks/data/BootReceiver.kt b/app/src/main/java/com/costular/atomtasks/data/BootReceiver.kt index 1ad3992d..2935f1a5 100644 --- a/app/src/main/java/com/costular/atomtasks/data/BootReceiver.kt +++ b/app/src/main/java/com/costular/atomtasks/data/BootReceiver.kt @@ -4,7 +4,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.work.WorkManager -import com.costular.atomtasks.tasks.manager.TaskReminderManager +import com.costular.atomtasks.tasks.helper.recurrence.RecurrenceScheduler import com.costular.atomtasks.tasks.worker.SetTasksRemindersWorker import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -13,11 +13,12 @@ import javax.inject.Inject class BootReceiver : BroadcastReceiver() { @Inject - lateinit var taskReminderManager: TaskReminderManager + lateinit var recurrenceScheduler: RecurrenceScheduler override fun onReceive(context: Context, intent: Intent) { if (intent.action == Intent.ACTION_BOOT_COMPLETED) { WorkManager.getInstance(context).enqueue(SetTasksRemindersWorker.start()) + recurrenceScheduler.initialize() } } } diff --git a/app/src/main/java/com/costular/atomtasks/ui/AtomTasksApp.kt b/app/src/main/java/com/costular/atomtasks/ui/AtomTasksApp.kt index b4e8b3a2..b2c7a5ab 100644 --- a/app/src/main/java/com/costular/atomtasks/ui/AtomTasksApp.kt +++ b/app/src/main/java/com/costular/atomtasks/ui/AtomTasksApp.kt @@ -8,7 +8,7 @@ import com.costular.atomtasks.BuildConfig import com.costular.atomtasks.core.logging.AtomLogger import com.costular.atomtasks.core.logging.FirebaseAtomLogger import com.costular.atomtasks.core.logging.LogcatAtomLogger -import com.costular.atomtasks.tasks.manager.AutoforwardManager +import com.costular.atomtasks.tasks.helper.AutoforwardManager import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase import dagger.hilt.android.HiltAndroidApp @@ -42,8 +42,8 @@ class AtomTasksApp : Application(), Configuration.Provider { AtomLogger.initialize(logger) } - override fun getWorkManagerConfiguration() = - Configuration.Builder() + override val workManagerConfiguration: Configuration + get() = Configuration.Builder() .setWorkerFactory(workerFactory) .setMinimumLoggingLevel(Log.DEBUG) .build() diff --git a/app/src/main/java/com/costular/atomtasks/ui/home/AppNavigator.kt b/app/src/main/java/com/costular/atomtasks/ui/home/AppNavigator.kt index a613c6f4..aeea8e7f 100644 --- a/app/src/main/java/com/costular/atomtasks/ui/home/AppNavigator.kt +++ b/app/src/main/java/com/costular/atomtasks/ui/home/AppNavigator.kt @@ -1,7 +1,7 @@ package com.costular.atomtasks.ui.home import androidx.navigation.NavController -import com.costular.atomtasks.agenda.AgendaNavigator +import com.costular.atomtasks.agenda.ui.AgendaNavigator import com.costular.atomtasks.agenda.destinations.TasksActionsBottomSheetDestination import com.costular.atomtasks.createtask.destinations.CreateTaskScreenDestination import com.costular.atomtasks.settings.SettingsNavigator diff --git a/baselineprofile/build.gradle.kts b/baselineprofile/build.gradle.kts deleted file mode 100644 index 4cb3b94d..00000000 --- a/baselineprofile/build.gradle.kts +++ /dev/null @@ -1,57 +0,0 @@ -import com.android.build.api.dsl.ManagedVirtualDevice - -@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed -plugins { - id("com.android.test") - id("org.jetbrains.kotlin.android") - id("androidx.baselineprofile") -} - -android { - namespace = "com.costular.atomtasks.baselineprofile" - compileSdk = 34 - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = "1.8" - } - - defaultConfig { - minSdk = 28 - targetSdk = 34 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - targetProjectPath = ":app" - - flavorDimensions += listOf("environment") - productFlavors { - create("development") { dimension = "environment" } - create("production") { dimension = "environment" } - } - - testOptions.managedDevices.devices { - create("pixel6Api34") { - device = "Pixel 6" - apiLevel = 34 - systemImageSource = "google" - } - } -} - -baselineProfile { - managedDevices += "pixel6Api34" - useConnectedDevices = false -} - -dependencies { - implementation(libs.androidx.test.ext.junit) - implementation(libs.espresso.core) - implementation(libs.uiautomator) - implementation(libs.benchmark.macro.junit4) -} diff --git a/baselineprofile/.gitignore b/benchmarks/.gitignore similarity index 100% rename from baselineprofile/.gitignore rename to benchmarks/.gitignore diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts new file mode 100644 index 00000000..583477a5 --- /dev/null +++ b/benchmarks/build.gradle.kts @@ -0,0 +1,69 @@ +import com.android.build.api.dsl.ManagedVirtualDevice +import com.costular.atomtasks.AtomBuildType +import com.costular.atomtasks.configureFlavors + +plugins { + id("atomtasks.android.test") + id("androidx.baselineprofile") +} + +android { + namespace = "com.costular.atomtasks.baselineprofile" + + defaultConfig { + minSdk = 28 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + buildConfigField("String", "APP_BUILD_TYPE_SUFFIX", "\"\"") + } + + buildFeatures { + buildConfig = true + } + + buildTypes { + // This benchmark buildType is used for benchmarking, and should function like your + // release build (for example, with minification on). It's signed with a debug key + // for easy local/CI testing. + create("benchmark") { + // Keep the build type debuggable so we can attach a debugger if needed. + isDebuggable = true + signingConfig = signingConfigs.getByName("debug") + matchingFallbacks.add("release") + buildConfigField( + "String", + "APP_BUILD_TYPE_SUFFIX", + "\"${AtomBuildType.BENCHMARK.applicationIdSuffix ?: ""}\"" + ) + } + } + + configureFlavors(this) { flavor -> + buildConfigField( + "String", + "APP_FLAVOR_SUFFIX", + "\"${flavor.applicationIdSuffix ?: ""}\"" + ) + } + + testOptions.managedDevices.devices { + create("pixel6Api34") { + device = "Pixel 6" + apiLevel = 34 + systemImageSource = "google" + } + } + targetProjectPath = ":app" +} + +baselineProfile { + managedDevices += "pixel6Api34" + useConnectedDevices = false +} + +dependencies { + implementation(libs.androidx.test.ext.junit) + implementation(libs.espresso.core) + implementation(libs.uiautomator) + implementation(libs.benchmark.macro.junit4) +} diff --git a/baselineprofile/src/main/AndroidManifest.xml b/benchmarks/src/main/AndroidManifest.xml similarity index 100% rename from baselineprofile/src/main/AndroidManifest.xml rename to benchmarks/src/main/AndroidManifest.xml diff --git a/baselineprofile/src/main/java/com/costular/atomtasks/baselineprofile/BaselineProfileGenerator.kt b/benchmarks/src/main/java/com/costular/atomtasks/baselineprofile/BaselineProfileGenerator.kt similarity index 100% rename from baselineprofile/src/main/java/com/costular/atomtasks/baselineprofile/BaselineProfileGenerator.kt rename to benchmarks/src/main/java/com/costular/atomtasks/baselineprofile/BaselineProfileGenerator.kt diff --git a/baselineprofile/src/main/java/com/costular/atomtasks/baselineprofile/StartupBenchmarks.kt b/benchmarks/src/main/java/com/costular/atomtasks/baselineprofile/StartupBenchmarks.kt similarity index 100% rename from baselineprofile/src/main/java/com/costular/atomtasks/baselineprofile/StartupBenchmarks.kt rename to benchmarks/src/main/java/com/costular/atomtasks/baselineprofile/StartupBenchmarks.kt diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index f728f084..db054f62 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -54,5 +54,9 @@ gradlePlugin { id = "atomtasks.android.hilt" implementationClass = "AndroidHiltConventionPlugin" } + register("androidTest") { + id = "atomtasks.android.test" + implementationClass = "AndroidTestConventionPlugin" + } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt index 6ddb9b64..b5a14822 100644 --- a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -31,7 +31,6 @@ class AndroidFeatureConventionPlugin : Plugin { dependencies { add("implementation", project(":core:designsystem")) add("implementation", project(":data")) - add("implementation", libs.findLibrary("compose.activity").get()) add("implementation", libs.findLibrary("compose.foundation").get()) add("implementation", libs.findLibrary("compose.runtime").get()) add("implementation", libs.findLibrary("compose.layout").get()) diff --git a/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt new file mode 100644 index 00000000..1286c837 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt @@ -0,0 +1,21 @@ +import com.android.build.gradle.TestExtension +import com.costular.atomtasks.configureKotlinAndroid +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +class AndroidTestConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.test") + apply("org.jetbrains.kotlin.android") + } + + extensions.configure { + configureKotlinAndroid(this) + defaultConfig.targetSdk = 34 + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/com/costular/atomtasks/BuildVariants.kt b/build-logic/convention/src/main/kotlin/com/costular/atomtasks/BuildVariants.kt index 48989cfe..4d6b73b8 100644 --- a/build-logic/convention/src/main/kotlin/com/costular/atomtasks/BuildVariants.kt +++ b/build-logic/convention/src/main/kotlin/com/costular/atomtasks/BuildVariants.kt @@ -3,6 +3,7 @@ package com.costular.atomtasks import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.ApplicationProductFlavor import com.android.build.api.dsl.CommonExtension +import com.android.build.api.dsl.ProductFlavor import com.android.build.gradle.internal.dsl.BaseAppModuleExtension enum class FlavorDimension( @@ -11,22 +12,32 @@ enum class FlavorDimension( Environment("environment") } -enum class Flavor( +enum class AtomFlavor( val naming: String, val dimension: FlavorDimension, val applicationIdSuffix: String? = null, ) { Development("development", FlavorDimension.Environment, applicationIdSuffix = ".dev"), - Production("production", FlavorDimension.Environment) + Production("production", FlavorDimension.Environment), } -fun configureFlavors(commonExtensions: CommonExtension<*, *, *, *, *>) { +enum class AtomBuildType(val applicationIdSuffix: String? = null) { + DEBUG, + RELEASE, + BENCHMARK(".benchmark") +} + +fun configureFlavors( + commonExtensions: CommonExtension<*, *, *, *, *>, + flavorConfigurationBlock: ProductFlavor.(atomFlavor: AtomFlavor) -> Unit = {}, +) { commonExtensions.apply { flavorDimensions += FlavorDimension.Environment.naming productFlavors { - Flavor.values().forEach { flavor -> + AtomFlavor.values().forEach { flavor -> create(flavor.naming) { dimension = flavor.dimension.naming + flavorConfigurationBlock(this, flavor) if (this@apply is ApplicationExtension && this is ApplicationProductFlavor) { flavor.applicationIdSuffix?.let { applicationIdSuffix = it @@ -39,19 +50,28 @@ fun configureFlavors(commonExtensions: CommonExtension<*, *, *, *, *>) { } fun configureBuildTypes(baseAppModuleExtension: BaseAppModuleExtension) { - with (baseAppModuleExtension) { + with(baseAppModuleExtension) { buildTypes { - getByName("release") { + val release = getByName("release") { isDebuggable = false isMinifyEnabled = true isShrinkResources = true proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") + applicationIdSuffix = AtomBuildType.RELEASE.applicationIdSuffix } getByName("debug") { isDebuggable = true isMinifyEnabled = false isShrinkResources = false proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") + applicationIdSuffix = AtomBuildType.DEBUG.applicationIdSuffix + } + create("benchmark") { + initWith(release) + matchingFallbacks.add("release") + proguardFiles("benchmark-rules.pro") + isMinifyEnabled = true + applicationIdSuffix = AtomBuildType.BENCHMARK.applicationIdSuffix } } } diff --git a/build-logic/convention/src/main/kotlin/com/costular/atomtasks/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/com/costular/atomtasks/KotlinAndroid.kt index f091b49e..a01c2064 100644 --- a/build-logic/convention/src/main/kotlin/com/costular/atomtasks/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/com/costular/atomtasks/KotlinAndroid.kt @@ -19,7 +19,7 @@ internal fun Project.configureKotlinAndroid( compileSdk = 34 defaultConfig { - minSdk = 23 + minSdk = 26 } compileOptions { diff --git a/build-logic/gradle/wrapper/gradle-wrapper.properties b/build-logic/gradle/wrapper/gradle-wrapper.properties index 458bc029..09bac0e5 100644 --- a/build-logic/gradle/wrapper/gradle-wrapper.properties +++ b/build-logic/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Jul 22 01:09:57 WEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index 1d032516..7b3f0a3f 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -13,4 +13,5 @@ dependencyResolutionManagement { } } +rootProject.name = "build-logic" include(":convention") diff --git a/common/tasks/build.gradle.kts b/common/tasks/build.gradle.kts index 1383b4bf..a563397c 100644 --- a/common/tasks/build.gradle.kts +++ b/common/tasks/build.gradle.kts @@ -21,11 +21,11 @@ android { dependencies { implementation(projects.core.designsystem) implementation(projects.core.analytics) + implementation(projects.core.ui) implementation(projects.data) implementation(projects.core.notifications) implementation(projects.core.logging) - implementation(libs.compose.activity) implementation(libs.compose.foundation) implementation(libs.compose.runtime) implementation(libs.compose.layout) @@ -43,7 +43,6 @@ dependencies { implementation(libs.accompanist.permissions) kapt(libs.hilt.ext.compiler) api(libs.reordeable) - implementation(libs.balloon) testImplementation(projects.common.tasks) testImplementation(projects.core.testing) diff --git a/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/AreExactRemindersAvailable_Factory.java b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/AreExactRemindersAvailable_Factory.java new file mode 100644 index 00000000..4c8ad9fe --- /dev/null +++ b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/AreExactRemindersAvailable_Factory.java @@ -0,0 +1,45 @@ +package com.costular.atomtasks.tasks.usecase; + +import com.costular.atomtasks.tasks.helper.TaskReminderManager; +import dagger.internal.DaggerGenerated; +import dagger.internal.Factory; +import dagger.internal.QualifierMetadata; +import dagger.internal.ScopeMetadata; +import javax.annotation.processing.Generated; +import javax.inject.Provider; + +@ScopeMetadata +@QualifierMetadata +@DaggerGenerated +@Generated( + value = "dagger.internal.codegen.ComponentProcessor", + comments = "https://dagger.dev" +) +@SuppressWarnings({ + "unchecked", + "rawtypes", + "KotlinInternal", + "KotlinInternalInJava" +}) +public final class AreExactRemindersAvailable_Factory implements Factory { + private final Provider tasksReminderManagerProvider; + + public AreExactRemindersAvailable_Factory( + Provider tasksReminderManagerProvider) { + this.tasksReminderManagerProvider = tasksReminderManagerProvider; + } + + @Override + public AreExactRemindersAvailable get() { + return newInstance(tasksReminderManagerProvider.get()); + } + + public static AreExactRemindersAvailable_Factory create( + Provider tasksReminderManagerProvider) { + return new AreExactRemindersAvailable_Factory(tasksReminderManagerProvider); + } + + public static AreExactRemindersAvailable newInstance(TaskReminderManager tasksReminderManager) { + return new AreExactRemindersAvailable(tasksReminderManager); + } +} diff --git a/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/AutoforwardTasksUseCase_Factory.java b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/AutoforwardTasksUseCase_Factory.java new file mode 100644 index 00000000..07bb5d5f --- /dev/null +++ b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/AutoforwardTasksUseCase_Factory.java @@ -0,0 +1,57 @@ +package com.costular.atomtasks.tasks.usecase; + +import com.costular.atomtasks.data.settings.IsAutoforwardTasksSettingEnabledUseCase; +import dagger.internal.DaggerGenerated; +import dagger.internal.Factory; +import dagger.internal.QualifierMetadata; +import dagger.internal.ScopeMetadata; +import javax.annotation.processing.Generated; +import javax.inject.Provider; + +@ScopeMetadata +@QualifierMetadata +@DaggerGenerated +@Generated( + value = "dagger.internal.codegen.ComponentProcessor", + comments = "https://dagger.dev" +) +@SuppressWarnings({ + "unchecked", + "rawtypes", + "KotlinInternal", + "KotlinInternalInJava" +}) +public final class AutoforwardTasksUseCase_Factory implements Factory { + private final Provider isAutoforwardTasksSettingEnabledUseCaseProvider; + + private final Provider observeTasksUseCaseProvider; + + private final Provider editTaskUseCaseProvider; + + public AutoforwardTasksUseCase_Factory( + Provider isAutoforwardTasksSettingEnabledUseCaseProvider, + Provider observeTasksUseCaseProvider, + Provider editTaskUseCaseProvider) { + this.isAutoforwardTasksSettingEnabledUseCaseProvider = isAutoforwardTasksSettingEnabledUseCaseProvider; + this.observeTasksUseCaseProvider = observeTasksUseCaseProvider; + this.editTaskUseCaseProvider = editTaskUseCaseProvider; + } + + @Override + public AutoforwardTasksUseCase get() { + return newInstance(isAutoforwardTasksSettingEnabledUseCaseProvider.get(), observeTasksUseCaseProvider.get(), editTaskUseCaseProvider.get()); + } + + public static AutoforwardTasksUseCase_Factory create( + Provider isAutoforwardTasksSettingEnabledUseCaseProvider, + Provider observeTasksUseCaseProvider, + Provider editTaskUseCaseProvider) { + return new AutoforwardTasksUseCase_Factory(isAutoforwardTasksSettingEnabledUseCaseProvider, observeTasksUseCaseProvider, editTaskUseCaseProvider); + } + + public static AutoforwardTasksUseCase newInstance( + IsAutoforwardTasksSettingEnabledUseCase isAutoforwardTasksSettingEnabledUseCase, + ObserveTasksUseCase observeTasksUseCase, EditTaskUseCase editTaskUseCase) { + return new AutoforwardTasksUseCase(isAutoforwardTasksSettingEnabledUseCase, observeTasksUseCase, editTaskUseCase); + } +} diff --git a/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/CreateTaskUseCase_Factory.java b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/CreateTaskUseCase_Factory.java new file mode 100644 index 00000000..11322136 --- /dev/null +++ b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/CreateTaskUseCase_Factory.java @@ -0,0 +1,56 @@ +package com.costular.atomtasks.tasks.usecase; + +import com.costular.atomtasks.tasks.helper.TaskReminderManager; +import com.costular.atomtasks.tasks.repository.TasksRepository; +import dagger.internal.DaggerGenerated; +import dagger.internal.Factory; +import dagger.internal.QualifierMetadata; +import dagger.internal.ScopeMetadata; +import javax.annotation.processing.Generated; +import javax.inject.Provider; + +@ScopeMetadata +@QualifierMetadata +@DaggerGenerated +@Generated( + value = "dagger.internal.codegen.ComponentProcessor", + comments = "https://dagger.dev" +) +@SuppressWarnings({ + "unchecked", + "rawtypes", + "KotlinInternal", + "KotlinInternalInJava" +}) +public final class CreateTaskUseCase_Factory implements Factory { + private final Provider tasksRepositoryProvider; + + private final Provider taskReminderManagerProvider; + + private final Provider populateRecurringTasksUseCaseProvider; + + public CreateTaskUseCase_Factory(Provider tasksRepositoryProvider, + Provider taskReminderManagerProvider, + Provider populateRecurringTasksUseCaseProvider) { + this.tasksRepositoryProvider = tasksRepositoryProvider; + this.taskReminderManagerProvider = taskReminderManagerProvider; + this.populateRecurringTasksUseCaseProvider = populateRecurringTasksUseCaseProvider; + } + + @Override + public CreateTaskUseCase get() { + return newInstance(tasksRepositoryProvider.get(), taskReminderManagerProvider.get(), populateRecurringTasksUseCaseProvider.get()); + } + + public static CreateTaskUseCase_Factory create(Provider tasksRepositoryProvider, + Provider taskReminderManagerProvider, + Provider populateRecurringTasksUseCaseProvider) { + return new CreateTaskUseCase_Factory(tasksRepositoryProvider, taskReminderManagerProvider, populateRecurringTasksUseCaseProvider); + } + + public static CreateTaskUseCase newInstance(TasksRepository tasksRepository, + TaskReminderManager taskReminderManager, + PopulateRecurringTasksUseCase populateRecurringTasksUseCase) { + return new CreateTaskUseCase(tasksRepository, taskReminderManager, populateRecurringTasksUseCase); + } +} diff --git a/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/GetTaskByIdUseCase_Factory.java b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/GetTaskByIdUseCase_Factory.java new file mode 100644 index 00000000..63f0f878 --- /dev/null +++ b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/GetTaskByIdUseCase_Factory.java @@ -0,0 +1,44 @@ +package com.costular.atomtasks.tasks.usecase; + +import com.costular.atomtasks.tasks.repository.TasksRepository; +import dagger.internal.DaggerGenerated; +import dagger.internal.Factory; +import dagger.internal.QualifierMetadata; +import dagger.internal.ScopeMetadata; +import javax.annotation.processing.Generated; +import javax.inject.Provider; + +@ScopeMetadata +@QualifierMetadata +@DaggerGenerated +@Generated( + value = "dagger.internal.codegen.ComponentProcessor", + comments = "https://dagger.dev" +) +@SuppressWarnings({ + "unchecked", + "rawtypes", + "KotlinInternal", + "KotlinInternalInJava" +}) +public final class GetTaskByIdUseCase_Factory implements Factory { + private final Provider tasksRepositoryProvider; + + public GetTaskByIdUseCase_Factory(Provider tasksRepositoryProvider) { + this.tasksRepositoryProvider = tasksRepositoryProvider; + } + + @Override + public GetTaskByIdUseCase get() { + return newInstance(tasksRepositoryProvider.get()); + } + + public static GetTaskByIdUseCase_Factory create( + Provider tasksRepositoryProvider) { + return new GetTaskByIdUseCase_Factory(tasksRepositoryProvider); + } + + public static GetTaskByIdUseCase newInstance(TasksRepository tasksRepository) { + return new GetTaskByIdUseCase(tasksRepository); + } +} diff --git a/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/GetTasksWithReminderInteractor_Factory.java b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/GetTasksWithReminderInteractor_Factory.java new file mode 100644 index 00000000..8f96a893 --- /dev/null +++ b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/GetTasksWithReminderInteractor_Factory.java @@ -0,0 +1,44 @@ +package com.costular.atomtasks.tasks.usecase; + +import com.costular.atomtasks.tasks.repository.TasksRepository; +import dagger.internal.DaggerGenerated; +import dagger.internal.Factory; +import dagger.internal.QualifierMetadata; +import dagger.internal.ScopeMetadata; +import javax.annotation.processing.Generated; +import javax.inject.Provider; + +@ScopeMetadata +@QualifierMetadata +@DaggerGenerated +@Generated( + value = "dagger.internal.codegen.ComponentProcessor", + comments = "https://dagger.dev" +) +@SuppressWarnings({ + "unchecked", + "rawtypes", + "KotlinInternal", + "KotlinInternalInJava" +}) +public final class GetTasksWithReminderInteractor_Factory implements Factory { + private final Provider tasksRepositoryProvider; + + public GetTasksWithReminderInteractor_Factory(Provider tasksRepositoryProvider) { + this.tasksRepositoryProvider = tasksRepositoryProvider; + } + + @Override + public GetTasksWithReminderInteractor get() { + return newInstance(tasksRepositoryProvider.get()); + } + + public static GetTasksWithReminderInteractor_Factory create( + Provider tasksRepositoryProvider) { + return new GetTasksWithReminderInteractor_Factory(tasksRepositoryProvider); + } + + public static GetTasksWithReminderInteractor newInstance(TasksRepository tasksRepository) { + return new GetTasksWithReminderInteractor(tasksRepository); + } +} diff --git a/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/MoveTaskUseCase_Factory.java b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/MoveTaskUseCase_Factory.java new file mode 100644 index 00000000..fa74ddc6 --- /dev/null +++ b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/MoveTaskUseCase_Factory.java @@ -0,0 +1,43 @@ +package com.costular.atomtasks.tasks.usecase; + +import com.costular.atomtasks.tasks.repository.TasksRepository; +import dagger.internal.DaggerGenerated; +import dagger.internal.Factory; +import dagger.internal.QualifierMetadata; +import dagger.internal.ScopeMetadata; +import javax.annotation.processing.Generated; +import javax.inject.Provider; + +@ScopeMetadata +@QualifierMetadata +@DaggerGenerated +@Generated( + value = "dagger.internal.codegen.ComponentProcessor", + comments = "https://dagger.dev" +) +@SuppressWarnings({ + "unchecked", + "rawtypes", + "KotlinInternal", + "KotlinInternalInJava" +}) +public final class MoveTaskUseCase_Factory implements Factory { + private final Provider tasksRepositoryProvider; + + public MoveTaskUseCase_Factory(Provider tasksRepositoryProvider) { + this.tasksRepositoryProvider = tasksRepositoryProvider; + } + + @Override + public MoveTaskUseCase get() { + return newInstance(tasksRepositoryProvider.get()); + } + + public static MoveTaskUseCase_Factory create(Provider tasksRepositoryProvider) { + return new MoveTaskUseCase_Factory(tasksRepositoryProvider); + } + + public static MoveTaskUseCase newInstance(TasksRepository tasksRepository) { + return new MoveTaskUseCase(tasksRepository); + } +} diff --git a/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/ObserveTasksUseCase_Factory.java b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/ObserveTasksUseCase_Factory.java new file mode 100644 index 00000000..3957b0a7 --- /dev/null +++ b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/ObserveTasksUseCase_Factory.java @@ -0,0 +1,44 @@ +package com.costular.atomtasks.tasks.usecase; + +import com.costular.atomtasks.tasks.repository.TasksRepository; +import dagger.internal.DaggerGenerated; +import dagger.internal.Factory; +import dagger.internal.QualifierMetadata; +import dagger.internal.ScopeMetadata; +import javax.annotation.processing.Generated; +import javax.inject.Provider; + +@ScopeMetadata +@QualifierMetadata +@DaggerGenerated +@Generated( + value = "dagger.internal.codegen.ComponentProcessor", + comments = "https://dagger.dev" +) +@SuppressWarnings({ + "unchecked", + "rawtypes", + "KotlinInternal", + "KotlinInternalInJava" +}) +public final class ObserveTasksUseCase_Factory implements Factory { + private final Provider tasksRepositoryProvider; + + public ObserveTasksUseCase_Factory(Provider tasksRepositoryProvider) { + this.tasksRepositoryProvider = tasksRepositoryProvider; + } + + @Override + public ObserveTasksUseCase get() { + return newInstance(tasksRepositoryProvider.get()); + } + + public static ObserveTasksUseCase_Factory create( + Provider tasksRepositoryProvider) { + return new ObserveTasksUseCase_Factory(tasksRepositoryProvider); + } + + public static ObserveTasksUseCase newInstance(TasksRepository tasksRepository) { + return new ObserveTasksUseCase(tasksRepository); + } +} diff --git a/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/PopulateRecurringTasksUseCase_Factory.java b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/PopulateRecurringTasksUseCase_Factory.java new file mode 100644 index 00000000..3ced1b5a --- /dev/null +++ b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/PopulateRecurringTasksUseCase_Factory.java @@ -0,0 +1,44 @@ +package com.costular.atomtasks.tasks.usecase; + +import com.costular.atomtasks.tasks.repository.TasksRepository; +import dagger.internal.DaggerGenerated; +import dagger.internal.Factory; +import dagger.internal.QualifierMetadata; +import dagger.internal.ScopeMetadata; +import javax.annotation.processing.Generated; +import javax.inject.Provider; + +@ScopeMetadata +@QualifierMetadata +@DaggerGenerated +@Generated( + value = "dagger.internal.codegen.ComponentProcessor", + comments = "https://dagger.dev" +) +@SuppressWarnings({ + "unchecked", + "rawtypes", + "KotlinInternal", + "KotlinInternalInJava" +}) +public final class PopulateRecurringTasksUseCase_Factory implements Factory { + private final Provider tasksRepositoryProvider; + + public PopulateRecurringTasksUseCase_Factory(Provider tasksRepositoryProvider) { + this.tasksRepositoryProvider = tasksRepositoryProvider; + } + + @Override + public PopulateRecurringTasksUseCase get() { + return newInstance(tasksRepositoryProvider.get()); + } + + public static PopulateRecurringTasksUseCase_Factory create( + Provider tasksRepositoryProvider) { + return new PopulateRecurringTasksUseCase_Factory(tasksRepositoryProvider); + } + + public static PopulateRecurringTasksUseCase newInstance(TasksRepository tasksRepository) { + return new PopulateRecurringTasksUseCase(tasksRepository); + } +} diff --git a/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/PostponeTaskUseCase_Factory.java b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/PostponeTaskUseCase_Factory.java new file mode 100644 index 00000000..467bbe6b --- /dev/null +++ b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/PostponeTaskUseCase_Factory.java @@ -0,0 +1,61 @@ +package com.costular.atomtasks.tasks.usecase; + +import com.costular.atomtasks.tasks.helper.TaskReminderManager; +import dagger.internal.DaggerGenerated; +import dagger.internal.Factory; +import dagger.internal.QualifierMetadata; +import dagger.internal.ScopeMetadata; +import javax.annotation.processing.Generated; +import javax.inject.Provider; + +@ScopeMetadata +@QualifierMetadata +@DaggerGenerated +@Generated( + value = "dagger.internal.codegen.ComponentProcessor", + comments = "https://dagger.dev" +) +@SuppressWarnings({ + "unchecked", + "rawtypes", + "KotlinInternal", + "KotlinInternalInJava" +}) +public final class PostponeTaskUseCase_Factory implements Factory { + private final Provider getTaskByIdUseCaseProvider; + + private final Provider updateTaskReminderInteractorProvider; + + private final Provider taskReminderManagerProvider; + + private final Provider editTaskUseCaseProvider; + + public PostponeTaskUseCase_Factory(Provider getTaskByIdUseCaseProvider, + Provider updateTaskReminderInteractorProvider, + Provider taskReminderManagerProvider, + Provider editTaskUseCaseProvider) { + this.getTaskByIdUseCaseProvider = getTaskByIdUseCaseProvider; + this.updateTaskReminderInteractorProvider = updateTaskReminderInteractorProvider; + this.taskReminderManagerProvider = taskReminderManagerProvider; + this.editTaskUseCaseProvider = editTaskUseCaseProvider; + } + + @Override + public PostponeTaskUseCase get() { + return newInstance(getTaskByIdUseCaseProvider.get(), updateTaskReminderInteractorProvider.get(), taskReminderManagerProvider.get(), editTaskUseCaseProvider.get()); + } + + public static PostponeTaskUseCase_Factory create( + Provider getTaskByIdUseCaseProvider, + Provider updateTaskReminderInteractorProvider, + Provider taskReminderManagerProvider, + Provider editTaskUseCaseProvider) { + return new PostponeTaskUseCase_Factory(getTaskByIdUseCaseProvider, updateTaskReminderInteractorProvider, taskReminderManagerProvider, editTaskUseCaseProvider); + } + + public static PostponeTaskUseCase newInstance(GetTaskByIdUseCase getTaskByIdUseCase, + UpdateTaskReminderInteractor updateTaskReminderInteractor, + TaskReminderManager taskReminderManager, EditTaskUseCase editTaskUseCase) { + return new PostponeTaskUseCase(getTaskByIdUseCase, updateTaskReminderInteractor, taskReminderManager, editTaskUseCase); + } +} diff --git a/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/RemoveTaskUseCase_Factory.java b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/RemoveTaskUseCase_Factory.java new file mode 100644 index 00000000..aa18eabd --- /dev/null +++ b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/RemoveTaskUseCase_Factory.java @@ -0,0 +1,50 @@ +package com.costular.atomtasks.tasks.usecase; + +import com.costular.atomtasks.tasks.helper.TaskReminderManager; +import com.costular.atomtasks.tasks.repository.TasksRepository; +import dagger.internal.DaggerGenerated; +import dagger.internal.Factory; +import dagger.internal.QualifierMetadata; +import dagger.internal.ScopeMetadata; +import javax.annotation.processing.Generated; +import javax.inject.Provider; + +@ScopeMetadata +@QualifierMetadata +@DaggerGenerated +@Generated( + value = "dagger.internal.codegen.ComponentProcessor", + comments = "https://dagger.dev" +) +@SuppressWarnings({ + "unchecked", + "rawtypes", + "KotlinInternal", + "KotlinInternalInJava" +}) +public final class RemoveTaskUseCase_Factory implements Factory { + private final Provider tasksRepositoryProvider; + + private final Provider taskReminderManagerProvider; + + public RemoveTaskUseCase_Factory(Provider tasksRepositoryProvider, + Provider taskReminderManagerProvider) { + this.tasksRepositoryProvider = tasksRepositoryProvider; + this.taskReminderManagerProvider = taskReminderManagerProvider; + } + + @Override + public RemoveTaskUseCase get() { + return newInstance(tasksRepositoryProvider.get(), taskReminderManagerProvider.get()); + } + + public static RemoveTaskUseCase_Factory create(Provider tasksRepositoryProvider, + Provider taskReminderManagerProvider) { + return new RemoveTaskUseCase_Factory(tasksRepositoryProvider, taskReminderManagerProvider); + } + + public static RemoveTaskUseCase newInstance(TasksRepository tasksRepository, + TaskReminderManager taskReminderManager) { + return new RemoveTaskUseCase(tasksRepository, taskReminderManager); + } +} diff --git a/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/UpdateTaskIsDoneUseCase_Factory.java b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/UpdateTaskIsDoneUseCase_Factory.java new file mode 100644 index 00000000..249e778c --- /dev/null +++ b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/UpdateTaskIsDoneUseCase_Factory.java @@ -0,0 +1,44 @@ +package com.costular.atomtasks.tasks.usecase; + +import com.costular.atomtasks.tasks.repository.TasksRepository; +import dagger.internal.DaggerGenerated; +import dagger.internal.Factory; +import dagger.internal.QualifierMetadata; +import dagger.internal.ScopeMetadata; +import javax.annotation.processing.Generated; +import javax.inject.Provider; + +@ScopeMetadata +@QualifierMetadata +@DaggerGenerated +@Generated( + value = "dagger.internal.codegen.ComponentProcessor", + comments = "https://dagger.dev" +) +@SuppressWarnings({ + "unchecked", + "rawtypes", + "KotlinInternal", + "KotlinInternalInJava" +}) +public final class UpdateTaskIsDoneUseCase_Factory implements Factory { + private final Provider tasksRepositoryProvider; + + public UpdateTaskIsDoneUseCase_Factory(Provider tasksRepositoryProvider) { + this.tasksRepositoryProvider = tasksRepositoryProvider; + } + + @Override + public UpdateTaskIsDoneUseCase get() { + return newInstance(tasksRepositoryProvider.get()); + } + + public static UpdateTaskIsDoneUseCase_Factory create( + Provider tasksRepositoryProvider) { + return new UpdateTaskIsDoneUseCase_Factory(tasksRepositoryProvider); + } + + public static UpdateTaskIsDoneUseCase newInstance(TasksRepository tasksRepository) { + return new UpdateTaskIsDoneUseCase(tasksRepository); + } +} diff --git a/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/UpdateTaskReminderInteractor_Factory.java b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/UpdateTaskReminderInteractor_Factory.java new file mode 100644 index 00000000..a154bbe8 --- /dev/null +++ b/common/tasks/build/generated/source/kapt/debug/com/costular/atomtasks/tasks/usecase/UpdateTaskReminderInteractor_Factory.java @@ -0,0 +1,44 @@ +package com.costular.atomtasks.tasks.usecase; + +import com.costular.atomtasks.tasks.repository.TasksRepository; +import dagger.internal.DaggerGenerated; +import dagger.internal.Factory; +import dagger.internal.QualifierMetadata; +import dagger.internal.ScopeMetadata; +import javax.annotation.processing.Generated; +import javax.inject.Provider; + +@ScopeMetadata +@QualifierMetadata +@DaggerGenerated +@Generated( + value = "dagger.internal.codegen.ComponentProcessor", + comments = "https://dagger.dev" +) +@SuppressWarnings({ + "unchecked", + "rawtypes", + "KotlinInternal", + "KotlinInternalInJava" +}) +public final class UpdateTaskReminderInteractor_Factory implements Factory { + private final Provider tasksRepositoryProvider; + + public UpdateTaskReminderInteractor_Factory(Provider tasksRepositoryProvider) { + this.tasksRepositoryProvider = tasksRepositoryProvider; + } + + @Override + public UpdateTaskReminderInteractor get() { + return newInstance(tasksRepositoryProvider.get()); + } + + public static UpdateTaskReminderInteractor_Factory create( + Provider tasksRepositoryProvider) { + return new UpdateTaskReminderInteractor_Factory(tasksRepositoryProvider); + } + + public static UpdateTaskReminderInteractor newInstance(TasksRepository tasksRepository) { + return new UpdateTaskReminderInteractor(tasksRepository); + } +} diff --git a/common/tasks/src/androidTest/java/com/costular/atomtasks/tasks/TaskCardTest.kt b/common/tasks/src/androidTest/java/com/costular/atomtasks/tasks/TaskCardTest.kt index 0e877c55..99945408 100644 --- a/common/tasks/src/androidTest/java/com/costular/atomtasks/tasks/TaskCardTest.kt +++ b/common/tasks/src/androidTest/java/com/costular/atomtasks/tasks/TaskCardTest.kt @@ -26,6 +26,7 @@ class TaskCardTest : AndroidTest() { onMark = onMarkCallback, onOpen = {}, isBeingDragged = false, + recurrenceType = null, ) } @@ -50,6 +51,7 @@ class TaskCardTest : AndroidTest() { onMark = {}, onOpen = onClickCallback, isBeingDragged = false, + recurrenceType = null, ) } diff --git a/common/tasks/src/androidTest/java/com/costular/atomtasks/tasks/manager/ReminderManagerImplTest.kt b/common/tasks/src/androidTest/java/com/costular/atomtasks/tasks/manager/ReminderManagerImplTest.kt index 3f4ed26a..31cd9d13 100644 --- a/common/tasks/src/androidTest/java/com/costular/atomtasks/tasks/manager/ReminderManagerImplTest.kt +++ b/common/tasks/src/androidTest/java/com/costular/atomtasks/tasks/manager/ReminderManagerImplTest.kt @@ -3,6 +3,8 @@ package com.costular.atomtasks.tasks.manager import android.app.PendingIntent import android.content.Intent import androidx.test.platform.app.InstrumentationRegistry +import com.costular.atomtasks.tasks.helper.TaskReminderManager +import com.costular.atomtasks.tasks.helper.TaskReminderManagerImpl import com.google.common.truth.Truth.assertThat import java.time.LocalDateTime import org.junit.After diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/CreateTaskExpanded.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/CreateTaskExpanded.kt index f52e4ddd..ad77cc86 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/CreateTaskExpanded.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/CreateTaskExpanded.kt @@ -1,3 +1,5 @@ +@file:Suppress("TooManyFunctions") + package com.costular.atomtasks.tasks.createtask import android.app.AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED @@ -7,6 +9,7 @@ import android.content.Intent import android.content.IntentFilter import android.os.Build import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -15,14 +18,18 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Alarm import androidx.compose.material.icons.outlined.Check +import androidx.compose.material.icons.outlined.Repeat import androidx.compose.material.icons.outlined.Today import androidx.compose.material3.AlertDialog import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -39,11 +46,14 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.costular.atomtasks.core.ui.R import com.costular.atomtasks.core.ui.utils.DateUtils.dayAsText -import com.costular.atomtasks.coreui.utils.ofLocalizedTime +import com.costular.atomtasks.core.ui.utils.ofLocalizedTime +import com.costular.atomtasks.tasks.format.localized +import com.costular.atomtasks.tasks.model.RecurrenceType import com.costular.designsystem.components.ClearableChip import com.costular.designsystem.components.PrimaryButton import com.costular.designsystem.dialogs.DatePickerDialog @@ -67,6 +77,7 @@ fun CreateTaskExpanded( onSave: (CreateTaskResult) -> Unit, modifier: Modifier = Modifier, reminder: LocalTime? = null, + recurrenceType: RecurrenceType? = null, ) { val context = LocalContext.current @@ -91,6 +102,10 @@ fun CreateTaskExpanded( viewModel.setReminder(reminder) } + LaunchedEffect(recurrenceType) { + viewModel.setRecurrence(recurrenceType) + } + LaunchedEffect(viewModel) { viewModel.uiEvents.collect { event -> when (event) { @@ -135,6 +150,14 @@ fun CreateTaskExpanded( } } + if (state.showSetRecurrence) { + RecurrenceTypePickerDialog( + recurrenceType = state.recurrenceType, + onRecurrenceTypeSelected = viewModel::setRecurrence, + onDismissRequest = viewModel::closeSelectRecurrence, + ) + } + CreateTaskExpanded( state = state, modifier = modifier, @@ -143,9 +166,9 @@ fun CreateTaskExpanded( onClickDate = viewModel::selectDate, onClickReminder = viewModel::selectReminder, onClearReminder = viewModel::clearReminder, - onSave = { - viewModel.requestSave() - }, + onSave = viewModel::requestSave, + onClickRecurrence = viewModel::selectRecurrence, + onClearRecurrence = viewModel::clearRecurrence, ) } @@ -222,72 +245,178 @@ internal fun CreateTaskExpanded( onClickDate: () -> Unit, onClickReminder: () -> Unit, onClearReminder: () -> Unit, + onClickRecurrence: () -> Unit, + onClearRecurrence: () -> Unit, onSave: () -> Unit, ) { - Column(modifier.padding(AppTheme.dimens.contentMargin)) { + Column(modifier = modifier) { CreateTaskInput( + modifier = Modifier.padding( + top = AppTheme.dimens.contentMargin, + start = AppTheme.dimens.contentMargin, + end = AppTheme.dimens.contentMargin, + ), value = state.name, focusRequester = focusRequester, onValueChange = onValueChange, + enabled = !state.saving ) - Spacer(Modifier.height(AppTheme.dimens.spacingMedium)) - - Row(modifier = Modifier.fillMaxWidth()) { - ClearableChip( - title = dayAsText(state.date), - icon = Icons.Outlined.Today, - isSelected = false, - onClick = onClickDate, - onClear = onClearReminder, - isError = false, - modifier = Modifier.testTag("CreateTaskExpandedDate"), - ) + Spacer(Modifier.height(AppTheme.dimens.spacingSmall)) + + CreateTaskPickers( + date = state.date, + reminder = state.reminder, + isReminderError = state.isReminderError, + recurrenceType = state.recurrenceType, + onClickDate = onClickDate, + onClearReminder = onClearReminder, + onClickReminder = onClickReminder, + onClickRecurrence = onClickRecurrence, + onClearRecurrence = onClearRecurrence, + ) - Spacer(Modifier.width(AppTheme.dimens.spacingMedium)) + Spacer(Modifier.height(AppTheme.dimens.spacingLarge)) - val reminderText = if (state.reminder != null) { - state.reminder.ofLocalizedTime() - } else { - stringResource(R.string.create_task_set_reminder) - } + SaveButton( + isEnabled = state.shouldShowSend, + saving = state.saving, + onSave = onSave, + modifier = Modifier + .fillMaxWidth() + .padding( + start = AppTheme.dimens.contentMargin, + end = AppTheme.dimens.contentMargin, + bottom = AppTheme.dimens.spacingMedium, + ), + ) + } +} - ClearableChip( - title = reminderText, - icon = Icons.Outlined.Alarm, - isSelected = state.reminder != null, - onClick = onClickReminder, - onClear = onClearReminder, - isError = state.isReminderError, - modifier = Modifier.testTag("CreateTaskExpandedReminder"), - ) - } +@Composable +private fun CreateTaskPickers( + date: LocalDate, + reminder: LocalTime?, + isReminderError: Boolean, + recurrenceType: RecurrenceType?, + onClickDate: () -> Unit, + onClearReminder: () -> Unit, + onClickReminder: () -> Unit, + onClickRecurrence: () -> Unit, + onClearRecurrence: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(horizontal = AppTheme.dimens.contentMargin), + ) { + DueDateButton( + date, + onClickDate, + onClearReminder, + ) - if (state.isReminderError) { - Spacer(Modifier.height(AppTheme.dimens.spacingSmall)) + Spacer(Modifier.width(AppTheme.dimens.spacingMedium)) - Text( - text = stringResource(R.string.create_task_reminder_past_error), - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.error, - ) - } + ReminderButton( + reminder, + isReminderError, + onClickReminder, + onClearReminder + ) - Spacer(Modifier.height(AppTheme.dimens.spacingXLarge)) + Spacer(Modifier.width(AppTheme.dimens.spacingMedium)) - SaveButton( - isEnabled = state.shouldShowSend, - onSave = onSave, - modifier = Modifier.fillMaxWidth(), + RecurrenceButton( + recurrenceType = recurrenceType, + onClick = onClickRecurrence, + onClearRecurrence = onClearRecurrence, + ) + } + + if (isReminderError) { + Text( + text = stringResource(R.string.create_task_reminder_past_error), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = AppTheme.dimens.contentMargin), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.labelMedium, ) } } +@Composable +private fun DueDateButton( + date: LocalDate, + onClickDate: () -> Unit, + onClearReminder: () -> Unit +) { + ClearableChip( + title = dayAsText(date), + icon = Icons.Outlined.Today, + isSelected = false, + onClick = onClickDate, + onClear = onClearReminder, + isError = false, + modifier = Modifier.testTag("CreateTaskExpandedDate"), + ) +} + +@Composable +private fun ReminderButton( + reminder: LocalTime?, + isError: Boolean, + onClickReminder: () -> Unit, + onClearReminder: () -> Unit +) { + val reminderText = if (reminder != null) { + reminder.ofLocalizedTime() + } else { + stringResource(R.string.create_task_set_reminder) + } + + ClearableChip( + title = reminderText, + icon = Icons.Outlined.Alarm, + isSelected = reminder != null, + onClick = onClickReminder, + onClear = onClearReminder, + isError = isError, + modifier = Modifier.testTag("CreateTaskExpandedReminder"), + ) +} + +@Composable +private fun RecurrenceButton( + recurrenceType: RecurrenceType?, + onClick: () -> Unit, + onClearRecurrence: () -> Unit, +) { + val buttonText = if (recurrenceType != null) { + recurrenceType.localized() + } else { + stringResource(R.string.create_task_set_recurrence) + } + + ClearableChip( + title = buttonText, + icon = Icons.Outlined.Repeat, + isSelected = recurrenceType != null, + isError = false, + onClick = onClick, + onClear = onClearRecurrence, + ) +} + @Composable private fun CreateTaskInput( value: String, + enabled: Boolean, focusRequester: FocusRequester, onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, ) { OutlinedTextField( value = value, @@ -298,18 +427,20 @@ private fun CreateTaskInput( style = MaterialTheme.typography.bodyLarge, ) }, - modifier = Modifier.Companion + modifier = modifier .fillMaxWidth() .testTag("CreateTaskInput") .focusRequester(focusRequester), textStyle = MaterialTheme.typography.bodyLarge, maxLines = 5, + enabled = enabled, ) } @Composable fun SaveButton( isEnabled: Boolean, + saving: Boolean, onSave: () -> Unit, modifier: Modifier = Modifier, ) { @@ -318,13 +449,21 @@ fun SaveButton( modifier = modifier.testTag("CreateTaskExpandedSave"), enabled = isEnabled, ) { - Icon( - imageVector = Icons.Outlined.Check, - contentDescription = null, - modifier = Modifier.size(ButtonDefaults.IconSize), - ) - Spacer(Modifier.size(ButtonDefaults.IconSpacing)) - Text(stringResource(R.string.save)) + if (saving) { + CircularProgressIndicator( + modifier = Modifier.size(ButtonDefaults.IconSize), + strokeWidth = 2.dp, + color = LocalContentColor.current, + ) + } else { + Icon( + imageVector = Icons.Outlined.Check, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.save)) + } } } @@ -370,6 +509,7 @@ fun CreateTaskExpandedPreview() { CreateTaskExpanded( state = CreateTaskExpandedState( name = "🏃🏻‍♀️ Go out for running!", + saving = false, ), focusRequester = FocusRequester(), onValueChange = {}, @@ -377,6 +517,8 @@ fun CreateTaskExpandedPreview() { onClickDate = {}, onSave = {}, onClearReminder = {}, + onClickRecurrence = {}, + onClearRecurrence = {}, ) } } @@ -389,6 +531,54 @@ fun CreateTaskExpandedWithPastReminderErrorPreview() { state = CreateTaskExpandedState( name = "🏃🏻‍♀️ Go out for running!", reminder = LocalTime.now().minusHours(1), + saving = false, + ), + focusRequester = FocusRequester(), + onValueChange = {}, + onClickReminder = {}, + onClickDate = {}, + onSave = {}, + onClearReminder = {}, + onClickRecurrence = {}, + onClearRecurrence = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +fun CreateTaskExpandedFilledDataPreview() { + AtomTheme { + CreateTaskExpanded( + state = CreateTaskExpandedState( + name = "🏃🏻‍♀️ Go out for running!", + reminder = LocalTime.now().plusHours(4), + date = LocalDate.now().plusDays(4), + recurrenceType = RecurrenceType.WEEKLY, + saving = false, + ), + focusRequester = FocusRequester(), + onValueChange = {}, + onClickReminder = {}, + onClickDate = {}, + onSave = {}, + onClearReminder = {}, + onClickRecurrence = {}, + onClearRecurrence = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +fun CreateSavingPreview() { + AtomTheme { + CreateTaskExpanded( + state = CreateTaskExpandedState( + name = "🏃🏻‍♀️ Go out for running!", + reminder = LocalTime.now().plusHours(4), + date = LocalDate.now().plusDays(4), + saving = true, ), focusRequester = FocusRequester(), onValueChange = {}, @@ -396,6 +586,8 @@ fun CreateTaskExpandedWithPastReminderErrorPreview() { onClickDate = {}, onSave = {}, onClearReminder = {}, + onClickRecurrence = {}, + onClearRecurrence = {}, ) } } diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/CreateTaskExpandedState.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/CreateTaskExpandedState.kt index f683c471..90233f78 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/CreateTaskExpandedState.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/CreateTaskExpandedState.kt @@ -1,5 +1,6 @@ package com.costular.atomtasks.tasks.createtask +import com.costular.atomtasks.tasks.model.RecurrenceType import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime @@ -10,7 +11,10 @@ data class CreateTaskExpandedState( val reminder: LocalTime? = null, val showSetDate: Boolean = false, val showSetReminder: Boolean = false, + val showSetRecurrence: Boolean = false, val shouldShowAlarmsRationale: Boolean = false, + val recurrenceType: RecurrenceType? = null, + val saving: Boolean = false, ) { val shouldShowSend: Boolean get() = name.isNotBlank() diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/CreateTaskExpandedViewModel.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/CreateTaskExpandedViewModel.kt index cf05d3b4..95585940 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/CreateTaskExpandedViewModel.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/CreateTaskExpandedViewModel.kt @@ -2,7 +2,8 @@ package com.costular.atomtasks.tasks.createtask import androidx.lifecycle.viewModelScope import com.costular.atomtasks.core.ui.mvi.MviViewModel -import com.costular.atomtasks.tasks.interactor.AreExactRemindersAvailable +import com.costular.atomtasks.tasks.model.RecurrenceType +import com.costular.atomtasks.tasks.usecase.AreExactRemindersAvailable import dagger.hilt.android.lifecycle.HiltViewModel import java.time.LocalDate import java.time.LocalTime @@ -36,7 +37,10 @@ class CreateTaskExpandedViewModel @Inject constructor( fun setDate(localDate: LocalDate) { setState { - copy(date = localDate, showSetDate = false) + copy( + date = localDate, + showSetDate = false + ) } } @@ -79,15 +83,34 @@ class CreateTaskExpandedViewModel @Inject constructor( } } + fun selectRecurrence() { + setState { copy(showSetRecurrence = true) } + } + + fun closeSelectRecurrence() { + setState { copy(showSetRecurrence = false) } + } + + fun setRecurrence( + recurrenceType: RecurrenceType? + ) { + setState { copy(recurrenceType = recurrenceType, showSetRecurrence = false) } + } + + fun clearRecurrence() { + setState { copy(recurrenceType = null, showSetRecurrence = false) } + } + fun requestSave() { + setState { + copy(saving = true) + } + sendEvent( CreateTaskUiEvents.SaveTask( state.value.asCreateTaskResult(), ), ) - setState { - CreateTaskExpandedState.Empty - } } private fun CreateTaskExpandedState.asCreateTaskResult(): CreateTaskResult = @@ -95,6 +118,7 @@ class CreateTaskExpandedViewModel @Inject constructor( name = name, date = date, reminder = reminder, + recurrenceType = recurrenceType, ) fun navigateToExactAlarmSettings() { diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/CreateTaskResult.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/CreateTaskResult.kt index 26aa72f2..6bce5097 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/CreateTaskResult.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/CreateTaskResult.kt @@ -1,5 +1,6 @@ package com.costular.atomtasks.tasks.createtask +import com.costular.atomtasks.tasks.model.RecurrenceType import java.time.LocalDate import java.time.LocalTime @@ -7,4 +8,5 @@ data class CreateTaskResult( val name: String, val date: LocalDate, val reminder: LocalTime?, + val recurrenceType: RecurrenceType?, ) diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/RecurrenceTypePickerDialog.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/RecurrenceTypePickerDialog.kt new file mode 100644 index 00000000..7c88d699 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/createtask/RecurrenceTypePickerDialog.kt @@ -0,0 +1,135 @@ +package com.costular.atomtasks.tasks.createtask + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.costular.atomtasks.tasks.model.RecurrenceType +import com.costular.designsystem.theme.AppTheme +import com.costular.designsystem.theme.AtomTheme +import com.costular.atomtasks.core.ui.R.string as S + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RecurrenceTypePickerDialog( + recurrenceType: RecurrenceType?, + onRecurrenceTypeSelected: (RecurrenceType?) -> Unit, + onDismissRequest: () -> Unit, +) { + BasicAlertDialog( + onDismissRequest = onDismissRequest, + ) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.large, + ) { + Column( + modifier = Modifier.padding(vertical = AppTheme.DialogPadding) + ) { + ListItem( + text = stringResource(S.create_task_recurrence_picker_none), + isSelected = recurrenceType == null, + onClick = { onRecurrenceTypeSelected(null) }) + + ListItem( + text = stringResource(S.create_task_recurrence_picker_daily), + isSelected = recurrenceType == RecurrenceType.DAILY, + onClick = { onRecurrenceTypeSelected(RecurrenceType.DAILY) }, + ) + + ListItem( + text = stringResource(S.create_task_recurrence_picker_weekdays), + isSelected = recurrenceType == RecurrenceType.WEEKDAYS, + onClick = { onRecurrenceTypeSelected(RecurrenceType.WEEKDAYS) }, + ) + + ListItem( + text = stringResource(S.create_task_recurrence_picker_weekly), + isSelected = recurrenceType == RecurrenceType.WEEKLY, + onClick = { onRecurrenceTypeSelected(RecurrenceType.WEEKLY) }, + ) + + ListItem( + text = stringResource(S.create_task_recurrence_picker_monthly), + isSelected = recurrenceType == RecurrenceType.MONTHLY, + onClick = { onRecurrenceTypeSelected(RecurrenceType.MONTHLY) }, + ) + + ListItem( + text = stringResource(S.create_task_recurrence_picker_yearly), + isSelected = recurrenceType == RecurrenceType.YEARLY, + onClick = { onRecurrenceTypeSelected(RecurrenceType.YEARLY) }, + ) + } + } + } +} + +@Composable +private fun ListItem( + text: String, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .clickable { onClick() }, + ) { + RadioButton( + modifier = Modifier.padding(start = AppTheme.DialogPadding), + selected = isSelected, + onClick = onClick + ) + + Spacer(Modifier.width(AppTheme.dimens.spacingMedium)) + + Text( + text = text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .padding(end = AppTheme.dimens.contentMargin), + ) + } +} + +@Preview +@Composable +private fun RecurrenceTypePickerPreview() { + AtomTheme { + var recurrenceType by remember { + mutableStateOf(null) + } + + RecurrenceTypePickerDialog( + recurrenceType = recurrenceType, + onRecurrenceTypeSelected = { + recurrenceType = it + }, + onDismissRequest = {}, + ) + } +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/di/ReminderManagerModule.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/di/ReminderManagerModule.kt index bdb7c37a..25495d89 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/di/ReminderManagerModule.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/di/ReminderManagerModule.kt @@ -1,8 +1,8 @@ package com.costular.atomtasks.tasks.di import android.content.Context -import com.costular.atomtasks.tasks.manager.TaskReminderManager -import com.costular.atomtasks.tasks.manager.TaskReminderManagerImpl +import com.costular.atomtasks.tasks.helper.TaskReminderManager +import com.costular.atomtasks.tasks.helper.TaskReminderManagerImpl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/di/TasksModule.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/di/TasksModule.kt index cb5d40ac..e77c5d9c 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/di/TasksModule.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/di/TasksModule.kt @@ -1,20 +1,24 @@ package com.costular.atomtasks.tasks.di -import com.costular.atomtasks.data.tasks.ReminderDao -import com.costular.atomtasks.data.tasks.TasksDao +import com.costular.atomtasks.tasks.helper.recurrence.RecurrenceManager +import com.costular.atomtasks.tasks.helper.recurrence.RecurrenceManagerImpl import com.costular.atomtasks.tasks.repository.DefaultTasksLocalDataSource import com.costular.atomtasks.tasks.repository.TaskLocalDataSource +import dagger.Binds import dagger.Module -import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent @InstallIn(SingletonComponent::class) @Module -object TasksModule { - @Provides - fun providesTaskLocalDataSource( - tasksDao: TasksDao, - reminderDao: ReminderDao, - ): TaskLocalDataSource = DefaultTasksLocalDataSource(tasksDao, reminderDao) +internal interface TasksModule { + @Binds + fun bindsTaskLocalDataSource( + defaultTasksLocalDataSource: DefaultTasksLocalDataSource, + ): TaskLocalDataSource + + @Binds + fun bindsRecurrenceManager( + recurrenceManagerImpl: RecurrenceManagerImpl + ): RecurrenceManager } diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/dialog/RemoveRecurrentTaskDialog.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/dialog/RemoveRecurrentTaskDialog.kt new file mode 100644 index 00000000..cb49e440 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/dialog/RemoveRecurrentTaskDialog.kt @@ -0,0 +1,171 @@ +package com.costular.atomtasks.tasks.dialog + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.costular.designsystem.theme.AppTheme +import com.costular.designsystem.theme.AtomTheme +import com.costular.atomtasks.core.ui.R.string as S + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RemoveRecurrentTaskDialog( + onCancel: () -> Unit, + onRemove: (RemoveRecurrentTaskResponse) -> Unit, +) { + var selected: RemoveRecurrentTaskResponse by remember { + mutableStateOf( + RemoveRecurrentTaskResponse.THIS + ) + } + + BasicAlertDialog( + onDismissRequest = onCancel, + ) { + Surface( + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation, + ) { + Column(modifier = Modifier.padding(vertical = AppTheme.DialogPadding)) { + Text( + text = "Remove recurrence task", + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(horizontal = AppTheme.DialogPadding) + ) + + Spacer(Modifier.height(AppTheme.dimens.spacingLarge)) + + SelectableItem(text = stringResource(S.remove_recurring_task_dialog_this), + isSelected = selected == RemoveRecurrentTaskResponse.THIS, + onClick = { + selected = RemoveRecurrentTaskResponse.THIS + } + ) + + SelectableItem(text = stringResource(S.remove_recurring_task_dialog_this_and_future), + isSelected = selected == RemoveRecurrentTaskResponse.THIS_AND_FUTURES, + onClick = { + selected = RemoveRecurrentTaskResponse.THIS_AND_FUTURES + } + ) + + SelectableItem(text = stringResource(S.remove_recurring_task_dialog_all), + isSelected = selected == RemoveRecurrentTaskResponse.ALL, + onClick = { + selected = RemoveRecurrentTaskResponse.ALL + } + ) + + Spacer(Modifier.height(AppTheme.dimens.spacingXLarge)) + + DialogButtons( + onCancel = onCancel, + onRemove = onRemove, + selected = selected + ) + } + } + } + +} + +@Composable +private fun DialogButtons( + onCancel: () -> Unit, + onRemove: (RemoveRecurrentTaskResponse) -> Unit, + selected: RemoveRecurrentTaskResponse +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(end = AppTheme.DialogPadding), + horizontalArrangement = Arrangement.End, + ) { + TextButton(onClick = onCancel) { + Text( + text = "Cancel", + style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Medium), + ) + } + TextButton(onClick = { + onRemove(selected) + }) { + Text( + text = "Remove", + style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Medium), + ) + } + } +} + +@Composable +private fun SelectableItem( + text: String, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .clickable { onClick() }, + ) { + RadioButton( + modifier = Modifier.padding(start = AppTheme.DialogPadding), + selected = isSelected, + onClick = onClick + ) + + Spacer(Modifier.width(AppTheme.dimens.spacingMedium)) + + Text( + text = text, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .padding(end = AppTheme.DialogPadding), + ) + } +} + +enum class RemoveRecurrentTaskResponse { + THIS, THIS_AND_FUTURES, ALL, +} + +@Preview +@Composable +fun RemoveRecurrentTaskDialogPreview() { + AtomTheme { + RemoveRecurrentTaskDialog( + onCancel = {}, + onRemove = {}, + ) + } +} diff --git a/core/designsystem/src/main/java/com/costular/designsystem/dialogs/RemoveTaskDialog.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/dialog/RemoveTaskDialog.kt similarity index 94% rename from core/designsystem/src/main/java/com/costular/designsystem/dialogs/RemoveTaskDialog.kt rename to common/tasks/src/main/java/com/costular/atomtasks/tasks/dialog/RemoveTaskDialog.kt index e0628d57..908ebf8d 100644 --- a/core/designsystem/src/main/java/com/costular/designsystem/dialogs/RemoveTaskDialog.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/dialog/RemoveTaskDialog.kt @@ -1,4 +1,4 @@ -package com.costular.designsystem.dialogs +package com.costular.atomtasks.tasks.dialog import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/fake/FakeData.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/fake/FakeData.kt index e69ace8d..b084a903 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/fake/FakeData.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/fake/FakeData.kt @@ -1,7 +1,10 @@ package com.costular.atomtasks.tasks.fake +import com.costular.atomtasks.tasks.model.RecurrenceType +import com.costular.atomtasks.tasks.model.Reminder import com.costular.atomtasks.tasks.model.Task import java.time.LocalDate +import java.time.LocalTime val TaskToday = Task( id = 10L, @@ -11,4 +14,68 @@ val TaskToday = Task( reminder = null, isDone = false, position = 0, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, +) +val TaskRecurring = Task( + id = 11L, + name = "Task test", + createdAt = LocalDate.now(), + day = LocalDate.now(), + reminder = null, + isDone = false, + position = 0, + isRecurring = true, + recurrenceEndDate = null, + recurrenceType = RecurrenceType.WEEKLY, + parentId = 1L, +) +val TaskWithReminder = Task( + id = 11L, + name = "Task test", + createdAt = LocalDate.now(), + day = LocalDate.now(), + reminder = Reminder( + id = 1L, + time = LocalTime.of(9, 0), + date = LocalDate.now(), + ), + isDone = false, + position = 0, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = RecurrenceType.WEEKLY, + parentId = 1L, +) +val TaskRecurringWithReminder = Task( + id = 11L, + name = "Task test", + createdAt = LocalDate.now(), + day = LocalDate.now(), + reminder = Reminder( + id = 1L, + time = LocalTime.of(9, 0), + date = LocalDate.now(), + ), + isDone = false, + position = 0, + isRecurring = true, + recurrenceEndDate = null, + recurrenceType = RecurrenceType.WEEKLY, + parentId = 1L, +) +val TaskFinished = Task( + id = 11L, + name = "Task test", + createdAt = LocalDate.now(), + day = LocalDate.now(), + reminder = null, + isDone = true, + position = 0, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = RecurrenceType.WEEKLY, + parentId = 1L, ) diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/format/RecurrenceTypeFormatter.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/format/RecurrenceTypeFormatter.kt new file mode 100644 index 00000000..8bc64238 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/format/RecurrenceTypeFormatter.kt @@ -0,0 +1,18 @@ +package com.costular.atomtasks.tasks.format + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.costular.atomtasks.tasks.model.RecurrenceType +import com.costular.atomtasks.core.ui.R.string as S + +@Composable +fun RecurrenceType.localized(): String { + val stringRes = when(this) { + RecurrenceType.DAILY -> S.task_recurrence_daily + RecurrenceType.WEEKDAYS -> S.task_recurrence_weekdays + RecurrenceType.WEEKLY -> S.task_recurrence_weekly + RecurrenceType.MONTHLY -> S.task_recurrence_monthly + RecurrenceType.YEARLY -> S.task_recurrence_yearly + } + return stringResource(stringRes) +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/manager/AutoforwardManager.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/AutoforwardManager.kt similarity index 73% rename from common/tasks/src/main/java/com/costular/atomtasks/tasks/manager/AutoforwardManager.kt rename to common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/AutoforwardManager.kt index d98cd709..e4673901 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/manager/AutoforwardManager.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/AutoforwardManager.kt @@ -1,16 +1,16 @@ -package com.costular.atomtasks.tasks.manager +package com.costular.atomtasks.tasks.helper import android.content.Context import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.WorkManager +import com.costular.atomtasks.core.usecase.invoke +import com.costular.atomtasks.core.util.getDelayUntil import com.costular.atomtasks.data.settings.IsAutoforwardTasksSettingEnabledUseCase import com.costular.atomtasks.tasks.worker.AutoforwardTasksWorker -import com.costular.core.usecase.invoke import dagger.hilt.android.qualifiers.ApplicationContext -import java.time.Duration +import kotlinx.coroutines.flow.first import java.time.LocalTime import javax.inject.Inject -import kotlinx.coroutines.flow.first class AutoforwardManager @Inject constructor( @ApplicationContext private val context: Context, @@ -20,7 +20,7 @@ class AutoforwardManager @Inject constructor( val isEnabled = isAutoforwardTasksSettingEnabledUseCase().first() if (isEnabled) { - val delay = getDelayUntilAutoforward() + val delay = getDelayUntil(LocalTime.parse(TIME_FOR_AUTOFORWARD)) val worker = AutoforwardTasksWorker.setUp(delay) WorkManager.getInstance(context) @@ -38,19 +38,6 @@ class AutoforwardManager @Inject constructor( WorkManager.getInstance(context).cancelAllWorkByTag(AutoforwardTasksWorker.TAG) } - private fun getDelayUntilAutoforward(): Duration { - val now = LocalTime.now() - val time = LocalTime.parse(TIME_FOR_AUTOFORWARD) - - return Duration.between(now, time).run { - if (isNegative) { - plusDays(1) - } else { - this - } - } - } - companion object { private const val TIME_FOR_AUTOFORWARD = "00:05" } diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/manager/TaskReminderManager.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/TaskReminderManager.kt similarity index 81% rename from common/tasks/src/main/java/com/costular/atomtasks/tasks/manager/TaskReminderManager.kt rename to common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/TaskReminderManager.kt index c16cefa1..546bae8b 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/manager/TaskReminderManager.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/TaskReminderManager.kt @@ -1,4 +1,4 @@ -package com.costular.atomtasks.tasks.manager +package com.costular.atomtasks.tasks.helper import java.time.LocalDateTime diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/manager/TaskReminderManagerImpl.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/TaskReminderManagerImpl.kt similarity index 97% rename from common/tasks/src/main/java/com/costular/atomtasks/tasks/manager/TaskReminderManagerImpl.kt rename to common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/TaskReminderManagerImpl.kt index 2c8b142c..5ef5b49b 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/manager/TaskReminderManagerImpl.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/TaskReminderManagerImpl.kt @@ -1,4 +1,4 @@ -package com.costular.atomtasks.tasks.manager +package com.costular.atomtasks.tasks.helper import android.app.AlarmManager import android.app.PendingIntent diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/DailyRecurrenceStrategy.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/DailyRecurrenceStrategy.kt new file mode 100644 index 00000000..7a61ae13 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/DailyRecurrenceStrategy.kt @@ -0,0 +1,15 @@ +package com.costular.atomtasks.tasks.helper.recurrence + +import java.time.LocalDate + +class DailyRecurrenceStrategy : RecurrenceStrategy { + override fun calculateNextOccurrences( + startDate: LocalDate, + numberOfOccurrences: Int, + drop: Int?, + ): List { + return (1..numberOfOccurrences) + .drop(drop ?: 0) + .map { startDate.plusDays(it.toLong()) } + } +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/MonthlyRecurrenceStrategy.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/MonthlyRecurrenceStrategy.kt new file mode 100644 index 00000000..2e23f2fd --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/MonthlyRecurrenceStrategy.kt @@ -0,0 +1,15 @@ +package com.costular.atomtasks.tasks.helper.recurrence + +import java.time.LocalDate + +class MonthlyRecurrenceStrategy : RecurrenceStrategy { + override fun calculateNextOccurrences( + startDate: LocalDate, + numberOfOccurrences: Int, + drop: Int?, + ): List { + return (1..numberOfOccurrences) + .drop(drop ?: 0) + .map { startDate.plusMonths(it.toLong()) } + } +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceLookAhead.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceLookAhead.kt new file mode 100644 index 00000000..93499dfa --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceLookAhead.kt @@ -0,0 +1,20 @@ +package com.costular.atomtasks.tasks.helper.recurrence + +import com.costular.atomtasks.tasks.model.RecurrenceType + +object RecurrenceLookAhead { + fun numberOfOccurrencesForType(recurrenceType: RecurrenceType): Int { + return when (recurrenceType) { + RecurrenceType.DAILY -> DaysToCreate + RecurrenceType.WEEKDAYS -> WeekdaysToCreate + RecurrenceType.WEEKLY -> WeeksToCreate + RecurrenceType.MONTHLY -> WeeksToCreate + RecurrenceType.YEARLY -> YearsToCreate + } + } + + private const val DaysToCreate = 14 + private const val WeekdaysToCreate = 10 + private const val WeeksToCreate = 4 + private const val YearsToCreate = 1 +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceManager.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceManager.kt new file mode 100644 index 00000000..f0d29219 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceManager.kt @@ -0,0 +1,7 @@ +package com.costular.atomtasks.tasks.helper.recurrence + +import java.time.LocalDate + +interface RecurrenceManager { + suspend fun createAheadTasks(date: LocalDate) +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceManagerImpl.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceManagerImpl.kt new file mode 100644 index 00000000..66e8348e --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceManagerImpl.kt @@ -0,0 +1,37 @@ +package com.costular.atomtasks.tasks.helper.recurrence + +import com.costular.atomtasks.tasks.helper.recurrence.RecurrenceLookAhead.numberOfOccurrencesForType +import com.costular.atomtasks.tasks.repository.TasksRepository +import com.costular.atomtasks.tasks.usecase.PopulateRecurringTasksUseCase +import java.time.LocalDate +import javax.inject.Inject +import kotlinx.coroutines.flow.first + +class RecurrenceManagerImpl @Inject constructor( + private val tasksRepository: TasksRepository, + private val populateRecurringTasksUseCase: PopulateRecurringTasksUseCase, +) : RecurrenceManager { + override suspend fun createAheadTasks(date: LocalDate) { + val tasks = tasksRepository.getTasks(day = date).first() + + tasks.forEach { task -> + if (!task.isRecurring || task.recurrenceType == null || task.parentId == null) { + return + } + + val aheadTasksCountForType = numberOfOccurrencesForType(task.recurrenceType) + val futureOccurrences = tasksRepository.numberFutureOccurrences(task.parentId, date) + + if (futureOccurrences >= aheadTasksCountForType) { + return + } + + populateRecurringTasksUseCase( + PopulateRecurringTasksUseCase.Params( + taskId = task.id, + drop = futureOccurrences, + ) + ) + } + } +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceScheduler.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceScheduler.kt new file mode 100644 index 00000000..69936f54 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceScheduler.kt @@ -0,0 +1,40 @@ +package com.costular.atomtasks.tasks.helper.recurrence + +import android.content.Context +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import com.costular.atomtasks.core.util.getDelayUntil +import com.costular.atomtasks.tasks.worker.PopulateTasksWorker +import dagger.hilt.android.qualifiers.ApplicationContext +import java.time.Duration +import java.time.LocalTime +import javax.inject.Inject + +class RecurrenceScheduler @Inject constructor( + @ApplicationContext private val context: Context, +) { + fun initialize() { + val delay = getDelayUntil(LocalTime.parse(TIME_FOR_POPULATE_WORKER)) + + val worker = PeriodicWorkRequestBuilder( + repeatInterval = Duration.ofHours(RepetitionHours), + flexTimeInterval = Duration.ofMinutes(FlexIntervalMinutes), + ) + .setInitialDelay(delay) + .build() + + WorkManager.getInstance(context) + .enqueueUniquePeriodicWork( + "populate_recurring_tasks", + ExistingPeriodicWorkPolicy.UPDATE, + worker, + ) + } + + companion object { + private const val TIME_FOR_POPULATE_WORKER = "00:05" + private const val RepetitionHours = 24L + private const val FlexIntervalMinutes = 15L + } +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceStrategy.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceStrategy.kt new file mode 100644 index 00000000..ec046a28 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceStrategy.kt @@ -0,0 +1,11 @@ +package com.costular.atomtasks.tasks.helper.recurrence + +import java.time.LocalDate + +interface RecurrenceStrategy { + fun calculateNextOccurrences( + startDate: LocalDate, + numberOfOccurrences: Int, + drop: Int? = null, + ): List +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceStrategyFactory.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceStrategyFactory.kt new file mode 100644 index 00000000..d206aeaa --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/RecurrenceStrategyFactory.kt @@ -0,0 +1,15 @@ +package com.costular.atomtasks.tasks.helper.recurrence + +import com.costular.atomtasks.tasks.model.RecurrenceType + +object RecurrenceStrategyFactory { + fun recurrenceStrategy(recurrenceType: RecurrenceType): RecurrenceStrategy { + return when (recurrenceType) { + RecurrenceType.DAILY -> DailyRecurrenceStrategy() + RecurrenceType.WEEKDAYS -> WeekdaysRecurrenceStrategy() + RecurrenceType.WEEKLY -> WeeklyRecurrenceStrategy() + RecurrenceType.MONTHLY -> MonthlyRecurrenceStrategy() + RecurrenceType.YEARLY -> YearlyRecurrenceStrategy() + } + } +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/WeekdaysRecurrenceStrategy.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/WeekdaysRecurrenceStrategy.kt new file mode 100644 index 00000000..71c22496 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/WeekdaysRecurrenceStrategy.kt @@ -0,0 +1,28 @@ +package com.costular.atomtasks.tasks.helper.recurrence + +import com.costular.atomtasks.core.util.WeekUtil.isWeekday +import java.time.LocalDate + +class WeekdaysRecurrenceStrategy : RecurrenceStrategy { + override fun calculateNextOccurrences( + startDate: LocalDate, + numberOfOccurrences: Int, + drop: Int?, + ): List { + var daysToReturn = mutableListOf() + var date = startDate + + while (daysToReturn.size < numberOfOccurrences) { + date = date.plusDays(1) + val isWeekday = date.isWeekday() + + if (isWeekday) { + daysToReturn.add(date) + } + } + + return daysToReturn + .drop(drop ?: 0) + .toList() + } +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/WeeklyRecurrenceStrategy.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/WeeklyRecurrenceStrategy.kt new file mode 100644 index 00000000..43ed2922 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/WeeklyRecurrenceStrategy.kt @@ -0,0 +1,15 @@ +package com.costular.atomtasks.tasks.helper.recurrence + +import java.time.LocalDate + +class WeeklyRecurrenceStrategy : RecurrenceStrategy { + override fun calculateNextOccurrences( + startDate: LocalDate, + numberOfOccurrences: Int, + drop: Int?, + ): List { + return (1..numberOfOccurrences) + .drop(drop ?: 0) + .map { startDate.plusWeeks(it.toLong()) } + } +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/YearlyRecurrenceStrategy.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/YearlyRecurrenceStrategy.kt new file mode 100644 index 00000000..f5846ebf --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/helper/recurrence/YearlyRecurrenceStrategy.kt @@ -0,0 +1,15 @@ +package com.costular.atomtasks.tasks.helper.recurrence + +import java.time.LocalDate + +class YearlyRecurrenceStrategy : RecurrenceStrategy { + override fun calculateNextOccurrences( + startDate: LocalDate, + numberOfOccurrences: Int, + drop: Int?, + ): List { + return (1..numberOfOccurrences) + .drop(drop ?: 0) + .map { startDate.plusYears(it.toLong()) } + } +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/CreateTaskInteractor.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/CreateTaskInteractor.kt deleted file mode 100644 index 57373d6a..00000000 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/CreateTaskInteractor.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.costular.atomtasks.tasks.interactor - -import com.costular.atomtasks.data.Interactor -import com.costular.atomtasks.tasks.manager.TaskReminderManager -import com.costular.atomtasks.tasks.repository.TasksRepository -import java.time.LocalDate -import java.time.LocalTime -import javax.inject.Inject - -class CreateTaskInteractor @Inject constructor( - private val tasksRepository: TasksRepository, - private val taskReminderManager: TaskReminderManager, -) : Interactor() { - - data class Params( - val name: String, - val date: LocalDate, - val reminderEnabled: Boolean, - val reminderTime: LocalTime?, - ) - - override suspend fun doWork(params: Params) { - val taskId = tasksRepository.createTask( - params.name, - params.date, - params.reminderEnabled, - params.reminderTime, - ) - - if (params.reminderEnabled && params.reminderTime != null) { - taskReminderManager.set(taskId, params.reminderTime.atDate(params.date)) - } - } -} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/RemoveTaskInteractor.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/RemoveTaskInteractor.kt deleted file mode 100644 index 9359f7c8..00000000 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/RemoveTaskInteractor.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.costular.atomtasks.tasks.interactor - -import com.costular.atomtasks.data.Interactor -import com.costular.atomtasks.tasks.manager.TaskReminderManager -import com.costular.atomtasks.tasks.repository.TasksRepository -import javax.inject.Inject - -class RemoveTaskInteractor @Inject constructor( - private val tasksRepository: TasksRepository, - private val taskReminderManager: TaskReminderManager, -) : Interactor() { - - data class Params(val taskId: Long) - - override suspend fun doWork(params: Params) { - tasksRepository.removeTask(params.taskId) - taskReminderManager.cancel(params.taskId) - } -} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/UpdateTaskIsDoneInteractor.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/UpdateTaskIsDoneInteractor.kt deleted file mode 100644 index 0289d7e5..00000000 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/UpdateTaskIsDoneInteractor.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.costular.atomtasks.tasks.interactor - -import com.costular.atomtasks.data.Interactor -import com.costular.atomtasks.tasks.repository.TasksRepository -import javax.inject.Inject - -class UpdateTaskIsDoneInteractor @Inject constructor( - private val tasksRepository: TasksRepository, -) : Interactor() { - - data class Params( - val taskId: Long, - val isDone: Boolean, - ) - - override suspend fun doWork(params: Params) { - tasksRepository.markTask(params.taskId, params.isDone) - } -} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/UpdateTaskUseCase.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/UpdateTaskUseCase.kt deleted file mode 100644 index 58caeff9..00000000 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/UpdateTaskUseCase.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.costular.atomtasks.tasks.interactor - -import com.costular.atomtasks.tasks.manager.TaskReminderManager -import com.costular.atomtasks.tasks.repository.TasksRepository -import com.costular.core.usecase.UseCase -import java.time.LocalDate -import java.time.LocalTime -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class UpdateTaskUseCase @Inject constructor( - private val tasksRepository: TasksRepository, - private val taskReminderManager: TaskReminderManager, -) : UseCase { - - data class Params( - val taskId: Long, - val name: String, - val date: LocalDate, - val reminderTime: LocalTime?, - ) - - override suspend fun invoke(params: Params) { - with(params) { - tasksRepository.updateTask( - taskId, - date, - name, - ) - - if (reminderTime != null) { - tasksRepository.updateTaskReminder(taskId, reminderTime, date) - taskReminderManager.set(taskId, reminderTime.atDate(date)) - } else { - tasksRepository.removeReminder(taskId) - taskReminderManager.cancel(taskId) - } - } - } -} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/CreateTaskError.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/CreateTaskError.kt new file mode 100644 index 00000000..542b77f6 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/CreateTaskError.kt @@ -0,0 +1,5 @@ +package com.costular.atomtasks.tasks.model + +sealed interface CreateTaskError { + data object UnknownError : CreateTaskError +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/PopulateRecurringTasksError.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/PopulateRecurringTasksError.kt new file mode 100644 index 00000000..29c194a1 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/PopulateRecurringTasksError.kt @@ -0,0 +1,6 @@ +package com.costular.atomtasks.tasks.model + +sealed interface PopulateRecurringTasksError { + data object NotRecurringTask : PopulateRecurringTasksError + data object UnknownError : PopulateRecurringTasksError +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/RecurrenceType.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/RecurrenceType.kt new file mode 100644 index 00000000..8a078e32 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/RecurrenceType.kt @@ -0,0 +1,9 @@ +package com.costular.atomtasks.tasks.model + +enum class RecurrenceType { + DAILY, + WEEKDAYS, + WEEKLY, + MONTHLY, + YEARLY, +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/RemovalStrategy.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/RemovalStrategy.kt new file mode 100644 index 00000000..968aeec1 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/RemovalStrategy.kt @@ -0,0 +1,7 @@ +package com.costular.atomtasks.tasks.model + +enum class RemovalStrategy { + SINGLE, + SINGLE_AND_FUTURE_ONES, + ALL, +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/RemoveTaskError.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/RemoveTaskError.kt new file mode 100644 index 00000000..1f3d87c1 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/RemoveTaskError.kt @@ -0,0 +1,5 @@ +package com.costular.atomtasks.tasks.model + +sealed interface RemoveTaskError { + data object UnknownError : RemoveTaskError +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/Task.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/Task.kt index c8487764..18eea41e 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/Task.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/Task.kt @@ -10,4 +10,8 @@ data class Task( val reminder: Reminder?, val isDone: Boolean, val position: Int, + val isRecurring: Boolean, + val recurrenceType: RecurrenceType?, + val recurrenceEndDate: LocalDate?, + val parentId: Long?, ) diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/TaskCard.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/TaskCard.kt index 60a9c18c..ed0fc18c 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/TaskCard.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/TaskCard.kt @@ -5,23 +5,26 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Alarm +import androidx.compose.material.icons.outlined.Repeat import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme @@ -42,23 +45,30 @@ import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.costular.atomtasks.coreui.utils.ofLocalizedTime +import com.costular.atomtasks.core.ui.utils.ofLocalizedTime +import com.costular.atomtasks.tasks.fake.TaskFinished +import com.costular.atomtasks.tasks.fake.TaskRecurring +import com.costular.atomtasks.tasks.fake.TaskRecurringWithReminder +import com.costular.atomtasks.tasks.fake.TaskToday +import com.costular.atomtasks.tasks.fake.TaskWithReminder +import com.costular.atomtasks.tasks.format.localized import com.costular.designsystem.components.Markable import com.costular.designsystem.decorator.strikeThrough import com.costular.designsystem.theme.AppTheme import com.costular.designsystem.theme.AtomTheme -import java.time.LocalDate -import java.time.LocalTime const val ReminderIconId = "reminder" +const val RecurringIconId = "recurring" -@OptIn(ExperimentalMaterial3Api::class) @Composable fun TaskCard( title: String, isFinished: Boolean, + recurrenceType: RecurrenceType?, reminder: Reminder?, onMark: () -> Unit, onOpen: () -> Unit, @@ -70,8 +80,12 @@ fun TaskCard( HandleHapticForInteractions(mutableInteractionSource) - val mediumColor = MaterialTheme.colorScheme.onSurfaceVariant - val shouldShowReminder = remember(isFinished, reminder) { reminder != null && !isFinished } + val contentColor = MaterialTheme.colorScheme.onSurfaceVariant + val shouldShowExtraDetails = remember( + isFinished, + reminder, + recurrenceType + ) { !isFinished && (reminder != null || recurrenceType != null) } ElevatedCard( onClick = onOpen, @@ -79,7 +93,8 @@ fun TaskCard( interactionSource = mutableInteractionSource, colors = CardDefaults.cardColors(), ) { - val reminderInlineContent = reminderInline(mediumColor) + val reminderInlineContent = reminderInline(contentColor) + val recurringInlineContent = recurringInline(contentColor) Row( modifier = Modifier.padding(AppTheme.dimens.spacingLarge), @@ -87,7 +102,7 @@ fun TaskCard( ) { Markable( isMarked = isFinished, - borderColor = mediumColor, + borderColor = contentColor, onClick = { onMark() }, contentColor = MaterialTheme.colorScheme.primary, onContentColor = MaterialTheme.colorScheme.onPrimary, @@ -98,32 +113,88 @@ fun TaskCard( Column { TaskTitle(isFinished = isFinished, title = title) - if (shouldShowReminder) { + if (shouldShowExtraDetails) { Spacer(Modifier.height(AppTheme.dimens.spacingSmall)) } - AnimatedVisibility(shouldShowReminder) { - if (reminder != null) { - Row { - val alarmText = buildAnnotatedString { - appendInlineContent(ReminderIconId, "[alarm]") - append(" ") - append(reminder.time.ofLocalizedTime()) - } - Text( - text = alarmText, - style = MaterialTheme.typography.bodyMedium, - color = mediumColor, - inlineContent = reminderInlineContent, - ) - } - } + AnimatedVisibility(shouldShowExtraDetails) { + this@Column.TaskDetails( + recurrenceType = recurrenceType, + reminder = reminder, + contentColor = contentColor, + reminderInlineContent = reminderInlineContent, + recurringInlineContent = recurringInlineContent + ) } } } } } +@Composable +private fun ColumnScope.TaskDetails( + recurrenceType: RecurrenceType?, + reminder: Reminder?, + contentColor: Color, + reminderInlineContent: Map, + recurringInlineContent: Map +) { + val recurringContent = if (recurrenceType != null) { + val label = recurrenceType.localized() + RecurringContent.Recurring(label) + } else { + RecurringContent.None + } + + val hasReminder = reminder != null + val hasRecurringInfo = recurringContent is RecurringContent.Recurring + + Row(verticalAlignment = Alignment.CenterVertically) { + if (reminder != null) { + Row { + val alarmText = buildAnnotatedString { + appendInlineContent(ReminderIconId, "[alarm]") + append(" ") + append(reminder.time.ofLocalizedTime()) + } + Text( + text = alarmText, + style = MaterialTheme.typography.bodyMedium, + color = contentColor, + inlineContent = reminderInlineContent, + ) + } + } + + if (hasReminder && hasRecurringInfo) { + Canvas( + modifier = Modifier + .padding(horizontal = AppTheme.dimens.spacingMedium) + .size(3.dp), + onDraw = { + drawCircle(color = contentColor.copy(alpha = 0.66f)) + } + ) + } + + if (hasRecurringInfo) { + val content = recurringContent as RecurringContent.Recurring + + val recurringText = buildAnnotatedString { + appendInlineContent(RecurringIconId, "[recurring]") + append(" ") + append(content.recurrenceLabel) + } + Text( + text = recurringText, + style = MaterialTheme.typography.bodyMedium, + color = contentColor, + inlineContent = recurringInlineContent, + ) + } + } +} + @Composable private fun TaskTitle(isFinished: Boolean, title: String) { var textLayoutResult by remember { mutableStateOf(null) } @@ -163,7 +234,7 @@ private fun HandleHapticForInteractions(interactionSource: MutableInteractionSou } @Composable -private fun reminderInline(mediumColor: Color) = mapOf( +private fun reminderInline(color: Color) = mapOf( Pair( ReminderIconId, InlineTextContent( @@ -176,12 +247,32 @@ private fun reminderInline(mediumColor: Color) = mapOf( Icon( imageVector = Icons.Outlined.Alarm, contentDescription = null, - tint = mediumColor, + tint = color, ) }, ), ) +@Composable +private fun recurringInline(color: Color) = mapOf( + Pair( + RecurringIconId, + InlineTextContent( + Placeholder( + width = 16.sp, + height = 16.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center + ) + ) { + Icon( + imageVector = Icons.Outlined.Repeat, + contentDescription = null, + tint = color, + ) + } + ) +) + @Composable private fun MutableInteractionSource.reorderableDragInteractions(isDragging: Boolean) { val dragState = remember { @@ -200,62 +291,42 @@ private fun MutableInteractionSource.reorderableDragInteractions(isDragging: Boo } } -@Preview(showBackground = true) -@Composable -private fun TaskCardPreview() { - AtomTheme { - TaskCard( - title = "Run every morning!", - isFinished = true, - onMark = {}, - onOpen = {}, - reminder = Reminder( - 1L, - LocalTime.parse("10:00"), - LocalDate.now(), - ), - isBeingDragged = false, - modifier = Modifier.fillMaxWidth(), - ) - } +private sealed interface RecurringContent { + + data object None : RecurringContent + + data class Recurring( + val recurrenceLabel: String, + ) : RecurringContent + } @Preview(showBackground = true) @Composable -private fun TaskCardUnfinishedPreview() { +private fun TaskCardPreview( + @PreviewParameter(TaskPreviewProvider::class) task: Task, +) { AtomTheme { TaskCard( - title = "Run every morning!", - isFinished = false, + title = task.name, + isFinished = task.isDone, + recurrenceType = task.recurrenceType, onMark = {}, onOpen = {}, - reminder = Reminder( - 1L, - LocalTime.parse("10:00"), - LocalDate.now(), - ), + reminder = task.reminder, isBeingDragged = false, modifier = Modifier.fillMaxWidth(), ) } } -@Preview(showBackground = true) -@Composable -private fun TaskCardLongNamePreview() { - AtomTheme { - TaskCard( - title = "A task with a really long name in order to see the strike thorugh", - isFinished = true, - onMark = {}, - onOpen = {}, - reminder = Reminder( - 1L, - LocalTime.parse("10:00"), - LocalDate.now(), - ), - isBeingDragged = false, - modifier = Modifier.fillMaxWidth(), +private class TaskPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + TaskToday, + TaskFinished, + TaskWithReminder, + TaskRecurring, + TaskRecurringWithReminder, ) - } } diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/TaskList.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/TaskList.kt index 8b9e921b..e72a2deb 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/TaskList.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/TaskList.kt @@ -13,10 +13,8 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -25,18 +23,8 @@ import com.costular.atomtasks.core.ui.R import com.costular.atomtasks.core.ui.utils.VariantsPreview import com.costular.designsystem.theme.AppTheme import com.costular.designsystem.theme.AtomTheme -import com.skydoves.balloon.ArrowOrientation -import com.skydoves.balloon.Balloon -import com.skydoves.balloon.BalloonAnimation -import com.skydoves.balloon.BalloonSizeSpec -import com.skydoves.balloon.compose.Balloon -import com.skydoves.balloon.compose.rememberBalloonBuilder -import com.skydoves.balloon.compose.setArrowColor -import com.skydoves.balloon.compose.setBackgroundColor -import com.skydoves.balloon.compose.setTextColor import java.time.LocalDate import java.time.LocalTime -import kotlinx.coroutines.delay import org.burnoutcrew.reorderable.ReorderableItem import org.burnoutcrew.reorderable.ReorderableLazyListState import org.burnoutcrew.reorderable.detectReorderAfterLongPress @@ -50,90 +38,39 @@ fun TaskList( onClick: (Task) -> Unit, onMarkTask: (taskId: Long, isDone: Boolean) -> Unit, state: ReorderableLazyListState, - shouldShowTaskOrderTutorial: Boolean, - onDismissTaskOrderTutorial: () -> Unit, modifier: Modifier = Modifier, padding: PaddingValues = PaddingValues(0.dp), ) { if (tasks.isEmpty()) { Empty(modifier.padding(AppTheme.dimens.contentMargin)) } else { - val backgroundColor = MaterialTheme.colorScheme.inverseSurface - val textColor = MaterialTheme.colorScheme.inverseOnSurface - - val builder = rememberBuilder(backgroundColor, textColor, onDismissTaskOrderTutorial) - - Balloon( - builder = builder, - balloonContent = { - Text( - text = stringResource(R.string.agenda_tutorial_task_order), - style = MaterialTheme.typography.bodySmall, - color = textColor, - ) - }, - ) { balloonWindow -> - LaunchedEffect(shouldShowTaskOrderTutorial) { - if (shouldShowTaskOrderTutorial) { - delay(TooltipDelay) - balloonWindow.showAlignTop(yOff = TooltipYOffset) - } else { - balloonWindow.dismiss() - } - } - - LazyColumn( - modifier = modifier - .reorderable(state) - .detectReorderAfterLongPress(state), - state = state.listState, - contentPadding = padding, - verticalArrangement = Arrangement.spacedBy(TooltipPadding.dp), - ) { - items(tasks, { it.id }) { task -> - ReorderableItem(state, key = task.id) { isDragging -> - TaskCard( - title = task.name, - onMark = { onMarkTask(task.id, !task.isDone) }, - onOpen = { onClick(task) }, - reminder = task.reminder, - isFinished = task.isDone, - modifier = Modifier - .fillMaxWidth() - .animateItemPlacement(), - isBeingDragged = isDragging, - ) - } + LazyColumn( + modifier = modifier + .reorderable(state) + .detectReorderAfterLongPress(state), + state = state.listState, + contentPadding = padding, + ) { + items(tasks, { it.id }) { task -> + ReorderableItem(state, key = task.id) { isDragging -> + TaskCard( + title = task.name, + onMark = { onMarkTask(task.id, !task.isDone) }, + onOpen = { onClick(task) }, + reminder = task.reminder, + isFinished = task.isDone, + recurrenceType = task.recurrenceType, + modifier = Modifier + .fillMaxWidth() + .animateItemPlacement(), + isBeingDragged = isDragging, + ) } } } } } -@Composable -private fun rememberBuilder( - backgroundColor: Color, - textColor: Color, - onDismissTaskOrderTutorial: () -> Unit -): Balloon.Builder { - val builder = rememberBalloonBuilder { - setCornerRadius(TooltipCornerRadius) - setBalloonAnimation(BalloonAnimation.FADE) - setBackgroundColor(backgroundColor) - setArrowColor(backgroundColor) - setTextColor(textColor) - setPadding(TooltipPadding) - setArrowOrientation(ArrowOrientation.BOTTOM) - setHeight(BalloonSizeSpec.WRAP) - setWidth(BalloonSizeSpec.WRAP) - setMarginHorizontal(TooltipHorizontalMargin) - setOnBalloonDismissListener { - onDismissTaskOrderTutorial() - } - } - return builder -} - @Composable fun Empty( modifier: Modifier = Modifier, @@ -192,6 +129,10 @@ private fun TaskListPreview() { ), isDone = false, position = 0, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ), Task( id = 1L, @@ -201,6 +142,10 @@ private fun TaskListPreview() { reminder = null, isDone = false, position = 0, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ), Task( id = 1L, @@ -210,18 +155,14 @@ private fun TaskListPreview() { reminder = null, isDone = true, position = 0, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ), ), onClick = {}, onMarkTask = { _, _ -> }, - shouldShowTaskOrderTutorial = false, - onDismissTaskOrderTutorial = {}, ) } } - -private const val TooltipCornerRadius = 4f -private const val TooltipHorizontalMargin = 24 -private const val TooltipDelay = 2000L -private const val TooltipYOffset = 48 -private const val TooltipPadding = 8 diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/UpdateTaskIsDoneError.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/UpdateTaskIsDoneError.kt new file mode 100644 index 00000000..1b2429a7 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/UpdateTaskIsDoneError.kt @@ -0,0 +1,5 @@ +package com.costular.atomtasks.tasks.model + +sealed interface UpdateTaskIsDoneError { + data object UnknownError : UpdateTaskIsDoneError +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/UpdateTaskUseCaseError.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/UpdateTaskUseCaseError.kt new file mode 100644 index 00000000..ff271c67 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/UpdateTaskUseCaseError.kt @@ -0,0 +1,6 @@ +package com.costular.atomtasks.tasks.model + +sealed interface UpdateTaskUseCaseError { + data object UnableToSave : UpdateTaskUseCaseError + data object UnknownError : UpdateTaskUseCaseError +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/mapper.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/mapper.kt index 47392b37..60a674b6 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/mapper.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/model/mapper.kt @@ -2,7 +2,7 @@ package com.costular.atomtasks.tasks.model import com.costular.atomtasks.data.tasks.ReminderEntity import com.costular.atomtasks.data.tasks.TaskAggregated -import com.costular.atomtasks.data.tasks.TaskEntity +import java.lang.IllegalArgumentException fun TaskAggregated.toDomain(): Task = Task( id = task.id, @@ -12,18 +12,31 @@ fun TaskAggregated.toDomain(): Task = Task( reminder = reminder?.toDomain(), isDone = task.isDone, position = task.position, + isRecurring = task.isRecurring, + recurrenceType = task.recurrenceType?.asRecurrenceType(), + recurrenceEndDate = task.recurrenceEndDate, + parentId = task.parentId, ) +fun String?.asRecurrenceType(): RecurrenceType = when (this) { + "daily" -> RecurrenceType.DAILY + "weekdays" -> RecurrenceType.WEEKDAYS + "weekly" -> RecurrenceType.WEEKLY + "monthly" -> RecurrenceType.MONTHLY + "yearly" -> RecurrenceType.YEARLY + else -> throw IllegalArgumentException("Unexpected recurrence type") +} + +fun RecurrenceType.asString(): String = when (this) { + RecurrenceType.DAILY -> "daily" + RecurrenceType.WEEKDAYS -> "weekdays" + RecurrenceType.WEEKLY -> "weekly" + RecurrenceType.MONTHLY -> "monthly" + RecurrenceType.YEARLY -> "yearly" +} + fun ReminderEntity.toDomain(): Reminder = Reminder( taskId, time, date, ) - -fun Task.toTaskEntity(): TaskEntity = TaskEntity( - id = id, - createdAt = createdAt, - name = name, - day = day, - isDone = isDone, -) diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/repository/DefaultTasksLocalDataSource.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/repository/DefaultTasksLocalDataSource.kt index 08758b05..651467a5 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/repository/DefaultTasksLocalDataSource.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/repository/DefaultTasksLocalDataSource.kt @@ -5,13 +5,19 @@ import com.costular.atomtasks.data.tasks.ReminderEntity import com.costular.atomtasks.data.tasks.TaskAggregated import com.costular.atomtasks.data.tasks.TaskEntity import com.costular.atomtasks.data.tasks.TasksDao +import com.costular.atomtasks.tasks.helper.recurrence.RecurrenceLookAhead.numberOfOccurrencesForType +import com.costular.atomtasks.tasks.helper.recurrence.RecurrenceStrategyFactory +import com.costular.atomtasks.tasks.model.RecurrenceType +import com.costular.atomtasks.tasks.model.RemovalStrategy +import com.costular.atomtasks.tasks.model.asString import java.time.LocalDate import java.time.LocalTime +import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first @Suppress("TooManyFunctions") -internal class DefaultTasksLocalDataSource( +internal class DefaultTasksLocalDataSource @Inject constructor( private val tasksDao: TasksDao, private val reminderDao: ReminderDao, ) : TaskLocalDataSource { @@ -56,6 +62,25 @@ internal class DefaultTasksLocalDataSource( tasksDao.removeTaskById(taskId) } + override suspend fun removeRecurringTask(taskId: Long, removalStrategy: RemovalStrategy) { + when (removalStrategy) { + RemovalStrategy.SINGLE -> { + tasksDao.removeTaskById(taskId) + } + + RemovalStrategy.ALL -> { + val parentId = getTaskById(taskId).first().task.parentId ?: taskId + tasksDao.removeTaskAndAllOccurrences(taskId, parentId) + } + + RemovalStrategy.SINGLE_AND_FUTURE_ONES -> { + getTaskById(taskId).first().task.parentId?.let { + tasksDao.removeTaskAndFutureOcurrences(taskId, it) + } + } + } + } + override suspend fun markTask(taskId: Long, isDone: Boolean) { tasksDao.updateTaskDone(taskId, isDone) } @@ -72,19 +97,93 @@ internal class DefaultTasksLocalDataSource( reminderDao.removeReminder(taskId) } - override suspend fun updateTask(taskId: Long, day: LocalDate, name: String) { + override suspend fun updateTask( + taskId: Long, + day: LocalDate, + name: String, + recurrenceType: RecurrenceType? + ) { val task = tasksDao.getTaskById(taskId).first() val oldDay = task.task.day + val newPosition = if (oldDay != day) { + tasksDao.getMaxPositionForDate(day) + 1 + } else { + task.task.position + } + val wasRecurring = task.task.isRecurring + if (wasRecurring) { + if (task.task.isParent) { + tasksDao.removeChildrenTasks(task.task.id) + } else { + tasksDao.removeFutureOccurrencesForRecurringTask( + id = taskId, + parentId = task.task.parentId ?: taskId + ) + } + } - if (oldDay != day) { - val maxPositionForNewDay = tasksDao.getMaxPositionForDate(day) + 1 - tasksDao.updateTask(taskId, day, name, maxPositionForNewDay) - } else { - tasksDao.updateTask(taskId, day, name) + tasksDao.updateTask( + taskId = taskId, + day = day, + name = name, + position = newPosition, + isRecurring = recurrenceType != null, + recurrence = recurrenceType?.asString(), + ) + + if (wasRecurring) { + populateRecurringTasks(recurrenceType, day, name, task) + } + } + + private suspend fun DefaultTasksLocalDataSource.populateRecurringTasks( + recurrenceType: RecurrenceType?, + day: LocalDate, + name: String, + task: TaskAggregated + ) { + requireNotNull(recurrenceType) + val recurrenceStrategy = RecurrenceStrategyFactory.recurrenceStrategy(recurrenceType) + + val nextDates = recurrenceStrategy.calculateNextOccurrences( + startDate = day, + numberOfOccurrences = numberOfOccurrencesForType(recurrenceType), + ) + + nextDates.forEach { dayToBeCreated -> + val taskId = createTask( + TaskEntity( + id = 0L, + createdAt = LocalDate.now(), + name = name, + day = dayToBeCreated, + isDone = false, + position = tasksDao.getMaxPositionForDate(dayToBeCreated) + 1, + isRecurring = true, + recurrenceType = recurrenceType.asString(), + recurrenceEndDate = null, + parentId = if (task.task.isParent) task.task.id else task.task.parentId + ), + ) + + if (task.reminder != null) { + val time = requireNotNull(task.reminder?.time) + + createReminderForTask( + time = time, + date = dayToBeCreated, + reminderEnabled = true, + taskId = taskId, + ) + } } } + override suspend fun numberFutureOccurrences(parentId: Long, from: LocalDate): Int { + return tasksDao.countFutureOccurrences(parentId, from) + } + override suspend fun moveTask(day: LocalDate, fromPosition: Int, toPosition: Int) { tasksDao.moveTask(day, fromPosition, toPosition) } diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/repository/DefaultTasksRepository.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/repository/DefaultTasksRepository.kt index 6141e0b5..ef683f03 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/repository/DefaultTasksRepository.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/repository/DefaultTasksRepository.kt @@ -1,7 +1,10 @@ package com.costular.atomtasks.tasks.repository import com.costular.atomtasks.data.tasks.TaskEntity +import com.costular.atomtasks.tasks.model.RecurrenceType +import com.costular.atomtasks.tasks.model.RemovalStrategy import com.costular.atomtasks.tasks.model.Task +import com.costular.atomtasks.tasks.model.asString import com.costular.atomtasks.tasks.model.toDomain import java.time.LocalDate import java.time.LocalTime @@ -9,6 +12,7 @@ import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +@Suppress("TooManyFunctions") internal class DefaultTasksRepository @Inject constructor( private val localDataSource: TaskLocalDataSource, ) : TasksRepository { @@ -18,22 +22,27 @@ internal class DefaultTasksRepository @Inject constructor( date: LocalDate, reminderEnabled: Boolean, reminderTime: LocalTime?, + recurrenceType: RecurrenceType?, + parentId: Long?, ): Long { val taskEntity = TaskEntity( - 0, - LocalDate.now(), - name, - date, - false, + id = 0, + createdAt = LocalDate.now(), + name = name, + day = date, + isDone = false, + recurrenceType = recurrenceType?.asString(), + isRecurring = recurrenceType != null, + parentId = parentId, ) val taskId = localDataSource.createTask(taskEntity) if (reminderEnabled) { localDataSource.createReminderForTask( - requireNotNull(reminderTime), - date, - reminderEnabled, - taskId, + time = requireNotNull(reminderTime), + date = date, + reminderEnabled = reminderEnabled, + taskId = taskId, ) } return taskId @@ -55,6 +64,10 @@ internal class DefaultTasksRepository @Inject constructor( localDataSource.removeTask(taskId) } + override suspend fun removeRecurringTask(taskId: Long, removalStrategy: RemovalStrategy) { + localDataSource.removeRecurringTask(taskId, removalStrategy) + } + override suspend fun markTask(taskId: Long, isDone: Boolean) { localDataSource.markTask(taskId, isDone) } @@ -71,8 +84,17 @@ internal class DefaultTasksRepository @Inject constructor( localDataSource.removeReminder(taskId) } - override suspend fun updateTask(taskId: Long, day: LocalDate, name: String) { - localDataSource.updateTask(taskId, day, name) + override suspend fun updateTask( + taskId: Long, + day: LocalDate, + name: String, + recurrenceType: RecurrenceType? + ) { + localDataSource.updateTask(taskId, day, name, recurrenceType) + } + + override suspend fun numberFutureOccurrences(parentId: Long, from: LocalDate): Int { + return localDataSource.numberFutureOccurrences(parentId, from) } override suspend fun moveTask(day: LocalDate, fromPosition: Int, toPosition: Int) { diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/repository/TaskLocalDataSource.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/repository/TaskLocalDataSource.kt index 2c6b6d09..cd1859b6 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/repository/TaskLocalDataSource.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/repository/TaskLocalDataSource.kt @@ -2,13 +2,14 @@ package com.costular.atomtasks.tasks.repository import com.costular.atomtasks.data.tasks.TaskAggregated import com.costular.atomtasks.data.tasks.TaskEntity +import com.costular.atomtasks.tasks.model.RecurrenceType +import com.costular.atomtasks.tasks.model.RemovalStrategy import java.time.LocalDate import java.time.LocalTime import kotlinx.coroutines.flow.Flow @Suppress("TooManyFunctions") interface TaskLocalDataSource { - suspend fun createTask(taskEntity: TaskEntity): Long suspend fun createReminderForTask( time: LocalTime, @@ -16,13 +17,23 @@ interface TaskLocalDataSource { reminderEnabled: Boolean, taskId: Long, ) + fun getTasks(day: LocalDate? = null): Flow> fun getTaskById(id: Long): Flow suspend fun getTasksWithReminder(): List suspend fun removeTask(taskId: Long) + suspend fun removeRecurringTask(taskId: Long, removalStrategy: RemovalStrategy) suspend fun markTask(taskId: Long, isDone: Boolean) suspend fun updateTaskReminder(taskId: Long, time: LocalTime, date: LocalDate) suspend fun removeReminder(taskId: Long) - suspend fun updateTask(taskId: Long, day: LocalDate, name: String) + suspend fun updateTask( + taskId: Long, + day: LocalDate, + name: String, + recurrenceType: RecurrenceType? + ) + + suspend fun numberFutureOccurrences(parentId: Long, from: LocalDate): Int + suspend fun moveTask(day: LocalDate, fromPosition: Int, toPosition: Int) } diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/repository/TasksRepository.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/repository/TasksRepository.kt index 217e83d9..a7a09dc0 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/repository/TasksRepository.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/repository/TasksRepository.kt @@ -1,26 +1,39 @@ package com.costular.atomtasks.tasks.repository +import com.costular.atomtasks.tasks.model.RecurrenceType +import com.costular.atomtasks.tasks.model.RemovalStrategy import com.costular.atomtasks.tasks.model.Task +import kotlinx.coroutines.flow.Flow import java.time.LocalDate import java.time.LocalTime -import kotlinx.coroutines.flow.Flow +@Suppress("TooManyFunctions") interface TasksRepository { - suspend fun createTask( name: String, date: LocalDate, reminderEnabled: Boolean, reminderTime: LocalTime?, + recurrenceType: RecurrenceType?, + parentId: Long?, ): Long fun getTaskById(id: Long): Flow fun getTasks(day: LocalDate? = null): Flow> suspend fun getTasksWithReminder(): List suspend fun removeTask(taskId: Long) + suspend fun removeRecurringTask(taskId: Long, removalStrategy: RemovalStrategy) suspend fun markTask(taskId: Long, isDone: Boolean) suspend fun updateTaskReminder(taskId: Long, reminderTime: LocalTime, reminderDate: LocalDate) suspend fun removeReminder(taskId: Long) - suspend fun updateTask(taskId: Long, day: LocalDate, name: String) + suspend fun updateTask( + taskId: Long, + day: LocalDate, + name: String, + recurrenceType: RecurrenceType? + ) + + suspend fun numberFutureOccurrences(parentId: Long, from: LocalDate): Int + suspend fun moveTask(day: LocalDate, fromPosition: Int, toPosition: Int) } diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/AreExactRemindersAvailable.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/AreExactRemindersAvailable.kt similarity index 64% rename from common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/AreExactRemindersAvailable.kt rename to common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/AreExactRemindersAvailable.kt index cac1ce23..b0b895f9 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/AreExactRemindersAvailable.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/AreExactRemindersAvailable.kt @@ -1,7 +1,7 @@ -package com.costular.atomtasks.tasks.interactor +package com.costular.atomtasks.tasks.usecase -import com.costular.atomtasks.tasks.manager.TaskReminderManager -import com.costular.core.usecase.UseCase +import com.costular.atomtasks.tasks.helper.TaskReminderManager +import com.costular.atomtasks.core.usecase.UseCase import javax.inject.Inject class AreExactRemindersAvailable @Inject constructor( diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/AutoforwardTasksUseCase.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/AutoforwardTasksUseCase.kt similarity index 78% rename from common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/AutoforwardTasksUseCase.kt rename to common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/AutoforwardTasksUseCase.kt index a973d52f..68372db1 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/AutoforwardTasksUseCase.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/AutoforwardTasksUseCase.kt @@ -1,8 +1,8 @@ -package com.costular.atomtasks.tasks.interactor +package com.costular.atomtasks.tasks.usecase +import com.costular.atomtasks.core.usecase.UseCase +import com.costular.atomtasks.core.usecase.invoke import com.costular.atomtasks.data.settings.IsAutoforwardTasksSettingEnabledUseCase -import com.costular.core.usecase.UseCase -import com.costular.core.usecase.invoke import java.time.LocalDate import javax.inject.Inject import kotlinx.coroutines.flow.first @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.firstOrNull class AutoforwardTasksUseCase @Inject constructor( private val isAutoforwardTasksSettingEnabledUseCase: IsAutoforwardTasksSettingEnabledUseCase, private val observeTasksUseCase: ObserveTasksUseCase, - private val updateTaskUseCase: UpdateTaskUseCase, + private val editTaskUseCase: EditTaskUseCase, ) : UseCase { data class Params( @@ -29,12 +29,13 @@ class AutoforwardTasksUseCase @Inject constructor( .firstOrNull() ?.filter { !it.isDone } ?.forEach { task -> - updateTaskUseCase( - UpdateTaskUseCase.Params( + editTaskUseCase( + EditTaskUseCase.Params( taskId = task.id, name = task.name, date = task.day.plusDays(1), reminderTime = task.reminder?.time, + recurrenceType = task.recurrenceType, ), ) } diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/CreateTaskUseCase.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/CreateTaskUseCase.kt new file mode 100644 index 00000000..07140080 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/CreateTaskUseCase.kt @@ -0,0 +1,50 @@ +package com.costular.atomtasks.tasks.usecase + +import com.costular.atomtasks.core.Either +import com.costular.atomtasks.core.logging.atomLog +import com.costular.atomtasks.core.usecase.UseCase +import com.costular.atomtasks.tasks.helper.TaskReminderManager +import com.costular.atomtasks.tasks.model.CreateTaskError +import com.costular.atomtasks.tasks.model.RecurrenceType +import com.costular.atomtasks.tasks.repository.TasksRepository +import java.time.LocalDate +import java.time.LocalTime +import javax.inject.Inject + +class CreateTaskUseCase @Inject constructor( + private val tasksRepository: TasksRepository, + private val taskReminderManager: TaskReminderManager, + private val populateRecurringTasksUseCase: PopulateRecurringTasksUseCase, +) : UseCase> { + + data class Params( + val name: String, + val date: LocalDate, + val reminderEnabled: Boolean, + val reminderTime: LocalTime?, + val recurrenceType: RecurrenceType?, + ) + + override suspend fun invoke(params: Params): Either { + return try { + val taskId = tasksRepository.createTask( + name = params.name, + date = params.date, + reminderEnabled = params.reminderEnabled, + reminderTime = params.reminderTime, + recurrenceType = params.recurrenceType, + parentId = null, + ) + if (params.reminderEnabled && params.reminderTime != null) { + taskReminderManager.set(taskId, params.reminderTime.atDate(params.date)) + } + + populateRecurringTasksUseCase(PopulateRecurringTasksUseCase.Params(taskId)) // TODO handle error handling + + Either.Result(Unit) + } catch (e: Exception) { + atomLog { e } + Either.Error(CreateTaskError.UnknownError) + } + } +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/EditTaskUseCase.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/EditTaskUseCase.kt new file mode 100644 index 00000000..55b7a7d4 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/EditTaskUseCase.kt @@ -0,0 +1,58 @@ +package com.costular.atomtasks.tasks.usecase + +import com.costular.atomtasks.core.Either +import com.costular.atomtasks.core.logging.atomLog +import com.costular.atomtasks.core.usecase.UseCase +import com.costular.atomtasks.tasks.helper.TaskReminderManager +import com.costular.atomtasks.tasks.model.RecurrenceType +import com.costular.atomtasks.tasks.model.UpdateTaskUseCaseError +import com.costular.atomtasks.tasks.repository.TasksRepository +import java.sql.SQLException +import java.time.LocalDate +import java.time.LocalTime +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class EditTaskUseCase @Inject constructor( + private val tasksRepository: TasksRepository, + private val taskReminderManager: TaskReminderManager, +) : UseCase> { + + data class Params( + val taskId: Long, + val name: String, + val date: LocalDate, + val reminderTime: LocalTime?, + val recurrenceType: RecurrenceType?, + ) + + override suspend fun invoke(params: Params): Either { + return try { + with(params) { + tasksRepository.updateTask( + taskId, + date, + name, + recurrenceType, + ) + + if (reminderTime != null) { + tasksRepository.updateTaskReminder(taskId, reminderTime, date) + taskReminderManager.set(taskId, reminderTime.atDate(date)) + } else { + tasksRepository.removeReminder(taskId) + taskReminderManager.cancel(taskId) + } + + Either.Result(Unit) + } + } catch (sql: SQLException) { + atomLog { sql } + Either.Error(UpdateTaskUseCaseError.UnableToSave) + } catch (e: Exception) { + atomLog { e } + Either.Error(UpdateTaskUseCaseError.UnknownError) + } + } +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/GetTaskByIdInteractor.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/GetTaskByIdUseCase.kt similarity index 53% rename from common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/GetTaskByIdInteractor.kt rename to common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/GetTaskByIdUseCase.kt index 6add3123..a0c394b7 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/GetTaskByIdInteractor.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/GetTaskByIdUseCase.kt @@ -1,17 +1,17 @@ -package com.costular.atomtasks.tasks.interactor +package com.costular.atomtasks.tasks.usecase -import com.costular.atomtasks.data.SubjectInteractor import com.costular.atomtasks.tasks.model.Task import com.costular.atomtasks.tasks.repository.TasksRepository +import com.costular.atomtasks.core.usecase.UseCase import javax.inject.Inject import kotlinx.coroutines.flow.Flow -class GetTaskByIdInteractor @Inject constructor( +class GetTaskByIdUseCase @Inject constructor( private val tasksRepository: TasksRepository, -) : SubjectInteractor() { +) : UseCase> { data class Params(val id: Long) - override fun createObservable(params: Params): Flow = + override suspend fun invoke(params: Params): Flow = tasksRepository.getTaskById(params.id) } diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/GetTasksWithReminderInteractor.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/GetTasksWithReminderInteractor.kt similarity index 90% rename from common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/GetTasksWithReminderInteractor.kt rename to common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/GetTasksWithReminderInteractor.kt index fa089940..d0e09db4 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/GetTasksWithReminderInteractor.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/GetTasksWithReminderInteractor.kt @@ -1,4 +1,4 @@ -package com.costular.atomtasks.tasks.interactor +package com.costular.atomtasks.tasks.usecase import com.costular.atomtasks.data.ResultInteractor import com.costular.atomtasks.tasks.model.Task diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/MoveTaskUseCase.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/MoveTaskUseCase.kt similarity index 84% rename from common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/MoveTaskUseCase.kt rename to common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/MoveTaskUseCase.kt index 17d4d300..85a57536 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/MoveTaskUseCase.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/MoveTaskUseCase.kt @@ -1,7 +1,7 @@ -package com.costular.atomtasks.tasks.interactor +package com.costular.atomtasks.tasks.usecase import com.costular.atomtasks.tasks.repository.TasksRepository -import com.costular.core.usecase.UseCase +import com.costular.atomtasks.core.usecase.UseCase import java.time.LocalDate import javax.inject.Inject diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/ObserveTasksUseCase.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/ObserveTasksUseCase.kt similarity index 83% rename from common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/ObserveTasksUseCase.kt rename to common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/ObserveTasksUseCase.kt index 79983ee1..93313234 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/ObserveTasksUseCase.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/ObserveTasksUseCase.kt @@ -1,8 +1,8 @@ -package com.costular.atomtasks.tasks.interactor +package com.costular.atomtasks.tasks.usecase import com.costular.atomtasks.tasks.model.Task import com.costular.atomtasks.tasks.repository.TasksRepository -import com.costular.core.usecase.ObservableUseCase +import com.costular.atomtasks.core.usecase.ObservableUseCase import java.time.LocalDate import javax.inject.Inject import kotlinx.coroutines.flow.Flow diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/PopulateRecurringTasksUseCase.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/PopulateRecurringTasksUseCase.kt new file mode 100644 index 00000000..d61b35d2 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/PopulateRecurringTasksUseCase.kt @@ -0,0 +1,55 @@ +package com.costular.atomtasks.tasks.usecase + +import com.costular.atomtasks.core.Either +import com.costular.atomtasks.core.logging.atomLog +import com.costular.atomtasks.core.usecase.UseCase +import com.costular.atomtasks.tasks.helper.recurrence.RecurrenceLookAhead.numberOfOccurrencesForType +import com.costular.atomtasks.tasks.helper.recurrence.RecurrenceStrategyFactory +import com.costular.atomtasks.tasks.model.PopulateRecurringTasksError +import com.costular.atomtasks.tasks.repository.TasksRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.first + +class PopulateRecurringTasksUseCase @Inject constructor( + private val tasksRepository: TasksRepository, +) : UseCase> { + + data class Params( + val taskId: Long, + val drop: Int? = null, + ) + + override suspend fun invoke(params: Params): Either { + return try { + val task = tasksRepository.getTaskById(params.taskId).first() + + if (!task.isRecurring || task.recurrenceType == null) { + return Either.Error(PopulateRecurringTasksError.NotRecurringTask) + } + + val recurrenceStrategy = + RecurrenceStrategyFactory.recurrenceStrategy(task.recurrenceType) + + val nextDates = recurrenceStrategy.calculateNextOccurrences( + startDate = task.day, + numberOfOccurrences = numberOfOccurrencesForType(task.recurrenceType), + drop = params.drop, + ) + + nextDates.forEach { dayToBeCreated -> + tasksRepository.createTask( + name = task.name, + date = dayToBeCreated, + reminderEnabled = task.reminder != null, + reminderTime = task.reminder?.time, + recurrenceType = task.recurrenceType, + parentId = task.parentId ?: task.id, + ) + } + Either.Result(Unit) + } catch (e: Exception) { + atomLog { e } + Either.Error(PopulateRecurringTasksError.UnknownError) + } + } +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/PostponeTaskUseCase.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/PostponeTaskUseCase.kt similarity index 73% rename from common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/PostponeTaskUseCase.kt rename to common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/PostponeTaskUseCase.kt index 4a53b6d8..65d6ab4c 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/PostponeTaskUseCase.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/PostponeTaskUseCase.kt @@ -1,21 +1,21 @@ -package com.costular.atomtasks.tasks.interactor +package com.costular.atomtasks.tasks.usecase import com.costular.atomtasks.core.logging.atomLog -import com.costular.atomtasks.tasks.manager.TaskReminderManager -import com.costular.core.Either -import com.costular.core.toError -import com.costular.core.toResult -import com.costular.core.usecase.UseCase +import com.costular.atomtasks.tasks.helper.TaskReminderManager +import com.costular.atomtasks.core.Either +import com.costular.atomtasks.core.toError +import com.costular.atomtasks.core.toResult +import com.costular.atomtasks.core.usecase.UseCase import java.time.LocalDate import java.time.LocalTime import javax.inject.Inject import kotlinx.coroutines.flow.first class PostponeTaskUseCase @Inject constructor( - private val getTaskByIdInteractor: GetTaskByIdInteractor, + private val getTaskByIdUseCase: GetTaskByIdUseCase, private val updateTaskReminderInteractor: UpdateTaskReminderInteractor, private val taskReminderManager: TaskReminderManager, - private val updateTaskUseCase: UpdateTaskUseCase, + private val editTaskUseCase: EditTaskUseCase, ) : UseCase> { data class Params( @@ -27,19 +27,19 @@ class PostponeTaskUseCase @Inject constructor( @Suppress("SwallowedException", "TooGenericExceptionCaught") override suspend fun invoke(params: Params): Either { return try { - getTaskByIdInteractor(GetTaskByIdInteractor.Params(params.taskId)) - val task = getTaskByIdInteractor.flow.first() + val task = getTaskByIdUseCase(GetTaskByIdUseCase.Params(params.taskId)).first() if (task.reminder == null) { return PostponeTaskFailure.MissingReminder.toError() } - updateTaskUseCase.invoke( - UpdateTaskUseCase.Params( + editTaskUseCase.invoke( + EditTaskUseCase.Params( taskId = task.id, name = task.name, date = params.day, reminderTime = params.time, + recurrenceType = task.recurrenceType, ) ) updateTaskReminderInteractor( diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/RemoveTaskUseCase.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/RemoveTaskUseCase.kt new file mode 100644 index 00000000..4bc6feb5 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/RemoveTaskUseCase.kt @@ -0,0 +1,36 @@ +package com.costular.atomtasks.tasks.usecase + +import com.costular.atomtasks.core.Either +import com.costular.atomtasks.core.logging.atomLog +import com.costular.atomtasks.core.usecase.UseCase +import com.costular.atomtasks.tasks.helper.TaskReminderManager +import com.costular.atomtasks.tasks.model.RemovalStrategy +import com.costular.atomtasks.tasks.model.RemoveTaskError +import com.costular.atomtasks.tasks.repository.TasksRepository +import javax.inject.Inject + +class RemoveTaskUseCase @Inject constructor( + private val tasksRepository: TasksRepository, + private val taskReminderManager: TaskReminderManager, +) : UseCase> { + + data class Params( + val taskId: Long, + val strategy: RemovalStrategy? = null, + ) + + override suspend fun invoke(params: Params): Either { + return try { + if (params.strategy == null) { + tasksRepository.removeTask(params.taskId) + taskReminderManager.cancel(params.taskId) + } else { + tasksRepository.removeRecurringTask(params.taskId, params.strategy) + } + Either.Result(Unit) + } catch (e: Exception) { + atomLog { e } + Either.Error(RemoveTaskError.UnknownError) + } + } +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/UpdateTaskIsDoneUseCase.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/UpdateTaskIsDoneUseCase.kt new file mode 100644 index 00000000..ec600278 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/UpdateTaskIsDoneUseCase.kt @@ -0,0 +1,28 @@ +package com.costular.atomtasks.tasks.usecase + +import com.costular.atomtasks.core.Either +import com.costular.atomtasks.core.logging.atomLog +import com.costular.atomtasks.core.usecase.UseCase +import com.costular.atomtasks.tasks.model.UpdateTaskIsDoneError +import com.costular.atomtasks.tasks.repository.TasksRepository +import javax.inject.Inject + +class UpdateTaskIsDoneUseCase @Inject constructor( + private val tasksRepository: TasksRepository, +) : UseCase> { + + data class Params( + val taskId: Long, + val isDone: Boolean, + ) + + override suspend fun invoke(params: Params): Either { + return try { + tasksRepository.markTask(params.taskId, params.isDone) + Either.Result(Unit) + } catch (e: Exception) { + atomLog { e } + Either.Error(UpdateTaskIsDoneError.UnknownError) + } + } +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/UpdateTaskReminderInteractor.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/UpdateTaskReminderInteractor.kt similarity index 85% rename from common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/UpdateTaskReminderInteractor.kt rename to common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/UpdateTaskReminderInteractor.kt index de41babb..78fe4169 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/interactor/UpdateTaskReminderInteractor.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/usecase/UpdateTaskReminderInteractor.kt @@ -1,7 +1,7 @@ -package com.costular.atomtasks.tasks.interactor +package com.costular.atomtasks.tasks.usecase import com.costular.atomtasks.tasks.repository.TasksRepository -import com.costular.core.usecase.UseCase +import com.costular.atomtasks.core.usecase.UseCase import java.time.LocalDate import java.time.LocalTime import javax.inject.Inject diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/AutoforwardTasksWorker.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/AutoforwardTasksWorker.kt index 9f7dbfd1..a268ecaf 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/AutoforwardTasksWorker.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/AutoforwardTasksWorker.kt @@ -7,7 +7,7 @@ import androidx.work.PeriodicWorkRequest import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkerParameters import com.costular.atomtasks.core.logging.atomLog -import com.costular.atomtasks.tasks.interactor.AutoforwardTasksUseCase +import com.costular.atomtasks.tasks.usecase.AutoforwardTasksUseCase import dagger.assisted.Assisted import dagger.assisted.AssistedInject import java.time.Duration diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/MarkTaskAsDoneWorker.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/MarkTaskAsDoneWorker.kt index fffefb95..3778a95e 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/MarkTaskAsDoneWorker.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/MarkTaskAsDoneWorker.kt @@ -6,7 +6,7 @@ import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.costular.atomtasks.core.logging.atomLog import com.costular.atomtasks.notifications.TaskNotificationManager -import com.costular.atomtasks.tasks.interactor.UpdateTaskIsDoneInteractor +import com.costular.atomtasks.tasks.usecase.UpdateTaskIsDoneUseCase import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -15,7 +15,7 @@ import dagger.assisted.AssistedInject class MarkTaskAsDoneWorker @AssistedInject constructor( @Assisted appContext: Context, @Assisted workerParams: WorkerParameters, - private val updateTaskIsDoneInteractor: UpdateTaskIsDoneInteractor, + private val updateTaskIsDoneUseCase: UpdateTaskIsDoneUseCase, private val taskNotificationManager: TaskNotificationManager, ) : CoroutineWorker(appContext, workerParams) { @@ -28,8 +28,8 @@ class MarkTaskAsDoneWorker @AssistedInject constructor( } taskNotificationManager.removeTaskNotification(taskId) - updateTaskIsDoneInteractor.executeSync( - UpdateTaskIsDoneInteractor.Params( + updateTaskIsDoneUseCase.invoke( + UpdateTaskIsDoneUseCase.Params( taskId, true, ), diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/NotifyTaskWorker.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/NotifyTaskWorker.kt index 72b63941..499368ba 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/NotifyTaskWorker.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/NotifyTaskWorker.kt @@ -6,7 +6,7 @@ import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.costular.atomtasks.core.logging.atomLog import com.costular.atomtasks.notifications.TaskNotificationManager -import com.costular.atomtasks.tasks.interactor.GetTaskByIdInteractor +import com.costular.atomtasks.tasks.usecase.GetTaskByIdUseCase import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.first @@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.first class NotifyTaskWorker @AssistedInject constructor( @Assisted appContext: Context, @Assisted workerParams: WorkerParameters, - private val getTaskByIdInteractor: GetTaskByIdInteractor, + private val getTaskByIdUseCase: GetTaskByIdUseCase, private val taskNotificationManager: TaskNotificationManager, ) : CoroutineWorker(appContext, workerParams) { @@ -28,8 +28,7 @@ class NotifyTaskWorker @AssistedInject constructor( throw IllegalArgumentException("Task id has not been passed") } - getTaskByIdInteractor(GetTaskByIdInteractor.Params(taskId)) - val task = getTaskByIdInteractor.flow.first() + val task = getTaskByIdUseCase(GetTaskByIdUseCase.Params(taskId)).first() if (task.reminder == null) { throw IllegalStateException("Reminder is null") diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/PopulateTasksWorker.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/PopulateTasksWorker.kt new file mode 100644 index 00000000..1b3d0478 --- /dev/null +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/PopulateTasksWorker.kt @@ -0,0 +1,26 @@ +package com.costular.atomtasks.tasks.worker + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.costular.atomtasks.core.logging.atomLog +import com.costular.atomtasks.tasks.helper.recurrence.RecurrenceManager +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import java.time.LocalDate + +class PopulateTasksWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted workerParams: WorkerParameters, + private val recurrenceManager: RecurrenceManager, +) : CoroutineWorker(appContext, workerParams) { + override suspend fun doWork(): Result { + return try { + recurrenceManager.createAheadTasks(LocalDate.now()) + Result.success() + } catch (e: Exception) { + atomLog { e } + Result.failure() + } + } +} diff --git a/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/SetTasksRemindersWorker.kt b/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/SetTasksRemindersWorker.kt index 002a077e..a1426278 100644 --- a/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/SetTasksRemindersWorker.kt +++ b/common/tasks/src/main/java/com/costular/atomtasks/tasks/worker/SetTasksRemindersWorker.kt @@ -6,8 +6,8 @@ import androidx.work.CoroutineWorker import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkerParameters import com.costular.atomtasks.core.logging.atomLog -import com.costular.atomtasks.tasks.interactor.GetTasksWithReminderInteractor -import com.costular.atomtasks.tasks.manager.TaskReminderManager +import com.costular.atomtasks.tasks.usecase.GetTasksWithReminderInteractor +import com.costular.atomtasks.tasks.helper.TaskReminderManager import dagger.assisted.Assisted import dagger.assisted.AssistedInject diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/createtask/CreateTaskExpandedViewModelTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/createtask/CreateTaskExpandedViewModelTest.kt index 51fc60e8..080ab1d6 100644 --- a/common/tasks/src/test/java/com/costular/atomtasks/tasks/createtask/CreateTaskExpandedViewModelTest.kt +++ b/common/tasks/src/test/java/com/costular/atomtasks/tasks/createtask/CreateTaskExpandedViewModelTest.kt @@ -2,10 +2,12 @@ package com.costular.atomtasks.tasks.createtask import app.cash.turbine.test import com.costular.atomtasks.core.testing.MviViewModelTest -import com.costular.atomtasks.tasks.interactor.AreExactRemindersAvailable +import com.costular.atomtasks.tasks.usecase.AreExactRemindersAvailable +import com.costular.atomtasks.tasks.model.RecurrenceType import com.google.common.truth.Truth.assertThat import io.mockk.mockk import java.time.LocalDate +import java.time.LocalDateTime import java.time.LocalTime import kotlin.time.ExperimentalTime import kotlinx.coroutines.test.runTest @@ -75,12 +77,13 @@ class CreateTaskExpandedViewModelTest : MviViewModelTest() { val name = "name" val date = LocalDate.of(2021, 12, 24) val reminder = LocalTime.of(9, 0) - val expected = - com.costular.atomtasks.tasks.createtask.CreateTaskResult(name, date, reminder) + val recurrence = RecurrenceType.WEEKLY + val expected = CreateTaskResult(name, date, reminder, recurrence) sut.setName(name) sut.setDate(date) sut.setReminder(reminder) + sut.setRecurrence(RecurrenceType.WEEKLY) sut.requestSave() sut.uiEvents.test { @@ -89,40 +92,22 @@ class CreateTaskExpandedViewModelTest : MviViewModelTest() { } } - @Test - fun `should reset state when save succeed`() = runTest { - val name = "name" - val date = LocalDate.of(2021, 12, 24) - val reminder = LocalTime.of(9, 0) - - sut.setName(name) - sut.setDate(date) - sut.setReminder(reminder) - sut.requestSave() - - sut.state.test { - assertThat(expectMostRecentItem()).isEqualTo(CreateTaskExpandedState.Empty) - } - } - @Test fun `should show error when reminder is in the past`() = runTest { - val date = LocalDate.now() - val reminder = LocalTime.now().minusHours(2) + val reminder = LocalDateTime.now().minusHours(2) - sut.setDate(date) - sut.setReminder(reminder) + sut.setDate(reminder.toLocalDate()) + sut.setReminder(reminder.toLocalTime()) assertThat(sut.state.value.isReminderError).isTrue() } @Test fun `should show no error when reminder is in the future`() = runTest { - val date = LocalDate.now().plusDays(1) - val reminder = LocalTime.now().minusHours(2) + val reminder = LocalDateTime.now().plusHours(26) - sut.setDate(date) - sut.setReminder(reminder) + sut.setDate(reminder.toLocalDate()) + sut.setReminder(reminder.toLocalTime()) assertThat(sut.state.value.isReminderError).isFalse() } diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/helper/recurrence/DailyRecurrenceStrategyTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/helper/recurrence/DailyRecurrenceStrategyTest.kt new file mode 100644 index 00000000..7513b043 --- /dev/null +++ b/common/tasks/src/test/java/com/costular/atomtasks/tasks/helper/recurrence/DailyRecurrenceStrategyTest.kt @@ -0,0 +1,40 @@ +package com.costular.atomtasks.tasks.helper.recurrence + +import com.google.common.truth.Truth.assertThat +import java.time.LocalDate +import org.junit.Test + +class DailyRecurrenceStrategyTest { + + val sut: DailyRecurrenceStrategy = DailyRecurrenceStrategy() + + @Test + fun `Should return next 7 days no matter if it's weekday or weekend`() { + val firstDay = LocalDate.of(2023, 12, 16) + val expected = listOf( + LocalDate.of(2023, 12, 17), + LocalDate.of(2023, 12, 18), + LocalDate.of(2023, 12, 19), + LocalDate.of(2023, 12, 20), + LocalDate.of(2023, 12, 21), + LocalDate.of(2023, 12, 22), + LocalDate.of(2023, 12, 23), + ) + + val result = sut.calculateNextOccurrences(firstDay, 7) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `Should return next day`() { + val firstDay = LocalDate.of(2023, 12, 15) + val expected = listOf( + LocalDate.of(2023, 12, 16), + ) + + val result = sut.calculateNextOccurrences(firstDay, 1) + + assertThat(result).isEqualTo(expected) + } +} diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/helper/recurrence/MonthlyRecurrenceStrategyTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/helper/recurrence/MonthlyRecurrenceStrategyTest.kt new file mode 100644 index 00000000..62776c8e --- /dev/null +++ b/common/tasks/src/test/java/com/costular/atomtasks/tasks/helper/recurrence/MonthlyRecurrenceStrategyTest.kt @@ -0,0 +1,36 @@ +package com.costular.atomtasks.tasks.helper.recurrence + +import com.google.common.truth.Truth +import java.time.LocalDate +import org.junit.Test + +class MonthlyRecurrenceStrategyTest { + + private val sut = MonthlyRecurrenceStrategy() + + @Test + fun `Should return next month`() { + val today = LocalDate.of(2023, 12, 15) + val expected = listOf( + LocalDate.of(2024, 1, 15) + ) + + val result = sut.calculateNextOccurrences(today, 1) + + Truth.assertThat(result).isEqualTo(expected) + } + + @Test + fun `Should return next months given 3 next occurrences`() { + val today = LocalDate.of(2023, 12, 15) + val expected = listOf( + LocalDate.of(2024, 1, 15), + LocalDate.of(2024, 2, 15), + LocalDate.of(2024, 3, 15), + ) + + val result = sut.calculateNextOccurrences(today, 3) + + Truth.assertThat(result).isEqualTo(expected) + } +} diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/helper/recurrence/WeekdaysRecurrenceStrategyTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/helper/recurrence/WeekdaysRecurrenceStrategyTest.kt new file mode 100644 index 00000000..4033d2b2 --- /dev/null +++ b/common/tasks/src/test/java/com/costular/atomtasks/tasks/helper/recurrence/WeekdaysRecurrenceStrategyTest.kt @@ -0,0 +1,38 @@ +package com.costular.atomtasks.tasks.helper.recurrence + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import java.time.LocalDate + +class WeekdaysRecurrenceStrategyTest { + + val sut: WeekdaysRecurrenceStrategy = WeekdaysRecurrenceStrategy() + + @Test + fun `Should return next weekday Monday 18 Dec given that start day is Fri 15 Dec`() { + val today = LocalDate.of(2023, 12, 15) + val expected = listOf( + LocalDate.of(2023, 12, 18) + ) + + val result = sut.calculateNextOccurrences(today, 1) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `Should return next 5 weekdays given that start day is Wed 13 Dec`() { + val today = LocalDate.of(2023, 12, 13) + val expected = listOf( + LocalDate.of(2023, 12, 14), + LocalDate.of(2023, 12, 15), + LocalDate.of(2023, 12, 18), + LocalDate.of(2023, 12, 19), + LocalDate.of(2023, 12, 20), + ) + + val result = sut.calculateNextOccurrences(today, 5) + + assertThat(result).isEqualTo(expected) + } +} diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/helper/recurrence/WeeklyRecurrenceStrategyTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/helper/recurrence/WeeklyRecurrenceStrategyTest.kt new file mode 100644 index 00000000..e0b97772 --- /dev/null +++ b/common/tasks/src/test/java/com/costular/atomtasks/tasks/helper/recurrence/WeeklyRecurrenceStrategyTest.kt @@ -0,0 +1,36 @@ +package com.costular.atomtasks.tasks.helper.recurrence + +import com.google.common.truth.Truth.assertThat +import java.time.LocalDate +import org.junit.Test + +class WeeklyRecurrenceStrategyTest { + + val sut: WeeklyRecurrenceStrategy = WeeklyRecurrenceStrategy() + + @Test + fun `Should return next week`() { + val today = LocalDate.of(2023, 12, 15) + val expected = listOf( + LocalDate.of(2023, 12, 22) + ) + + val result = sut.calculateNextOccurrences(today, 1) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `Should return next weeks given 3 next occurrences`() { + val today = LocalDate.of(2023, 12, 15) + val expected = listOf( + LocalDate.of(2023, 12, 22), + LocalDate.of(2023, 12, 29), + LocalDate.of(2024, 1, 5), + ) + + val result = sut.calculateNextOccurrences(today, 3) + + assertThat(result).isEqualTo(expected) + } +} diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/helper/recurrence/YearlyRecurrenceStrategyTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/helper/recurrence/YearlyRecurrenceStrategyTest.kt new file mode 100644 index 00000000..c60be0e2 --- /dev/null +++ b/common/tasks/src/test/java/com/costular/atomtasks/tasks/helper/recurrence/YearlyRecurrenceStrategyTest.kt @@ -0,0 +1,36 @@ +package com.costular.atomtasks.tasks.helper.recurrence + +import com.google.common.truth.Truth +import java.time.LocalDate +import org.junit.Test + +class YearlyRecurrenceStrategyTest { + + private val sut = YearlyRecurrenceStrategy() + + @Test + fun `Should return next year`() { + val today = LocalDate.of(2023, 12, 15) + val expected = listOf( + LocalDate.of(2024, 12, 15) + ) + + val result = sut.calculateNextOccurrences(today, 1) + + Truth.assertThat(result).isEqualTo(expected) + } + + @Test + fun `Should return next years given 3 next occurrences`() { + val today = LocalDate.of(2023, 1, 15) + val expected = listOf( + LocalDate.of(2024, 1, 15), + LocalDate.of(2025, 1, 15), + LocalDate.of(2026, 1, 15), + ) + + val result = sut.calculateNextOccurrences(today, 3) + + Truth.assertThat(result).isEqualTo(expected) + } +} diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/CreateTaskInteractorTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/CreateTaskInteractorTest.kt deleted file mode 100644 index 1ab27c80..00000000 --- a/common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/CreateTaskInteractorTest.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.costular.atomtasks.tasks.interactor - -import com.costular.atomtasks.tasks.repository.TasksRepository -import com.costular.atomtasks.tasks.manager.TaskReminderManager -import io.mockk.coEvery -import io.mockk.mockk -import io.mockk.verify -import java.time.LocalDate -import java.time.LocalTime -import kotlin.time.ExperimentalTime -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runBlockingTest -import org.junit.Before -import org.junit.Test - -@ExperimentalTime -@ExperimentalCoroutinesApi -class CreateTaskInteractorTest { - - private lateinit var createTaskInteractor: CreateTaskInteractor - - private val tasksRepository: TasksRepository = mockk(relaxed = true) - private val taskReminderManager: TaskReminderManager = mockk(relaxed = true) - - @Before - fun setUp() { - createTaskInteractor = CreateTaskInteractor( - tasksRepository = tasksRepository, - taskReminderManager = taskReminderManager, - ) - } - - @Test - fun `should call repository with given input when create task`() = runBlockingTest { - val name = "Call my mom" - val date = LocalDate.of(2021, 1, 7) - val reminder = LocalTime.of(9, 0) - - createTaskInteractor.executeSync( - CreateTaskInteractor.Params( - name = name, - date = date, - reminderEnabled = true, - reminderTime = reminder, - ), - ) - - coEvery { tasksRepository.createTask(name, date, true, reminder) } - } - - @Test - fun `should set reminder when create task given reminder's been passed correctly`() = - runBlockingTest { - val name = "Call my mom" - val date = LocalDate.of(2021, 1, 7) - val reminder = LocalTime.of(9, 0) - val taskId = 100L - - coEvery { - tasksRepository.createTask(name, date, true, reminder) - } returns taskId - - createTaskInteractor.executeSync( - CreateTaskInteractor.Params( - name = name, - date = date, - reminderEnabled = true, - reminderTime = reminder, - ), - ) - - verify { taskReminderManager.set(taskId, reminder.atDate(date)) } - } -} diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/repository/DefaultTasksLocalDataSourceTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/repository/DefaultTasksLocalDataSourceTest.kt new file mode 100644 index 00000000..7f064ec1 --- /dev/null +++ b/common/tasks/src/test/java/com/costular/atomtasks/tasks/repository/DefaultTasksLocalDataSourceTest.kt @@ -0,0 +1,192 @@ +package com.costular.atomtasks.tasks.repository + +import com.costular.atomtasks.data.tasks.ReminderDao +import com.costular.atomtasks.data.tasks.TaskAggregated +import com.costular.atomtasks.data.tasks.TaskEntity +import com.costular.atomtasks.data.tasks.TasksDao +import com.costular.atomtasks.tasks.model.RecurrenceType +import com.google.common.truth.Truth.assertThat +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import java.time.LocalDate +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class DefaultTasksLocalDataSourceTest { + + lateinit var sut: TaskLocalDataSource + + private val tasksDao: TasksDao = mockk(relaxUnitFun = true) + private val reminderDao: ReminderDao = mockk(relaxUnitFun = true) + + @Before + fun setUp() { + sut = DefaultTasksLocalDataSource( + tasksDao = tasksDao, + reminderDao = reminderDao, + ) + } + + @Test + fun `Should generate new position when update task given the day is different`() = runTest { + val id = 1121L + val lastPosition = 1 + val newDay = LocalDate.now().plusDays(1) + + val taskAggregated = TaskAggregated( + task = TaskEntity( + id = id, + name = "Pierre Jimenez", + createdAt = LocalDate.now(), + day = LocalDate.now(), + isDone = false, + position = 1, + isRecurring = false, + recurrenceType = null, + recurrenceEndDate = null, + parentId = null, + ), + reminder = null, + ) + coEvery { tasksDao.getTaskById(id) } returns flowOf(taskAggregated) + coEvery { tasksDao.getMaxPositionForDate(newDay) } returns lastPosition + + sut.updateTask( + id, + day = newDay, + name = "Whatever", + recurrenceType = null, + ) + + coVerify { + tasksDao.updateTask( + taskId = id, + day = newDay, + name = "Whatever", + position = lastPosition + 1, + isRecurring = false, + recurrence = null, + ) + } + } + + @Test + fun `Should call task dao accordingly when update task`() = runTest { + val id = 1121L + + val taskAggregated = TaskAggregated( + task = TaskEntity( + id = id, + name = "Pierre Jimenez", + createdAt = LocalDate.now(), + day = LocalDate.now(), + isDone = false, + position = 1, + isRecurring = false, + recurrenceType = null, + recurrenceEndDate = null, + parentId = null, + ), + reminder = null, + ) + coEvery { tasksDao.getTaskById(id) } returns flowOf(taskAggregated) + + sut.updateTask( + id, + day = taskAggregated.task.day, + name = "Whatever", + recurrenceType = RecurrenceType.DAILY, + ) + + coVerify { + tasksDao.updateTask( + taskId = id, + day = taskAggregated.task.day, + name = "Whatever", + position = taskAggregated.task.position, + isRecurring = true, + recurrence = "daily", + ) + } + } + + @Test + fun `Should remove future occurrences for recurring task when update task recurring task`() = + runTest { + val id = 1121L + + val taskAggregated = TaskAggregated( + task = TaskEntity( + id = id, + name = "Pierre Jimenez", + createdAt = LocalDate.now(), + day = LocalDate.now(), + isDone = false, + position = 1, + isRecurring = true, + recurrenceType = "daily", + recurrenceEndDate = null, + parentId = null, + ), + reminder = null, + ) + coEvery { tasksDao.getTaskById(id) } returns flowOf(taskAggregated) + coEvery { tasksDao.getMaxPositionForDate(any()) } returns 1 + coEvery { tasksDao.createTask(any()) } returns 1L + + sut.updateTask( + id, + day = taskAggregated.task.day, + name = "Whatever", + recurrenceType = RecurrenceType.WEEKLY, + ) + + coVerify(exactly = 1) { + tasksDao.removeChildrenTasks(id) + } + + coVerify(exactly = 1) { + tasksDao.updateTask( + taskId = id, + day = taskAggregated.task.day, + name = "Whatever", + position = 1, + isRecurring = true, + recurrence = "weekly", + ) + } + } + + @Test + fun `Should call dao when count future occurrences for recurring task`() = runTest { + val id = 10L + val expectedCount = 5 + + val taskAggregated = TaskAggregated( + task = TaskEntity( + id = id, + name = "Pierre Jimenez", + createdAt = LocalDate.now(), + day = LocalDate.now(), + isDone = false, + position = 1, + isRecurring = false, + recurrenceType = null, + recurrenceEndDate = null, + parentId = null, + ), + reminder = null, + ) + coEvery { tasksDao.getTaskById(id) } returns flowOf(taskAggregated) + coEvery { + tasksDao.countFutureOccurrences(10L, LocalDate.now()) + } returns expectedCount + + val result = sut.numberFutureOccurrences(10L, LocalDate.now()) + + assertThat(result).isEqualTo(expectedCount) + } +} diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/AutoforwardTasksUseCaseTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/AutoforwardTasksUseCaseTest.kt similarity index 79% rename from common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/AutoforwardTasksUseCaseTest.kt rename to common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/AutoforwardTasksUseCaseTest.kt index e659b962..eb305933 100644 --- a/common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/AutoforwardTasksUseCaseTest.kt +++ b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/AutoforwardTasksUseCaseTest.kt @@ -1,8 +1,8 @@ -package com.costular.atomtasks.tasks.interactor +package com.costular.atomtasks.tasks.usecase import com.costular.atomtasks.data.settings.IsAutoforwardTasksSettingEnabledUseCase import com.costular.atomtasks.tasks.model.Task -import com.costular.core.usecase.invoke +import com.costular.atomtasks.core.usecase.invoke import io.mockk.coVerify import io.mockk.every import io.mockk.mockk @@ -18,7 +18,7 @@ class AutoforwardTasksUseCaseTest { private val isAutoforwardTasksSettingEnabledUseCase: IsAutoforwardTasksSettingEnabledUseCase = mockk() private val observeTasksUseCase: ObserveTasksUseCase = mockk(relaxed = true) - private val updateTaskUseCase: UpdateTaskUseCase = mockk(relaxed = true) + private val editTaskUseCase: EditTaskUseCase = mockk(relaxed = true) private lateinit var sut: AutoforwardTasksUseCase @@ -27,7 +27,7 @@ class AutoforwardTasksUseCaseTest { sut = AutoforwardTasksUseCase( isAutoforwardTasksSettingEnabledUseCase, observeTasksUseCase, - updateTaskUseCase, + editTaskUseCase, ) } @@ -39,7 +39,7 @@ class AutoforwardTasksUseCaseTest { sut(AutoforwardTasksUseCase.Params(date)) verify(exactly = 0) { observeTasksUseCase.invoke(any()) } - coVerify(exactly = 0) { updateTaskUseCase.invoke(any()) } + coVerify(exactly = 0) { editTaskUseCase.invoke(any()) } } @Test @@ -50,7 +50,7 @@ class AutoforwardTasksUseCaseTest { sut(AutoforwardTasksUseCase.Params(Day)) - coVerify(exactly = 0) { updateTaskUseCase.invoke(any()) } + coVerify(exactly = 0) { editTaskUseCase.invoke(any()) } } @Test @@ -66,12 +66,13 @@ class AutoforwardTasksUseCaseTest { } UndoneTasks.forEach { task -> coVerify { - updateTaskUseCase.invoke( - UpdateTaskUseCase.Params( + editTaskUseCase.invoke( + EditTaskUseCase.Params( task.id, task.name, task.day.plusDays(1), task.reminder?.time, + task.recurrenceType, ), ) } @@ -106,6 +107,10 @@ class AutoforwardTasksUseCaseTest { reminder = null, isDone = true, position = 1, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ) val UndoneTasks = listOf( Task( @@ -116,6 +121,10 @@ class AutoforwardTasksUseCaseTest { reminder = null, isDone = false, position = 2, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ), Task( id = 2L, @@ -125,6 +134,10 @@ class AutoforwardTasksUseCaseTest { reminder = null, isDone = false, position = 3, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ), ) } diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/CreateTaskUseCaseTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/CreateTaskUseCaseTest.kt new file mode 100644 index 00000000..6abbc9f0 --- /dev/null +++ b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/CreateTaskUseCaseTest.kt @@ -0,0 +1,90 @@ +package com.costular.atomtasks.tasks.usecase + +import com.costular.atomtasks.tasks.helper.TaskReminderManager +import com.costular.atomtasks.tasks.model.RecurrenceType +import com.costular.atomtasks.tasks.repository.TasksRepository +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import java.time.LocalDate +import java.time.LocalTime +import kotlin.time.ExperimentalTime + +@ExperimentalTime +@ExperimentalCoroutinesApi +class CreateTaskUseCaseTest { + + lateinit var createTaskUseCase: CreateTaskUseCase + + private val tasksRepository: TasksRepository = mockk(relaxed = true) + private val taskReminderManager: TaskReminderManager = mockk(relaxed = true) + private val populateRecurringTasksUseCase: PopulateRecurringTasksUseCase = mockk() + + @Before + fun setUp() { + createTaskUseCase = CreateTaskUseCase( + tasksRepository = tasksRepository, + taskReminderManager = taskReminderManager, + populateRecurringTasksUseCase = populateRecurringTasksUseCase, + ) + } + + @Test + fun `should call repository with given input when create task`() = runTest { + val name = "Call my mom" + val date = LocalDate.of(2021, 1, 7) + val reminder = LocalTime.of(9, 0) + + createTaskUseCase.invoke( + CreateTaskUseCase.Params( + name = name, + date = date, + reminderEnabled = true, + reminderTime = reminder, + recurrenceType = RecurrenceType.WEEKLY, + ), + ) + + coVerify(exactly = 1) { + tasksRepository.createTask( + name, + date, + true, + reminder, + RecurrenceType.WEEKLY, + parentId = null, + ) + } + } + + @Test + fun `should set reminder when create task given reminder's been passed correctly`() = runTest { + val name = "Call my mom" + val date = LocalDate.of(2021, 1, 7) + val reminder = LocalTime.of(9, 0) + val taskId = 100L + + coEvery { + tasksRepository.createTask(name, date, true, reminder, RecurrenceType.YEARLY, null) + } returns taskId + + createTaskUseCase.invoke( + CreateTaskUseCase.Params( + name = name, + date = date, + reminderEnabled = true, + reminderTime = reminder, + recurrenceType = RecurrenceType.YEARLY, + ), + ) + + verify(exactly = 1) { taskReminderManager.set(taskId, reminder.atDate(date)) } + } + + +} diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/DefaultTasksRepositoryTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/DefaultTasksRepositoryTest.kt similarity index 71% rename from common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/DefaultTasksRepositoryTest.kt rename to common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/DefaultTasksRepositoryTest.kt index 1ef69b97..e29e38af 100644 --- a/common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/DefaultTasksRepositoryTest.kt +++ b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/DefaultTasksRepositoryTest.kt @@ -1,11 +1,11 @@ -package com.costular.atomtasks.tasks.interactor +package com.costular.atomtasks.tasks.usecase import com.costular.atomtasks.data.tasks.ReminderEntity import com.costular.atomtasks.data.tasks.TaskAggregated import com.costular.atomtasks.data.tasks.TaskEntity -import com.costular.atomtasks.tasks.repository.DefaultTasksRepository import com.costular.atomtasks.tasks.model.Reminder import com.costular.atomtasks.tasks.model.Task +import com.costular.atomtasks.tasks.repository.DefaultTasksRepository import com.costular.atomtasks.tasks.repository.TaskLocalDataSource import com.costular.atomtasks.tasks.repository.TasksRepository import com.google.common.truth.Truth.assertThat @@ -17,7 +17,6 @@ import java.time.LocalDate import java.time.LocalTime import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -34,7 +33,7 @@ class DefaultTasksRepositoryTest { } @Test - fun `should call local data source add task when add task`() = runBlockingTest { + fun `should call local data source add task when add task`() = runTest { val taskName = "task name" val taskDate = LocalDate.now() @@ -45,6 +44,8 @@ class DefaultTasksRepositoryTest { date = taskDate, reminderEnabled = false, reminderTime = null, + recurrenceType = null, + parentId = null, ) coVerify { localDataSource.createTask(capture(taskSlot)) } @@ -52,32 +53,34 @@ class DefaultTasksRepositoryTest { } @Test - fun `should call local data source add reminder when add task with reminder`() = - runBlockingTest { - val taskName = "task name" - val taskDate = LocalDate.of(2022, 6, 4) - val taskReminderTime = LocalTime.of(9, 0) - val reminderEnabled = true - - val taskId = sut.createTask( - name = taskName, - date = taskDate, - reminderEnabled = reminderEnabled, - reminderTime = taskReminderTime, - ) + fun `should call local data source add reminder when add task with reminder`() = runTest { + val taskName = "task name" + val taskDate = LocalDate.of(2022, 6, 4) + val taskReminderTime = LocalTime.of(9, 0) + val reminderEnabled = true + val parentId = 200L + + val taskId = sut.createTask( + name = taskName, + date = taskDate, + reminderEnabled = reminderEnabled, + reminderTime = taskReminderTime, + recurrenceType = null, + parentId = null, + ) - coVerify { - localDataSource.createReminderForTask( - taskReminderTime, - taskDate, - reminderEnabled, - taskId, - ) - } + coVerify { + localDataSource.createReminderForTask( + taskReminderTime, + taskDate, + reminderEnabled, + taskId, + ) } + } @Test - fun `should get task by id`() = runBlockingTest { + fun `should get task by id`() = runTest { val taskId = 101L val taskName = "just whatever" val createdAt = LocalDate.now() @@ -113,6 +116,10 @@ class DefaultTasksRepositoryTest { ), isDone = true, position = 1, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ) coEvery { localDataSource.getTaskById(taskId) } returns flowOf(result) @@ -132,14 +139,13 @@ class DefaultTasksRepositoryTest { val taskName = "Task name" val taskDay = LocalDate.of(2022, 6, 4) - sut.updateTask(taskId, taskDay, taskName) + sut.updateTask(taskId, taskDay, taskName, null) - coVerify { localDataSource.updateTask(taskId, taskDay, taskName) } + coVerify { localDataSource.updateTask(taskId, taskDay, taskName, null) } } @Test fun `should call local data source move task when move task`() = runTest { - val taskId = 1L val from = 1 val to = 3 @@ -147,4 +153,17 @@ class DefaultTasksRepositoryTest { coVerify(exactly = 1) { localDataSource.moveTask(LocalDate.now(), from, to) } } + + @Test + fun `Should call local data source when call number future occurrences`() = runTest { + val parentId = 10L + val day = LocalDate.now() + val expectedCount = 10 + + coEvery { localDataSource.numberFutureOccurrences(parentId, day) } returns expectedCount + + val result = sut.numberFutureOccurrences(parentId, day) + + assertThat(result).isEqualTo(expectedCount) + } } diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/UpdateTaskUseCaseTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/EditTaskUseCaseTest.kt similarity index 79% rename from common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/UpdateTaskUseCaseTest.kt rename to common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/EditTaskUseCaseTest.kt index aa980b19..e37984e9 100644 --- a/common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/UpdateTaskUseCaseTest.kt +++ b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/EditTaskUseCaseTest.kt @@ -1,7 +1,7 @@ -package com.costular.atomtasks.tasks.interactor +package com.costular.atomtasks.tasks.usecase +import com.costular.atomtasks.tasks.helper.TaskReminderManager import com.costular.atomtasks.tasks.repository.TasksRepository -import com.costular.atomtasks.tasks.manager.TaskReminderManager import io.mockk.coVerify import io.mockk.mockk import java.time.LocalDate @@ -10,16 +10,16 @@ import kotlinx.coroutines.test.runBlockingTest import org.junit.Before import org.junit.Test -class UpdateTaskUseCaseTest { +class EditTaskUseCaseTest { - private lateinit var sut: UpdateTaskUseCase + private lateinit var sut: EditTaskUseCase private val repository: TasksRepository = mockk(relaxed = true) private val taskReminderManager: TaskReminderManager = mockk(relaxed = true) @Before fun setUp() { - sut = UpdateTaskUseCase(repository, taskReminderManager) + sut = EditTaskUseCase(repository, taskReminderManager) } @Test @@ -29,15 +29,16 @@ class UpdateTaskUseCaseTest { val newDay = LocalDate.now() sut( - UpdateTaskUseCase.Params( + EditTaskUseCase.Params( taskId = taskId, name = name, date = newDay, reminderTime = null, + recurrenceType = null, ), ) - coVerify { repository.updateTask(taskId, newDay, name) } + coVerify { repository.updateTask(taskId, newDay, name, null) } } @Test @@ -48,11 +49,12 @@ class UpdateTaskUseCaseTest { val reminder = LocalTime.of(9, 0) sut( - UpdateTaskUseCase.Params( + EditTaskUseCase.Params( taskId = taskId, name = name, date = newDay, reminderTime = reminder, + recurrenceType = null, ), ) @@ -67,11 +69,12 @@ class UpdateTaskUseCaseTest { val reminder = LocalTime.of(9, 0) sut( - UpdateTaskUseCase.Params( + EditTaskUseCase.Params( taskId = taskId, name = name, date = newDay, reminderTime = reminder, + recurrenceType = null, ), ) @@ -86,11 +89,12 @@ class UpdateTaskUseCaseTest { val reminder = null sut( - UpdateTaskUseCase.Params( + EditTaskUseCase.Params( taskId = taskId, name = name, date = newDay, reminderTime = reminder, + recurrenceType = null, ), ) @@ -105,11 +109,12 @@ class UpdateTaskUseCaseTest { val reminder = null sut( - UpdateTaskUseCase.Params( + EditTaskUseCase.Params( taskId = taskId, name = name, date = newDay, reminderTime = reminder, + recurrenceType = null, ), ) diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/PopulateRecurringTasksUseCaseTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/PopulateRecurringTasksUseCaseTest.kt new file mode 100644 index 00000000..0632da73 --- /dev/null +++ b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/PopulateRecurringTasksUseCaseTest.kt @@ -0,0 +1,75 @@ +package com.costular.atomtasks.tasks.usecase + +import com.costular.atomtasks.core.Either +import com.costular.atomtasks.tasks.fake.TaskRecurring +import com.costular.atomtasks.tasks.fake.TaskToday +import com.costular.atomtasks.tasks.model.PopulateRecurringTasksError +import com.costular.atomtasks.tasks.model.PopulateRecurringTasksError.NotRecurringTask +import com.costular.atomtasks.tasks.model.RecurrenceType +import com.costular.atomtasks.tasks.model.Task +import com.costular.atomtasks.tasks.repository.TasksRepository +import com.google.common.truth.Truth.assertThat +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class PopulateRecurringTasksUseCaseTest { + + lateinit var sut: PopulateRecurringTasksUseCase + + private val tasksRepository: TasksRepository = mockk(relaxed = true) + + @Before + fun setUp() { + sut = PopulateRecurringTasksUseCase(tasksRepository) + } + + @Test + fun `Should return not recurring task error when call usecase given the task is not recurring`() = + runTest { + val task = TaskToday + givenTask(task) + + val result = sut.invoke(PopulateRecurringTasksUseCase.Params(10L)) + + assertThat(result).isEqualTo(Either.Error(NotRecurringTask)) + } + + @Test + fun `Should return Either right when populate recurring given recurrence is daily`() = runTest { + val task = TaskRecurring.copy(recurrenceType = RecurrenceType.DAILY) + givenTask(task) + + val result = sut(PopulateRecurringTasksUseCase.Params(100L)) + + assertThat(result).isEqualTo(Either.Result(Unit)) + } + + @Test + fun `Should return Either left when populate recurring given create task throws an exception`() = + runTest { + val task = TaskRecurring.copy(recurrenceType = RecurrenceType.DAILY) + givenTask(task) + coEvery { + tasksRepository.createTask( + any(), + any(), + any(), + any(), + any(), + any() + ) + } throws Exception("") + + val result = sut(PopulateRecurringTasksUseCase.Params(100L)) + + assertThat(result).isEqualTo(Either.Error(PopulateRecurringTasksError.UnknownError)) + } + + private fun givenTask(task: Task) { + coEvery { tasksRepository.getTaskById(any()) } returns flowOf(task) + } +} diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/PostponeTaskUseCaseTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/PostponeTaskUseCaseTest.kt similarity index 80% rename from common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/PostponeTaskUseCaseTest.kt rename to common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/PostponeTaskUseCaseTest.kt index fe07b3da..539d1d83 100644 --- a/common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/PostponeTaskUseCaseTest.kt +++ b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/PostponeTaskUseCaseTest.kt @@ -1,12 +1,11 @@ -package com.costular.atomtasks.tasks.interactor +package com.costular.atomtasks.tasks.usecase -import com.costular.atomtasks.tasks.manager.TaskReminderManager +import com.costular.atomtasks.tasks.helper.TaskReminderManager import com.costular.atomtasks.tasks.model.Reminder import com.costular.atomtasks.tasks.model.Task -import com.costular.core.toError +import com.costular.atomtasks.core.toError import com.google.common.truth.Truth import io.mockk.coEvery -import io.mockk.every import io.mockk.mockk import java.time.LocalDate import java.time.LocalTime @@ -19,18 +18,18 @@ class PostponeTaskUseCaseTest { lateinit var sut: PostponeTaskUseCase - private val getTaskByIdInteractor: GetTaskByIdInteractor = mockk(relaxed = true) + private val getTaskByIdUseCase: GetTaskByIdUseCase = mockk(relaxed = true) private val updateTaskReminderInteractor: UpdateTaskReminderInteractor = mockk(relaxed = true) private val taskReminderManager: TaskReminderManager = mockk(relaxed = true) - private val updateTaskUseCase: UpdateTaskUseCase = mockk(relaxed = true) + private val editTaskUseCase: EditTaskUseCase = mockk(relaxed = true) @Before fun setUp() { sut = PostponeTaskUseCase( - getTaskByIdInteractor = getTaskByIdInteractor, + getTaskByIdUseCase = getTaskByIdUseCase, updateTaskReminderInteractor = updateTaskReminderInteractor, taskReminderManager = taskReminderManager, - updateTaskUseCase = updateTaskUseCase, + editTaskUseCase = editTaskUseCase, ) } @@ -117,19 +116,20 @@ class PostponeTaskUseCaseTest { ) coEvery { - updateTaskUseCase.invoke( - UpdateTaskUseCase.Params( + editTaskUseCase.invoke( + EditTaskUseCase.Params( taskId = taskId, name = "Whatever", date = day, - reminderTime = time + reminderTime = time, + recurrenceType = null, ) ) } } private fun givenTaskWithoutReminder() { - every { getTaskByIdInteractor.flow } returns flowOf( + coEvery { getTaskByIdUseCase(any()) } returns flowOf( Task( id = 1L, name = "Whatever", @@ -138,12 +138,16 @@ class PostponeTaskUseCaseTest { reminder = null, isDone = false, position = 0, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ) ) } private fun givenTaskWithReminder() { - every { getTaskByIdInteractor.flow } returns flowOf( + coEvery { getTaskByIdUseCase(any()) } returns flowOf( Task( id = 1L, name = "Whatever", @@ -156,6 +160,10 @@ class PostponeTaskUseCaseTest { ), isDone = false, position = 0, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ) ) } diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/RemoveTaskUseCaseTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/RemoveTaskUseCaseTest.kt new file mode 100644 index 00000000..336bf426 --- /dev/null +++ b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/RemoveTaskUseCaseTest.kt @@ -0,0 +1,70 @@ +package com.costular.atomtasks.tasks.usecase + +import com.costular.atomtasks.core.Either +import com.costular.atomtasks.tasks.helper.TaskReminderManager +import com.costular.atomtasks.tasks.model.RemovalStrategy +import com.costular.atomtasks.tasks.model.RemoveTaskError +import com.costular.atomtasks.tasks.repository.TasksRepository +import com.google.common.truth.Truth +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class RemoveTaskUseCaseTest { + + lateinit var sut: RemoveTaskUseCase + + private val tasksRepository: TasksRepository = mockk(relaxUnitFun = true) + private val tasksReminderManager: TaskReminderManager = mockk(relaxUnitFun = true) + + @Before + fun setUp() { + sut = RemoveTaskUseCase( + tasksRepository = tasksRepository, + taskReminderManager = tasksReminderManager + ) + } + + @Test + fun `Should call repository remove task with proper params when execute usecase`() = runTest { + val taskId = 102L + + sut.invoke(RemoveTaskUseCase.Params(taskId)) + + coVerify(exactly = 1) { tasksRepository.removeTask(taskId) } + } + + @Test + fun `Should call task reminder manager to cancel task with proper params when execute usecase`() = + runTest { + val taskId = 102L + + sut.invoke(RemoveTaskUseCase.Params(taskId)) + + coVerify(exactly = 1) { tasksReminderManager.cancel(taskId) } + } + + @Test + fun `Should return either error when execute usecase throws an exception`() = runTest { + val taskId = 102L + coEvery { tasksRepository.removeTask(taskId) } throws Exception("") + + val result = sut.invoke(RemoveTaskUseCase.Params(taskId)) + + Truth.assertThat(result).isEqualTo(Either.Error(RemoveTaskError.UnknownError)) + } + + @Test + fun `Should call task repository remove recurring when execute usecase given removal strategy was passed via params`() = + runTest { + val taskId = 102L + val removalStrategy = RemovalStrategy.SINGLE_AND_FUTURE_ONES + + sut.invoke(RemoveTaskUseCase.Params(taskId, removalStrategy)) + + coVerify(exactly = 1) { tasksRepository.removeRecurringTask(taskId, removalStrategy) } + } +} diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/UpdateTaskIsDoneUseCaseTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/UpdateTaskIsDoneUseCaseTest.kt new file mode 100644 index 00000000..23f57ab5 --- /dev/null +++ b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/UpdateTaskIsDoneUseCaseTest.kt @@ -0,0 +1,57 @@ +package com.costular.atomtasks.tasks.usecase + +import com.costular.atomtasks.core.Either +import com.costular.atomtasks.tasks.fake.TaskToday +import com.costular.atomtasks.tasks.model.UpdateTaskIsDoneError +import com.costular.atomtasks.tasks.repository.TasksRepository +import com.google.common.truth.Truth.assertThat +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class UpdateTaskIsDoneUseCaseTest { + + lateinit var sut: UpdateTaskIsDoneUseCase + + private val tasksRepository: TasksRepository = mockk() + + @Before + fun setUp() { + sut = UpdateTaskIsDoneUseCase(tasksRepository) + } + + @Test + fun `Should call mark task repository method when invoke usecase`() = runTest { + val taskId = 100L + val isDone = true + + sut(UpdateTaskIsDoneUseCase.Params(taskId, isDone)) + + coVerify { tasksRepository.markTask(taskId, isDone) } + } + + @Test + fun `Should return Either result when invoke usecase given repository returned success`() = + runTest { + coEvery { tasksRepository.markTask(1L, false) } returns Unit + coEvery { tasksRepository.getTaskById(1L) } returns flowOf(TaskToday) + + val result = sut(UpdateTaskIsDoneUseCase.Params(1L, false)) + + assertThat(result).isEqualTo(Either.Result(Unit)) + } + + @Test + fun `Should return Either error when invoke usecase given repostory threw an exception`() = + runTest { + coEvery { tasksRepository.markTask(1L, true) } throws Exception("") + + val result = sut(UpdateTaskIsDoneUseCase.Params(1L, true)) + + assertThat(result).isEqualTo(Either.Error(UpdateTaskIsDoneError.UnknownError)) + } +} diff --git a/common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/UpdateTaskReminderInteractorTest.kt b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/UpdateTaskReminderInteractorTest.kt similarity index 95% rename from common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/UpdateTaskReminderInteractorTest.kt rename to common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/UpdateTaskReminderInteractorTest.kt index 34b745ee..fe680fad 100644 --- a/common/tasks/src/test/java/com/costular/atomtasks/tasks/interactor/UpdateTaskReminderInteractorTest.kt +++ b/common/tasks/src/test/java/com/costular/atomtasks/tasks/usecase/UpdateTaskReminderInteractorTest.kt @@ -1,4 +1,4 @@ -package com.costular.atomtasks.tasks.interactor +package com.costular.atomtasks.tasks.usecase import com.costular.atomtasks.tasks.repository.TasksRepository import io.mockk.coVerify diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts index a1232331..47dd5dd4 100644 --- a/core/designsystem/build.gradle.kts +++ b/core/designsystem/build.gradle.kts @@ -36,7 +36,7 @@ dependencies { implementation(libs.accompanist.systemui) implementation(libs.viewmodel) implementation(libs.compose.ui.text.fonts) - implementation(libs.calendar) + api(libs.calendar) testImplementation(projects.core.testing) testImplementation(libs.android.junit) diff --git a/core/designsystem/src/main/java/com/costular/designsystem/components/ClearableChip.kt b/core/designsystem/src/main/java/com/costular/designsystem/components/ClearableChip.kt index ae15e33f..c434bc52 100644 --- a/core/designsystem/src/main/java/com/costular/designsystem/components/ClearableChip.kt +++ b/core/designsystem/src/main/java/com/costular/designsystem/components/ClearableChip.kt @@ -6,18 +6,26 @@ import androidx.compose.material.icons.filled.CalendarToday import androidx.compose.material.icons.filled.Close import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.AssistChipDefaults.assistChipBorder +import androidx.compose.material3.ChipColors +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.costular.atomtasks.core.ui.R import com.costular.designsystem.theme.AppTheme import com.costular.designsystem.theme.AtomTheme +import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement as MinimumTouchArea +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ClearableChip( title: String, @@ -29,30 +37,19 @@ fun ClearableChip( modifier: Modifier = Modifier, ) { val border = if (isError) { - AssistChipDefaults.assistChipBorder(borderColor = MaterialTheme.colorScheme.error) - } else { - AssistChipDefaults.assistChipBorder() - } - - val chipColors = if (isError) { - AssistChipDefaults.assistChipColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - labelColor = MaterialTheme.colorScheme.onErrorContainer, - leadingIconContentColor = MaterialTheme.colorScheme.onErrorContainer, + assistChipBorder( + enabled = true, + borderColor = MaterialTheme.colorScheme.error ) } else { - AssistChipDefaults.assistChipColors() + assistChipBorder(enabled = true) } + val chipColors = buildChipColors(isSelected = isSelected, isError = isError) + AssistChip( modifier = modifier, - onClick = { - if (isSelected) { - onClear() - } else { - onClick() - } - }, + onClick = onClick, leadingIcon = { Icon( imageVector = icon, @@ -67,14 +64,9 @@ fun ClearableChip( }, trailingIcon = { if (isSelected) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource( - R.string.content_description_chip_clear, - ), - modifier = Modifier - .size(AppTheme.ChipIconSize), - ) + CompositionLocalProvider(MinimumTouchArea provides false) { + ClearIcon(onClear = onClear) + } } }, border = border, @@ -82,6 +74,47 @@ fun ClearableChip( ) } +@Composable +private fun buildChipColors( + isSelected: Boolean, + isError: Boolean +): ChipColors = when { + isSelected && !isError -> { + AssistChipDefaults.assistChipColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + labelColor = MaterialTheme.colorScheme.onPrimaryContainer, + leadingIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + trailingIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + + isError -> { + AssistChipDefaults.assistChipColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + labelColor = MaterialTheme.colorScheme.onErrorContainer, + leadingIconContentColor = MaterialTheme.colorScheme.onErrorContainer, + ) + } + + else -> AssistChipDefaults.assistChipColors() +} + +@Composable +private fun ClearIcon(onClear: () -> Unit) { + IconButton( + modifier = Modifier.size(24.dp), + onClick = onClear, + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource( + R.string.content_description_chip_clear, + ), + modifier = Modifier.size(AppTheme.ChipIconSize), + ) + } +} + @Preview @Composable fun ClearableChipPreview() { diff --git a/core/designsystem/src/main/java/com/costular/designsystem/components/DatePicker.kt b/core/designsystem/src/main/java/com/costular/designsystem/components/DatePicker.kt index 647874d1..0f32e4de 100644 --- a/core/designsystem/src/main/java/com/costular/designsystem/components/DatePicker.kt +++ b/core/designsystem/src/main/java/com/costular/designsystem/components/DatePicker.kt @@ -1,5 +1,8 @@ package com.costular.designsystem.components +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -7,13 +10,16 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ChevronLeft +import androidx.compose.material.icons.outlined.ChevronRight +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.LocalAbsoluteTonalElevation import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme @@ -23,7 +29,10 @@ import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -32,20 +41,28 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.costular.atomtasks.coreui.utils.ofLocalized -import com.costular.core.util.DateTimeFormatters +import com.costular.atomtasks.core.ui.utils.ofLocalized +import com.costular.atomtasks.core.ui.utils.rememberFirstDayOfWeek +import com.costular.atomtasks.core.util.DateTimeFormatters import com.costular.designsystem.theme.AppTheme import com.costular.designsystem.theme.AtomTheme +import com.kizitonwose.calendar.compose.CalendarLayoutInfo +import com.kizitonwose.calendar.compose.CalendarState import com.kizitonwose.calendar.compose.HorizontalCalendar import com.kizitonwose.calendar.compose.rememberCalendarState import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.core.DayPosition -import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale +import com.kizitonwose.calendar.core.nextMonth +import com.kizitonwose.calendar.core.previousMonth import com.kizitonwose.calendar.core.yearMonth import java.time.LocalDate import java.time.YearMonth import java.time.format.TextStyle +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch + +private const val MonthsToShow = 100L @Composable fun DatePicker( @@ -53,59 +70,109 @@ fun DatePicker( selectedDay: LocalDate = LocalDate.now(), onDateSelected: (LocalDate) -> Unit, ) { - val month = remember(selectedDay) { selectedDay.yearMonth } - val firstDayOfWeek = remember { firstDayOfWeekFromLocale() } + val currentMonth = remember(selectedDay) { selectedDay.yearMonth } + val startMonth = remember { currentMonth.minusMonths(MonthsToShow) } + val endMonth = remember { currentMonth.plusMonths(MonthsToShow) } + val firstDayOfWeek = rememberFirstDayOfWeek() + val coroutineScope = rememberCoroutineScope() val state = rememberCalendarState( - firstVisibleMonth = month, + startMonth = startMonth, + endMonth = endMonth, + firstVisibleMonth = currentMonth, firstDayOfWeek = firstDayOfWeek, ) + val visibleMonth = rememberFirstCompletelyVisibleMonth(state) - LaunchedEffect(selectedDay) { - state.animateScrollToMonth(YearMonth.from(selectedDay)) + Column( + modifier = modifier.animateContentSize( + animationSpec = tween( + durationMillis = 200, + easing = FastOutLinearInEasing, + ) + ), + ) { + MonthNameWithNavigation( + currentMonth = visibleMonth.yearMonth, + onPreviousMonth = { + coroutineScope.launch { + state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.previousMonth) + } + }, + onNextMonth = { + coroutineScope.launch { + state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.nextMonth) + } + } + ) + + HorizontalCalendar( + state = state, + dayContent = { day -> + Day( + day = day, + isSelected = selectedDay == day.date, + onClick = { onDateSelected(it.date) }, + ) + }, + monthHeader = { month -> + Weekdays( + month = month, + ) + }, + ) } +} - HorizontalCalendar( - state = state, - dayContent = { day -> - Day( - day = day, - isSelected = selectedDay == day.date, - onClick = { onDateSelected(it.date) }, +@Composable +private fun MonthNameWithNavigation( + currentMonth: YearMonth, + onPreviousMonth: () -> Unit, + onNextMonth: () -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton(onClick = onPreviousMonth) { + Icon( + imageVector = Icons.Outlined.ChevronLeft, + contentDescription = null, ) - }, - monthHeader = { month -> - MonthHeader(month) - }, - modifier = modifier, - ) + } + + Text( + text = currentMonth.ofLocalized(DateTimeFormatters.monthFormatter), + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f), + ) + + IconButton(onClick = onNextMonth) { + Icon( + imageVector = Icons.Outlined.ChevronRight, + contentDescription = null, + ) + } + } } @Composable -private fun MonthHeader( +private fun Weekdays( month: CalendarMonth, ) { - Column { - MonthHeader( - month = month.yearMonth.ofLocalized(DateTimeFormatters.monthFormatter) - ) + Row(modifier = Modifier.fillMaxWidth()) { + val mediumEmphasis = MaterialTheme.colorScheme.onSurfaceVariant - Spacer(Modifier.height(AppTheme.dimens.spacingLarge)) - - Row(modifier = Modifier.fillMaxWidth()) { - val mediumEmphasis = MaterialTheme.colorScheme.onSurfaceVariant - - month.weekDays.first().map { it.date.dayOfWeek }.forEach { dayOfWeek -> - CompositionLocalProvider(LocalContentColor provides mediumEmphasis) { - Text( - textAlign = TextAlign.Center, - text = dayOfWeek.ofLocalized(TextStyle.SHORT), - modifier = Modifier - .weight(1f) - .wrapContentHeight(), - style = MaterialTheme.typography.bodySmall, - ) - } + month.weekDays.first().map { it.date.dayOfWeek }.forEach { dayOfWeek -> + CompositionLocalProvider(LocalContentColor provides mediumEmphasis) { + Text( + textAlign = TextAlign.Center, + text = dayOfWeek.ofLocalized(TextStyle.SHORT), + modifier = Modifier + .weight(1f) + .wrapContentHeight(), + style = MaterialTheme.typography.bodySmall, + ) } } } @@ -157,18 +224,37 @@ private fun BoxScope.Day( } @Composable -fun MonthHeader( - month: String, - modifier: Modifier = Modifier, -) { - Text( - modifier = modifier.fillMaxWidth(), - text = month, - style = MaterialTheme.typography.titleMedium, - textAlign = TextAlign.Center, - ) +private fun rememberFirstCompletelyVisibleMonth(state: CalendarState): CalendarMonth { + val visibleMonth = remember(state) { mutableStateOf(state.firstVisibleMonth) } + // Only take non-null values as null will be produced when the + // list is mid-scroll as no index will be completely visible. + LaunchedEffect(state) { + snapshotFlow { state.layoutInfo.completelyVisibleMonths.firstOrNull() } + .filterNotNull() + .collect { month -> visibleMonth.value = month } + } + return visibleMonth.value } +private val CalendarLayoutInfo.completelyVisibleMonths: List + get() { + val visibleItemsInfo = this.visibleMonthsInfo.toMutableList() + return if (visibleItemsInfo.isEmpty()) { + emptyList() + } else { + val lastItem = visibleItemsInfo.last() + val viewportSize = this.viewportEndOffset + this.viewportStartOffset + if (lastItem.offset + lastItem.size > viewportSize) { + visibleItemsInfo.removeLast() + } + val firstItem = visibleItemsInfo.firstOrNull() + if (firstItem != null && firstItem.offset < this.viewportStartOffset) { + visibleItemsInfo.removeFirst() + } + visibleItemsInfo.map { it.month } + } + } + @Preview(showBackground = true) @Composable fun DatePickerPreview() { diff --git a/core/designsystem/src/main/java/com/costular/designsystem/components/HorizontalCalendar.kt b/core/designsystem/src/main/java/com/costular/designsystem/components/WeekCalendar.kt similarity index 65% rename from core/designsystem/src/main/java/com/costular/designsystem/components/HorizontalCalendar.kt rename to core/designsystem/src/main/java/com/costular/designsystem/components/WeekCalendar.kt index 8bcdb52b..2f524bab 100644 --- a/core/designsystem/src/main/java/com/costular/designsystem/components/HorizontalCalendar.kt +++ b/core/designsystem/src/main/java/com/costular/designsystem/components/WeekCalendar.kt @@ -4,11 +4,9 @@ import androidx.compose.foundation.Canvas import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CardDefaults @@ -16,6 +14,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -23,38 +22,53 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.costular.atomtasks.coreui.date.Day -import com.costular.atomtasks.coreui.date.asDay -import com.costular.atomtasks.coreui.utils.ofLocalized -import com.costular.core.util.DateTimeFormatters.shortDayOfWeekFormatter -import com.costular.core.util.WeekUtil +import com.costular.atomtasks.core.ui.date.Day +import com.costular.atomtasks.core.ui.date.asDay +import com.costular.atomtasks.core.ui.utils.ofLocalized +import com.costular.atomtasks.core.util.DateTimeFormatters.shortDayOfWeekFormatter import com.costular.designsystem.theme.AtomTheme +import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState +import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState import java.time.LocalDate +private const val DaysToShow = 365L + @Composable -fun HorizontalCalendar( +fun WeekCalendar( + modifier: Modifier = Modifier, selectedDay: Day = LocalDate.now().asDay(), - weekDays: List = remember(selectedDay) { WeekUtil.getWeekDays(selectedDay.date) }, + startDate: LocalDate = remember(selectedDay) { selectedDay.date.minusDays(DaysToShow) }, + endDate: LocalDate = remember(selectedDay) { selectedDay.date.plusDays(DaysToShow) }, onSelectDay: (LocalDate) -> Unit, - modifier: Modifier = Modifier, + weekCalendarState: WeekCalendarState = rememberWeekCalendarState( + startDate = startDate, + endDate = endDate, + firstVisibleWeekDate = selectedDay.date, + ) ) { - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - weekDays.forEach { date -> + LaunchedEffect(selectedDay) { + weekCalendarState.animateScrollToWeek(selectedDay.date) + } + + com.kizitonwose.calendar.compose.WeekCalendar( + modifier = modifier, + state = weekCalendarState, + dayContent = { weekDay -> CalendarDay( - date = date, - isSelected = date == selectedDay.date, - onClick = { onSelectDay(date) }, - isToday = LocalDate.now() == date, + date = weekDay.date, + isSelected = weekDay.date == selectedDay.date, + isToday = LocalDate.now() == weekDay.date, + onClick = { + onSelectDay(weekDay.date) + }, + modifier = Modifier.padding(horizontal = 4.dp) ) } - } + ) } @Composable -private fun RowScope.CalendarDay( +private fun CalendarDay( date: LocalDate, isToday: Boolean, isSelected: Boolean, @@ -73,7 +87,6 @@ private fun RowScope.CalendarDay( OutlinedCard( colors = cardColors, modifier = modifier - .weight(1f) .height(70.dp) .clip(RoundedCornerShape(12.dp)) .clickable { onClick() } @@ -120,7 +133,7 @@ fun IndicatorToday( @Composable private fun HorizontalCalendarPreview() { AtomTheme { - HorizontalCalendar( + WeekCalendar( selectedDay = LocalDate.now().asDay(), onSelectDay = {} ) diff --git a/core/designsystem/src/main/java/com/costular/designsystem/theme/Theme.kt b/core/designsystem/src/main/java/com/costular/designsystem/theme/Theme.kt index 56c26017..e8f97773 100644 --- a/core/designsystem/src/main/java/com/costular/designsystem/theme/Theme.kt +++ b/core/designsystem/src/main/java/com/costular/designsystem/theme/Theme.kt @@ -123,7 +123,7 @@ object AppTheme { get() = LocalAppDimens.current val ChipIconSize = 18.dp - + val DialogPadding = 24.dp const val DisabledAlpha = 0.38f } diff --git a/core/jobs/.gitignore b/core/jobs/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/jobs/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/jobs/build.gradle.kts b/core/jobs/build.gradle.kts new file mode 100644 index 00000000..78654698 --- /dev/null +++ b/core/jobs/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("atomtasks.android.library") + id("kotlin-android") + id("atomtasks.detekt") + id("atomtasks.android.library.jacoco") + id("dagger.hilt.android.plugin") + kotlin("kapt") +} + +android { + namespace = "com.costular.atomtasks.core.jobs" +} + +dependencies { + implementation(libs.hilt) + kapt(libs.hilt.compiler) + implementation(libs.work) + + testImplementation(libs.work.testing) +} diff --git a/core/jobs/consumer-rules.pro b/core/jobs/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/core/jobs/proguard-rules.pro b/core/jobs/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core/jobs/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/jobs/src/main/AndroidManifest.xml b/core/jobs/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/core/jobs/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/jobs/src/main/java/com/costular/atomtasks/core/jobs/DailyJobCreator.kt b/core/jobs/src/main/java/com/costular/atomtasks/core/jobs/DailyJobCreator.kt new file mode 100644 index 00000000..722e8b24 --- /dev/null +++ b/core/jobs/src/main/java/com/costular/atomtasks/core/jobs/DailyJobCreator.kt @@ -0,0 +1,29 @@ +package com.costular.atomtasks.core.jobs + +import androidx.work.OneTimeWorkRequest +import java.time.Duration +import java.time.LocalTime + +internal class DailyJobCreator( + private val time: LocalTime +) : JobCreator { + override suspend fun create( + workerBuilder: OneTimeWorkRequest.Builder, + ): OneTimeWorkRequest { + return workerBuilder + .setInitialDelay(getDelayUntilAutoforward()) + .build() + } + + private fun getDelayUntilAutoforward(): Duration { + val now = LocalTime.now() + + return Duration.between(now, time).run { + if (isNegative) { + plusDays(1) + } else { + this + } + } + } +} diff --git a/core/jobs/src/main/java/com/costular/atomtasks/core/jobs/JobCreator.kt b/core/jobs/src/main/java/com/costular/atomtasks/core/jobs/JobCreator.kt new file mode 100644 index 00000000..dc97ed87 --- /dev/null +++ b/core/jobs/src/main/java/com/costular/atomtasks/core/jobs/JobCreator.kt @@ -0,0 +1,9 @@ +package com.costular.atomtasks.core.jobs + +import androidx.work.OneTimeWorkRequest + +interface JobCreator { + suspend fun create( + workerBuilder: OneTimeWorkRequest.Builder, + ): OneTimeWorkRequest +} diff --git a/core/jobs/src/main/java/com/costular/atomtasks/core/jobs/JobScheduleFactory.kt b/core/jobs/src/main/java/com/costular/atomtasks/core/jobs/JobScheduleFactory.kt new file mode 100644 index 00000000..2c8f8fa0 --- /dev/null +++ b/core/jobs/src/main/java/com/costular/atomtasks/core/jobs/JobScheduleFactory.kt @@ -0,0 +1,9 @@ +package com.costular.atomtasks.core.jobs + +object JobScheduleFactory { + fun jobCreator(jobScheduleFrequency: JobScheduleFrequency): JobCreator { + return when (jobScheduleFrequency) { + is JobScheduleFrequency.Daily -> DailyJobCreator(jobScheduleFrequency.time) + } + } +} diff --git a/core/jobs/src/main/java/com/costular/atomtasks/core/jobs/JobScheduleFrequency.kt b/core/jobs/src/main/java/com/costular/atomtasks/core/jobs/JobScheduleFrequency.kt new file mode 100644 index 00000000..a9db8f50 --- /dev/null +++ b/core/jobs/src/main/java/com/costular/atomtasks/core/jobs/JobScheduleFrequency.kt @@ -0,0 +1,11 @@ +package com.costular.atomtasks.core.jobs + +import java.time.LocalTime + +sealed class JobScheduleFrequency( + open val time: LocalTime +) { + data class Daily(override val time: LocalTime) : JobScheduleFrequency(time) +} + +fun daily(time: LocalTime): JobScheduleFrequency = JobScheduleFrequency.Daily(time) diff --git a/core/preferences/src/main/java/com/costular/atomtasks/preferences/DataStoreModule.kt b/core/preferences/src/main/java/com/costular/atomtasks/preferences/DataStoreModule.kt index aebe0cfc..cc8a070c 100644 --- a/core/preferences/src/main/java/com/costular/atomtasks/preferences/DataStoreModule.kt +++ b/core/preferences/src/main/java/com/costular/atomtasks/preferences/DataStoreModule.kt @@ -7,7 +7,7 @@ import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.preferencesDataStoreFile -import com.costular.core.net.DispatcherProvider +import com.costular.atomtasks.core.net.DispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/core/review/src/main/java/com/costular/atomtasks/review/usecase/ShouldAskReviewUseCase.kt b/core/review/src/main/java/com/costular/atomtasks/review/usecase/ShouldAskReviewUseCase.kt index c320d42c..4a35661b 100644 --- a/core/review/src/main/java/com/costular/atomtasks/review/usecase/ShouldAskReviewUseCase.kt +++ b/core/review/src/main/java/com/costular/atomtasks/review/usecase/ShouldAskReviewUseCase.kt @@ -3,8 +3,8 @@ package com.costular.atomtasks.review.usecase import com.costular.atomtasks.core.logging.atomLog import com.costular.atomtasks.review.model.ShouldAskReviewError import com.costular.atomtasks.review.strategy.ReviewStrategy -import com.costular.core.Either -import com.costular.core.usecase.UseCase +import com.costular.atomtasks.core.Either +import com.costular.atomtasks.core.usecase.UseCase import java.lang.Exception import javax.inject.Inject diff --git a/core/review/src/test/java/com/costular/atomtasks/review/usecase/ShouldAskReviewUseCaseTest.kt b/core/review/src/test/java/com/costular/atomtasks/review/usecase/ShouldAskReviewUseCaseTest.kt index f73156ef..c8801952 100644 --- a/core/review/src/test/java/com/costular/atomtasks/review/usecase/ShouldAskReviewUseCaseTest.kt +++ b/core/review/src/test/java/com/costular/atomtasks/review/usecase/ShouldAskReviewUseCaseTest.kt @@ -1,6 +1,6 @@ package com.costular.atomtasks.review.usecase -import com.costular.core.Either +import com.costular.atomtasks.core.Either import com.google.common.truth.Truth.assertThat import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameterInjector diff --git a/core/src/main/java/com/costular/core/Either.kt b/core/src/main/java/com/costular/atomtasks/core/Either.kt similarity index 94% rename from core/src/main/java/com/costular/core/Either.kt rename to core/src/main/java/com/costular/atomtasks/core/Either.kt index a9600108..e378513d 100644 --- a/core/src/main/java/com/costular/core/Either.kt +++ b/core/src/main/java/com/costular/atomtasks/core/Either.kt @@ -1,7 +1,7 @@ -package com.costular.core +package com.costular.atomtasks.core -import com.costular.core.Either.Error -import com.costular.core.Either.Result +import com.costular.atomtasks.core.Either.Error +import com.costular.atomtasks.core.Either.Result sealed class Either { data class Error(val error: Error) : Either() diff --git a/core/src/main/java/com/costular/core/InvokeStatus.kt b/core/src/main/java/com/costular/atomtasks/core/InvokeStatus.kt similarity index 82% rename from core/src/main/java/com/costular/core/InvokeStatus.kt rename to core/src/main/java/com/costular/atomtasks/core/InvokeStatus.kt index fb6bebba..368cd5e5 100644 --- a/core/src/main/java/com/costular/core/InvokeStatus.kt +++ b/core/src/main/java/com/costular/atomtasks/core/InvokeStatus.kt @@ -1,4 +1,4 @@ -package com.costular.core +package com.costular.atomtasks.core sealed class InvokeStatus object InvokeStarted : InvokeStatus() diff --git a/core/src/main/java/com/costular/core/di/DispatcherProviderModule.kt b/core/src/main/java/com/costular/atomtasks/core/di/DispatcherProviderModule.kt similarity index 65% rename from core/src/main/java/com/costular/core/di/DispatcherProviderModule.kt rename to core/src/main/java/com/costular/atomtasks/core/di/DispatcherProviderModule.kt index 0fde0698..594bba22 100644 --- a/core/src/main/java/com/costular/core/di/DispatcherProviderModule.kt +++ b/core/src/main/java/com/costular/atomtasks/core/di/DispatcherProviderModule.kt @@ -1,7 +1,7 @@ -package com.costular.core.di +package com.costular.atomtasks.core.di -import com.costular.core.net.AppDispatcherProvider -import com.costular.core.net.DispatcherProvider +import com.costular.atomtasks.core.net.AppDispatcherProvider +import com.costular.atomtasks.core.net.DispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/core/src/main/java/com/costular/core/net/AppDispatcherProvider.kt b/core/src/main/java/com/costular/atomtasks/core/net/AppDispatcherProvider.kt similarity index 89% rename from core/src/main/java/com/costular/core/net/AppDispatcherProvider.kt rename to core/src/main/java/com/costular/atomtasks/core/net/AppDispatcherProvider.kt index 3a54232e..b3594c66 100644 --- a/core/src/main/java/com/costular/core/net/AppDispatcherProvider.kt +++ b/core/src/main/java/com/costular/atomtasks/core/net/AppDispatcherProvider.kt @@ -1,4 +1,4 @@ -package com.costular.core.net +package com.costular.atomtasks.core.net import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers diff --git a/core/src/main/java/com/costular/core/net/DispatcherProvider.kt b/core/src/main/java/com/costular/atomtasks/core/net/DispatcherProvider.kt similarity index 82% rename from core/src/main/java/com/costular/core/net/DispatcherProvider.kt rename to core/src/main/java/com/costular/atomtasks/core/net/DispatcherProvider.kt index 80443ab1..ebc518e1 100644 --- a/core/src/main/java/com/costular/core/net/DispatcherProvider.kt +++ b/core/src/main/java/com/costular/atomtasks/core/net/DispatcherProvider.kt @@ -1,4 +1,4 @@ -package com.costular.core.net +package com.costular.atomtasks.core.net import kotlinx.coroutines.CoroutineDispatcher diff --git a/core/src/main/java/com/costular/core/usecase/UseCase.kt b/core/src/main/java/com/costular/atomtasks/core/usecase/UseCase.kt similarity index 91% rename from core/src/main/java/com/costular/core/usecase/UseCase.kt rename to core/src/main/java/com/costular/atomtasks/core/usecase/UseCase.kt index b8497f05..4395babc 100644 --- a/core/src/main/java/com/costular/core/usecase/UseCase.kt +++ b/core/src/main/java/com/costular/atomtasks/core/usecase/UseCase.kt @@ -1,4 +1,4 @@ -package com.costular.core.usecase +package com.costular.atomtasks.core.usecase import kotlinx.coroutines.flow.Flow diff --git a/core/src/main/java/com/costular/core/util/DateTimeFormatters.kt b/core/src/main/java/com/costular/atomtasks/core/util/DateTimeFormatters.kt similarity index 92% rename from core/src/main/java/com/costular/core/util/DateTimeFormatters.kt rename to core/src/main/java/com/costular/atomtasks/core/util/DateTimeFormatters.kt index 052033ff..be1cc959 100644 --- a/core/src/main/java/com/costular/core/util/DateTimeFormatters.kt +++ b/core/src/main/java/com/costular/atomtasks/core/util/DateTimeFormatters.kt @@ -1,4 +1,4 @@ -package com.costular.core.util +package com.costular.atomtasks.core.util import java.time.LocalTime import java.time.format.DateTimeFormatter diff --git a/core/src/main/java/com/costular/core/util/PredefinedTimes.kt b/core/src/main/java/com/costular/atomtasks/core/util/PredefinedTimes.kt similarity index 83% rename from core/src/main/java/com/costular/core/util/PredefinedTimes.kt rename to core/src/main/java/com/costular/atomtasks/core/util/PredefinedTimes.kt index 47c2ae15..8d754090 100644 --- a/core/src/main/java/com/costular/core/util/PredefinedTimes.kt +++ b/core/src/main/java/com/costular/atomtasks/core/util/PredefinedTimes.kt @@ -1,4 +1,4 @@ -package com.costular.core.util +package com.costular.atomtasks.core.util import java.time.LocalTime diff --git a/core/src/main/java/com/costular/core/util/SupportedLanguages.kt b/core/src/main/java/com/costular/atomtasks/core/util/SupportedLanguages.kt similarity index 71% rename from core/src/main/java/com/costular/core/util/SupportedLanguages.kt rename to core/src/main/java/com/costular/atomtasks/core/util/SupportedLanguages.kt index fee6b4e2..60535141 100644 --- a/core/src/main/java/com/costular/core/util/SupportedLanguages.kt +++ b/core/src/main/java/com/costular/atomtasks/core/util/SupportedLanguages.kt @@ -1,4 +1,4 @@ -package com.costular.core.util +package com.costular.atomtasks.core.util import java.util.Locale diff --git a/core/src/main/java/com/costular/atomtasks/core/util/TimeUtil.kt b/core/src/main/java/com/costular/atomtasks/core/util/TimeUtil.kt new file mode 100644 index 00000000..ce36fdb8 --- /dev/null +++ b/core/src/main/java/com/costular/atomtasks/core/util/TimeUtil.kt @@ -0,0 +1,16 @@ +package com.costular.atomtasks.core.util + +import java.time.Duration +import java.time.LocalTime + +fun getDelayUntil(time: LocalTime): Duration { + val now = LocalTime.now() + + return Duration.between(now, time).run { + if (isNegative) { + plusDays(1) + } else { + this + } + } +} diff --git a/core/src/main/java/com/costular/core/util/WeekUtil.kt b/core/src/main/java/com/costular/atomtasks/core/util/WeekUtil.kt similarity index 63% rename from core/src/main/java/com/costular/core/util/WeekUtil.kt rename to core/src/main/java/com/costular/atomtasks/core/util/WeekUtil.kt index 466c5141..550db6d1 100644 --- a/core/src/main/java/com/costular/core/util/WeekUtil.kt +++ b/core/src/main/java/com/costular/atomtasks/core/util/WeekUtil.kt @@ -1,27 +1,17 @@ -package com.costular.core.util +package com.costular.atomtasks.core.util import java.time.DayOfWeek import java.time.LocalDate -import java.time.temporal.TemporalAdjusters import java.time.temporal.WeekFields import java.util.Locale object WeekUtil { - - @Suppress("MagicNumber") - fun getWeekDays( - localDate: LocalDate, - firstDayOfWeek: DayOfWeek = getFirstDayOfWeek(), - ): List { - val start = localDate.with(TemporalAdjusters.previousOrSame(firstDayOfWeek)) - return (0..6) - .map { daysToAdd -> start.plusDays(daysToAdd.toLong()) } - .toList() - } - fun getFirstDayOfWeek(locale: Locale = Locale.getDefault()): DayOfWeek = WeekFields.of(locale).firstDayOfWeek + fun LocalDate.isWeekday(): Boolean = + dayOfWeek != DayOfWeek.SATURDAY && dayOfWeek != DayOfWeek.SUNDAY + fun LocalDate.isWeekend(): Boolean = dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY diff --git a/core/src/test/java/com/costular/core/EitherTest.kt b/core/src/test/java/com/costular/core/EitherTest.kt index 22006c65..83efde45 100644 --- a/core/src/test/java/com/costular/core/EitherTest.kt +++ b/core/src/test/java/com/costular/core/EitherTest.kt @@ -1,5 +1,8 @@ package com.costular.core +import com.costular.atomtasks.core.Either +import com.costular.atomtasks.core.getOrElse +import com.costular.atomtasks.core.orNull import com.google.common.truth.Truth import org.junit.Test diff --git a/core/src/test/java/com/costular/core/util/WeekUtilTest.kt b/core/src/test/java/com/costular/core/util/WeekUtilTest.kt index 21262964..fac55516 100644 --- a/core/src/test/java/com/costular/core/util/WeekUtilTest.kt +++ b/core/src/test/java/com/costular/core/util/WeekUtilTest.kt @@ -1,59 +1,14 @@ package com.costular.core.util -import com.costular.core.util.WeekUtil.findNextWeek -import com.costular.core.util.WeekUtil.findNextWeekend +import com.costular.atomtasks.core.util.WeekUtil.findNextWeek +import com.costular.atomtasks.core.util.WeekUtil.findNextWeekend import com.google.common.truth.Truth.assertThat -import java.time.DayOfWeek import java.time.LocalDate import java.util.Locale import org.junit.Test class WeekUtilTest { - @Test - fun `should show 13 nov as the last day of the week when 13 nov 2022 is passed given week starts on monday`() { - givenWeekStartsOnMonday() - - val day = LocalDate.of(2022, 11, 13) - - val result = WeekUtil.getWeekDays(day, DayOfWeek.MONDAY) - - assertThat(result.last()).isEqualTo(day) - } - - @Test - fun `should show 7 nov as the first day of the week when 13 nov 2022 is passed given week starts on monday`() { - givenWeekStartsOnMonday() - - val day = LocalDate.of(2022, 11, 13) - val expected = LocalDate.of(2022, 11, 7) - - val result = WeekUtil.getWeekDays(day, DayOfWeek.MONDAY) - - assertThat(result.first()).isEqualTo(expected) - } - - @Test - fun `should show 13 nov as the first day of the week when 13 nov 2022 is passed given week starts on sunday`() { - val day = LocalDate.of(2022, 11, 13) - - val result = WeekUtil.getWeekDays(day, DayOfWeek.SUNDAY) - - assertThat(result.first()).isEqualTo(day) - } - - @Test - fun `should show 16 nov as the last day of the week when 13 nov 2022 is passed given week starts on sunday`() { - givenWeekStartsOnMonday() - - val day = LocalDate.of(2022, 11, 13) - val expected = LocalDate.of(2022, 11, 19) - - val result = WeekUtil.getWeekDays(day, DayOfWeek.SUNDAY) - - assertThat(result.last()).isEqualTo(expected) - } - @Test fun `Should return 2023-10-07 (Saturday) when nextWeekend called given date is 2023-10-02 (Monday)`() { givenWeekStartsOnMonday() diff --git a/core/testing/src/main/java/com/costular/atomtasks/core/testing/di/TestDispatcherProviderModule.kt b/core/testing/src/main/java/com/costular/atomtasks/core/testing/di/TestDispatcherProviderModule.kt index 8eb1cc28..c05bfbce 100644 --- a/core/testing/src/main/java/com/costular/atomtasks/core/testing/di/TestDispatcherProviderModule.kt +++ b/core/testing/src/main/java/com/costular/atomtasks/core/testing/di/TestDispatcherProviderModule.kt @@ -1,8 +1,8 @@ package com.costular.atomtasks.core.testing.di import com.costular.atomtasks.core.testing.net.TestDispatcherProvider -import com.costular.core.di.DispatcherProviderModule -import com.costular.core.net.DispatcherProvider +import com.costular.atomtasks.core.di.DispatcherProviderModule +import com.costular.atomtasks.core.net.DispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.components.SingletonComponent diff --git a/core/testing/src/main/java/com/costular/atomtasks/core/testing/net/TestDispatcherProvider.kt b/core/testing/src/main/java/com/costular/atomtasks/core/testing/net/TestDispatcherProvider.kt index ef28e10d..02aef480 100644 --- a/core/testing/src/main/java/com/costular/atomtasks/core/testing/net/TestDispatcherProvider.kt +++ b/core/testing/src/main/java/com/costular/atomtasks/core/testing/net/TestDispatcherProvider.kt @@ -1,6 +1,6 @@ package com.costular.atomtasks.core.testing.net -import com.costular.core.net.DispatcherProvider +import com.costular.atomtasks.core.net.DispatcherProvider import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.test.TestDispatcher diff --git a/core/ui/src/main/java/com/costular/atomtasks/coreui/DestinationsScaffold.kt b/core/ui/src/main/java/com/costular/atomtasks/core/ui/DestinationsScaffold.kt similarity index 100% rename from core/ui/src/main/java/com/costular/atomtasks/coreui/DestinationsScaffold.kt rename to core/ui/src/main/java/com/costular/atomtasks/core/ui/DestinationsScaffold.kt diff --git a/core/ui/src/main/java/com/costular/atomtasks/coreui/date/Day.kt b/core/ui/src/main/java/com/costular/atomtasks/core/ui/date/Day.kt similarity index 78% rename from core/ui/src/main/java/com/costular/atomtasks/coreui/date/Day.kt rename to core/ui/src/main/java/com/costular/atomtasks/core/ui/date/Day.kt index 11e2ab74..ea005a24 100644 --- a/core/ui/src/main/java/com/costular/atomtasks/coreui/date/Day.kt +++ b/core/ui/src/main/java/com/costular/atomtasks/core/ui/date/Day.kt @@ -1,4 +1,4 @@ -package com.costular.atomtasks.coreui.date +package com.costular.atomtasks.core.ui.date import androidx.compose.runtime.Immutable import java.time.LocalDate diff --git a/core/ui/src/main/java/com/costular/atomtasks/coreui/mvi/MviViewModel.kt b/core/ui/src/main/java/com/costular/atomtasks/core/ui/mvi/MviViewModel.kt similarity index 100% rename from core/ui/src/main/java/com/costular/atomtasks/coreui/mvi/MviViewModel.kt rename to core/ui/src/main/java/com/costular/atomtasks/core/ui/mvi/MviViewModel.kt diff --git a/core/ui/src/main/java/com/costular/atomtasks/coreui/mvi/UiEvent.kt b/core/ui/src/main/java/com/costular/atomtasks/core/ui/mvi/UiEvent.kt similarity index 100% rename from core/ui/src/main/java/com/costular/atomtasks/coreui/mvi/UiEvent.kt rename to core/ui/src/main/java/com/costular/atomtasks/core/ui/mvi/UiEvent.kt diff --git a/core/ui/src/main/java/com/costular/atomtasks/coreui/utils/DarkPreview.kt b/core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/DarkPreview.kt similarity index 85% rename from core/ui/src/main/java/com/costular/atomtasks/coreui/utils/DarkPreview.kt rename to core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/DarkPreview.kt index 50ebd20f..d3b56c22 100644 --- a/core/ui/src/main/java/com/costular/atomtasks/coreui/utils/DarkPreview.kt +++ b/core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/DarkPreview.kt @@ -1,4 +1,4 @@ -package com.costular.atomtasks.coreui.utils +package com.costular.atomtasks.core.ui.utils import android.content.res.Configuration import androidx.compose.ui.tooling.preview.Preview diff --git a/core/ui/src/main/java/com/costular/atomtasks/coreui/utils/DateTimeExtensions.kt b/core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/DateTimeExtensions.kt similarity index 77% rename from core/ui/src/main/java/com/costular/atomtasks/coreui/utils/DateTimeExtensions.kt rename to core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/DateTimeExtensions.kt index ad20ce8a..7c037a8d 100644 --- a/core/ui/src/main/java/com/costular/atomtasks/coreui/utils/DateTimeExtensions.kt +++ b/core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/DateTimeExtensions.kt @@ -1,15 +1,16 @@ -package com.costular.atomtasks.coreui.utils +package com.costular.atomtasks.core.ui.utils import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalConfiguration -import com.costular.core.util.DateTimeFormatters.formatTime +import com.costular.atomtasks.core.util.DateTimeFormatters.formatTime import java.time.DayOfWeek import java.time.LocalTime import java.time.YearMonth import java.time.format.DateTimeFormatter import java.time.format.TextStyle import java.time.temporal.TemporalAccessor +import java.time.temporal.WeekFields @Composable fun LocalTime.ofLocalizedTime(): String { @@ -42,3 +43,11 @@ fun DateTimeFormatter.ofLocalized(temporalAccessor: TemporalAccessor): String { this.withLocale(locale).format(temporalAccessor) } } + +@Composable +fun rememberFirstDayOfWeek(): DayOfWeek { + val locale = LocalConfiguration.current.locale + return remember(locale) { + WeekFields.of(locale).firstDayOfWeek + } +} diff --git a/core/ui/src/main/java/com/costular/atomtasks/coreui/utils/DateUtils.kt b/core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/DateUtils.kt similarity index 91% rename from core/ui/src/main/java/com/costular/atomtasks/coreui/utils/DateUtils.kt rename to core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/DateUtils.kt index 4266dfe8..17d2b22a 100644 --- a/core/ui/src/main/java/com/costular/atomtasks/coreui/utils/DateUtils.kt +++ b/core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/DateUtils.kt @@ -3,11 +3,10 @@ package com.costular.atomtasks.core.ui.utils import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.costular.atomtasks.core.ui.R -import com.costular.core.util.DateTimeFormatters +import com.costular.atomtasks.core.util.DateTimeFormatters import java.time.LocalDate object DateUtils { - @Composable fun dayAsText(day: LocalDate): String { return when (day) { diff --git a/core/ui/src/main/java/com/costular/atomtasks/coreui/utils/DevicesPreview.kt b/core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/DevicesPreview.kt similarity index 100% rename from core/ui/src/main/java/com/costular/atomtasks/coreui/utils/DevicesPreview.kt rename to core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/DevicesPreview.kt diff --git a/core/ui/src/main/java/com/costular/atomtasks/coreui/utils/LargeFontPreview.kt b/core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/LargeFontPreview.kt similarity index 79% rename from core/ui/src/main/java/com/costular/atomtasks/coreui/utils/LargeFontPreview.kt rename to core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/LargeFontPreview.kt index 56fc8500..6f4e1cec 100644 --- a/core/ui/src/main/java/com/costular/atomtasks/coreui/utils/LargeFontPreview.kt +++ b/core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/LargeFontPreview.kt @@ -1,4 +1,4 @@ -package com.costular.atomtasks.coreui.utils +package com.costular.atomtasks.core.ui.utils import androidx.compose.ui.tooling.preview.Preview diff --git a/core/ui/src/main/java/com/costular/atomtasks/coreui/utils/PreviewUtils.kt b/core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/PreviewUtils.kt similarity index 100% rename from core/ui/src/main/java/com/costular/atomtasks/coreui/utils/PreviewUtils.kt rename to core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/PreviewUtils.kt diff --git a/core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/VariantsPreview.kt b/core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/VariantsPreview.kt new file mode 100644 index 00000000..273b1735 --- /dev/null +++ b/core/ui/src/main/java/com/costular/atomtasks/core/ui/utils/VariantsPreview.kt @@ -0,0 +1,5 @@ +package com.costular.atomtasks.core.ui.utils + +@LargeFontPreview +@DarkPreview +annotation class VariantsPreview diff --git a/core/ui/src/main/java/com/costular/atomtasks/coreui/utils/VariantsPreview.kt b/core/ui/src/main/java/com/costular/atomtasks/coreui/utils/VariantsPreview.kt deleted file mode 100644 index 0d4154f4..00000000 --- a/core/ui/src/main/java/com/costular/atomtasks/coreui/utils/VariantsPreview.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.costular.atomtasks.core.ui.utils - -import com.costular.atomtasks.coreui.utils.DarkPreview -import com.costular.atomtasks.coreui.utils.LargeFontPreview - -@LargeFontPreview -@DarkPreview -annotation class VariantsPreview diff --git a/core/ui/src/main/res/values-es/strings.xml b/core/ui/src/main/res/values-es/strings.xml index 748e6d71..f55eb15c 100644 --- a/core/ui/src/main/res/values-es/strings.xml +++ b/core/ui/src/main/res/values-es/strings.xml @@ -19,6 +19,16 @@ Nueva tarea Editar tarea Hoy + Repetir + Diario + Días laborables + Semanal + Mensual + Anual + Todos los días + Cada semana + Cada mes + Cada año ¿Estás seguro que quieres eliminar esta tarea? Recordatorio El recordatorio ya ha pasado, escoge un momento en el futuro @@ -67,4 +77,9 @@ Vale Sobre Atom ¿Quieres ordenar tus tareas? Mantén presionado y arrastra + Todos los días laborables + No se repite + Esta tarea + Esta tarea y las posteriores + Todas las tareas diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 960bea52..b96e496c 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -32,6 +32,18 @@ Today Choose a Date Set reminder + Repeat + Daily + Weekdays + Weekly + Monthly + Yearly + Does not repeat + Every Day + Every Weekday + Every Week + Every Month + Every Year Are you sure to remove this task? No reminder Enable reminder @@ -80,5 +92,8 @@ To use the reminder feature, we require the notification permission. Without it, reminders won\'t work at all. No thanks I\'m in + This task + This and following tasks + All tasks diff --git a/data/schemas/com.costular.atomtasks.data.database.AtomTasksDatabase/6.json b/data/schemas/com.costular.atomtasks.data.database.AtomTasksDatabase/6.json index bb10c9d8..4fdd6a25 100644 --- a/data/schemas/com.costular.atomtasks.data.database.AtomTasksDatabase/6.json +++ b/data/schemas/com.costular.atomtasks.data.database.AtomTasksDatabase/6.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 6, - "identityHash": "0fb719da15b9a11591a0ec26f11628e6", + "identityHash": "9c05b2aae06c9803e7a94304a84a76ad", "entities": [ { "tableName": "tasks", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `created_at` TEXT NOT NULL, `name` TEXT NOT NULL, `date` TEXT NOT NULL, `is_done` INTEGER NOT NULL, `position` INTEGER NOT NULL DEFAULT 0)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `created_at` TEXT NOT NULL, `name` TEXT NOT NULL, `date` TEXT NOT NULL, `is_done` INTEGER NOT NULL, `position` INTEGER NOT NULL DEFAULT 0, `is_recurring` INTEGER NOT NULL DEFAULT 0, `recurrence_type` TEXT, `recurrence_end_date` TEXT, `parent_id` INTEGER, FOREIGN KEY(`parent_id`) REFERENCES `tasks`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", "fields": [ { "fieldPath": "id", @@ -44,6 +44,31 @@ "affinity": "INTEGER", "notNull": true, "defaultValue": "0" + }, + { + "fieldPath": "isRecurring", + "columnName": "is_recurring", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "recurrenceType", + "columnName": "recurrence_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recurrenceEndDate", + "columnName": "recurrence_end_date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "INTEGER", + "notNull": false } ], "primaryKey": { @@ -71,9 +96,39 @@ ], "orders": [], "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tasks_position_date` ON `${TABLE_NAME}` (`position`, `date`)" + }, + { + "name": "index_tasks_date", + "unique": false, + "columnNames": [ + "date" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tasks_date` ON `${TABLE_NAME}` (`date`)" + }, + { + "name": "index_tasks_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tasks_parent_id` ON `${TABLE_NAME}` (`parent_id`)" } ], - "foreignKeys": [] + "foreignKeys": [ + { + "table": "tasks", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "parent_id" + ], + "referencedColumns": [ + "id" + ] + } + ] }, { "tableName": "reminders", @@ -117,7 +172,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0fb719da15b9a11591a0ec26f11628e6')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9c05b2aae06c9803e7a94304a84a76ad')" ] } } \ No newline at end of file diff --git a/data/schemas/com.costular.atomtasks.data.database.AtomTasksDatabase/7.json b/data/schemas/com.costular.atomtasks.data.database.AtomTasksDatabase/7.json new file mode 100644 index 00000000..0ded37d3 --- /dev/null +++ b/data/schemas/com.costular.atomtasks.data.database.AtomTasksDatabase/7.json @@ -0,0 +1,178 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "9c05b2aae06c9803e7a94304a84a76ad", + "entities": [ + { + "tableName": "tasks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `created_at` TEXT NOT NULL, `name` TEXT NOT NULL, `date` TEXT NOT NULL, `is_done` INTEGER NOT NULL, `position` INTEGER NOT NULL DEFAULT 0, `is_recurring` INTEGER NOT NULL DEFAULT 0, `recurrence_type` TEXT, `recurrence_end_date` TEXT, `parent_id` INTEGER, FOREIGN KEY(`parent_id`) REFERENCES `tasks`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "day", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isRecurring", + "columnName": "is_recurring", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "recurrenceType", + "columnName": "recurrence_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recurrenceEndDate", + "columnName": "recurrence_end_date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_tasks_created_at", + "unique": false, + "columnNames": [ + "created_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tasks_created_at` ON `${TABLE_NAME}` (`created_at`)" + }, + { + "name": "index_tasks_position_date", + "unique": true, + "columnNames": [ + "position", + "date" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tasks_position_date` ON `${TABLE_NAME}` (`position`, `date`)" + }, + { + "name": "index_tasks_date", + "unique": false, + "columnNames": [ + "date" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tasks_date` ON `${TABLE_NAME}` (`date`)" + }, + { + "name": "index_tasks_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tasks_parent_id` ON `${TABLE_NAME}` (`parent_id`)" + } + ], + "foreignKeys": [ + { + "table": "tasks", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "parent_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "reminders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reminder_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `time` TEXT NOT NULL, `date` TEXT NOT NULL, `task_id` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "reminderId", + "columnName": "reminder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "taskId", + "columnName": "task_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "reminder_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9c05b2aae06c9803e7a94304a84a76ad')" + ] + } +} \ No newline at end of file diff --git a/data/src/androidTest/assets/com.costular.atomtasks.data.database.AtomTasksDatabase/6.json b/data/src/androidTest/assets/com.costular.atomtasks.data.database.AtomTasksDatabase/6.json index bb10c9d8..4fdd6a25 100644 --- a/data/src/androidTest/assets/com.costular.atomtasks.data.database.AtomTasksDatabase/6.json +++ b/data/src/androidTest/assets/com.costular.atomtasks.data.database.AtomTasksDatabase/6.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 6, - "identityHash": "0fb719da15b9a11591a0ec26f11628e6", + "identityHash": "9c05b2aae06c9803e7a94304a84a76ad", "entities": [ { "tableName": "tasks", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `created_at` TEXT NOT NULL, `name` TEXT NOT NULL, `date` TEXT NOT NULL, `is_done` INTEGER NOT NULL, `position` INTEGER NOT NULL DEFAULT 0)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `created_at` TEXT NOT NULL, `name` TEXT NOT NULL, `date` TEXT NOT NULL, `is_done` INTEGER NOT NULL, `position` INTEGER NOT NULL DEFAULT 0, `is_recurring` INTEGER NOT NULL DEFAULT 0, `recurrence_type` TEXT, `recurrence_end_date` TEXT, `parent_id` INTEGER, FOREIGN KEY(`parent_id`) REFERENCES `tasks`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", "fields": [ { "fieldPath": "id", @@ -44,6 +44,31 @@ "affinity": "INTEGER", "notNull": true, "defaultValue": "0" + }, + { + "fieldPath": "isRecurring", + "columnName": "is_recurring", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "recurrenceType", + "columnName": "recurrence_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recurrenceEndDate", + "columnName": "recurrence_end_date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "INTEGER", + "notNull": false } ], "primaryKey": { @@ -71,9 +96,39 @@ ], "orders": [], "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tasks_position_date` ON `${TABLE_NAME}` (`position`, `date`)" + }, + { + "name": "index_tasks_date", + "unique": false, + "columnNames": [ + "date" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tasks_date` ON `${TABLE_NAME}` (`date`)" + }, + { + "name": "index_tasks_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tasks_parent_id` ON `${TABLE_NAME}` (`parent_id`)" } ], - "foreignKeys": [] + "foreignKeys": [ + { + "table": "tasks", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "parent_id" + ], + "referencedColumns": [ + "id" + ] + } + ] }, { "tableName": "reminders", @@ -117,7 +172,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0fb719da15b9a11591a0ec26f11628e6')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9c05b2aae06c9803e7a94304a84a76ad')" ] } } \ No newline at end of file diff --git a/data/src/androidTest/assets/com.costular.atomtasks.data.database.AtomTasksDatabase/7.json b/data/src/androidTest/assets/com.costular.atomtasks.data.database.AtomTasksDatabase/7.json new file mode 100644 index 00000000..0ded37d3 --- /dev/null +++ b/data/src/androidTest/assets/com.costular.atomtasks.data.database.AtomTasksDatabase/7.json @@ -0,0 +1,178 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "9c05b2aae06c9803e7a94304a84a76ad", + "entities": [ + { + "tableName": "tasks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `created_at` TEXT NOT NULL, `name` TEXT NOT NULL, `date` TEXT NOT NULL, `is_done` INTEGER NOT NULL, `position` INTEGER NOT NULL DEFAULT 0, `is_recurring` INTEGER NOT NULL DEFAULT 0, `recurrence_type` TEXT, `recurrence_end_date` TEXT, `parent_id` INTEGER, FOREIGN KEY(`parent_id`) REFERENCES `tasks`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "day", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isRecurring", + "columnName": "is_recurring", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "recurrenceType", + "columnName": "recurrence_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recurrenceEndDate", + "columnName": "recurrence_end_date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_tasks_created_at", + "unique": false, + "columnNames": [ + "created_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tasks_created_at` ON `${TABLE_NAME}` (`created_at`)" + }, + { + "name": "index_tasks_position_date", + "unique": true, + "columnNames": [ + "position", + "date" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tasks_position_date` ON `${TABLE_NAME}` (`position`, `date`)" + }, + { + "name": "index_tasks_date", + "unique": false, + "columnNames": [ + "date" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tasks_date` ON `${TABLE_NAME}` (`date`)" + }, + { + "name": "index_tasks_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tasks_parent_id` ON `${TABLE_NAME}` (`parent_id`)" + } + ], + "foreignKeys": [ + { + "table": "tasks", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "parent_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "reminders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reminder_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `time` TEXT NOT NULL, `date` TEXT NOT NULL, `task_id` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "reminderId", + "columnName": "reminder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "taskId", + "columnName": "task_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "reminder_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9c05b2aae06c9803e7a94304a84a76ad')" + ] + } +} \ No newline at end of file diff --git a/data/src/androidTest/java/com/costular/atomtasks/data/database/MigrationsTest.kt b/data/src/androidTest/java/com/costular/atomtasks/data/database/MigrationsTest.kt index 92ed9335..8c4ba1df 100644 --- a/data/src/androidTest/java/com/costular/atomtasks/data/database/MigrationsTest.kt +++ b/data/src/androidTest/java/com/costular/atomtasks/data/database/MigrationsTest.kt @@ -15,7 +15,7 @@ import org.junit.runner.RunWith class MigrationsTest { private val ALL_MIGRATIONS = arrayOf(MIGRATION_4_5) private val TEST_DB = "migration-test" - private val LATEST_VERSION = 6 + private val LATEST_VERSION = 7 @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( @@ -113,4 +113,30 @@ class MigrationsTest { } cursor.close() } + + @Test + @Throws(IOException::class) + fun testMigration6To7() { + val db = helper.createDatabase(TEST_DB, 6).apply { + execSQL( + "INSERT INTO tasks (created_at, name, date, is_done) VALUES " + + "('2023-08-23', 'This is a test', '2023-08-23', 0); " + ) + } + + helper.runMigrationsAndValidate(TEST_DB, 7, true) + + val cursor = db.query("SELECT * FROM tasks", arrayOf()) + + if (cursor.moveToFirst()) { + val positionColumnIndex = cursor.getColumnIndex("name") + + if (positionColumnIndex != -1) { + cursor.moveToFirst() + val name = cursor.getString(positionColumnIndex) + Truth.assertThat(name).isEqualTo("This is a test") + } + } + cursor.close() + } } diff --git a/data/src/androidTest/java/com/costular/atomtasks/data/database/TaskDatabaseTest.kt b/data/src/androidTest/java/com/costular/atomtasks/data/database/TaskDatabaseTest.kt index 559dbd22..804af1c7 100644 --- a/data/src/androidTest/java/com/costular/atomtasks/data/database/TaskDatabaseTest.kt +++ b/data/src/androidTest/java/com/costular/atomtasks/data/database/TaskDatabaseTest.kt @@ -7,7 +7,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import app.cash.turbine.test import com.costular.atomtasks.data.tasks.TaskEntity import com.costular.atomtasks.data.tasks.TasksDao -import com.google.common.truth.Truth +import com.google.common.truth.Truth.assertThat import java.io.IOException import java.time.LocalDate import kotlin.time.ExperimentalTime @@ -56,8 +56,8 @@ class TaskDatabaseTest { tasksDao.observeAllTasks().test { val item = awaitItem() - Truth.assertThat(item.first().task.name).isEqualTo("whatever") - Truth.assertThat(item.size).isEqualTo(1) + assertThat(item.first().task.name).isEqualTo("whatever") + assertThat(item.size).isEqualTo(1) cancelAndIgnoreRemainingEvents() } } @@ -83,7 +83,7 @@ class TaskDatabaseTest { tasksDao.createTask(task2) val result = tasksDao.observeAllTasks().first() - Truth.assertThat(result.size).isEqualTo(2) + assertThat(result.size).isEqualTo(2) } @Test @@ -109,8 +109,8 @@ class TaskDatabaseTest { tasksDao.addTask(task1) val result = tasksDao.observeAllTasks().first() - Truth.assertThat(result.first().task.position).isEqualTo(1) - Truth.assertThat(result.last().task.position).isEqualTo(2) + assertThat(result.first().task.position).isEqualTo(1) + assertThat(result.last().task.position).isEqualTo(2) } @Test @@ -137,7 +137,7 @@ class TaskDatabaseTest { tasksDao.updateTaskPosition(task1Id, 3) val result = tasksDao.observeAllTasks().first() - Truth.assertThat(result.last().task.id).isEqualTo(task1Id) + assertThat(result.last().task.id).isEqualTo(task1Id) } @Test @@ -163,7 +163,7 @@ class TaskDatabaseTest { tasksDao.createTask(task2) val result = tasksDao.getMaxPositionForDate(LocalDate.now()) - Truth.assertThat(result).isEqualTo(1) + assertThat(result).isEqualTo(1) } @Test @@ -199,9 +199,9 @@ class TaskDatabaseTest { tasksDao.moveTask(LocalDate.now(), 1, 3) val result = tasksDao.observeAllTasks().first() - Truth.assertThat(result.find { it.task.id == id2 }!!.task.position).isEqualTo(1) - Truth.assertThat(result.find { it.task.id == id3 }!!.task.position).isEqualTo(2) - Truth.assertThat(result.find { it.task.id == id1 }!!.task.position).isEqualTo(3) + assertThat(result.find { it.task.id == id2 }!!.task.position).isEqualTo(1) + assertThat(result.find { it.task.id == id3 }!!.task.position).isEqualTo(2) + assertThat(result.find { it.task.id == id1 }!!.task.position).isEqualTo(3) } @Test @@ -255,11 +255,11 @@ class TaskDatabaseTest { tasksDao.moveTask(LocalDate.now(), 1, 2) val result = tasksDao.observeAllTasks().first() - Truth.assertThat(result.find { it.task.id == id1 }!!.task.position).isEqualTo(1) - Truth.assertThat(result.find { it.task.id == id2 }!!.task.position).isEqualTo(2) - Truth.assertThat(result.find { it.task.id == id3 }!!.task.position).isEqualTo(3) - Truth.assertThat(result.find { it.task.id == id12 }!!.task.position).isEqualTo(1) - Truth.assertThat(result.find { it.task.id == id11 }!!.task.position).isEqualTo(2) + assertThat(result.find { it.task.id == id1 }!!.task.position).isEqualTo(1) + assertThat(result.find { it.task.id == id2 }!!.task.position).isEqualTo(2) + assertThat(result.find { it.task.id == id3 }!!.task.position).isEqualTo(3) + assertThat(result.find { it.task.id == id12 }!!.task.position).isEqualTo(1) + assertThat(result.find { it.task.id == id11 }!!.task.position).isEqualTo(2) } @Test @@ -295,9 +295,9 @@ class TaskDatabaseTest { tasksDao.moveTask(LocalDate.now(), 3, 1) val result = tasksDao.observeAllTasks().first() - Truth.assertThat(result.find { it.task.id == id3 }!!.task.position).isEqualTo(1) - Truth.assertThat(result.find { it.task.id == id1 }!!.task.position).isEqualTo(2) - Truth.assertThat(result.find { it.task.id == id2 }!!.task.position).isEqualTo(3) + assertThat(result.find { it.task.id == id3 }!!.task.position).isEqualTo(1) + assertThat(result.find { it.task.id == id1 }!!.task.position).isEqualTo(2) + assertThat(result.find { it.task.id == id2 }!!.task.position).isEqualTo(3) } @Test @@ -325,8 +325,8 @@ class TaskDatabaseTest { tasksDao.moveTask(LocalDate.now(), 2, 1) val result = tasksDao.observeAllTasks().first() - Truth.assertThat(result.find { it.task.id == id1 }!!.task.position).isEqualTo(2) - Truth.assertThat(result.find { it.task.id == id2 }!!.task.position).isEqualTo(1) + assertThat(result.find { it.task.id == id1 }!!.task.position).isEqualTo(2) + assertThat(result.find { it.task.id == id2 }!!.task.position).isEqualTo(1) } @Test @@ -356,6 +356,178 @@ class TaskDatabaseTest { val result = tasksDao.getDoneTasksCount() - Truth.assertThat(result).isEqualTo(2) + assertThat(result).isEqualTo(2) + } + + @Test + fun testAddTaskWithRecurrence() = runTest { + val task = TaskEntity( + id = 0L, + createdAt = LocalDate.now(), + name = "whatever", + day = LocalDate.now(), + isDone = false, + position = 1, + isRecurring = true, + recurrenceType = "daily" + ) + + val id = tasksDao.createTask(task) + val taskAggregated = tasksDao.getTaskById(id).first() + + assertThat(taskAggregated.task.recurrenceType).isEqualTo("daily") + } + + @Test + fun testRemoveAllRecurringTasksByChild() = runTest { + val task = TaskEntity( + id = 0L, + createdAt = LocalDate.now(), + name = "Parent", + day = LocalDate.now(), + isDone = false, + position = 1, + isRecurring = true, + recurrenceType = "daily" + ) + val parentTaskId = tasksDao.addTask(task) + + val childTask = TaskEntity( + id = 0L, + createdAt = LocalDate.now(), + name = "Child1", + day = LocalDate.now().plusDays(1), + isDone = false, + isRecurring = true, + recurrenceType = "daily", + parentId = parentTaskId, + ) + val secondChildTask = TaskEntity( + id = 0L, + createdAt = LocalDate.now(), + name = "Child2", + day = LocalDate.now().plusDays(1), + isDone = false, + isRecurring = true, + recurrenceType = "daily", + parentId = parentTaskId, + ) + val childId = tasksDao.addTask(childTask) + val secondChildId = tasksDao.addTask(secondChildTask) + + tasksDao.removeTaskAndAllOccurrences(childId, parentTaskId) + + assertThat(tasksDao.getAllTasks().size).isEqualTo(0) + } + + @Test + fun testRemoveAllRecurringTasksByParent() = runTest { + val task = TaskEntity( + id = 0L, + createdAt = LocalDate.now(), + name = "Parent", + day = LocalDate.now(), + isDone = false, + position = 1, + isRecurring = true, + recurrenceType = "daily" + ) + val parentTaskId = tasksDao.addTask(task) + val recurringTask = TaskEntity( + id = 0L, + createdAt = LocalDate.now(), + name = "Recurrent Task", + day = LocalDate.now().plusDays(1), + isDone = false, + isRecurring = true, + recurrenceType = "daily", + parentId = parentTaskId, + ) + val secondRecurringTask = TaskEntity( + id = 0L, + createdAt = LocalDate.now(), + name = "Recurrent Task", + day = LocalDate.now().plusDays(1), + isDone = false, + isRecurring = true, + recurrenceType = "daily", + parentId = parentTaskId, + ) + + val childId = tasksDao.addTask(recurringTask) + val secondChildId = tasksDao.addTask(secondRecurringTask) + + tasksDao.removeTaskAndAllOccurrences(parentTaskId, parentTaskId) + + assertThat(tasksDao.getAllTasks().size).isEqualTo(0) + } + + @Test + fun testRemoveFutureRecurringTasks() = runTest { + val task = TaskEntity( + id = 0L, + createdAt = LocalDate.now(), + name = "Parent", + day = LocalDate.now(), + isDone = false, + position = 1, + isRecurring = true, + recurrenceType = "daily" + ) + val parentTaskId = tasksDao.addTask(task) + + val child = TaskEntity( + id = 0L, + createdAt = LocalDate.now(), + name = "Recurrent Task", + day = LocalDate.now(), + isDone = false, + isRecurring = true, + recurrenceType = "daily", + parentId = parentTaskId, + ) + val secondChild = TaskEntity( + id = 0L, + createdAt = LocalDate.now(), + name = "Recurrent Task", + day = LocalDate.now().plusDays(1), + isDone = false, + isRecurring = true, + recurrenceType = "daily", + parentId = parentTaskId, + ) + val thirdChild = TaskEntity( + id = 0L, + createdAt = LocalDate.now(), + name = "Recurrent Task", + day = LocalDate.now().plusDays(2), + isDone = false, + isRecurring = true, + recurrenceType = "daily", + parentId = parentTaskId, + ) + val fourthChild = TaskEntity( + id = 0L, + createdAt = LocalDate.now(), + name = "Recurrent Task", + day = LocalDate.now().plusDays(2), + isDone = false, + isRecurring = true, + recurrenceType = "daily", + parentId = parentTaskId, + ) + + val childId = tasksDao.addTask(child) + val secondChildId = tasksDao.addTask(secondChild) + val thirdChildId = tasksDao.addTask(thirdChild) + val fourthChildId = tasksDao.addTask(fourthChild) + + tasksDao.removeTaskAndFutureOcurrences(secondChildId, parentTaskId) + + val result = tasksDao.getAllTasks() + assertThat(result.size).isEqualTo(2) + assertThat(result.find { it.task.id == secondChildId }).isNull() + assertThat(result.find { it.task.id == thirdChildId }).isNull() + assertThat(result.find { it.task.id == fourthChildId }).isNull() } } diff --git a/data/src/main/java/com/costular/atomtasks/data/Interactor.kt b/data/src/main/java/com/costular/atomtasks/data/Interactor.kt index 7b3ebd90..8746e8dd 100644 --- a/data/src/main/java/com/costular/atomtasks/data/Interactor.kt +++ b/data/src/main/java/com/costular/atomtasks/data/Interactor.kt @@ -1,9 +1,9 @@ package com.costular.atomtasks.data -import com.costular.core.InvokeError -import com.costular.core.InvokeStarted -import com.costular.core.InvokeStatus -import com.costular.core.InvokeSuccess +import com.costular.atomtasks.core.InvokeError +import com.costular.atomtasks.core.InvokeStarted +import com.costular.atomtasks.core.InvokeStatus +import com.costular.atomtasks.core.InvokeSuccess import java.util.concurrent.TimeUnit import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow diff --git a/data/src/main/java/com/costular/atomtasks/data/database/AtomTasksDatabase.kt b/data/src/main/java/com/costular/atomtasks/data/database/AtomTasksDatabase.kt index db6ef5e4..11900620 100644 --- a/data/src/main/java/com/costular/atomtasks/data/database/AtomTasksDatabase.kt +++ b/data/src/main/java/com/costular/atomtasks/data/database/AtomTasksDatabase.kt @@ -12,13 +12,17 @@ import com.costular.atomtasks.data.tasks.TasksDao @TypeConverters(DbTypeConverters::class) @Database( entities = [TaskEntity::class, ReminderEntity::class], - version = 6, + version = 7, exportSchema = true, autoMigrations = [ AutoMigration( from = 5, to = 6, spec = MigrationDeleteIsEnabledReminder::class, + ), + AutoMigration( + from = 6, + to = 7, ) ] ) diff --git a/data/src/main/java/com/costular/atomtasks/data/settings/IsAutoforwardTasksSettingEnabledUseCase.kt b/data/src/main/java/com/costular/atomtasks/data/settings/IsAutoforwardTasksSettingEnabledUseCase.kt index 72994a60..2352e6c0 100644 --- a/data/src/main/java/com/costular/atomtasks/data/settings/IsAutoforwardTasksSettingEnabledUseCase.kt +++ b/data/src/main/java/com/costular/atomtasks/data/settings/IsAutoforwardTasksSettingEnabledUseCase.kt @@ -1,6 +1,6 @@ package com.costular.atomtasks.data.settings -import com.costular.core.usecase.ObservableUseCase +import com.costular.atomtasks.core.usecase.ObservableUseCase import javax.inject.Inject import kotlinx.coroutines.flow.Flow diff --git a/data/src/main/java/com/costular/atomtasks/data/tasks/TaskEntity.kt b/data/src/main/java/com/costular/atomtasks/data/tasks/TaskEntity.kt index e09e1e05..8da891d9 100644 --- a/data/src/main/java/com/costular/atomtasks/data/tasks/TaskEntity.kt +++ b/data/src/main/java/com/costular/atomtasks/data/tasks/TaskEntity.kt @@ -2,12 +2,21 @@ package com.costular.atomtasks.data.tasks import androidx.room.ColumnInfo import androidx.room.Entity +import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey import java.time.LocalDate @Entity( tableName = "tasks", + foreignKeys = [ + ForeignKey( + entity = TaskEntity::class, + parentColumns = arrayOf("id"), + childColumns = arrayOf("parent_id"), + onDelete = ForeignKey.SET_NULL, + ) + ], indices = [ Index( value = ["created_at"], @@ -17,6 +26,14 @@ import java.time.LocalDate value = ["position", "date"], unique = true, ), + Index( + value = ["date"], + unique = false, + ), + Index( + value = ["parent_id"], + unique = false, + ) ], ) data class TaskEntity( @@ -26,4 +43,10 @@ data class TaskEntity( @ColumnInfo(name = "date") val day: LocalDate, @ColumnInfo(name = "is_done") val isDone: Boolean = false, @ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0, -) + @ColumnInfo(name = "is_recurring", defaultValue = "0") val isRecurring: Boolean = false, + @ColumnInfo(name = "recurrence_type") val recurrenceType: String? = null, + @ColumnInfo(name = "recurrence_end_date") val recurrenceEndDate: LocalDate? = null, + @ColumnInfo(name = "parent_id") val parentId: Long? = null, +) { + val isParent: Boolean get() = isRecurring && recurrenceType != null && parentId == null +} diff --git a/data/src/main/java/com/costular/atomtasks/data/tasks/TasksDao.kt b/data/src/main/java/com/costular/atomtasks/data/tasks/TasksDao.kt index c9c955e6..3919a1b2 100644 --- a/data/src/main/java/com/costular/atomtasks/data/tasks/TasksDao.kt +++ b/data/src/main/java/com/costular/atomtasks/data/tasks/TasksDao.kt @@ -40,17 +40,80 @@ interface TasksDao { @Query("DELETE FROM tasks WHERE id = :id") suspend fun removeTaskById(id: Long) + @Query( + value = + """ + DELETE FROM tasks + WHERE + (id = :id + OR (parent_id = :parentId OR parent_id = (SELECT parent_id FROM tasks WHERE id = :id)) + AND date >= (SELECT date FROM tasks WHERE id = :id)) + """ + ) + suspend fun removeTaskAndFutureOcurrences(id: Long, parentId: Long) + + @Query( + value = + """ + DELETE FROM tasks + WHERE + id = :id + OR parent_id = :parentId + OR id = :parentId + """ + ) + suspend fun removeTaskAndAllOccurrences(id: Long, parentId: Long) + + @Query( + value = + """ + DELETE FROM tasks + WHERE + parent_id = :parentId + AND id != :parentId + """ + ) + suspend fun removeChildrenTasks(parentId: Long) + + @Query( + value = + """ + DELETE FROM tasks + WHERE parent_id = :parentId + AND date > (SELECT date FROM tasks WHERE id = :id) + AND id != :parentId; + """ + ) + suspend fun removeFutureOccurrencesForRecurringTask(id: Long, parentId: Long) + + @Query("SELECT COUNT(*) FROM tasks WHERE parent_id = :parentId AND date > :currentDate") + suspend fun countFutureOccurrences(parentId: Long, currentDate: LocalDate): Int + @Query("UPDATE tasks SET is_done = :isDone WHERE id = :id") suspend fun updateTaskDone(id: Long, isDone: Boolean) @Query("UPDATE tasks SET position = :position WHERE id = :id") suspend fun updateTaskPosition(id: Long, position: Int) - @Query("UPDATE tasks SET name = :name, date = :day WHERE id = :taskId") - suspend fun updateTask(taskId: Long, day: LocalDate, name: String) - - @Query("UPDATE tasks SET name = :name, date = :day, position = :position WHERE id = :taskId") - suspend fun updateTask(taskId: Long, day: LocalDate, name: String, position: Int) + @Query( + """ + UPDATE tasks SET + name = :name, + date = :day, + position = :position, + is_recurring = :isRecurring, + recurrence_type = :recurrence + WHERE id = :taskId + """ + ) + suspend fun updateTask( + taskId: Long, + day: LocalDate, + name: String, + position: Int, + isRecurring: Boolean, + recurrence: String?, + ) @Query("SELECT MAX(position) FROM tasks WHERE date = :day") suspend fun getMaxPositionForDate(day: LocalDate): Int diff --git a/data/src/main/java/com/costular/atomtasks/data/tutorial/ShouldShowTaskOrderTutorialUseCase.kt b/data/src/main/java/com/costular/atomtasks/data/tutorial/ShouldShowTaskOrderTutorialUseCase.kt index 86073537..4dbf2aab 100644 --- a/data/src/main/java/com/costular/atomtasks/data/tutorial/ShouldShowTaskOrderTutorialUseCase.kt +++ b/data/src/main/java/com/costular/atomtasks/data/tutorial/ShouldShowTaskOrderTutorialUseCase.kt @@ -1,6 +1,6 @@ package com.costular.atomtasks.data.tutorial -import com.costular.core.usecase.UseCase +import com.costular.atomtasks.core.usecase.UseCase import javax.inject.Inject import kotlinx.coroutines.flow.Flow diff --git a/data/src/main/java/com/costular/atomtasks/data/tutorial/TaskOrderTutorialDismissedUseCase.kt b/data/src/main/java/com/costular/atomtasks/data/tutorial/TaskOrderTutorialDismissedUseCase.kt index 879f53a3..ccb6d6e1 100644 --- a/data/src/main/java/com/costular/atomtasks/data/tutorial/TaskOrderTutorialDismissedUseCase.kt +++ b/data/src/main/java/com/costular/atomtasks/data/tutorial/TaskOrderTutorialDismissedUseCase.kt @@ -1,6 +1,6 @@ package com.costular.atomtasks.data.tutorial -import com.costular.core.usecase.UseCase +import com.costular.atomtasks.core.usecase.UseCase import javax.inject.Inject class TaskOrderTutorialDismissedUseCase @Inject constructor( diff --git a/feature/agenda/build.gradle.kts b/feature/agenda/build.gradle.kts index a19a54c0..b1163712 100644 --- a/feature/agenda/build.gradle.kts +++ b/feature/agenda/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { implementation(projects.common.tasks) implementation(projects.core.review) + implementation(projects.core.logging) implementation(libs.calendar) implementation(libs.kotlinx.collections.immutable) diff --git a/feature/agenda/src/androidTest/java/com/costular/atomtasks/agenda/AgendaTest.kt b/feature/agenda/src/androidTest/java/com/costular/atomtasks/agenda/AgendaTest.kt index d122a9b2..0fe420a6 100644 --- a/feature/agenda/src/androidTest/java/com/costular/atomtasks/agenda/AgendaTest.kt +++ b/feature/agenda/src/androidTest/java/com/costular/atomtasks/agenda/AgendaTest.kt @@ -4,15 +4,20 @@ import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSiz import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import com.costular.atomtasks.agenda.ui.AgendaScreen +import com.costular.atomtasks.agenda.ui.AgendaState +import com.costular.atomtasks.agenda.ui.TasksState import com.costular.atomtasks.core.testing.ui.AndroidTest import com.costular.atomtasks.core.testing.ui.getString import com.costular.atomtasks.core.ui.R -import com.costular.atomtasks.coreui.date.asDay +import com.costular.atomtasks.core.ui.date.asDay +import com.costular.atomtasks.tasks.model.RemovalStrategy import com.costular.atomtasks.tasks.model.Task import dagger.hilt.android.testing.HiltAndroidTest import java.time.LocalDate import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import org.burnoutcrew.reorderable.ItemPosition import org.junit.Test @HiltAndroidTest @@ -63,6 +68,10 @@ class AgendaTest : AndroidTest() { isDone = true, day = LocalDate.now(), position = 1, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ) givenAgenda( @@ -92,6 +101,10 @@ class AgendaTest : AndroidTest() { reminder = null, isDone = isDone, position = 1, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ), ) @@ -120,6 +133,10 @@ class AgendaTest : AndroidTest() { reminder = null, isDone = isDone, position = 1, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ), ) @@ -149,6 +166,10 @@ class AgendaTest : AndroidTest() { reminder = null, isDone = isDone, position = 1, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ), ) @@ -168,7 +189,6 @@ class AgendaTest : AndroidTest() { composeTestRule.setContent { AgendaScreen( state = state, - onMarkTask = { _, _ -> }, windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(480.dp, 800.dp)), onSelectDate = {}, onSelectToday = {}, @@ -176,9 +196,10 @@ class AgendaTest : AndroidTest() { dismissDelete = {}, openTaskAction = {}, onToggleExpandCollapse = {}, + onMarkTask = { long: Long, bool: Boolean -> }, + deleteRecurringTask = { id: Long, strategy: RemovalStrategy -> }, onMoveTask = { _, _ -> }, - onDragTask = { _, _ -> }, - onDismissTaskOrderTutorial = {}, + onDragTask = { from: ItemPosition, to: ItemPosition -> }, ) } } diff --git a/feature/agenda/src/main/java/com/costular/atomtasks/agenda/ui/AgendaHeader.kt b/feature/agenda/src/main/java/com/costular/atomtasks/agenda/ui/AgendaHeader.kt new file mode 100644 index 00000000..9ca083cf --- /dev/null +++ b/feature/agenda/src/main/java/com/costular/atomtasks/agenda/ui/AgendaHeader.kt @@ -0,0 +1,227 @@ +package com.costular.atomtasks.agenda.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.outlined.Today +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.costular.atomtasks.core.ui.R +import com.costular.atomtasks.core.ui.date.Day +import com.costular.atomtasks.core.ui.date.asDay +import com.costular.atomtasks.core.ui.utils.DateUtils +import com.costular.designsystem.components.DatePicker +import com.costular.designsystem.components.ScreenHeader +import com.costular.designsystem.components.WeekCalendar +import com.costular.designsystem.theme.AppTheme +import com.costular.designsystem.theme.AtomTheme +import com.costular.designsystem.util.supportWideScreen +import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState +import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState +import java.time.LocalDate +import kotlinx.coroutines.launch + +private const val DaysToShow = 365L + +@Suppress("LongMethod") +@Composable +internal fun AgendaHeader( + modifier: Modifier = Modifier, + selectedDay: Day, + onSelectDate: (LocalDate) -> Unit, + onSelectToday: () -> Unit, + canExpand: Boolean, + isExpanded: Boolean, + onToggleExpandCollapse: () -> Unit, +) { + val coroutineScope = rememberCoroutineScope() + val startDate = remember(selectedDay) { selectedDay.date.minusDays(DaysToShow) } + val endDate = remember(selectedDay) { selectedDay.date.plusDays(DaysToShow) } + + val weekCalendarState = rememberWeekCalendarState( + startDate = startDate, + endDate = endDate, + firstVisibleWeekDate = selectedDay.date, + ) + + val shadowElevation = if (isExpanded) { + 6.dp + } else { + 2.dp + } + + Surface( + modifier = modifier, + shape = RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp), + shadowElevation = shadowElevation, + ) { + Column { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + val selectedDayText = DateUtils.dayAsText(selectedDay.date) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .weight(1f) + .clickable(enabled = canExpand, onClick = onToggleExpandCollapse), + ) { + ScreenHeader( + text = selectedDayText, + modifier = Modifier + .testTag(TestTagHeader) + .padding( + top = AppTheme.dimens.spacingLarge, + bottom = AppTheme.dimens.spacingLarge, + start = AppTheme.dimens.spacingLarge, + ), + ) + + val degrees by animateFloatAsState( + targetValue = if (isExpanded) 180f else 0f, + animationSpec = tween( + durationMillis = 200, + easing = FastOutSlowInEasing, + ), + label = "Header collapsable arrow", + ) + + if (canExpand) { + IconButton(onClick = { onToggleExpandCollapse() }) { + Icon( + imageVector = Icons.Default.ExpandMore, + contentDescription = null, + modifier = Modifier.rotate(degrees), + ) + } + } + } + + IconButton( + onClick = { + coroutineScope.launch { + weekCalendarState.animateScrollToWeek(LocalDate.now()) + } + onSelectToday() + }, + modifier = Modifier + .padding(end = AppTheme.dimens.spacingLarge) + .width(40.dp) + .height(40.dp), + ) { + Icon( + imageVector = Icons.Outlined.Today, + contentDescription = stringResource(R.string.today), + ) + } + } + + AnimatedContent( + targetState = isExpanded && canExpand, + label = "Header calendar", + ) { isCollapsed -> + if (isCollapsed) { + ExpandedCalendar(selectedDay, onSelectDate) + } else { + CollapsedCalendar( + selectedDay, + onSelectDate, + weekCalendarState, + ) + } + } + } + } +} + +@Composable +private fun ExpandedCalendar( + selectedDay: Day, + onSelectDate: (LocalDate) -> Unit, +) { + DatePicker( + modifier = Modifier + .supportWideScreen(480.dp) + .padding(horizontal = AppTheme.dimens.contentMargin) + .padding(bottom = AppTheme.dimens.spacingMedium), + selectedDay = selectedDay.date, + onDateSelected = onSelectDate, + ) +} + +@Composable +private fun CollapsedCalendar( + selectedDay: Day, + onSelectDate: (LocalDate) -> Unit, + weekCalendarState: WeekCalendarState = rememberWeekCalendarState(), +) { + WeekCalendar( + modifier = Modifier + .supportWideScreen() + .padding( + start = AppTheme.dimens.spacingLarge, + end = AppTheme.dimens.spacingLarge, + bottom = AppTheme.dimens.spacingLarge, + ), + selectedDay = selectedDay, + onSelectDay = onSelectDate, + weekCalendarState = weekCalendarState, + ) +} + +@Preview +@Composable +private fun HeaderCollapsedPreview() { + AtomTheme { + AgendaHeader( + selectedDay = LocalDate.now().asDay(), + onSelectDate = {}, + onSelectToday = {}, + canExpand = true, + isExpanded = false, + onToggleExpandCollapse = {}, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Preview +@Composable +private fun HeaderExpandedPreview() { + AtomTheme { + AgendaHeader( + selectedDay = LocalDate.now().asDay(), + onSelectDate = {}, + onSelectToday = {}, + canExpand = true, + isExpanded = true, + onToggleExpandCollapse = {}, + modifier = Modifier.fillMaxWidth(), + ) + } +} diff --git a/feature/agenda/src/main/java/com/costular/atomtasks/agenda/AgendaNavigator.kt b/feature/agenda/src/main/java/com/costular/atomtasks/agenda/ui/AgendaNavigator.kt similarity index 87% rename from feature/agenda/src/main/java/com/costular/atomtasks/agenda/AgendaNavigator.kt rename to feature/agenda/src/main/java/com/costular/atomtasks/agenda/ui/AgendaNavigator.kt index 61b4ee33..9db913e8 100644 --- a/feature/agenda/src/main/java/com/costular/atomtasks/agenda/AgendaNavigator.kt +++ b/feature/agenda/src/main/java/com/costular/atomtasks/agenda/ui/AgendaNavigator.kt @@ -1,4 +1,4 @@ -package com.costular.atomtasks.agenda +package com.costular.atomtasks.agenda.ui interface AgendaNavigator { fun navigateToCreateTask( diff --git a/feature/agenda/src/main/java/com/costular/atomtasks/agenda/AgendaScreen.kt b/feature/agenda/src/main/java/com/costular/atomtasks/agenda/ui/AgendaScreen.kt similarity index 62% rename from feature/agenda/src/main/java/com/costular/atomtasks/agenda/AgendaScreen.kt rename to feature/agenda/src/main/java/com/costular/atomtasks/agenda/ui/AgendaScreen.kt index 21dfadb5..0c45ebf8 100644 --- a/feature/agenda/src/main/java/com/costular/atomtasks/agenda/AgendaScreen.kt +++ b/feature/agenda/src/main/java/com/costular/atomtasks/agenda/ui/AgendaScreen.kt @@ -1,26 +1,10 @@ -package com.costular.atomtasks.agenda +package com.costular.atomtasks.agenda.ui -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ExpandMore -import androidx.compose.material.icons.outlined.Today -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Surface import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable @@ -28,29 +12,27 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.costular.atomtasks.agenda.actions.TaskActionsResult import com.costular.atomtasks.agenda.destinations.TasksActionsBottomSheetDestination -import com.costular.atomtasks.core.ui.utils.DateUtils.dayAsText import com.costular.atomtasks.core.ui.utils.DevicesPreview import com.costular.atomtasks.core.ui.utils.generateWindowSizeClass -import com.costular.atomtasks.coreui.date.Day import com.costular.atomtasks.review.ui.ReviewHandler +import com.costular.atomtasks.tasks.dialog.RemoveRecurrentTaskDialog +import com.costular.atomtasks.tasks.dialog.RemoveRecurrentTaskResponse.ALL +import com.costular.atomtasks.tasks.dialog.RemoveRecurrentTaskResponse.THIS +import com.costular.atomtasks.tasks.dialog.RemoveRecurrentTaskResponse.THIS_AND_FUTURES +import com.costular.atomtasks.tasks.dialog.RemoveTaskDialog import com.costular.atomtasks.tasks.model.Reminder +import com.costular.atomtasks.tasks.model.RemovalStrategy import com.costular.atomtasks.tasks.model.Task import com.costular.atomtasks.tasks.model.TaskList import com.costular.designsystem.components.CircularLoadingIndicator -import com.costular.designsystem.components.DatePicker -import com.costular.designsystem.components.HorizontalCalendar -import com.costular.designsystem.components.ScreenHeader -import com.costular.designsystem.dialogs.RemoveTaskDialog import com.costular.designsystem.theme.AppTheme import com.costular.designsystem.theme.AtomTheme import com.costular.designsystem.util.supportWideScreen @@ -62,7 +44,6 @@ import java.time.LocalTime import kotlinx.collections.immutable.persistentListOf import org.burnoutcrew.reorderable.ItemPosition import org.burnoutcrew.reorderable.rememberReorderableLazyListState -import com.costular.atomtasks.core.ui.R.string as S const val TestTagHeader = "AgendaTitle" @@ -122,6 +103,7 @@ internal fun AgendaScreen( onSelectToday = viewModel::setSelectedDayToday, onMarkTask = viewModel::onMarkTask, deleteTask = viewModel::deleteTask, + deleteRecurringTask = viewModel::deleteRecurringTask, dismissDelete = viewModel::dismissDelete, openTaskAction = { task -> viewModel.onOpenTaskActions() @@ -131,10 +113,10 @@ internal fun AgendaScreen( isDone = task.isDone, ) }, + onToggleExpandCollapse = viewModel::toggleHeader, onMoveTask = viewModel::onMoveTask, onDragTask = viewModel::onDragTask, - onDismissTaskOrderTutorial = viewModel::orderTaskTutorialDismissed, ) } @@ -180,27 +162,41 @@ fun AgendaScreen( onSelectToday: () -> Unit, onMarkTask: (Long, Boolean) -> Unit, deleteTask: (id: Long) -> Unit, + deleteRecurringTask: (id: Long, strategy: RemovalStrategy) -> Unit, dismissDelete: () -> Unit, openTaskAction: (Task) -> Unit, onToggleExpandCollapse: () -> Unit, onMoveTask: (Int, Int) -> Unit, onDragTask: (ItemPosition, ItemPosition) -> Unit, - onDismissTaskOrderTutorial: () -> Unit, ) { if (state.deleteTaskAction is DeleteTaskAction.Shown) { - RemoveTaskDialog( - onAccept = { - deleteTask(state.deleteTaskAction.taskId) - }, - onCancel = { - dismissDelete() - }, - ) + if (state.deleteTaskAction.isRecurring) { + RemoveRecurrentTaskDialog( + onCancel = dismissDelete, + onRemove = { response -> + val removalStrategy = when (response) { + THIS -> RemovalStrategy.SINGLE + THIS_AND_FUTURES -> RemovalStrategy.SINGLE_AND_FUTURE_ONES + ALL -> RemovalStrategy.ALL + } + deleteRecurringTask(state.deleteTaskAction.taskId, removalStrategy) + } + ) + } else { + RemoveTaskDialog( + onAccept = { + deleteTask(state.deleteTaskAction.taskId) + }, + onCancel = { + dismissDelete() + }, + ) + } } Column { AgendaHeader( - state = state, + selectedDay = state.selectedDay, onSelectDate = onSelectDate, canExpand = windowSizeClass.canExpand, isExpanded = state.isHeaderExpanded, @@ -215,7 +211,6 @@ fun AgendaScreen( modifier = Modifier.supportWideScreen(), onMoveTask = onMoveTask, onDragTask = onDragTask, - onDismissTaskOrderTutorial = onDismissTaskOrderTutorial, ) } } @@ -227,7 +222,6 @@ private fun TasksContent( onMarkTask: (Long, Boolean) -> Unit, onDragTask: (ItemPosition, ItemPosition) -> Unit, onMoveTask: (Int, Int) -> Unit, - onDismissTaskOrderTutorial: () -> Unit, modifier: Modifier = Modifier, ) { val haptic = LocalHapticFeedback.current @@ -248,8 +242,6 @@ private fun TasksContent( haptic.performHapticFeedback(HapticFeedbackType.LongPress) } }, - shouldShowTaskOrderTutorial = state.shouldShowCardOrderTutorial, - onDismissTaskOrderTutorial = onDismissTaskOrderTutorial, padding = PaddingValues( start = AppTheme.dimens.contentMargin, end = AppTheme.dimens.contentMargin, @@ -276,133 +268,6 @@ private fun TasksContent( } } -@Suppress("LongMethod") -@Composable -private fun AgendaHeader( - modifier: Modifier = Modifier, - state: AgendaState, - onSelectDate: (LocalDate) -> Unit, - onSelectToday: () -> Unit, - canExpand: Boolean, - isExpanded: Boolean, - onToggleExpandCollapse: () -> Unit, -) { - val shadowElevation = if (isExpanded) { - 6.dp - } else { - 2.dp - } - - Surface( - modifier = modifier, - shape = RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp), - shadowElevation = shadowElevation, - ) { - Column { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - val selectedDayText = dayAsText(state.selectedDay.date) - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .weight(1f) - .clickable(enabled = canExpand, onClick = onToggleExpandCollapse), - ) { - ScreenHeader( - text = selectedDayText, - modifier = Modifier - .testTag(TestTagHeader) - .padding( - top = AppTheme.dimens.spacingLarge, - bottom = AppTheme.dimens.spacingLarge, - start = AppTheme.dimens.spacingLarge, - ), - ) - - val degrees by animateFloatAsState( - targetValue = if (isExpanded) 180f else 0f, - animationSpec = tween( - durationMillis = 200, - easing = FastOutSlowInEasing, - ), - label = "Header collapsable arrow", - ) - - if (canExpand) { - IconButton(onClick = { onToggleExpandCollapse() }) { - Icon( - imageVector = Icons.Default.ExpandMore, - contentDescription = null, - modifier = Modifier.rotate(degrees), - ) - } - } - } - - IconButton( - onClick = onSelectToday, - modifier = Modifier - .padding(end = AppTheme.dimens.spacingLarge) - .width(40.dp) - .height(40.dp), - ) { - Icon( - imageVector = Icons.Outlined.Today, - contentDescription = stringResource(S.today), - ) - } - } - - AnimatedContent( - targetState = isExpanded && canExpand, - label = "Header calendar", - ) { isCollapsed -> - if (isCollapsed) { - HeaderCalendarExpanded(state.selectedDay, onSelectDate) - } else { - HeaderCalendarCollapsed(state.selectedDay, onSelectDate) - } - } - } - } -} - -@Composable -private fun HeaderCalendarExpanded( - selectedDay: Day, - onSelectDate: (LocalDate) -> Unit, -) { - DatePicker( - modifier = Modifier - .supportWideScreen(480.dp) - .padding(horizontal = AppTheme.dimens.contentMargin) - .padding(bottom = AppTheme.dimens.spacingMedium), - selectedDay = selectedDay.date, - onDateSelected = onSelectDate, - ) -} - -@Composable -private fun HeaderCalendarCollapsed( - selectedDay: Day, - onSelectDate: (LocalDate) -> Unit, -) { - HorizontalCalendar( - modifier = Modifier - .supportWideScreen() - .padding( - start = AppTheme.dimens.spacingLarge, - end = AppTheme.dimens.spacingLarge, - bottom = AppTheme.dimens.spacingLarge, - ), - selectedDay = selectedDay, - onSelectDay = onSelectDate, - ) -} - private val WindowSizeClass.canExpand: Boolean get() = widthSizeClass == WindowWidthSizeClass.Compact || @@ -431,6 +296,10 @@ fun AgendaPreview() { ), isDone = false, position = 0, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ), Task( id = 2L, @@ -444,6 +313,10 @@ fun AgendaPreview() { ), isDone = true, position = 0, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ), ), ), @@ -454,11 +327,11 @@ fun AgendaPreview() { onMarkTask = { _, _ -> }, onToggleExpandCollapse = {}, deleteTask = {}, + deleteRecurringTask = { _, _ -> }, dismissDelete = {}, openTaskAction = {}, onMoveTask = { _, _ -> }, onDragTask = { _, _ -> }, - onDismissTaskOrderTutorial = {}, ) } } diff --git a/feature/agenda/src/main/java/com/costular/atomtasks/agenda/AgendaState.kt b/feature/agenda/src/main/java/com/costular/atomtasks/agenda/ui/AgendaState.kt similarity index 85% rename from feature/agenda/src/main/java/com/costular/atomtasks/agenda/AgendaState.kt rename to feature/agenda/src/main/java/com/costular/atomtasks/agenda/ui/AgendaState.kt index eba5c840..e92c0760 100644 --- a/feature/agenda/src/main/java/com/costular/atomtasks/agenda/AgendaState.kt +++ b/feature/agenda/src/main/java/com/costular/atomtasks/agenda/ui/AgendaState.kt @@ -1,7 +1,7 @@ -package com.costular.atomtasks.agenda +package com.costular.atomtasks.agenda.ui -import com.costular.atomtasks.coreui.date.Day -import com.costular.atomtasks.coreui.date.asDay +import com.costular.atomtasks.core.ui.date.Day +import com.costular.atomtasks.core.ui.date.asDay import com.costular.atomtasks.tasks.model.Task import java.time.LocalDate import kotlinx.collections.immutable.ImmutableList @@ -26,6 +26,7 @@ sealed class DeleteTaskAction { data class Shown( val taskId: Long, + val isRecurring: Boolean, ) : DeleteTaskAction() } diff --git a/feature/agenda/src/main/java/com/costular/atomtasks/agenda/AgendaViewModel.kt b/feature/agenda/src/main/java/com/costular/atomtasks/agenda/ui/AgendaViewModel.kt similarity index 81% rename from feature/agenda/src/main/java/com/costular/atomtasks/agenda/AgendaViewModel.kt rename to feature/agenda/src/main/java/com/costular/atomtasks/agenda/ui/AgendaViewModel.kt index d52013f8..9aaeccfe 100644 --- a/feature/agenda/src/main/java/com/costular/atomtasks/agenda/AgendaViewModel.kt +++ b/feature/agenda/src/main/java/com/costular/atomtasks/agenda/ui/AgendaViewModel.kt @@ -1,4 +1,4 @@ -package com.costular.atomtasks.agenda +package com.costular.atomtasks.agenda.ui import androidx.lifecycle.viewModelScope import com.costular.atomtasks.agenda.analytics.AgendaAnalytics @@ -13,22 +13,23 @@ import com.costular.atomtasks.agenda.analytics.AgendaAnalytics.OrderTask import com.costular.atomtasks.agenda.analytics.AgendaAnalytics.SelectToday import com.costular.atomtasks.agenda.analytics.AgendaAnalytics.ShowConfirmDeleteDialog import com.costular.atomtasks.analytics.AtomAnalytics +import com.costular.atomtasks.core.ui.date.asDay import com.costular.atomtasks.core.ui.mvi.MviViewModel -import com.costular.atomtasks.coreui.date.asDay import com.costular.atomtasks.data.tutorial.ShouldShowTaskOrderTutorialUseCase import com.costular.atomtasks.data.tutorial.TaskOrderTutorialDismissedUseCase import com.costular.atomtasks.review.usecase.ShouldAskReviewUseCase -import com.costular.atomtasks.tasks.interactor.MoveTaskUseCase -import com.costular.atomtasks.tasks.interactor.ObserveTasksUseCase -import com.costular.atomtasks.tasks.interactor.RemoveTaskInteractor -import com.costular.atomtasks.tasks.interactor.UpdateTaskIsDoneInteractor -import com.costular.atomtasks.tasks.manager.AutoforwardManager +import com.costular.atomtasks.tasks.helper.AutoforwardManager +import com.costular.atomtasks.tasks.helper.recurrence.RecurrenceScheduler +import com.costular.atomtasks.tasks.model.RemovalStrategy +import com.costular.atomtasks.tasks.usecase.MoveTaskUseCase +import com.costular.atomtasks.tasks.usecase.ObserveTasksUseCase +import com.costular.atomtasks.tasks.usecase.RemoveTaskUseCase +import com.costular.atomtasks.tasks.usecase.UpdateTaskIsDoneUseCase import dagger.hilt.android.lifecycle.HiltViewModel import java.time.LocalDate import javax.inject.Inject import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import org.burnoutcrew.reorderable.ItemPosition @@ -37,22 +38,28 @@ import org.burnoutcrew.reorderable.ItemPosition @HiltViewModel class AgendaViewModel @Inject constructor( private val observeTasksUseCase: ObserveTasksUseCase, - private val updateTaskIsDoneInteractor: UpdateTaskIsDoneInteractor, - private val removeTaskInteractor: RemoveTaskInteractor, + private val updateTaskIsDoneUseCase: UpdateTaskIsDoneUseCase, + private val removeTaskUseCase: RemoveTaskUseCase, private val autoforwardManager: AutoforwardManager, private val moveTaskUseCase: MoveTaskUseCase, private val atomAnalytics: AtomAnalytics, private val shouldShowTaskOrderTutorialUseCase: ShouldShowTaskOrderTutorialUseCase, private val taskOrderTutorialDismissedUseCase: TaskOrderTutorialDismissedUseCase, private val shouldShowAskReviewUseCase: ShouldAskReviewUseCase, + private val recurrenceScheduler: RecurrenceScheduler, ) : MviViewModel(AgendaState()) { init { loadTasks() scheduleAutoforwardTasks() + initializeRecurrenceScheduler() retrieveTutorials() } + private fun initializeRecurrenceScheduler() { + recurrenceScheduler.initialize() + } + private fun retrieveTutorials() { viewModelScope.launch { shouldShowTaskOrderTutorialUseCase(Unit) @@ -89,7 +96,7 @@ class AgendaViewModel @Inject constructor( } fun onMarkTask(taskId: Long, isDone: Boolean) = viewModelScope.launch { - updateTaskIsDoneInteractor(UpdateTaskIsDoneInteractor.Params(taskId, isDone)).collect() + updateTaskIsDoneUseCase(UpdateTaskIsDoneUseCase.Params(taskId, isDone)) checkIfReviewShouldBeShown(isDone) val event = if (isDone) { @@ -119,8 +126,15 @@ class AgendaViewModel @Inject constructor( } fun actionDelete(id: Long) { + val tasks = state.value.tasks + + if (tasks !is TasksState.Success) { + return + } + + val task = tasks.data.find { it.id == id } setState { - copy(deleteTaskAction = DeleteTaskAction.Shown(id)) + copy(deleteTaskAction = DeleteTaskAction.Shown(id, task?.isRecurring ?: false)) } atomAnalytics.track(ShowConfirmDeleteDialog) @@ -128,16 +142,20 @@ class AgendaViewModel @Inject constructor( fun deleteTask(id: Long) { viewModelScope.launch { - removeTaskInteractor(RemoveTaskInteractor.Params(id)) - .onStart { - hideAskDelete() - } - .collect() + hideAskDelete() + removeTaskUseCase(RemoveTaskUseCase.Params(id)) } atomAnalytics.track(ConfirmDelete) } + fun deleteRecurringTask(id: Long, removalStrategy: RemovalStrategy) { + viewModelScope.launch { + removeTaskUseCase(RemoveTaskUseCase.Params(id, removalStrategy)) + hideAskDelete() + } + } + fun onDragTask(from: ItemPosition, to: ItemPosition) { val data = state.value val tasks = data.tasks @@ -200,6 +218,8 @@ class AgendaViewModel @Inject constructor( } else { atomAnalytics.track(ExpandCalendar) } + + } fun onEditTask() { diff --git a/feature/agenda/src/test/java/com/costular/atomtasks/agenda/AgendaViewModelTest.kt b/feature/agenda/src/test/java/com/costular/atomtasks/agenda/AgendaViewModelTest.kt index 7f39041d..93d26605 100644 --- a/feature/agenda/src/test/java/com/costular/atomtasks/agenda/AgendaViewModelTest.kt +++ b/feature/agenda/src/test/java/com/costular/atomtasks/agenda/AgendaViewModelTest.kt @@ -1,25 +1,30 @@ package com.costular.atomtasks.agenda import app.cash.turbine.test -import com.costular.atomtasks.agenda.DeleteTaskAction.Hidden -import com.costular.atomtasks.agenda.DeleteTaskAction.Shown import com.costular.atomtasks.agenda.analytics.AgendaAnalytics +import com.costular.atomtasks.agenda.ui.AgendaViewModel +import com.costular.atomtasks.agenda.ui.DeleteTaskAction.Hidden +import com.costular.atomtasks.agenda.ui.DeleteTaskAction.Shown +import com.costular.atomtasks.agenda.ui.TasksState import com.costular.atomtasks.analytics.AtomAnalytics +import com.costular.atomtasks.core.Either import com.costular.atomtasks.core.testing.MviViewModelTest +import com.costular.atomtasks.core.toResult +import com.costular.atomtasks.core.usecase.invoke import com.costular.atomtasks.data.tutorial.ShouldShowTaskOrderTutorialUseCase import com.costular.atomtasks.data.tutorial.TaskOrderTutorialDismissedUseCase import com.costular.atomtasks.review.usecase.ShouldAskReviewUseCase -import com.costular.atomtasks.tasks.interactor.MoveTaskUseCase -import com.costular.atomtasks.tasks.interactor.ObserveTasksUseCase -import com.costular.atomtasks.tasks.interactor.RemoveTaskInteractor -import com.costular.atomtasks.tasks.interactor.UpdateTaskIsDoneInteractor -import com.costular.atomtasks.tasks.manager.AutoforwardManager +import com.costular.atomtasks.tasks.helper.AutoforwardManager +import com.costular.atomtasks.tasks.helper.recurrence.RecurrenceScheduler import com.costular.atomtasks.tasks.model.Task -import com.costular.core.Either -import com.costular.core.usecase.invoke +import com.costular.atomtasks.tasks.usecase.MoveTaskUseCase +import com.costular.atomtasks.tasks.usecase.ObserveTasksUseCase +import com.costular.atomtasks.tasks.usecase.RemoveTaskUseCase +import com.costular.atomtasks.tasks.usecase.UpdateTaskIsDoneUseCase import com.google.common.truth.Truth.assertThat import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.mockk import io.mockk.verify import java.time.LocalDate @@ -36,17 +41,18 @@ class AgendaViewModelTest : MviViewModelTest() { lateinit var sut: AgendaViewModel - private val observeTasksUseCase: ObserveTasksUseCase = mockk(relaxed = true) - private val updateTaskIsDoneInteractor: UpdateTaskIsDoneInteractor = mockk(relaxed = true) - private val removeTaskInteractor: RemoveTaskInteractor = mockk(relaxed = true) - private val autoforwardManager: AutoforwardManager = mockk(relaxed = true) - private val moveTaskUseCase: MoveTaskUseCase = mockk(relaxed = true) - private val atomAnalytics: AtomAnalytics = mockk(relaxed = true) + private val observeTasksUseCase: ObserveTasksUseCase = mockk() + private val updateTaskIsDoneUseCase: UpdateTaskIsDoneUseCase = mockk(relaxUnitFun = true) + private val removeTaskUseCase: RemoveTaskUseCase = mockk(relaxUnitFun = true) + private val autoforwardManager: AutoforwardManager = mockk(relaxUnitFun = true) + private val recurrenceScheduler: RecurrenceScheduler = mockk(relaxUnitFun = true) + private val moveTaskUseCase: MoveTaskUseCase = mockk(relaxUnitFun = true) + private val atomAnalytics: AtomAnalytics = mockk(relaxUnitFun = true) private val shouldShowTaskOrderTutorialUseCase: ShouldShowTaskOrderTutorialUseCase = - mockk(relaxed = true) + mockk(relaxUnitFun = true) private val taskOrderTutorialDismissedUseCase: TaskOrderTutorialDismissedUseCase = - mockk(relaxed = true) - private val shouldAskReviewUseCase: ShouldAskReviewUseCase = mockk(relaxed = true) + mockk(relaxUnitFun = true) + private val shouldAskReviewUseCase: ShouldAskReviewUseCase = mockk(relaxUnitFun = true) @Before fun setUp() { @@ -94,8 +100,8 @@ class AgendaViewModelTest : MviViewModelTest() { sut.onMarkTask(expected.first().id, true) coVerify { - updateTaskIsDoneInteractor( - UpdateTaskIsDoneInteractor.Params( + updateTaskIsDoneUseCase( + UpdateTaskIsDoneUseCase.Params( taskId = expected.first().id, isDone = true, ), @@ -149,7 +155,7 @@ class AgendaViewModelTest : MviViewModelTest() { sut.actionDelete(taskId) sut.deleteTask(taskId) - coVerify { removeTaskInteractor(RemoveTaskInteractor.Params(taskId)) } + coVerify { removeTaskUseCase(RemoveTaskUseCase.Params(taskId)) } } @Test @@ -275,8 +281,14 @@ class AgendaViewModelTest : MviViewModelTest() { @Test fun `should track event when mark task as not done`() = runTest { val expected = DEFAULT_TASKS - coEvery { observeTasksUseCase.invoke(any()) } returns flowOf(expected) + every { + observeTasksUseCase.invoke(any()) + } returns flowOf(expected) + + givenOrderTasksTutorial(true) + + initializeViewModel() sut.loadTasks() sut.onMarkTask(expected.last().id, false) @@ -305,6 +317,7 @@ class AgendaViewModelTest : MviViewModelTest() { val taskId = DEFAULT_TASKS.first().id coEvery { observeTasksUseCase.invoke(any()) } returns flowOf(tasks) + sut.loadTasks() sut.actionDelete(taskId) sut.deleteTask(taskId) @@ -316,6 +329,7 @@ class AgendaViewModelTest : MviViewModelTest() { @Test fun `should track navigate to day when select a new day`() = runTest { + every { observeTasksUseCase.invoke(any()) } returns flowOf(emptyList()) val day = LocalDate.of(2023, 9, 16) sut.setSelectedDay(day) @@ -343,7 +357,7 @@ class AgendaViewModelTest : MviViewModelTest() { @Test fun `should show order task when land on screen given the tutorial hasn't been shown for the user yet`() = runTest { - coEvery { shouldShowTaskOrderTutorialUseCase.invoke() } returns flowOf(true) + givenOrderTasksTutorial(true) initializeViewModel() @@ -362,6 +376,7 @@ class AgendaViewModelTest : MviViewModelTest() { @Test fun `should expose ask review when mark task as done given usecase returns true`() = runTest { coEvery { shouldAskReviewUseCase() } returns Either.Result(true) + coEvery { updateTaskIsDoneUseCase.invoke(any()) } returns Unit.toResult() givenSuccessTasks() sut.onMarkTask(DEFAULT_TASKS.first().id, true) @@ -396,6 +411,10 @@ class AgendaViewModelTest : MviViewModelTest() { coEvery { observeTasksUseCase.invoke(any()) } returns flowOf(DEFAULT_TASKS) } + private fun givenOrderTasksTutorial(isEnabled: Boolean) { + coEvery { shouldShowTaskOrderTutorialUseCase.invoke(Unit) } returns flowOf(isEnabled) + } + companion object { const val TASK1_ID = 1L const val TASK2ID = 2L @@ -408,6 +427,10 @@ class AgendaViewModelTest : MviViewModelTest() { reminder = null, isDone = false, position = 1, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ), Task( id = TASK2ID, @@ -417,21 +440,32 @@ class AgendaViewModelTest : MviViewModelTest() { reminder = null, isDone = true, position = 2, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ), ) } private fun initializeViewModel() { + coEvery { observeTasksUseCase.invoke(any()) } returns flowOf(emptyList()) + coEvery { updateTaskIsDoneUseCase.invoke(any()) } returns Unit.toResult() + coEvery { removeTaskUseCase.invoke(any()) } returns Unit.toResult() + coEvery { shouldAskReviewUseCase.invoke(Unit) } returns false.toResult() + givenOrderTasksTutorial(true) + sut = AgendaViewModel( observeTasksUseCase = observeTasksUseCase, - updateTaskIsDoneInteractor = updateTaskIsDoneInteractor, - removeTaskInteractor = removeTaskInteractor, + updateTaskIsDoneUseCase = updateTaskIsDoneUseCase, + removeTaskUseCase = removeTaskUseCase, autoforwardManager = autoforwardManager, moveTaskUseCase = moveTaskUseCase, atomAnalytics = atomAnalytics, shouldShowTaskOrderTutorialUseCase = shouldShowTaskOrderTutorialUseCase, taskOrderTutorialDismissedUseCase = taskOrderTutorialDismissedUseCase, shouldShowAskReviewUseCase = shouldAskReviewUseCase, + recurrenceScheduler = recurrenceScheduler, ) } } diff --git a/feature/createtask/src/main/java/com/costular/atomtasks/createtask/CreateTaskScreen.kt b/feature/createtask/src/main/java/com/costular/atomtasks/createtask/CreateTaskScreen.kt index ade284ef..1761160f 100644 --- a/feature/createtask/src/main/java/com/costular/atomtasks/createtask/CreateTaskScreen.kt +++ b/feature/createtask/src/main/java/com/costular/atomtasks/createtask/CreateTaskScreen.kt @@ -50,6 +50,7 @@ fun CreateTaskScreen( result.name, result.date, result.reminder, + result.recurrenceType, ) }, modifier = Modifier diff --git a/feature/createtask/src/main/java/com/costular/atomtasks/createtask/CreateTaskState.kt b/feature/createtask/src/main/java/com/costular/atomtasks/createtask/CreateTaskState.kt index d1d2fbd7..7fbe2db2 100644 --- a/feature/createtask/src/main/java/com/costular/atomtasks/createtask/CreateTaskState.kt +++ b/feature/createtask/src/main/java/com/costular/atomtasks/createtask/CreateTaskState.kt @@ -3,9 +3,7 @@ package com.costular.atomtasks.createtask sealed interface CreateTaskState { data object Uninitialized : CreateTaskState - data object Loading : CreateTaskState data object Saving : CreateTaskState data object Success : CreateTaskState data object Failure : CreateTaskState - } diff --git a/feature/createtask/src/main/java/com/costular/atomtasks/createtask/CreateTaskViewModel.kt b/feature/createtask/src/main/java/com/costular/atomtasks/createtask/CreateTaskViewModel.kt index 43436248..de309b72 100644 --- a/feature/createtask/src/main/java/com/costular/atomtasks/createtask/CreateTaskViewModel.kt +++ b/feature/createtask/src/main/java/com/costular/atomtasks/createtask/CreateTaskViewModel.kt @@ -4,10 +4,8 @@ import androidx.lifecycle.viewModelScope import com.costular.atomtasks.analytics.AtomAnalytics import com.costular.atomtasks.core.ui.mvi.MviViewModel import com.costular.atomtasks.createtask.analytics.CreateTaskAnalytics -import com.costular.atomtasks.tasks.interactor.CreateTaskInteractor -import com.costular.core.InvokeError -import com.costular.core.InvokeStarted -import com.costular.core.InvokeSuccess +import com.costular.atomtasks.tasks.usecase.CreateTaskUseCase +import com.costular.atomtasks.tasks.model.RecurrenceType import dagger.hilt.android.lifecycle.HiltViewModel import java.time.LocalDate import java.time.LocalTime @@ -16,7 +14,7 @@ import kotlinx.coroutines.launch @HiltViewModel class CreateTaskViewModel @Inject constructor( - private val createTaskInteractor: CreateTaskInteractor, + private val createTaskUseCase: CreateTaskUseCase, private val atomAnalytics: AtomAnalytics, ) : MviViewModel(CreateTaskState.Uninitialized) { @@ -24,32 +22,28 @@ class CreateTaskViewModel @Inject constructor( name: String, date: LocalDate, reminder: LocalTime?, + recurrence: RecurrenceType?, ) { viewModelScope.launch { - createTaskInteractor( - CreateTaskInteractor.Params( - name, - date, - reminder != null, - reminder, - ), - ) - .collect { status -> - when (status) { - is InvokeStarted -> { - setState { CreateTaskState.Loading } - } - - is InvokeSuccess -> { - setState { CreateTaskState.Success } - atomAnalytics.track(CreateTaskAnalytics.TaskCreated) - } + setState { CreateTaskState.Saving } - is InvokeError -> { - setState { CreateTaskState.Failure } - } - } + createTaskUseCase( + CreateTaskUseCase.Params( + name = name, + date = date, + reminderEnabled = reminder != null, + reminderTime = reminder, + recurrenceType = recurrence, + ), + ).fold( + ifError = { + setState { CreateTaskState.Failure } + }, + ifResult = { + atomAnalytics.track(CreateTaskAnalytics.TaskCreated) + setState { CreateTaskState.Success } } + ) } } } diff --git a/feature/createtask/src/test/java/com/costular/atomtasks/createtask/CreateTaskViewModelTest.kt b/feature/createtask/src/test/java/com/costular/atomtasks/createtask/CreateTaskViewModelTest.kt index f0737e8a..a1671625 100644 --- a/feature/createtask/src/test/java/com/costular/atomtasks/createtask/CreateTaskViewModelTest.kt +++ b/feature/createtask/src/test/java/com/costular/atomtasks/createtask/CreateTaskViewModelTest.kt @@ -2,12 +2,12 @@ package com.costular.atomtasks.createtask import app.cash.turbine.test import com.costular.atomtasks.analytics.AtomAnalytics +import com.costular.atomtasks.core.Either import com.costular.atomtasks.core.testing.MviViewModelTest import com.costular.atomtasks.createtask.analytics.CreateTaskAnalytics -import com.costular.atomtasks.tasks.interactor.CreateTaskInteractor -import com.costular.core.InvokeError -import com.costular.core.InvokeStarted -import com.costular.core.InvokeSuccess +import com.costular.atomtasks.tasks.usecase.CreateTaskUseCase +import com.costular.atomtasks.tasks.model.CreateTaskError +import com.costular.atomtasks.tasks.model.RecurrenceType import com.google.common.truth.Truth.assertThat import io.mockk.coEvery import io.mockk.mockk @@ -15,7 +15,6 @@ import io.mockk.verify import java.time.LocalDate import java.time.LocalTime import kotlin.time.ExperimentalTime -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -23,7 +22,7 @@ import org.junit.Test @ExperimentalTime class CreateTaskViewModelTest : MviViewModelTest() { - private val createTaskInteractor: CreateTaskInteractor = mockk() + private val createTaskUseCase: CreateTaskUseCase = mockk() private val atomAnalytics: AtomAnalytics = mockk(relaxed = true) lateinit var sut: CreateTaskViewModel @@ -31,7 +30,7 @@ class CreateTaskViewModelTest : MviViewModelTest() { @Before fun setUp() { sut = CreateTaskViewModel( - createTaskInteractor = createTaskInteractor, + createTaskUseCase = createTaskUseCase, atomAnalytics = atomAnalytics, ) } @@ -42,20 +41,23 @@ class CreateTaskViewModelTest : MviViewModelTest() { val date = LocalDate.of(2022, 2, 10) val reminder = LocalTime.of(0, 0) coEvery { - createTaskInteractor( - CreateTaskInteractor.Params( - name, - date, - true, - reminder, + createTaskUseCase( + CreateTaskUseCase.Params( + name = name, + date = date, + reminderEnabled = true, + reminderTime = reminder, + recurrenceType = RecurrenceType.YEARLY, ), ) - } returns flow { - emit(InvokeStarted) - emit(InvokeSuccess) - } + } returns Either.Result(Unit) - sut.createTask(name, date, reminder) + sut.createTask( + name = name, + date = date, + reminder = reminder, + recurrence = RecurrenceType.YEARLY, + ) sut.state.test { assertThat(expectMostRecentItem()).isInstanceOf(CreateTaskState.Success::class.java) @@ -65,15 +67,27 @@ class CreateTaskViewModelTest : MviViewModelTest() { @Test fun `should expose failure when create task fails`() = runTest { - val exception = Exception("some error") + val today = LocalDate.now() + val now = LocalTime.now() coEvery { - createTaskInteractor(any()) - } returns flow { - emit(InvokeError(exception)) - } + createTaskUseCase( + CreateTaskUseCase.Params( + "whatever", + date = today, + reminderEnabled = true, + reminderTime = now, + recurrenceType = RecurrenceType.YEARLY, + ) + ) + } returns Either.Error(CreateTaskError.UnknownError) - sut.createTask("whatever", LocalDate.now(), LocalTime.now()) + sut.createTask( + name = "whatever", + date = today, + reminder = now, + recurrence = RecurrenceType.YEARLY + ) sut.state.test { assertThat(expectMostRecentItem()).isInstanceOf(CreateTaskState.Failure::class.java) @@ -87,20 +101,23 @@ class CreateTaskViewModelTest : MviViewModelTest() { val date = LocalDate.of(2022, 2, 10) val reminder = LocalTime.of(0, 0) coEvery { - createTaskInteractor( - CreateTaskInteractor.Params( + createTaskUseCase( + CreateTaskUseCase.Params( name, date, true, reminder, + RecurrenceType.WEEKDAYS, ), ) - } returns flow { - emit(InvokeStarted) - emit(InvokeSuccess) - } + } returns Either.Result(Unit) - sut.createTask(name, date, reminder) + sut.createTask( + name = name, + date = date, + reminder = reminder, + recurrence = RecurrenceType.WEEKDAYS, + ) verify(exactly = 1) { atomAnalytics.track(CreateTaskAnalytics.TaskCreated) diff --git a/feature/edittask/src/main/java/com/costular/atomtasks/edittask/EditTaskScreen.kt b/feature/edittask/src/main/java/com/costular/atomtasks/edittask/EditTaskScreen.kt index 72e387ed..9803ba4b 100644 --- a/feature/edittask/src/main/java/com/costular/atomtasks/edittask/EditTaskScreen.kt +++ b/feature/edittask/src/main/java/com/costular/atomtasks/edittask/EditTaskScreen.kt @@ -55,9 +55,11 @@ fun EditTaskScreen( result.name, result.date, result.reminder, + result.recurrenceType, ) }, reminder = task.reminder, + recurrenceType = task.recurrenceType, modifier = Modifier .fillMaxWidth() .navigationBarsPadding() diff --git a/feature/edittask/src/main/java/com/costular/atomtasks/edittask/EditTaskViewModel.kt b/feature/edittask/src/main/java/com/costular/atomtasks/edittask/EditTaskViewModel.kt index 22a3adc1..4637194f 100644 --- a/feature/edittask/src/main/java/com/costular/atomtasks/edittask/EditTaskViewModel.kt +++ b/feature/edittask/src/main/java/com/costular/atomtasks/edittask/EditTaskViewModel.kt @@ -3,8 +3,9 @@ package com.costular.atomtasks.ui.features.edittask import androidx.compose.runtime.Stable import androidx.lifecycle.viewModelScope import com.costular.atomtasks.core.ui.mvi.MviViewModel -import com.costular.atomtasks.tasks.interactor.GetTaskByIdInteractor -import com.costular.atomtasks.tasks.interactor.UpdateTaskUseCase +import com.costular.atomtasks.tasks.usecase.GetTaskByIdUseCase +import com.costular.atomtasks.tasks.usecase.EditTaskUseCase +import com.costular.atomtasks.tasks.model.RecurrenceType import dagger.hilt.android.lifecycle.HiltViewModel import java.time.LocalDate import java.time.LocalTime @@ -14,14 +15,13 @@ import kotlinx.coroutines.launch @HiltViewModel class EditTaskViewModel @Inject constructor( - private val getTaskByIdInteractor: GetTaskByIdInteractor, - private val updateTaskUseCase: UpdateTaskUseCase, + private val getTaskByIdUseCase: GetTaskByIdUseCase, + private val editTaskUseCase: EditTaskUseCase, ) : MviViewModel(EditTaskState.Empty) { fun loadTask(taskId: Long) { viewModelScope.launch { - getTaskByIdInteractor(GetTaskByIdInteractor.Params(taskId)) - getTaskByIdInteractor.flow + getTaskByIdUseCase(GetTaskByIdUseCase.Params(taskId)) .onStart { setState { copy(taskState = TaskState.Loading) } } @@ -33,6 +33,7 @@ class EditTaskViewModel @Inject constructor( name = task.name, date = task.day, reminder = task.reminder?.time, + recurrenceType = task.recurrenceType, ), ) } @@ -45,6 +46,7 @@ class EditTaskViewModel @Inject constructor( name: String, date: LocalDate, reminder: LocalTime?, + recurrenceType: RecurrenceType?, ) { viewModelScope.launch { val state = state.value @@ -52,19 +54,22 @@ class EditTaskViewModel @Inject constructor( return@launch } - try { - updateTaskUseCase( - UpdateTaskUseCase.Params( - taskId = state.taskState.taskId, - name = name, - date = date, - reminderTime = reminder, - ), - ) - setState { copy(savingTask = SavingState.Success) } - } catch (e: Exception) { - setState { copy(savingTask = SavingState.Failure) } - } + editTaskUseCase( + EditTaskUseCase.Params( + taskId = state.taskState.taskId, + name = name, + date = date, + reminderTime = reminder, + recurrenceType = recurrenceType, + ), + ).fold( + ifError = { + setState { copy(savingTask = SavingState.Failure) } + }, + ifResult = { + setState { copy(savingTask = SavingState.Success) } + } + ) } } } @@ -90,6 +95,7 @@ sealed class TaskState { val name: String, val date: LocalDate, val reminder: LocalTime?, + val recurrenceType: RecurrenceType?, ) : TaskState() } diff --git a/feature/edittask/src/test/java/com/costular/atomtasks/edittask/EditTaskViewModelTest.kt b/feature/edittask/src/test/java/com/costular/atomtasks/edittask/EditTaskViewModelTest.kt index a49916d5..ca037da4 100644 --- a/feature/edittask/src/test/java/com/costular/atomtasks/edittask/EditTaskViewModelTest.kt +++ b/feature/edittask/src/test/java/com/costular/atomtasks/edittask/EditTaskViewModelTest.kt @@ -1,17 +1,19 @@ package com.costular.atomtasks.edittask import app.cash.turbine.test +import com.costular.atomtasks.core.Either import com.costular.atomtasks.core.testing.MviViewModelTest +import com.costular.atomtasks.core.toError import com.costular.atomtasks.tasks.fake.TaskToday -import com.costular.atomtasks.tasks.interactor.GetTaskByIdInteractor -import com.costular.atomtasks.tasks.interactor.UpdateTaskUseCase +import com.costular.atomtasks.tasks.model.UpdateTaskUseCaseError +import com.costular.atomtasks.tasks.usecase.EditTaskUseCase +import com.costular.atomtasks.tasks.usecase.GetTaskByIdUseCase import com.costular.atomtasks.ui.features.edittask.EditTaskViewModel import com.costular.atomtasks.ui.features.edittask.SavingState import com.costular.atomtasks.ui.features.edittask.TaskState import com.google.common.truth.Truth.assertThat import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.mockk import java.time.LocalDate import java.time.LocalTime @@ -24,20 +26,20 @@ class EditTaskViewModelTest : MviViewModelTest() { lateinit var sut: EditTaskViewModel - private val getTaskByIdInteractor: GetTaskByIdInteractor = mockk(relaxed = true) - private val updateTaskUseCase: UpdateTaskUseCase = mockk(relaxed = true) + private val getTaskByIdUseCase: GetTaskByIdUseCase = mockk(relaxed = true) + private val editTaskUseCase: EditTaskUseCase = mockk(relaxed = true) @Before fun setUp() { sut = EditTaskViewModel( - getTaskByIdInteractor = getTaskByIdInteractor, - updateTaskUseCase = updateTaskUseCase, + getTaskByIdUseCase = getTaskByIdUseCase, + editTaskUseCase = editTaskUseCase, ) } @Test fun `should load task successfully`() = runTest { - every { getTaskByIdInteractor.flow } returns flowOf(TaskToday) + coEvery { getTaskByIdUseCase.invoke(any()) } returns flowOf(TaskToday) sut.loadTask(TaskToday.id) @@ -62,14 +64,16 @@ class EditTaskViewModelTest : MviViewModelTest() { name = "whatever", date = LocalDate.now(), reminder = null, + recurrenceType = null, ) - coVerify(exactly = 0) { updateTaskUseCase(any()) } + coVerify(exactly = 0) { editTaskUseCase(any()) } } @Test fun `should emit success when edit task succeeded`() = runTest { - every { getTaskByIdInteractor.flow } returns flowOf(TaskToday) + coEvery { getTaskByIdUseCase.invoke(any()) } returns flowOf(TaskToday) + coEvery { editTaskUseCase.invoke(any()) } returns Either.Result(Unit) val newTask = "whatever" val newDate = LocalDate.now().plusDays(1) @@ -80,6 +84,7 @@ class EditTaskViewModelTest : MviViewModelTest() { name = newTask, date = newDate, reminder = newReminder, + recurrenceType = null, ) sut.state.test { @@ -90,9 +95,10 @@ class EditTaskViewModelTest : MviViewModelTest() { @Test fun `should emit error when edit task fails`() = runTest { - val exception = Exception("some error") - every { getTaskByIdInteractor.flow } returns flowOf(TaskToday) - coEvery { updateTaskUseCase.invoke(any()) } throws exception + coEvery { getTaskByIdUseCase.invoke(any()) } returns flowOf(TaskToday) + coEvery { + editTaskUseCase.invoke(any()) + } returns UpdateTaskUseCaseError.UnknownError.toError() val newTask = "whatever" val newDate = LocalDate.now().plusDays(1) @@ -103,6 +109,7 @@ class EditTaskViewModelTest : MviViewModelTest() { name = newTask, date = newDate, reminder = newReminder, + recurrenceType = null, ) assertThat(sut.state.value.savingTask).isInstanceOf(SavingState.Failure::class.java) diff --git a/feature/postpone-task/src/main/java/com/costular/atomtasks/postponetask/domain/GetPostponeChoiceListUseCase.kt b/feature/postpone-task/src/main/java/com/costular/atomtasks/postponetask/domain/GetPostponeChoiceListUseCase.kt index 44eada87..fa034857 100644 --- a/feature/postpone-task/src/main/java/com/costular/atomtasks/postponetask/domain/GetPostponeChoiceListUseCase.kt +++ b/feature/postpone-task/src/main/java/com/costular/atomtasks/postponetask/domain/GetPostponeChoiceListUseCase.kt @@ -1,6 +1,6 @@ package com.costular.atomtasks.postponetask.domain -import com.costular.core.usecase.UseCase +import com.costular.atomtasks.core.usecase.UseCase import javax.inject.Inject class GetPostponeChoiceListUseCase @Inject constructor(): UseCase> { diff --git a/feature/postpone-task/src/main/java/com/costular/atomtasks/postponetask/domain/PostponeChoiceCalculator.kt b/feature/postpone-task/src/main/java/com/costular/atomtasks/postponetask/domain/PostponeChoiceCalculator.kt index 04675a27..88818b50 100644 --- a/feature/postpone-task/src/main/java/com/costular/atomtasks/postponetask/domain/PostponeChoiceCalculator.kt +++ b/feature/postpone-task/src/main/java/com/costular/atomtasks/postponetask/domain/PostponeChoiceCalculator.kt @@ -1,8 +1,8 @@ package com.costular.atomtasks.postponetask.domain -import com.costular.core.util.PredefinedTimes -import com.costular.core.util.WeekUtil.findNextWeek -import com.costular.core.util.WeekUtil.findNextWeekend +import com.costular.atomtasks.core.util.PredefinedTimes +import com.costular.atomtasks.core.util.WeekUtil.findNextWeek +import com.costular.atomtasks.core.util.WeekUtil.findNextWeekend import java.time.Clock import java.time.LocalDate import java.time.LocalDateTime diff --git a/feature/postpone-task/src/main/java/com/costular/atomtasks/postponetask/ui/PostponeTaskViewModel.kt b/feature/postpone-task/src/main/java/com/costular/atomtasks/postponetask/ui/PostponeTaskViewModel.kt index ec11e7bf..2eb2138c 100644 --- a/feature/postpone-task/src/main/java/com/costular/atomtasks/postponetask/ui/PostponeTaskViewModel.kt +++ b/feature/postpone-task/src/main/java/com/costular/atomtasks/postponetask/ui/PostponeTaskViewModel.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.viewModelScope import com.costular.atomtasks.core.ui.mvi.MviViewModel import com.costular.atomtasks.postponetask.domain.GetPostponeChoiceListUseCase import com.costular.atomtasks.postponetask.domain.PostponeChoice -import com.costular.atomtasks.tasks.interactor.PostponeTaskUseCase +import com.costular.atomtasks.tasks.usecase.PostponeTaskUseCase import com.costular.atomtasks.notifications.TaskNotificationManager import com.costular.atomtasks.postponetask.domain.PostponeChoiceCalculator import dagger.hilt.android.lifecycle.HiltViewModel diff --git a/feature/postpone-task/src/test/java/com/costular/atomtasks/postponetask/ui/PostponeTaskViewModelTest.kt b/feature/postpone-task/src/test/java/com/costular/atomtasks/postponetask/ui/PostponeTaskViewModelTest.kt index c0ff9a98..f575e773 100644 --- a/feature/postpone-task/src/test/java/com/costular/atomtasks/postponetask/ui/PostponeTaskViewModelTest.kt +++ b/feature/postpone-task/src/test/java/com/costular/atomtasks/postponetask/ui/PostponeTaskViewModelTest.kt @@ -1,11 +1,12 @@ package com.costular.atomtasks.postponetask.ui import com.costular.atomtasks.core.testing.MviViewModelTest -import com.costular.atomtasks.postponetask.domain.GetPostponeChoiceListUseCase -import com.costular.atomtasks.postponetask.domain.PostponeChoice -import com.costular.atomtasks.tasks.interactor.PostponeTaskUseCase +import com.costular.atomtasks.core.toResult import com.costular.atomtasks.notifications.TaskNotificationManager import com.costular.atomtasks.postponetask.domain.DefaultPostponeChoiceCalculator +import com.costular.atomtasks.postponetask.domain.GetPostponeChoiceListUseCase +import com.costular.atomtasks.postponetask.domain.PostponeChoice +import com.costular.atomtasks.tasks.usecase.PostponeTaskUseCase import com.google.common.truth.Truth.assertThat import io.mockk.coEvery import io.mockk.coVerify @@ -21,9 +22,10 @@ class PostponeTaskViewModelTest : MviViewModelTest() { private lateinit var sut: PostponeTaskViewModel - private val getPostponeChoiceListUseCase: GetPostponeChoiceListUseCase = mockk(relaxed = true) - private val postponeTaskUseCase: PostponeTaskUseCase = mockk(relaxed = true) - private val taskNotificationManager: TaskNotificationManager = mockk(relaxed = true) + private val getPostponeChoiceListUseCase: GetPostponeChoiceListUseCase = + mockk(relaxUnitFun = true) + private val postponeTaskUseCase: PostponeTaskUseCase = mockk(relaxUnitFun = true) + private val taskNotificationManager: TaskNotificationManager = mockk(relaxUnitFun = true) @Before fun setup() { @@ -72,6 +74,7 @@ class PostponeTaskViewModelTest : MviViewModelTest() { fun `Should cancel the notification manager when the task is postponed`() = runTest { val taskId = 123L coEvery { getPostponeChoiceListUseCase(Unit) } returns FakeChoices + coEvery { postponeTaskUseCase.invoke(any()) } returns Unit.toResult() sut.initialize(taskId) sut.onSelectPostponeChoice(PostponeChoice.OneHour) diff --git a/feature/settings/src/main/java/com/costular/atomtasks/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/costular/atomtasks/settings/SettingsScreen.kt index 4344bc23..618d9223 100644 --- a/feature/settings/src/main/java/com/costular/atomtasks/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/costular/atomtasks/settings/SettingsScreen.kt @@ -46,7 +46,7 @@ object EmptySettingsNavigator : SettingsNavigator { override fun navigateToSelectTheme(theme: String) = Unit } -@Destination(start = true) +@Destination @Composable fun SettingsScreen( navigator: SettingsNavigator, diff --git a/feature/settings/src/main/java/com/costular/atomtasks/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/costular/atomtasks/settings/SettingsViewModel.kt index 55562fd2..a397890c 100644 --- a/feature/settings/src/main/java/com/costular/atomtasks/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/costular/atomtasks/settings/SettingsViewModel.kt @@ -10,7 +10,7 @@ import com.costular.atomtasks.data.settings.SetThemeUseCase import com.costular.atomtasks.data.settings.Theme import com.costular.atomtasks.settings.analytics.SettingsChangeAutoforward import com.costular.atomtasks.settings.analytics.SettingsChangeTheme -import com.costular.core.usecase.invoke +import com.costular.atomtasks.core.usecase.invoke import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.collect diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c65eefc4..3ab3a4a1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,40 +1,39 @@ [versions] -kotlin = "1.9.10" -coroutines = "1.7.2" +kotlin = "1.9.21" +coroutines = "1.7.3" core = "1.12.0" appCompat = "1.7.0-alpha03" lifecycle = "2.6.2" viewModelCompose = "2.6.2" -mockk = "1.13.3" +mockk = "1.13.9" hilt = "2.46.1" hiltAndroidx = "1.0.0" testRunner = "1.5.0" testJetpack = "2.2.0" -composeMaterial3 = "1.2.0-alpha07" -accompanist = "0.31.6-rc" -workManager = "2.8.1" -room = "2.5.2" +composeMaterial3 = "1.2.0-beta01" +accompanist = "0.32.0" +workManager = "2.9.0" +room = "2.6.1" paparazzi = "1.3.1" -composeDestinations = "1.9.53" +composeDestinations = "1.9.55" junit5 = "5.9.0" -balloon = "1.6.1" -ksp = "1.9.10-1.0.13" +ksp = "1.9.21-1.0.16" jacoco = "0.8.7" -androidxComposeBom = "2023.09.00" -composeCompiler = "1.5.3" +androidxComposeBom = "2023.10.01" +composeCompiler = "1.5.6" reordeable = "0.9.6" -collectionsImmutable = "0.3.5" +collectionsImmutable = "0.3.7" junit = "4.13.2" androidx-test-ext-junit = "1.1.5" espresso-core = "3.5.1" -material = "1.9.0" -benchmarkBaselineProfileGradlePlugin = "1.2.0" +benchmarkBaselineProfileGradlePlugin = "1.2.2" uiautomator = "2.2.0" -benchmark-macro-junit4 = "1.2.0" +benchmarkMacroJunit4 = "1.2.2" profileinstaller = "1.3.1" playReview = "2.0.1" +material = "1.10.0" [libraries] coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } @@ -45,14 +44,14 @@ lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", v hilt = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } hilt-ext-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltAndroidx" } -hilt-navigation-compose = "androidx.hilt:hilt-navigation-compose:1.0.0" -hilt-work = "androidx.hilt:hilt-work:1.0.0" +hilt-navigation-compose = "androidx.hilt:hilt-navigation-compose:1.1.0" +hilt-work = "androidx.hilt:hilt-work:1.1.0" hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } startup = "androidx.startup:startup-runtime:1.1.1" preferences = "androidx.preference:preference-ktx:1.2.1" preferences-datastore = "androidx.datastore:datastore-preferences:1.0.0" compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" } -compose-activity = "androidx.activity:activity-compose:1.7.2" +compose-activity = "androidx.activity:activity-compose:1.8.2" compose-ui = { module = "androidx.compose.ui:ui" } compose-foundation = { module = "androidx.compose.foundation:foundation" } compose-layout = { module = "androidx.compose.foundation:foundation-layout" } @@ -63,25 +62,24 @@ compose-ui-test = { module = "androidx.compose.ui:ui-test-junit4" } compose-ui-manifest = { module = "androidx.compose.ui:ui-test-manifest" } compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "composeMaterial3" } compose-material3-windowsize = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "composeMaterial3" } -compose-ui-text-fonts = { module = "androidx.compose.ui:ui-text-google-fonts", version = "1.6.0-alpha05" } +compose-ui-text-fonts = { module = "androidx.compose.ui:ui-text-google-fonts" } accompanist-systemui = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" } work = { module = "androidx.work:work-runtime-ktx", version.ref = "workManager" } work-testing = { module = "androidx.work:work-testing", version.ref = "workManager" } -fragment = "androidx.fragment:fragment-ktx:1.7.0-alpha04" +fragment = "androidx.fragment:fragment-ktx:1.7.0-alpha07" room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" } -calendar = "com.kizitonwose.calendar:compose:2.0.4" +calendar = "com.kizitonwose.calendar:compose:2.5.0-beta01" android-junit = "androidx.test.ext:junit-ktx:1.1.5" -firebase-bom = "com.google.firebase:firebase-bom:32.4.0" +firebase-bom = "com.google.firebase:firebase-bom:32.7.0" firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx" } compose-destinations = { module = "io.github.raamcosta.compose-destinations:animations-core", version.ref = "composeDestinations" } compose-destinations-ksp = { module = "io.github.raamcosta.compose-destinations:ksp", version.ref = "composeDestinations" } reordeable = { group = "org.burnoutcrew.composereorderable", name = "reorderable", version.ref = "reordeable" } -balloon = { group = "com.github.skydoves", name = "balloon-compose", version.ref = "balloon" } play-review = { group = "com.google.android.play", name = "review-ktx", version.ref = "playReview" } @@ -94,23 +92,23 @@ androidx-test = { module = "androidx.arch.core:core-testing", version.ref = "tes turbine = "app.cash.turbine:turbine:1.0.0" androidx-test-runner = { module = "androidx.test:runner", version.ref = "testRunner" } androidx-test-rules = { module = "androidx.test:rules", version.ref = "testRunner" } -android-desugarjdk = "com.android.tools:desugar_jdk_libs:2.0.3" -robolectric = "org.robolectric:robolectric:4.10.3" -testparameterinjector = "com.google.testparameterinjector:test-parameter-injector:1.11" +android-desugarjdk = "com.android.tools:desugar_jdk_libs:2.0.4" +robolectric = "org.robolectric:robolectric:4.11.1" +testparameterinjector = "com.google.testparameterinjector:test-parameter-injector:1.12" kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "collectionsImmutable" } -androidGradle = "com.android.tools.build:gradle:8.2.0-rc02" +androidGradle = "com.android.tools.build:gradle:8.2.1" kotlinGradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } -detektGradle = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.1" +detektGradle = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.4" ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } junit-junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } -material = { group = "com.google.android.material", name = "material", version.ref = "material" } androidx-benchmark-baseline-profile-gradle-plugin = { group = "androidx.benchmark", name = "benchmark-baseline-profile-gradle-plugin", version.ref = "benchmarkBaselineProfileGradlePlugin" } uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } -benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmark-macro-junit4" } +benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" } profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } [plugins] ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e38cf13c..b6451acd 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Nov 02 19:24:17 WET 2023 +#Sat Jan 13 15:25:46 WET 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/screenshot_testing/src/test/java/com/costular/atomtasks/screenshottesting/designsystem/HorizontalCalendarSnapshotTest.kt b/screenshot_testing/src/test/java/com/costular/atomtasks/screenshottesting/designsystem/HorizontalCalendarSnapshotTest.kt deleted file mode 100644 index 5cd69561..00000000 --- a/screenshot_testing/src/test/java/com/costular/atomtasks/screenshottesting/designsystem/HorizontalCalendarSnapshotTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.costular.atomtasks.screenshottesting.designsystem - -import app.cash.paparazzi.Paparazzi -import com.costular.atomtasks.coreui.date.asDay -import com.costular.atomtasks.screenshottesting.utils.FontSize -import com.costular.atomtasks.screenshottesting.utils.PaparazziFactory -import com.costular.atomtasks.screenshottesting.utils.Theme -import com.costular.atomtasks.screenshottesting.utils.asFloat -import com.costular.atomtasks.screenshottesting.utils.isDarkTheme -import com.costular.atomtasks.screenshottesting.utils.screenshot -import com.costular.designsystem.components.HorizontalCalendar -import com.google.testing.junit.testparameterinjector.TestParameter -import com.google.testing.junit.testparameterinjector.TestParameterInjector -import java.time.LocalDate -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(TestParameterInjector::class) -class HorizontalCalendarSnapshotTest { - - @TestParameter - private lateinit var fontScale: FontSize - - @TestParameter - private lateinit var themeMode: Theme - - @get:Rule - val paparazzi: Paparazzi = PaparazziFactory.create() - - @Test - fun horizontalCalendarTest() { - paparazzi.screenshot( - isDarkTheme = themeMode.isDarkTheme(), - fontScale = fontScale.asFloat() - ) { - HorizontalCalendar( - onSelectDay = {}, - weekDays = WeekDays, - selectedDay = LocalDate.of(2023, 9, 1).asDay(), - ) - } - } - - private companion object { - val WeekDays = listOf( - LocalDate.of(2023, 8, 28), - LocalDate.of(2023, 8, 29), - LocalDate.of(2023, 8, 30), - LocalDate.of(2023, 8, 31), - LocalDate.of(2023, 9, 1), - LocalDate.of(2023, 9, 2), - LocalDate.of(2023, 9, 3) - ) - } -} diff --git a/screenshot_testing/src/test/java/com/costular/atomtasks/screenshottesting/designsystem/TaskSnapshotTest.kt b/screenshot_testing/src/test/java/com/costular/atomtasks/screenshottesting/designsystem/TaskCardScreenshotTest.kt similarity index 71% rename from screenshot_testing/src/test/java/com/costular/atomtasks/screenshottesting/designsystem/TaskSnapshotTest.kt rename to screenshot_testing/src/test/java/com/costular/atomtasks/screenshottesting/designsystem/TaskCardScreenshotTest.kt index 724e756a..35ab3660 100644 --- a/screenshot_testing/src/test/java/com/costular/atomtasks/screenshottesting/designsystem/TaskSnapshotTest.kt +++ b/screenshot_testing/src/test/java/com/costular/atomtasks/screenshottesting/designsystem/TaskCardScreenshotTest.kt @@ -7,6 +7,7 @@ import com.costular.atomtasks.screenshottesting.utils.Theme import com.costular.atomtasks.screenshottesting.utils.asFloat import com.costular.atomtasks.screenshottesting.utils.isDarkTheme import com.costular.atomtasks.screenshottesting.utils.screenshot +import com.costular.atomtasks.tasks.model.RecurrenceType import com.costular.atomtasks.tasks.model.Reminder import com.costular.atomtasks.tasks.model.TaskCard import com.google.testing.junit.testparameterinjector.TestParameter @@ -18,7 +19,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(TestParameterInjector::class) -class TaskSnapshotTest { +class TaskCardScreenshotTest { @TestParameter private lateinit var fontScale: FontSize @@ -42,6 +43,7 @@ class TaskSnapshotTest { onMark = {}, onOpen = {}, isBeingDragged = false, + recurrenceType = null, ) } } @@ -59,6 +61,7 @@ class TaskSnapshotTest { onMark = {}, onOpen = {}, isBeingDragged = false, + recurrenceType = null, ) } } @@ -80,12 +83,13 @@ class TaskSnapshotTest { onMark = { }, onOpen = { }, isBeingDragged = false, + recurrenceType = null, ) } } @Test - fun taskFinishedWithReminder() { + fun taskFinished() { paparazzi.screenshot( isDarkTheme = themeMode.isDarkTheme(), fontScale = fontScale.asFloat(), @@ -93,14 +97,51 @@ class TaskSnapshotTest { TaskCard( title = "This is a task with reminder", isFinished = true, + reminder = null, + onMark = { }, + onOpen = { }, + isBeingDragged = false, + recurrenceType = null, + ) + } + } + + @Test + fun taskRecurring() { + paparazzi.screenshot( + isDarkTheme = themeMode.isDarkTheme(), + fontScale = fontScale.asFloat(), + ) { + TaskCard( + title = "This is a recurring task", + isFinished = false, + reminder = null, + onMark = { }, + onOpen = { }, + isBeingDragged = false, + recurrenceType = RecurrenceType.WEEKLY, + ) + } + } + + @Test + fun taskRecurringWithReminder() { + paparazzi.screenshot( + isDarkTheme = themeMode.isDarkTheme(), + fontScale = fontScale.asFloat(), + ) { + TaskCard( + title = "This is a task with reminder", + isFinished = false, reminder = Reminder( - 0L, - LocalTime.of(9, 0), - LocalDate.now(), + id = 1L, + time = LocalTime.of(9, 0), + date = LocalDate.now(), ), onMark = { }, onOpen = { }, isBeingDragged = false, + recurrenceType = RecurrenceType.DAILY, ) } } @@ -123,6 +164,7 @@ class TaskSnapshotTest { onMark = { }, onOpen = { }, isBeingDragged = false, + recurrenceType = null, ) } } diff --git a/screenshot_testing/src/test/java/com/costular/atomtasks/screenshottesting/designsystem/TaskListSnapshotTest.kt b/screenshot_testing/src/test/java/com/costular/atomtasks/screenshottesting/designsystem/TaskListSnapshotTest.kt index 773bfbe6..5021506a 100644 --- a/screenshot_testing/src/test/java/com/costular/atomtasks/screenshottesting/designsystem/TaskListSnapshotTest.kt +++ b/screenshot_testing/src/test/java/com/costular/atomtasks/screenshottesting/designsystem/TaskListSnapshotTest.kt @@ -64,6 +64,10 @@ class TaskListSnapshotTest { null, true, position = 1, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ), Task( id = 0L, @@ -77,6 +81,10 @@ class TaskListSnapshotTest { ), isDone = false, position = 2, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ), Task( id = 0L, @@ -86,6 +94,10 @@ class TaskListSnapshotTest { reminder = null, isDone = false, position = 3, + isRecurring = false, + recurrenceEndDate = null, + recurrenceType = null, + parentId = null, ), ), ) @@ -102,8 +114,6 @@ class TaskListSnapshotTest { onMarkTask = { _, _ -> }, modifier = Modifier.fillMaxWidth(), state = rememberReorderableLazyListState(onMove = { _, _ -> }), - shouldShowTaskOrderTutorial = false, - onDismissTaskOrderTutorial = {}, ) } } diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconAndText[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconAndText[BIG,DARK].png index c925ebfb..386f15f4 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconAndText[BIG,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconAndText[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconAndText[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconAndText[BIG,LIGHT].png index 0a845ccd..bddc251d 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconAndText[BIG,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconAndText[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconAndText[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconAndText[NORMAL,DARK].png index e96005a9..42bc874e 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconAndText[NORMAL,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconAndText[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconAndText[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconAndText[NORMAL,LIGHT].png index c3f45ea3..04fb1cf2 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconAndText[NORMAL,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconAndText[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconTitleAndActions[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconTitleAndActions[BIG,DARK].png index 31398cf9..63c5b5c4 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconTitleAndActions[BIG,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconTitleAndActions[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconTitleAndActions[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconTitleAndActions[BIG,LIGHT].png index 4c944fdf..c33f1640 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconTitleAndActions[BIG,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconTitleAndActions[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconTitleAndActions[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconTitleAndActions[NORMAL,DARK].png index ccf463f4..48877fc7 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconTitleAndActions[NORMAL,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconTitleAndActions[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconTitleAndActions[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconTitleAndActions[NORMAL,LIGHT].png index 9409e30c..38b3675c 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconTitleAndActions[NORMAL,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_AtomTopBarSnapshotTest_atomTopBarWithBackIconTitleAndActions[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelectedWithError[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelectedWithError[BIG,DARK].png index 72de5b52..03217065 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelectedWithError[BIG,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelectedWithError[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelectedWithError[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelectedWithError[BIG,LIGHT].png index 4b956240..b31d753c 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelectedWithError[BIG,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelectedWithError[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelectedWithError[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelectedWithError[NORMAL,DARK].png index 22cb9864..371e2012 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelectedWithError[NORMAL,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelectedWithError[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelectedWithError[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelectedWithError[NORMAL,LIGHT].png index 225d4381..32c415f4 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelectedWithError[NORMAL,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelectedWithError[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelected[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelected[BIG,DARK].png index 16e6f6c8..186583df 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelected[BIG,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelected[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelected[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelected[BIG,LIGHT].png index c06ae6cb..ccb72e6c 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelected[BIG,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelected[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelected[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelected[NORMAL,DARK].png index c6a3f0f6..f1953928 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelected[NORMAL,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelected[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelected[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelected[NORMAL,LIGHT].png index a97b97d7..88cdf752 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelected[NORMAL,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipSelected[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipUnselected[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipUnselected[BIG,DARK].png index d7eff3ff..8f91b427 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipUnselected[BIG,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipUnselected[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipUnselected[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipUnselected[BIG,LIGHT].png index d75dc3f4..e79777b2 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipUnselected[BIG,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipUnselected[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipUnselected[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipUnselected[NORMAL,DARK].png index 6c103ce3..bd3d319e 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipUnselected[NORMAL,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipUnselected[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipUnselected[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipUnselected[NORMAL,LIGHT].png index 959fbaa6..119f8ab0 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipUnselected[NORMAL,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ClearableChipSnapshotTest_removableChipUnselected[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_HorizontalCalendarSnapshotTest_horizontalCalendarTest[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_HorizontalCalendarSnapshotTest_horizontalCalendarTest[BIG,DARK].png deleted file mode 100644 index 2a9a64ed..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_HorizontalCalendarSnapshotTest_horizontalCalendarTest[BIG,DARK].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_HorizontalCalendarSnapshotTest_horizontalCalendarTest[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_HorizontalCalendarSnapshotTest_horizontalCalendarTest[BIG,LIGHT].png deleted file mode 100644 index 98cbbf90..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_HorizontalCalendarSnapshotTest_horizontalCalendarTest[BIG,LIGHT].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_HorizontalCalendarSnapshotTest_horizontalCalendarTest[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_HorizontalCalendarSnapshotTest_horizontalCalendarTest[NORMAL,DARK].png deleted file mode 100644 index 845fa3e9..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_HorizontalCalendarSnapshotTest_horizontalCalendarTest[NORMAL,DARK].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_HorizontalCalendarSnapshotTest_horizontalCalendarTest[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_HorizontalCalendarSnapshotTest_horizontalCalendarTest[NORMAL,LIGHT].png deleted file mode 100644 index a680b98c..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_HorizontalCalendarSnapshotTest_horizontalCalendarTest[NORMAL,LIGHT].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_emptyOutlinedTextField[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_emptyOutlinedTextField[BIG,DARK].png index 816c8a88..f59b5447 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_emptyOutlinedTextField[BIG,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_emptyOutlinedTextField[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_emptyOutlinedTextField[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_emptyOutlinedTextField[BIG,LIGHT].png index d48ece93..41d052c5 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_emptyOutlinedTextField[BIG,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_emptyOutlinedTextField[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_emptyOutlinedTextField[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_emptyOutlinedTextField[NORMAL,DARK].png index 3c8505f1..7374b99e 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_emptyOutlinedTextField[NORMAL,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_emptyOutlinedTextField[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_emptyOutlinedTextField[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_emptyOutlinedTextField[NORMAL,LIGHT].png index 7bdcf653..41a79ad6 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_emptyOutlinedTextField[NORMAL,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_emptyOutlinedTextField[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_outlinedTextField[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_outlinedTextField[BIG,DARK].png index 5f5e51f9..6fd492ca 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_outlinedTextField[BIG,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_outlinedTextField[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_outlinedTextField[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_outlinedTextField[BIG,LIGHT].png index 1ed11d9e..469deffd 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_outlinedTextField[BIG,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_outlinedTextField[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_outlinedTextField[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_outlinedTextField[NORMAL,DARK].png index 12a0e2bf..09e4bc68 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_outlinedTextField[NORMAL,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_outlinedTextField[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_outlinedTextField[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_outlinedTextField[NORMAL,LIGHT].png index 74c76205..d9193055 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_outlinedTextField[NORMAL,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_OutlinedTextFieldSnapshotTest_outlinedTextField[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_PrimaryButtonScreenshotTest_primaryButtonWithText[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_PrimaryButtonScreenshotTest_primaryButtonWithText[BIG,DARK].png index 343b1d95..f572f74a 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_PrimaryButtonScreenshotTest_primaryButtonWithText[BIG,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_PrimaryButtonScreenshotTest_primaryButtonWithText[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_PrimaryButtonScreenshotTest_primaryButtonWithText[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_PrimaryButtonScreenshotTest_primaryButtonWithText[BIG,LIGHT].png index 4ac62278..13016b34 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_PrimaryButtonScreenshotTest_primaryButtonWithText[BIG,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_PrimaryButtonScreenshotTest_primaryButtonWithText[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_PrimaryButtonScreenshotTest_primaryButtonWithText[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_PrimaryButtonScreenshotTest_primaryButtonWithText[NORMAL,DARK].png index 021e16c6..9c496057 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_PrimaryButtonScreenshotTest_primaryButtonWithText[NORMAL,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_PrimaryButtonScreenshotTest_primaryButtonWithText[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_PrimaryButtonScreenshotTest_primaryButtonWithText[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_PrimaryButtonScreenshotTest_primaryButtonWithText[NORMAL,LIGHT].png index 4c36cea6..74127ae6 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_PrimaryButtonScreenshotTest_primaryButtonWithText[NORMAL,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_PrimaryButtonScreenshotTest_primaryButtonWithText[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenHeaderSnapshotTest_header[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenHeaderSnapshotTest_header[BIG,DARK].png index 416d1081..e71b82b6 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenHeaderSnapshotTest_header[BIG,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenHeaderSnapshotTest_header[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenHeaderSnapshotTest_header[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenHeaderSnapshotTest_header[BIG,LIGHT].png index 3ee6cea4..a7f763b9 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenHeaderSnapshotTest_header[BIG,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenHeaderSnapshotTest_header[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenHeaderSnapshotTest_header[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenHeaderSnapshotTest_header[NORMAL,DARK].png index 416d1081..e71b82b6 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenHeaderSnapshotTest_header[NORMAL,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenHeaderSnapshotTest_header[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenHeaderSnapshotTest_header[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenHeaderSnapshotTest_header[NORMAL,LIGHT].png index 3ee6cea4..a7f763b9 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenHeaderSnapshotTest_header[NORMAL,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenHeaderSnapshotTest_header[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenSubheaderSnapshotTest_subheader[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenSubheaderSnapshotTest_subheader[BIG,DARK].png index 287b52aa..36d22bbd 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenSubheaderSnapshotTest_subheader[BIG,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenSubheaderSnapshotTest_subheader[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenSubheaderSnapshotTest_subheader[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenSubheaderSnapshotTest_subheader[BIG,LIGHT].png index fab38b0a..0e81ae97 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenSubheaderSnapshotTest_subheader[BIG,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenSubheaderSnapshotTest_subheader[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenSubheaderSnapshotTest_subheader[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenSubheaderSnapshotTest_subheader[NORMAL,DARK].png index 9ef1c75f..cc904ad1 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenSubheaderSnapshotTest_subheader[NORMAL,DARK].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenSubheaderSnapshotTest_subheader[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenSubheaderSnapshotTest_subheader[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenSubheaderSnapshotTest_subheader[NORMAL,LIGHT].png index 0b54daa5..d338343a 100644 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenSubheaderSnapshotTest_subheader[NORMAL,LIGHT].png and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_ScreenSubheaderSnapshotTest_subheader[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskDoneTest[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskDoneTest[BIG,DARK].png new file mode 100644 index 00000000..79cbc2c1 Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskDoneTest[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskDoneTest[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskDoneTest[BIG,LIGHT].png new file mode 100644 index 00000000..d9cf2662 Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskDoneTest[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskDoneTest[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskDoneTest[NORMAL,DARK].png new file mode 100644 index 00000000..67fd5a20 Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskDoneTest[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskDoneTest[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskDoneTest[NORMAL,LIGHT].png new file mode 100644 index 00000000..ed01f01a Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskDoneTest[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinishedWithLongName[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinishedWithLongName[BIG,DARK].png new file mode 100644 index 00000000..2dda9603 Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinishedWithLongName[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinishedWithLongName[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinishedWithLongName[BIG,LIGHT].png new file mode 100644 index 00000000..7004add3 Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinishedWithLongName[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinishedWithLongName[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinishedWithLongName[NORMAL,DARK].png new file mode 100644 index 00000000..ecdd2f25 Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinishedWithLongName[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinishedWithLongName[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinishedWithLongName[NORMAL,LIGHT].png new file mode 100644 index 00000000..434672d8 Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinishedWithLongName[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinished[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinished[BIG,DARK].png new file mode 100644 index 00000000..031e897b Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinished[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinished[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinished[BIG,LIGHT].png new file mode 100644 index 00000000..7b5f124f Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinished[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinished[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinished[NORMAL,DARK].png new file mode 100644 index 00000000..7b1f077d Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinished[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinished[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinished[NORMAL,LIGHT].png new file mode 100644 index 00000000..5d132cd7 Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskFinished[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurringWithReminder[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurringWithReminder[BIG,DARK].png new file mode 100644 index 00000000..6072fa3b Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurringWithReminder[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurringWithReminder[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurringWithReminder[BIG,LIGHT].png new file mode 100644 index 00000000..4583b564 Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurringWithReminder[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurringWithReminder[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurringWithReminder[NORMAL,DARK].png new file mode 100644 index 00000000..af0bd76f Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurringWithReminder[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurringWithReminder[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurringWithReminder[NORMAL,LIGHT].png new file mode 100644 index 00000000..a2c46c7c Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurringWithReminder[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurring[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurring[BIG,DARK].png new file mode 100644 index 00000000..2e6be95f Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurring[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurring[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurring[BIG,LIGHT].png new file mode 100644 index 00000000..11733470 Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurring[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurring[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurring[NORMAL,DARK].png new file mode 100644 index 00000000..6c001a9d Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurring[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurring[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurring[NORMAL,LIGHT].png new file mode 100644 index 00000000..3436c1d1 Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskRecurring[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskTest[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskTest[BIG,DARK].png new file mode 100644 index 00000000..0500d73c Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskTest[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskTest[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskTest[BIG,LIGHT].png new file mode 100644 index 00000000..f9651484 Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskTest[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskTest[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskTest[NORMAL,DARK].png new file mode 100644 index 00000000..ca6bc1a7 Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskTest[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskTest[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskTest[NORMAL,LIGHT].png new file mode 100644 index 00000000..d3a3e319 Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskTest[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskWithReminder[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskWithReminder[BIG,DARK].png new file mode 100644 index 00000000..21f2a821 Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskWithReminder[BIG,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskWithReminder[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskWithReminder[BIG,LIGHT].png new file mode 100644 index 00000000..4967d0cc Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskWithReminder[BIG,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskWithReminder[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskWithReminder[NORMAL,DARK].png new file mode 100644 index 00000000..f82dbd7b Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskWithReminder[NORMAL,DARK].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskWithReminder[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskWithReminder[NORMAL,LIGHT].png new file mode 100644 index 00000000..49a3352c Binary files /dev/null and b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskCardScreenshotTest_taskWithReminder[NORMAL,LIGHT].png differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskDoneTest[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskDoneTest[BIG,DARK].png deleted file mode 100644 index 8ee50b25..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskDoneTest[BIG,DARK].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskDoneTest[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskDoneTest[BIG,LIGHT].png deleted file mode 100644 index 7d7e2e74..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskDoneTest[BIG,LIGHT].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskDoneTest[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskDoneTest[NORMAL,DARK].png deleted file mode 100644 index 5799e2ee..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskDoneTest[NORMAL,DARK].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskDoneTest[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskDoneTest[NORMAL,LIGHT].png deleted file mode 100644 index 75ceffae..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskDoneTest[NORMAL,LIGHT].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithLongName[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithLongName[BIG,DARK].png deleted file mode 100644 index 16006e09..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithLongName[BIG,DARK].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithLongName[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithLongName[BIG,LIGHT].png deleted file mode 100644 index 9a57898c..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithLongName[BIG,LIGHT].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithLongName[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithLongName[NORMAL,DARK].png deleted file mode 100644 index dba5e21b..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithLongName[NORMAL,DARK].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithLongName[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithLongName[NORMAL,LIGHT].png deleted file mode 100644 index c9468f78..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithLongName[NORMAL,LIGHT].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithReminder[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithReminder[BIG,DARK].png deleted file mode 100644 index 0cc9c5cd..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithReminder[BIG,DARK].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithReminder[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithReminder[BIG,LIGHT].png deleted file mode 100644 index 4c551723..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithReminder[BIG,LIGHT].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithReminder[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithReminder[NORMAL,DARK].png deleted file mode 100644 index 90fc55c1..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithReminder[NORMAL,DARK].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithReminder[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithReminder[NORMAL,LIGHT].png deleted file mode 100644 index 7d36f2f4..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskFinishedWithReminder[NORMAL,LIGHT].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskTest[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskTest[BIG,DARK].png deleted file mode 100644 index dea4047b..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskTest[BIG,DARK].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskTest[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskTest[BIG,LIGHT].png deleted file mode 100644 index a4214e14..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskTest[BIG,LIGHT].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskTest[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskTest[NORMAL,DARK].png deleted file mode 100644 index 8eece8b9..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskTest[NORMAL,DARK].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskTest[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskTest[NORMAL,LIGHT].png deleted file mode 100644 index c401f62d..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskTest[NORMAL,LIGHT].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskWithReminder[BIG,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskWithReminder[BIG,DARK].png deleted file mode 100644 index 261d2f2b..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskWithReminder[BIG,DARK].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskWithReminder[BIG,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskWithReminder[BIG,LIGHT].png deleted file mode 100644 index c0435ec4..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskWithReminder[BIG,LIGHT].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskWithReminder[NORMAL,DARK].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskWithReminder[NORMAL,DARK].png deleted file mode 100644 index b14b2435..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskWithReminder[NORMAL,DARK].png and /dev/null differ diff --git a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskWithReminder[NORMAL,LIGHT].png b/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskWithReminder[NORMAL,LIGHT].png deleted file mode 100644 index de64bb0c..00000000 Binary files a/screenshot_testing/src/test/snapshots/images/com.costular.atomtasks.screenshottesting.designsystem_TaskSnapshotTest_taskWithReminder[NORMAL,LIGHT].png and /dev/null differ diff --git a/settings.gradle.kts b/settings.gradle.kts index 49b1a4e9..cfa5a17d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,6 +34,7 @@ include(":core:analytics") include(":feature:postpone-task") include(":core:notifications") include(":core:logging") -include(":baselineprofile") +include(":benchmarks") include(":core:review") include(":core:preferences") +include(":core:jobs")