mirror of
https://github.com/wismna/DoNext.git
synced 2026-07-25 20:23:06 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 150058437f | |||
| 897b1892e5 |
@@ -33,6 +33,9 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
|
||||
- name: Run unit tests
|
||||
run: ./gradlew :donextv2:testDebugUnitTest
|
||||
|
||||
- name: Decode signing key
|
||||
run: echo "${{ secrets.SIGNING_KEY_BASE64 }}" | base64 --decode > ${{ env.MODULE }}/upload.jks
|
||||
|
||||
|
||||
@@ -10,3 +10,4 @@
|
||||
/captures
|
||||
/fastlane/keys/
|
||||
/donext*/release/
|
||||
.claude
|
||||
Generated
+1118
-110
File diff suppressed because it is too large
Load Diff
Generated
+1
@@ -4,6 +4,7 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="donextv2">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="donext">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
|
||||
Generated
-1
@@ -6,7 +6,6 @@
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
|
||||
+5
-5
@@ -13,9 +13,9 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.13.0'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21'
|
||||
classpath 'org.jetbrains.kotlin:compose-compiler-gradle-plugin:2.0.21'
|
||||
classpath 'com.android.tools.build:gradle:9.3.0'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.10'
|
||||
classpath 'org.jetbrains.kotlin:compose-compiler-gradle-plugin:2.2.10'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
@@ -23,8 +23,8 @@ buildscript {
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
|
||||
id("com.google.dagger.hilt.android") version "2.57.2" apply false
|
||||
id("com.google.devtools.ksp") version '2.3.2' apply false
|
||||
id("com.google.dagger.hilt.android") version "2.60.1" apply false
|
||||
}
|
||||
|
||||
allprojects {
|
||||
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdk 36
|
||||
compileSdk 37
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.wismna.geoffroy.donext"
|
||||
@@ -13,7 +13,7 @@ android {
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
namespace 'com.wismna.geoffroy.donext'
|
||||
@@ -26,12 +26,12 @@ android {
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.appcompat:appcompat:1.7.1'
|
||||
implementation 'com.google.android.material:material:1.13.0'
|
||||
implementation 'com.google.android.material:material:1.14.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.percentlayout:percentlayout:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.4.0'
|
||||
implementation 'androidx.browser:browser:1.9.0'
|
||||
implementation 'net.danlew:android.joda:2.13.1'
|
||||
implementation 'androidx.browser:browser:1.10.0'
|
||||
implementation 'net.danlew:android.joda:2.14.2'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
@@ -42,7 +42,7 @@ dependencies {
|
||||
implementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
||||
|
||||
// Lifecycle components
|
||||
def lifecycleVersion = '2.9.4'
|
||||
def lifecycleVersion = '2.11.0'
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||
|
||||
+14
-17
@@ -14,14 +14,14 @@ ksp {
|
||||
|
||||
android {
|
||||
namespace = "com.wismna.geoffroy.donext"
|
||||
compileSdk = 36
|
||||
compileSdk = 37
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.wismna.geoffroy.donext"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 35
|
||||
versionName = "2.0.1"
|
||||
versionCode = 36
|
||||
versionName = "2.0.2"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -65,9 +65,6 @@ android {
|
||||
compose = true
|
||||
viewBinding = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.1.1"
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
@@ -82,35 +79,35 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.4")
|
||||
implementation("androidx.activity:activity-compose:1.11.0")
|
||||
implementation(platform("androidx.compose:compose-bom:2025.11.00"))
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.11.0")
|
||||
implementation("androidx.activity:activity-compose:1.13.0")
|
||||
implementation(platform("androidx.compose:compose-bom:2026.06.01"))
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
implementation("androidx.compose.material3:material3-window-size-class:1.4.0")
|
||||
implementation("androidx.compose.material:material-icons-extended:1.7.8")
|
||||
implementation("androidx.navigation:navigation-compose:2.9.6")
|
||||
implementation("androidx.hilt:hilt-navigation-compose:1.3.0")
|
||||
implementation("sh.calvin.reorderable:reorderable:3.0.0")
|
||||
implementation("androidx.navigation:navigation-compose:2.9.8")
|
||||
implementation("androidx.hilt:hilt-navigation-compose:1.4.0")
|
||||
implementation("sh.calvin.reorderable:reorderable:3.1.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("io.mockk:mockk:1.14.6")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
|
||||
testImplementation("io.mockk:mockk:1.14.11")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0")
|
||||
testImplementation("com.google.truth:truth:1.4.5")
|
||||
androidTestImplementation("androidx.test.ext:junit-ktx:1.3.0")
|
||||
androidTestImplementation(platform("androidx.compose:compose-bom:2025.11.00"))
|
||||
androidTestImplementation(platform("androidx.compose:compose-bom:2026.06.01"))
|
||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||
androidTestImplementation("com.google.truth:truth:1.4.5")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||
|
||||
val roomVersion = "2.8.3"
|
||||
val roomVersion = "2.8.4"
|
||||
implementation("androidx.room:room-runtime:$roomVersion")
|
||||
androidTestImplementation("androidx.room:room-testing:$roomVersion")
|
||||
ksp("androidx.room:room-compiler:$roomVersion")
|
||||
|
||||
val hiltVersion = "2.57.2"
|
||||
val hiltVersion = "2.60.1"
|
||||
implementation("com.google.dagger:hilt-android:$hiltVersion")
|
||||
ksp("com.google.dagger:hilt-android-compiler:$hiltVersion")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.wismna.geoffroy.donext.data.injection
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ClockModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideClock(): Clock = Clock.systemDefaultZone()
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.wismna.geoffroy.donext.data.injection
|
||||
|
||||
import com.wismna.geoffroy.donext.data.local.repository.TaskListRepositoryImpl
|
||||
import com.wismna.geoffroy.donext.data.local.repository.TaskRepositoryImpl
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
@@ -15,4 +17,9 @@ abstract class RepositoryModule {
|
||||
abstract fun bindTaskRepository(
|
||||
impl: TaskRepositoryImpl
|
||||
): TaskRepository
|
||||
|
||||
@Binds
|
||||
abstract fun bindTaskListRepository(
|
||||
impl: TaskListRepositoryImpl
|
||||
): TaskListRepository
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.wismna.geoffroy.donext.data.local.repository
|
||||
|
||||
import com.wismna.geoffroy.donext.data.local.dao.TaskListDao
|
||||
import com.wismna.geoffroy.donext.data.toDomain
|
||||
import com.wismna.geoffroy.donext.data.toEntity
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
class TaskListRepositoryImpl @Inject constructor(
|
||||
private val taskListDao: TaskListDao
|
||||
): TaskListRepository {
|
||||
override fun getTaskLists(): Flow<List<TaskList>> {
|
||||
return taskListDao.getTaskLists().map {entities -> entities.map { it.toDomain() }}
|
||||
}
|
||||
|
||||
override suspend fun getTaskListById(taskListId: Long): TaskList? {
|
||||
return taskListDao.getTaskListById(taskListId)?.toDomain()
|
||||
}
|
||||
|
||||
override suspend fun insertTaskList(taskList: TaskList) {
|
||||
taskListDao.insertTaskList(taskList.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun updateTaskList(taskList: TaskList) {
|
||||
taskListDao.updateTaskList(taskList.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun deleteTaskList(taskListId: Long, isDeleted: Boolean) {
|
||||
taskListDao.deleteTaskList(taskListId, isDeleted)
|
||||
}
|
||||
|
||||
override fun getTaskListsWithOverdue(nowMillis: Long): Flow<List<TaskListWithOverdue>> {
|
||||
return taskListDao.getTaskListsWithOverdue(nowMillis)
|
||||
}
|
||||
}
|
||||
+2
-28
@@ -1,22 +1,17 @@
|
||||
package com.wismna.geoffroy.donext.data.local.repository
|
||||
|
||||
import com.wismna.geoffroy.donext.data.local.dao.TaskDao
|
||||
import com.wismna.geoffroy.donext.data.local.dao.TaskListDao
|
||||
import com.wismna.geoffroy.donext.data.toDomain
|
||||
import com.wismna.geoffroy.donext.data.toEntity
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskWithListName
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.map
|
||||
|
||||
class TaskRepositoryImpl @Inject constructor(
|
||||
private val taskDao: TaskDao,
|
||||
private val taskListDao: TaskListDao
|
||||
private val taskDao: TaskDao
|
||||
): TaskRepository {
|
||||
override fun getTasksForList(listId: Long): Flow<List<Task>> {
|
||||
return taskDao.getTasksForList(listId).map {entity -> entity.map { it.toDomain() }}
|
||||
@@ -58,28 +53,7 @@ class TaskRepositoryImpl @Inject constructor(
|
||||
taskDao.permanentDeleteAllDeletedTasks()
|
||||
}
|
||||
|
||||
override fun getTaskLists(): Flow<List<TaskList>> {
|
||||
return taskListDao.getTaskLists().map {entities -> entities.map { it.toDomain() }}
|
||||
}
|
||||
|
||||
override suspend fun getTaskListById(taskListId: Long): TaskList? {
|
||||
return taskListDao.getTaskListById(taskListId)?.toDomain()
|
||||
}
|
||||
|
||||
override suspend fun insertTaskList(taskList: TaskList) {
|
||||
taskListDao.insertTaskList(taskList.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun updateTaskList(taskList: TaskList) {
|
||||
taskListDao.updateTaskList(taskList.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun deleteTaskList(taskListId: Long, isDeleted: Boolean) {
|
||||
override suspend fun toggleAllTasksInListDeleted(taskListId: Long, isDeleted: Boolean) {
|
||||
taskDao.toggleAllTasksFromListDeleted(taskListId, isDeleted)
|
||||
taskListDao.deleteTaskList(taskListId, isDeleted)
|
||||
}
|
||||
|
||||
override fun getTaskListsWithOverdue(nowMillis: Long): Flow<List<TaskListWithOverdue>> {
|
||||
return taskListDao.getTaskListsWithOverdue(nowMillis).map { it }
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.wismna.geoffroy.donext.domain.repository
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TaskListRepository {
|
||||
fun getTaskLists(): Flow<List<TaskList>>
|
||||
suspend fun getTaskListById(taskListId: Long): TaskList?
|
||||
suspend fun insertTaskList(taskList: TaskList)
|
||||
suspend fun updateTaskList(taskList: TaskList)
|
||||
suspend fun deleteTaskList(taskListId: Long, isDeleted: Boolean)
|
||||
fun getTaskListsWithOverdue(nowMillis: Long): Flow<List<TaskListWithOverdue>>
|
||||
}
|
||||
+1
-9
@@ -1,8 +1,6 @@
|
||||
package com.wismna.geoffroy.donext.domain.repository
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskWithListName
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -17,11 +15,5 @@ interface TaskRepository {
|
||||
suspend fun toggleTaskDone(taskId: Long, isDone: Boolean)
|
||||
suspend fun permanentlyDeleteTask(taskId: Long)
|
||||
suspend fun permanentlyDeleteAllDeletedTask()
|
||||
|
||||
fun getTaskLists(): Flow<List<TaskList>>
|
||||
suspend fun getTaskListById(taskListId: Long): TaskList?
|
||||
suspend fun insertTaskList(taskList: TaskList)
|
||||
suspend fun updateTaskList(taskList: TaskList)
|
||||
suspend fun deleteTaskList(taskListId: Long, isDeleted: Boolean)
|
||||
fun getTaskListsWithOverdue(nowMillis: Long): Flow<List<TaskListWithOverdue>>
|
||||
suspend fun toggleAllTasksInListDeleted(taskListId: Long, isDeleted: Boolean)
|
||||
}
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class AddTaskListUseCase @Inject constructor(
|
||||
private val repository: TaskRepository
|
||||
private val repository: TaskListRepository
|
||||
) {
|
||||
suspend operator fun invoke(title: String, order: Int) {
|
||||
repository.insertTaskList(
|
||||
|
||||
+6
-3
@@ -1,12 +1,15 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeleteTaskListUseCase@Inject constructor(
|
||||
private val repository: TaskRepository
|
||||
class DeleteTaskListUseCase @Inject constructor(
|
||||
private val taskRepository: TaskRepository,
|
||||
private val taskListRepository: TaskListRepository
|
||||
) {
|
||||
suspend operator fun invoke(taskListId: Long, isDeleted: Boolean) {
|
||||
repository.deleteTaskList(taskListId, isDeleted)
|
||||
taskRepository.toggleAllTasksInListDeleted(taskListId, isDeleted)
|
||||
taskListRepository.deleteTaskList(taskListId, isDeleted)
|
||||
}
|
||||
}
|
||||
+10
-6
@@ -3,20 +3,24 @@ package com.wismna.geoffroy.donext.domain.usecase
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.time.Clock
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetDueTodayTasksUseCase @Inject constructor(private val repository: TaskRepository) {
|
||||
class GetDueTodayTasksUseCase @Inject constructor(
|
||||
private val repository: TaskRepository,
|
||||
private val clock: Clock
|
||||
) {
|
||||
operator fun invoke(): Flow<List<Task>> {
|
||||
val todayStart = LocalDate.now()
|
||||
.atStartOfDay(ZoneOffset.UTC)
|
||||
val today = LocalDate.now(clock)
|
||||
val todayStart = today
|
||||
.atStartOfDay(clock.zone)
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
|
||||
val todayEnd = LocalDate.now()
|
||||
val todayEnd = today
|
||||
.plusDays(1)
|
||||
.atStartOfDay(ZoneOffset.UTC)
|
||||
.atStartOfDay(clock.zone)
|
||||
.toInstant()
|
||||
.toEpochMilli() - 1
|
||||
return repository.getDueTodayTasks(
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetTaskListsUseCase @Inject constructor(private val repository: TaskRepository) {
|
||||
class GetTaskListsUseCase @Inject constructor(private val repository: TaskListRepository) {
|
||||
operator fun invoke(): Flow<List<TaskList>> = repository.getTaskLists()
|
||||
}
|
||||
+9
-6
@@ -1,17 +1,20 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.time.Clock
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetTaskListsWithOverdueUseCase @Inject constructor(private val taskRepository: TaskRepository) {
|
||||
class GetTaskListsWithOverdueUseCase @Inject constructor(
|
||||
private val taskListRepository: TaskListRepository,
|
||||
private val clock: Clock
|
||||
) {
|
||||
operator fun invoke(): Flow<List<TaskListWithOverdue>> {
|
||||
return taskRepository.getTaskListsWithOverdue(
|
||||
LocalDate.now()
|
||||
.atStartOfDay(ZoneOffset.UTC)
|
||||
return taskListRepository.getTaskListsWithOverdue(
|
||||
LocalDate.now(clock)
|
||||
.atStartOfDay(clock.zone)
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
)
|
||||
|
||||
+7
-5
@@ -1,23 +1,25 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class ToggleTaskDeletedUseCase @Inject constructor(
|
||||
private val repository: TaskRepository
|
||||
private val taskRepository: TaskRepository,
|
||||
private val taskListRepository: TaskListRepository
|
||||
) {
|
||||
suspend operator fun invoke(taskId: Long, isDeleted: Boolean) {
|
||||
if (!isDeleted) {
|
||||
val task = repository.getTaskById(taskId)
|
||||
val task = taskRepository.getTaskById(taskId)
|
||||
if (task != null) {
|
||||
// If task list was soft-deleted, restore it as well
|
||||
val taskList = repository.getTaskListById(task.taskListId)
|
||||
val taskList = taskListRepository.getTaskListById(task.taskListId)
|
||||
if (taskList != null && taskList.isDeleted) {
|
||||
repository.updateTaskList(taskList.copy(isDeleted = false))
|
||||
taskListRepository.updateTaskList(taskList.copy(isDeleted = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repository.toggleTaskDeleted(taskId, isDeleted)
|
||||
taskRepository.toggleTaskDeleted(taskId, isDeleted)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class UpdateTaskListUseCase @Inject constructor(
|
||||
private val repository: TaskRepository
|
||||
private val repository: TaskListRepository
|
||||
) {
|
||||
suspend operator fun invoke(taskListId: Long, title: String, order: Int) {
|
||||
repository.updateTaskList(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package com.wismna.geoffroy.donext.presentation.viewmodel
|
||||
package com.wismna.geoffroy.donext.presentation.model
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.extension.toLocalDate
|
||||
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||
@@ -10,7 +10,7 @@ import java.time.format.FormatStyle
|
||||
import java.time.format.TextStyle
|
||||
import java.util.Locale
|
||||
|
||||
class TaskItemViewModel(
|
||||
class TaskItemUiState(
|
||||
task: Task,
|
||||
private val clock: Clock = Clock.systemDefaultZone()) {
|
||||
val id: Long = task.id!!
|
||||
+2
-1
@@ -63,6 +63,7 @@ fun RecycleBinScreen(
|
||||
|
||||
val grouped = tasks.groupBy { it.listName }
|
||||
val context = LocalContext.current
|
||||
val taskDeletedMessage = stringResource(R.string.toast_task_permanently_deleted)
|
||||
|
||||
if (taskToDelete != null) {
|
||||
AlertDialog(
|
||||
@@ -75,7 +76,7 @@ fun RecycleBinScreen(
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.onConfirmDelete()
|
||||
Toast.makeText(context, "Task deleted", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, taskDeletedMessage, Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
|
||||
+18
-17
@@ -28,6 +28,7 @@ import androidx.compose.material3.SwipeToDismissBoxValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
@@ -41,7 +42,7 @@ import androidx.compose.ui.unit.sp
|
||||
import com.wismna.geoffroy.donext.R
|
||||
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
|
||||
import com.wismna.geoffroy.donext.presentation.model.TaskItemUiState
|
||||
|
||||
@Composable
|
||||
fun TaskItemScreen(
|
||||
@@ -51,7 +52,7 @@ fun TaskItemScreen(
|
||||
onSwipeRight: () -> Unit,
|
||||
onTaskClick: (task: Task) -> Unit
|
||||
) {
|
||||
val viewModel = TaskItemViewModel(task)
|
||||
val uiState = remember(task) { TaskItemUiState(task) }
|
||||
|
||||
val dismissState = rememberSwipeToDismissBoxState(
|
||||
confirmValueChange = {
|
||||
@@ -63,17 +64,17 @@ fun TaskItemScreen(
|
||||
)
|
||||
|
||||
val baseStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||
fontWeight = when (viewModel.priority) {
|
||||
fontWeight = when (uiState.priority) {
|
||||
Priority.HIGH -> FontWeight.Bold
|
||||
Priority.NORMAL -> FontWeight.Normal
|
||||
Priority.LOW -> FontWeight.Normal
|
||||
},
|
||||
color = when (viewModel.priority) {
|
||||
color = when (uiState.priority) {
|
||||
Priority.HIGH -> MaterialTheme.colorScheme.onSurface
|
||||
Priority.NORMAL -> MaterialTheme.colorScheme.onSurface
|
||||
Priority.LOW -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
textDecoration = if (viewModel.isDone) TextDecoration.LineThrough else TextDecoration.None
|
||||
textDecoration = if (uiState.isDone) TextDecoration.LineThrough else TextDecoration.None
|
||||
)
|
||||
Card(
|
||||
modifier = modifier,
|
||||
@@ -87,8 +88,8 @@ fun TaskItemScreen(
|
||||
backgroundContent = {
|
||||
DismissBackground(
|
||||
dismissState,
|
||||
viewModel.isDone,
|
||||
viewModel.isDeleted
|
||||
uiState.isDone,
|
||||
uiState.isDeleted
|
||||
)
|
||||
},
|
||||
content = {
|
||||
@@ -97,7 +98,7 @@ fun TaskItemScreen(
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(8.dp)
|
||||
.alpha(if (viewModel.isDone || viewModel.priority == Priority.LOW) 0.5f else 1f),
|
||||
.alpha(if (uiState.isDone || uiState.priority == Priority.LOW) 0.5f else 1f),
|
||||
verticalAlignment = Alignment.CenterVertically // centers checkbox + content
|
||||
) {
|
||||
Box(
|
||||
@@ -108,12 +109,12 @@ fun TaskItemScreen(
|
||||
) {
|
||||
// Title
|
||||
Text(
|
||||
text = viewModel.name,
|
||||
text = uiState.name,
|
||||
fontSize = 18.sp,
|
||||
style = baseStyle,
|
||||
modifier = Modifier
|
||||
.align(
|
||||
if (viewModel.description.isNullOrBlank()) Alignment.CenterStart
|
||||
if (uiState.description.isNullOrBlank()) Alignment.CenterStart
|
||||
else Alignment.TopStart
|
||||
),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
@@ -121,19 +122,19 @@ fun TaskItemScreen(
|
||||
)
|
||||
|
||||
// Due date badge
|
||||
viewModel.dueDateText?.let { dueMillis ->
|
||||
uiState.dueDateText?.let { dueMillis ->
|
||||
Badge(
|
||||
modifier = Modifier
|
||||
.align(
|
||||
if (viewModel.description.isNullOrBlank()) Alignment.CenterEnd
|
||||
if (uiState.description.isNullOrBlank()) Alignment.CenterEnd
|
||||
else Alignment.TopEnd
|
||||
),
|
||||
containerColor = if (viewModel.isOverdue) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primaryContainer
|
||||
containerColor = if (uiState.isOverdue) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primaryContainer
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 1.dp, end = 1.dp),
|
||||
text = viewModel.dueDateText,
|
||||
color = if (viewModel.isOverdue) Color.White else MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
text = uiState.dueDateText,
|
||||
color = if (uiState.isOverdue) Color.White else MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
@@ -147,9 +148,9 @@ fun TaskItemScreen(
|
||||
.padding(top = 24.dp),
|
||||
contentAlignment = Alignment.TopStart
|
||||
) {
|
||||
if (!viewModel.description.isNullOrBlank()) {
|
||||
if (!uiState.description.isNullOrBlank()) {
|
||||
Text(
|
||||
text = viewModel.description,
|
||||
text = uiState.description,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
style = baseStyle.copy(
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
||||
|
||||
@@ -189,7 +189,7 @@ fun TaskScreen(
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) { Text("Cancel") }
|
||||
) { Text(stringResource(R.string.task_cancel)) }
|
||||
|
||||
// --- Save Button ---
|
||||
Button(
|
||||
|
||||
+5
-3
@@ -71,12 +71,14 @@ class TaskViewModel @Inject constructor(
|
||||
|
||||
fun save(onDone: (() -> Unit)? = null) {
|
||||
if (title.isBlank()) return
|
||||
val listId = taskListId ?: return
|
||||
|
||||
viewModelScope.launch {
|
||||
if (isEditing()) {
|
||||
updateTaskUseCase(editingTaskId!!, taskListId!!, title, description, priority, dueDate, isDone)
|
||||
val currentEditingTaskId = editingTaskId
|
||||
if (currentEditingTaskId != null) {
|
||||
updateTaskUseCase(currentEditingTaskId, listId, title, description, priority, dueDate, isDone)
|
||||
} else {
|
||||
createTaskUseCase(taskListId!!, title, description, priority, dueDate)
|
||||
createTaskUseCase(listId, title, description, priority, dueDate)
|
||||
}
|
||||
onDone?.invoke()
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<string name="dialog_delete_task_description">Êtes-vous sûr de vouloir supprimer cette tâche ? Cette action ne peut être annulée.</string>
|
||||
<string name="dialog_delete_task_cancel">Annuler</string>
|
||||
<string name="dialog_delete_task_delete">Supprimer</string>
|
||||
<string name="toast_task_permanently_deleted">Tâche supprimée</string>
|
||||
<string name="dialog_empty_task_title">Vider la corbeille</string>
|
||||
<string name="dialog_empty_task_description">Êtes-vous sûr de vider la corbeille ? Cette action ne peut être annulée.</string>
|
||||
<string name="dialog_empty_task_cancel">Annuler</string>
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
<string name="dialog_delete_task_description">Are you sure you want to permanently delete this task? This cannot be undone.</string>
|
||||
<string name="dialog_delete_task_cancel">Cancel</string>
|
||||
<string name="dialog_delete_task_delete">Delete</string>
|
||||
<string name="toast_task_permanently_deleted">Task deleted</string>
|
||||
|
||||
<string name="dialog_empty_task_title">Empty Recycle Bin</string>
|
||||
<string name="dialog_empty_task_description">Are you sure you want to permanently delete all tasks in the recycle bin? This cannot be undone.</string>
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.coVerifyOrder
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class DeleteTaskListUseCaseTest {
|
||||
|
||||
private val taskRepository = mockk<TaskRepository>(relaxed = true)
|
||||
private val taskListRepository = mockk<TaskListRepository>(relaxed = true)
|
||||
private val useCase = DeleteTaskListUseCase(taskRepository, taskListRepository)
|
||||
|
||||
@Test
|
||||
fun `deleting a list also toggles all its tasks, list last`() = runTest {
|
||||
useCase(5L, true)
|
||||
|
||||
coVerifyOrder {
|
||||
taskRepository.toggleAllTasksInListDeleted(5L, true)
|
||||
taskListRepository.deleteTaskList(5L, true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `restoring a list also restores all its tasks`() = runTest {
|
||||
useCase(5L, false)
|
||||
|
||||
coVerify { taskRepository.toggleAllTasksInListDeleted(5L, false) }
|
||||
coVerify { taskListRepository.deleteTaskList(5L, false) }
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.time.Clock
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
||||
class GetDueTodayTasksUseCaseTest {
|
||||
|
||||
@Test
|
||||
fun `computes today's window using the clock's own zone, not UTC`() {
|
||||
// Paris in summer is UTC+2: local midnight for this date is 22:00 UTC
|
||||
// the *previous* day. A window anchored to UTC midnight would miss it.
|
||||
val zone = ZoneId.of("Europe/Paris")
|
||||
val today = LocalDate.of(2026, 7, 16)
|
||||
val clock = Clock.fixed(today.atStartOfDay(zone).plusHours(10).toInstant(), zone)
|
||||
|
||||
val repository = mockk<TaskRepository>()
|
||||
every { repository.getDueTodayTasks(any(), any()) } returns flowOf(emptyList())
|
||||
|
||||
GetDueTodayTasksUseCase(repository, clock).invoke()
|
||||
|
||||
val expectedStart = today.atStartOfDay(zone).toInstant().toEpochMilli()
|
||||
val expectedEnd = today.plusDays(1).atStartOfDay(zone).toInstant().toEpochMilli() - 1
|
||||
|
||||
verify { repository.getDueTodayTasks(expectedStart, expectedEnd) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `window covers exactly local midnight to the millisecond before the next local midnight`() {
|
||||
val zone = ZoneId.of("America/Montreal")
|
||||
val today = LocalDate.of(2026, 1, 5)
|
||||
val clock = Clock.fixed(today.atStartOfDay(zone).plusHours(6).toInstant(), zone)
|
||||
|
||||
val repository = mockk<TaskRepository>()
|
||||
every { repository.getDueTodayTasks(any(), any()) } returns flowOf(emptyList())
|
||||
|
||||
GetDueTodayTasksUseCase(repository, clock).invoke()
|
||||
|
||||
val expectedStart = today.atStartOfDay(zone).toInstant().toEpochMilli()
|
||||
val expectedEnd = today.plusDays(1).atStartOfDay(zone).toInstant().toEpochMilli() - 1
|
||||
assertThat(expectedEnd - expectedStart).isEqualTo(24L * 60 * 60 * 1000 - 1)
|
||||
|
||||
verify { repository.getDueTodayTasks(expectedStart, expectedEnd) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passes through the tasks emitted by the repository`() = runTest {
|
||||
val zone = ZoneId.systemDefault()
|
||||
val clock = Clock.fixed(LocalDate.of(2026, 1, 1).atStartOfDay(zone).toInstant(), zone)
|
||||
val tasks = listOf(
|
||||
Task(
|
||||
id = 1L,
|
||||
taskListId = 1L,
|
||||
name = "Task",
|
||||
description = "",
|
||||
priority = Priority.NORMAL,
|
||||
isDone = false,
|
||||
isDeleted = false
|
||||
)
|
||||
)
|
||||
|
||||
val repository = mockk<TaskRepository>()
|
||||
every { repository.getDueTodayTasks(any(), any()) } returns flowOf(tasks)
|
||||
|
||||
val result = GetDueTodayTasksUseCase(repository, clock).invoke().first()
|
||||
|
||||
assertThat(result).isEqualTo(tasks)
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.time.Clock
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
||||
class GetTaskListsWithOverdueUseCaseTest {
|
||||
|
||||
@Test
|
||||
fun `computes the overdue cutoff at local midnight, not UTC midnight`() {
|
||||
// Paris in summer is UTC+2: local midnight for this date is 22:00 UTC
|
||||
// the *previous* day. A cutoff anchored to UTC midnight would flag
|
||||
// today's tasks as overdue a full day too early.
|
||||
val zone = ZoneId.of("Europe/Paris")
|
||||
val today = LocalDate.of(2026, 7, 16)
|
||||
val clock = Clock.fixed(today.atStartOfDay(zone).plusHours(10).toInstant(), zone)
|
||||
|
||||
val repository = mockk<TaskListRepository>()
|
||||
every { repository.getTaskListsWithOverdue(any()) } returns flowOf(emptyList())
|
||||
|
||||
GetTaskListsWithOverdueUseCase(repository, clock).invoke()
|
||||
|
||||
val expectedCutoff = today.atStartOfDay(zone).toInstant().toEpochMilli()
|
||||
verify { repository.getTaskListsWithOverdue(expectedCutoff) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passes through the lists emitted by the repository`() = runTest {
|
||||
val zone = ZoneId.systemDefault()
|
||||
val clock = Clock.fixed(LocalDate.of(2026, 1, 1).atStartOfDay(zone).toInstant(), zone)
|
||||
val lists = listOf(TaskListWithOverdue(id = 1L, name = "Work", overdueCount = 2))
|
||||
|
||||
val repository = mockk<TaskListRepository>()
|
||||
every { repository.getTaskListsWithOverdue(any()) } returns flowOf(lists)
|
||||
|
||||
val result = GetTaskListsWithOverdueUseCase(repository, clock).invoke().first()
|
||||
|
||||
assertThat(result).isEqualTo(lists)
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class ToggleTaskDeletedUseCaseTest {
|
||||
|
||||
private val taskRepository = mockk<TaskRepository>(relaxed = true)
|
||||
private val taskListRepository = mockk<TaskListRepository>(relaxed = true)
|
||||
private val useCase = ToggleTaskDeletedUseCase(taskRepository, taskListRepository)
|
||||
|
||||
private val task = Task(
|
||||
id = 1L,
|
||||
taskListId = 5L,
|
||||
name = "Task",
|
||||
description = "",
|
||||
priority = Priority.NORMAL,
|
||||
isDone = false,
|
||||
isDeleted = true
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `restoring a task also restores its soft-deleted parent list`() = runTest {
|
||||
val deletedList = TaskList(id = 5L, name = "List", isDeleted = true, order = 0)
|
||||
coEvery { taskRepository.getTaskById(1L) } returns task
|
||||
coEvery { taskListRepository.getTaskListById(5L) } returns deletedList
|
||||
|
||||
useCase(1L, false)
|
||||
|
||||
coVerify { taskListRepository.updateTaskList(deletedList.copy(isDeleted = false)) }
|
||||
coVerify { taskRepository.toggleTaskDeleted(1L, false) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `restoring a task whose parent list is not deleted leaves the list untouched`() = runTest {
|
||||
val activeList = TaskList(id = 5L, name = "List", isDeleted = false, order = 0)
|
||||
coEvery { taskRepository.getTaskById(1L) } returns task
|
||||
coEvery { taskListRepository.getTaskListById(5L) } returns activeList
|
||||
|
||||
useCase(1L, false)
|
||||
|
||||
coVerify(exactly = 0) { taskListRepository.updateTaskList(any()) }
|
||||
coVerify { taskRepository.toggleTaskDeleted(1L, false) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `soft-deleting a task never touches its parent list`() = runTest {
|
||||
useCase(1L, true)
|
||||
|
||||
coVerify(exactly = 0) { taskRepository.getTaskById(any()) }
|
||||
coVerify(exactly = 0) { taskListRepository.getTaskListById(any()) }
|
||||
coVerify { taskRepository.toggleTaskDeleted(1L, true) }
|
||||
}
|
||||
}
|
||||
+25
-25
@@ -1,4 +1,4 @@
|
||||
package com.wismna.geoffroy.donext.presentation.viewmodel
|
||||
package com.wismna.geoffroy.donext.presentation.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||
@@ -11,7 +11,7 @@ import java.time.format.FormatStyle
|
||||
import java.time.format.TextStyle
|
||||
import java.util.*
|
||||
|
||||
class TaskItemViewModelTest {
|
||||
class TaskItemUiStateTest {
|
||||
|
||||
private val fixedClock: Clock = Clock.fixed(
|
||||
LocalDate.of(2025, 1, 10)
|
||||
@@ -47,80 +47,80 @@ class TaskItemViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `initializes fields from Task`() {
|
||||
val viewModel = TaskItemViewModel(baseTask)
|
||||
val uiState = TaskItemUiState(baseTask)
|
||||
|
||||
assertThat(viewModel.id).isEqualTo(baseTask.id)
|
||||
assertThat(viewModel.name).isEqualTo(baseTask.name)
|
||||
assertThat(viewModel.description).isEqualTo(baseTask.description)
|
||||
assertThat(viewModel.isDone).isFalse()
|
||||
assertThat(viewModel.isDeleted).isFalse()
|
||||
assertThat(viewModel.priority).isEqualTo(Priority.NORMAL)
|
||||
assertThat(uiState.id).isEqualTo(baseTask.id)
|
||||
assertThat(uiState.name).isEqualTo(baseTask.name)
|
||||
assertThat(uiState.description).isEqualTo(baseTask.description)
|
||||
assertThat(uiState.isDone).isFalse()
|
||||
assertThat(uiState.isDeleted).isFalse()
|
||||
assertThat(uiState.priority).isEqualTo(Priority.NORMAL)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isOverdue is true when due date is before today`() {
|
||||
val overdueTask = baseTask.copy(dueDate = millisForDaysFromFixedToday(-1))
|
||||
val viewModel = TaskItemViewModel(overdueTask)
|
||||
val uiState = TaskItemUiState(overdueTask)
|
||||
|
||||
assertThat(viewModel.isOverdue).isTrue()
|
||||
assertThat(uiState.isOverdue).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isOverdue is false when due date is today`() {
|
||||
val dueToday = baseTask.copy(dueDate = millisForDaysFromFixedToday(0))
|
||||
val viewModel = TaskItemViewModel(dueToday, fixedClock)
|
||||
val uiState = TaskItemUiState(dueToday, fixedClock)
|
||||
|
||||
assertThat(viewModel.isOverdue).isFalse()
|
||||
assertThat(uiState.isOverdue).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isOverdue is false when due date is null`() {
|
||||
val viewModel = TaskItemViewModel(baseTask.copy(dueDate = null))
|
||||
val uiState = TaskItemUiState(baseTask.copy(dueDate = null))
|
||||
|
||||
assertThat(viewModel.isOverdue).isFalse()
|
||||
assertThat(uiState.isOverdue).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dueDateText is Today when due date is today`() {
|
||||
val dueToday = baseTask.copy(dueDate = millisForDaysFromFixedToday(0))
|
||||
val viewModel = TaskItemViewModel(dueToday, fixedClock)
|
||||
val uiState = TaskItemUiState(dueToday, fixedClock)
|
||||
|
||||
assertThat(viewModel.dueDateText).isEqualTo("Today")
|
||||
assertThat(uiState.dueDateText).isEqualTo("Today")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dueDateText is Tomorrow when due date is tomorrow`() {
|
||||
val dueTomorrow = baseTask.copy(dueDate = millisForDaysFromFixedToday(1))
|
||||
val viewModel = TaskItemViewModel(dueTomorrow, fixedClock)
|
||||
val uiState = TaskItemUiState(dueTomorrow, fixedClock)
|
||||
|
||||
assertThat(viewModel.dueDateText).isEqualTo("Tomorrow")
|
||||
assertThat(uiState.dueDateText).isEqualTo("Tomorrow")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dueDateText is Yesterday when due date was yesterday`() {
|
||||
val dueYesterday = baseTask.copy(dueDate = millisForDaysFromFixedToday(-1))
|
||||
val viewModel = TaskItemViewModel(dueYesterday, fixedClock)
|
||||
val uiState = TaskItemUiState(dueYesterday, fixedClock)
|
||||
|
||||
assertThat(viewModel.dueDateText).isEqualTo("Yesterday")
|
||||
assertThat(uiState.dueDateText).isEqualTo("Yesterday")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dueDateText is day of week when within next 7 days`() {
|
||||
val dueIn3Days = baseTask.copy(dueDate = millisForDaysFromFixedToday(3))
|
||||
val viewModel = TaskItemViewModel(dueIn3Days, fixedClock)
|
||||
val uiState = TaskItemUiState(dueIn3Days, fixedClock)
|
||||
|
||||
val expected = today
|
||||
.plusDays(3)
|
||||
.dayOfWeek
|
||||
.getDisplayName(TextStyle.SHORT, Locale.getDefault())
|
||||
|
||||
assertThat(viewModel.dueDateText).isEqualTo(expected)
|
||||
assertThat(uiState.dueDateText).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dueDateText is formatted date when more than 7 days away`() {
|
||||
val dueIn10Days = baseTask.copy(dueDate = millisForDaysFromFixedToday(10))
|
||||
val viewModel = TaskItemViewModel(dueIn10Days)
|
||||
val uiState = TaskItemUiState(dueIn10Days)
|
||||
|
||||
val expected = today
|
||||
.plusDays(10)
|
||||
@@ -129,6 +129,6 @@ class TaskItemViewModelTest {
|
||||
.withLocale(Locale.getDefault())
|
||||
)
|
||||
|
||||
assertThat(viewModel.dueDateText).isEqualTo(expected)
|
||||
assertThat(uiState.dueDateText).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
+10
@@ -180,6 +180,16 @@ class TaskViewModelTest {
|
||||
coVerify(exactly = 0) { updateTaskUseCase(any(), any(), any(), any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `save before taskListId is set does nothing`() = runTest {
|
||||
viewModel.onTitleChanged("Task")
|
||||
viewModel.save()
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 0) { createTaskUseCase(any(), any(), any(), any(), any()) }
|
||||
coVerify(exactly = 0) { updateTaskUseCase(any(), any(), any(), any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `save creates task when not editing`() = runTest {stickyEventsFlow.emit(UiEvent.CreateNewTask(3L))
|
||||
advanceUntilIdle()
|
||||
|
||||
@@ -16,7 +16,17 @@
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
android.builtInKotlin=false
|
||||
android.defaults.buildfeatures.resvalues=true
|
||||
android.dependency.useConstraints=true
|
||||
android.enableAppCompileTimeRClass=false
|
||||
android.enableJetifier=false
|
||||
android.newDsl=false
|
||||
android.r8.optimizedResourceShrinking=false
|
||||
android.r8.strictFullModeForKeepRules=false
|
||||
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
|
||||
android.uniquePackageNames=false
|
||||
android.useAndroidX=true
|
||||
android.usesSdkInManifest.disallowed=false
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.jvmargs=-Xmx4g
|
||||
@@ -0,0 +1,13 @@
|
||||
#This file is generated by updateDaemonJvm
|
||||
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect
|
||||
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect
|
||||
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect
|
||||
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect
|
||||
toolchainVendor=JETBRAINS
|
||||
toolchainVersion=21
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
plugins {
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.10.0'
|
||||
}
|
||||
include ':donext'
|
||||
include ':donextv2'
|
||||
|
||||
Reference in New Issue
Block a user