Compare commits

..

5 Commits

Author SHA1 Message Date
Geoffroy Bonneville
b71fa4fdb7 Add an Empty Recycle Bin action button
Refactor Task Item Screen to include the Card
WIP on fix overdue dates calculation on task items
2025-09-24 21:29:23 -04:00
Geoffroy Bonneville
cf770ddb83 Fix some toasts
Change some icons
Fix warnings
2025-09-24 16:24:38 -04:00
Geoffroy Bonneville
ba2e259c7c Remove TODO 2025-09-24 16:09:50 -04:00
Geoffroy Bonneville
208f8bab3a Add the Due Today screen 2025-09-24 16:09:24 -04:00
Geoffroy Bonneville
2d4be63d81 Implement Recycle Bin
Improve task items UI
2025-09-24 12:33:11 -04:00
27 changed files with 665 additions and 196 deletions

View File

@@ -87,4 +87,75 @@ class TaskDaoTest {
TestCase.assertEquals(1, lists.first().first().overdueCount) TestCase.assertEquals(1, lists.first().first().overdueCount)
} }
@Test
fun dueToday_correctlyCalculated() = runBlocking {
listDao.insertTaskList(TaskListEntity(name = "Tasks", order = 0))
val listId = listDao.getTaskLists().first().first().id
val todayStart = Instant.parse("2025-09-15T00:00:00Z").toEpochMilli()
val todayEnd = Instant.parse("2025-09-15T23:59:99Z").toEpochMilli()
// One task due yesterday
taskDao.insertTask(
TaskEntity(
name = "Yesterday",
taskListId = listId,
dueDate = Instant.parse("2025-09-14T12:00:00Z").toEpochMilli(),
isDone = false,
description = null,
priority = Priority.NORMAL
)
)
// One task due today
taskDao.insertTask(
TaskEntity(
name = "Today",
taskListId = listId,
dueDate = Instant.parse("2025-09-15T12:00:00Z").toEpochMilli(),
isDone = false,
description = null,
priority = Priority.NORMAL
)
)
// One task due in the future
taskDao.insertTask(
TaskEntity(
name = "Tomorrow",
taskListId = listId,
dueDate = Instant.parse("2025-09-16T12:00:00Z").toEpochMilli(),
isDone = false,
description = null,
priority = Priority.NORMAL
)
)
// One task due in the future
taskDao.insertTask(
TaskEntity(
name = "TodayDone",
taskListId = listId,
dueDate = Instant.parse("2025-09-15T12:00:00Z").toEpochMilli(),
isDone = true,
description = null,
priority = Priority.NORMAL
)
)
// One task due in the future
taskDao.insertTask(
TaskEntity(
name = "TodayDeleted",
taskListId = listId,
dueDate = Instant.parse("2025-09-15T12:00:00Z").toEpochMilli(),
isDone = false,
isDeleted = true,
description = null,
priority = Priority.NORMAL
)
)
val tasks = taskDao.getDueTodayTasks(todayStart, todayEnd)
TestCase.assertEquals(1, tasks.first().count())
TestCase.assertEquals("Prepare slides", tasks.first().first().name)
}
} }

View File

@@ -13,6 +13,16 @@ interface TaskDao {
@Query("SELECT * FROM tasks WHERE task_list_id = :listId AND deleted = 0 ORDER BY done ASC, priority DESC") @Query("SELECT * FROM tasks WHERE task_list_id = :listId AND deleted = 0 ORDER BY done ASC, priority DESC")
fun getTasksForList(listId: Long): Flow<List<TaskEntity>> fun getTasksForList(listId: Long): Flow<List<TaskEntity>>
@Query("""
SELECT * FROM tasks
WHERE due_date BETWEEN :todayStart AND :todayEnd AND deleted = 0 AND done = 0
ORDER BY done ASC, priority DESC
""")
fun getDueTodayTasks(todayStart: Long, todayEnd: Long): Flow<List<TaskEntity>>
@Query("SELECT * FROM tasks WHERE deleted = 1")
fun getDeletedTasks(): Flow<List<TaskEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: TaskEntity) suspend fun insertTask(task: TaskEntity)
@@ -27,4 +37,10 @@ interface TaskDao {
@Query("UPDATE tasks SET deleted = :deleted WHERE task_list_id = :taskListId") @Query("UPDATE tasks SET deleted = :deleted WHERE task_list_id = :taskListId")
suspend fun toggleAllTasksFromListDeleted(taskListId: Long, deleted: Boolean) suspend fun toggleAllTasksFromListDeleted(taskListId: Long, deleted: Boolean)
@Query("DELETE FROM tasks WHERE id = :taskId")
suspend fun permanentDeleteTask(taskId: Long)
@Query("DELETE FROM tasks WHERE deleted = 1")
suspend fun permanentDeleteAllDeletedTasks()
} }

View File

@@ -11,6 +11,7 @@ import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.map
class TaskRepositoryImpl @Inject constructor( class TaskRepositoryImpl @Inject constructor(
private val taskDao: TaskDao, private val taskDao: TaskDao,
@@ -20,6 +21,14 @@ class TaskRepositoryImpl @Inject constructor(
return taskDao.getTasksForList(listId).map {entity -> entity.map { it.toDomain() }} return taskDao.getTasksForList(listId).map {entity -> entity.map { it.toDomain() }}
} }
override fun getDueTodayTasks(todayStart: Long, todayEnd: Long): Flow<List<Task>> {
return taskDao.getDueTodayTasks(todayStart, todayEnd).map {entity -> entity.map { it.toDomain() }}
}
override fun getDeletedTasks(): Flow<List<Task>> {
return taskDao.getDeletedTasks().map {entity -> entity.map { it.toDomain() }}
}
override suspend fun insertTask(task: Task) { override suspend fun insertTask(task: Task) {
taskDao.insertTask(task.toEntity()) taskDao.insertTask(task.toEntity())
} }
@@ -28,7 +37,7 @@ class TaskRepositoryImpl @Inject constructor(
taskDao.updateTask(task.toEntity()) taskDao.updateTask(task.toEntity())
} }
override suspend fun deleteTask(taskId: Long, isDeleted: Boolean) { override suspend fun toggleTaskDeleted(taskId: Long, isDeleted: Boolean) {
taskDao.toggleTaskDeleted(taskId, isDeleted) taskDao.toggleTaskDeleted(taskId, isDeleted)
} }
@@ -36,6 +45,14 @@ class TaskRepositoryImpl @Inject constructor(
taskDao.toggleTaskDone(taskId, isDone) taskDao.toggleTaskDone(taskId, isDone)
} }
override suspend fun permanentlyDeleteTask(taskId: Long) {
taskDao.permanentDeleteTask(taskId)
}
override suspend fun permanentlyDeleteAllDeletedTask() {
taskDao.permanentDeleteAllDeletedTasks()
}
override fun getTaskLists(): Flow<List<TaskList>> { override fun getTaskLists(): Flow<List<TaskList>> {
return taskListDao.getTaskLists().map {entities -> entities.map { it.toDomain() }} return taskListDao.getTaskLists().map {entities -> entities.map { it.toDomain() }}
} }

View File

@@ -10,9 +10,19 @@ sealed class AppDestination(
title = name, title = name,
) )
object DueTodayList : AppDestination(
route = "todayList",
title = "Due Today",
showBackButton = true,
)
object ManageLists : AppDestination( object ManageLists : AppDestination(
route = "manageLists", route = "manageLists",
title = "Manage Lists", title = "Manage Lists",
showBackButton = true, showBackButton = true,
) )
object RecycleBin : AppDestination(
route = "recycleBin",
title = "Recycle Bin",
showBackButton = true,
)
} }

View File

@@ -7,10 +7,14 @@ import kotlinx.coroutines.flow.Flow
interface TaskRepository { interface TaskRepository {
fun getTasksForList(listId: Long): Flow<List<Task>> fun getTasksForList(listId: Long): Flow<List<Task>>
fun getDueTodayTasks(todayStart: Long, todayEnd: Long): Flow<List<Task>>
fun getDeletedTasks(): Flow<List<Task>>
suspend fun insertTask(task: Task) suspend fun insertTask(task: Task)
suspend fun updateTask(task: Task) suspend fun updateTask(task: Task)
suspend fun deleteTask(taskId: Long, isDeleted: Boolean) suspend fun toggleTaskDeleted(taskId: Long, isDeleted: Boolean)
suspend fun toggleTaskDone(taskId: Long, isDone: Boolean) suspend fun toggleTaskDone(taskId: Long, isDone: Boolean)
suspend fun permanentlyDeleteTask(taskId: Long)
suspend fun permanentlyDeleteAllDeletedTask()
fun getTaskLists(): Flow<List<TaskList>> fun getTaskLists(): Flow<List<TaskList>>
suspend fun insertTaskList(taskList: TaskList) suspend fun insertTaskList(taskList: TaskList)

View File

@@ -0,0 +1,12 @@
package com.wismna.geoffroy.donext.domain.usecase
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import javax.inject.Inject
class EmptyRecycleBinUseCase @Inject constructor(
private val repository: TaskRepository
) {
suspend operator fun invoke() {
repository.permanentlyDeleteAllDeletedTask()
}
}

View File

@@ -0,0 +1,10 @@
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 javax.inject.Inject
class GetDeletedTasksUseCase @Inject constructor(private val repository: TaskRepository) {
operator fun invoke(): Flow<List<Task>> = repository.getDeletedTasks()
}

View File

@@ -0,0 +1,26 @@
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.LocalDate
import java.time.ZoneOffset
import javax.inject.Inject
class GetDueTodayTasksUseCase @Inject constructor(private val repository: TaskRepository) {
operator fun invoke(): Flow<List<Task>> {
val todayStart = LocalDate.now()
.atStartOfDay(ZoneOffset.UTC)
.toInstant()
.toEpochMilli()
val todayEnd = LocalDate.now()
.plusDays(1)
.atStartOfDay(ZoneOffset.UTC)
.toInstant()
.toEpochMilli() - 1
return repository.getDueTodayTasks(
todayStart, todayEnd
)
}
}

View File

@@ -7,13 +7,11 @@ import java.time.LocalDate
import java.time.ZoneOffset import java.time.ZoneOffset
import javax.inject.Inject import javax.inject.Inject
class GetTaskListsWithOverdueUseCase @Inject constructor( class GetTaskListsWithOverdueUseCase @Inject constructor(private val taskRepository: TaskRepository) {
private val taskRepository: TaskRepository
) {
operator fun invoke(): Flow<List<TaskListWithOverdue>> { operator fun invoke(): Flow<List<TaskListWithOverdue>> {
return taskRepository.getTaskListsWithOverdue( return taskRepository.getTaskListsWithOverdue(
LocalDate.now() LocalDate.now()
.atStartOfDay(ZoneOffset.UTC) // or system default .atStartOfDay(ZoneOffset.UTC)
.toInstant() .toInstant()
.toEpochMilli() .toEpochMilli()
) )

View File

@@ -3,10 +3,10 @@ package com.wismna.geoffroy.donext.domain.usecase
import com.wismna.geoffroy.donext.domain.repository.TaskRepository import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import javax.inject.Inject import javax.inject.Inject
class DeleteTaskUseCase @Inject constructor( class PermanentlyDeleteTaskUseCase @Inject constructor(
private val repository: TaskRepository private val repository: TaskRepository
) { ) {
suspend operator fun invoke(taskId: Long) { suspend operator fun invoke(taskId: Long) {
repository.deleteTask(taskId, true) repository.permanentlyDeleteTask(taskId)
} }
} }

View File

@@ -0,0 +1,12 @@
package com.wismna.geoffroy.donext.domain.usecase
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import javax.inject.Inject
class ToggleTaskDeletedUseCase @Inject constructor(
private val repository: TaskRepository
) {
suspend operator fun invoke(taskId: Long, isDeleted: Boolean) {
repository.toggleTaskDeleted(taskId, isDeleted)
}
}

View File

@@ -0,0 +1,59 @@
package com.wismna.geoffroy.donext.presentation.screen
import android.widget.Toast
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.presentation.viewmodel.DueTodayViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
@Composable
fun DueTodayTasksScreen(
modifier: Modifier = Modifier,
viewModel: DueTodayViewModel = hiltViewModel(),
onTaskClick: (task: Task) -> Unit
) {
val tasks = viewModel.dueTodayTasks
if (tasks.isEmpty()) {
// Placeholder when recycle bin is empty
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Nothing due today !")
}
} else {
val context = LocalContext.current
LazyColumn(
modifier = modifier.padding(8.dp)
) {
items(tasks, key = { it.id!! }) { task ->
TaskItemScreen(
modifier = Modifier.animateItem(),
viewModel = TaskItemViewModel(task),
onTaskClick = { onTaskClick(task) },
onSwipeLeft = {
viewModel.updateTaskDone(task.id!!)
Toast.makeText(context, "Task done", Toast.LENGTH_SHORT).show()
},
onSwipeRight = {
viewModel.deleteTask(task.id!!)
Toast.makeText(context, "Task moved to recycle bin", Toast.LENGTH_SHORT)
.show()
}
)
}
}
}
}

View File

@@ -11,8 +11,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerState
@@ -26,6 +26,7 @@ import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
@@ -84,9 +85,11 @@ fun MainScreen(
MenuScreen ( MenuScreen (
currentDestination = viewModel.currentDestination, currentDestination = viewModel.currentDestination,
onNavigate = { route -> onNavigate = { route ->
scope.launch { drawerState.close() } scope.launch {
navController.navigate(route) { drawerState.close()
restoreState = true navController.navigate(route) {
restoreState = true
}
} }
} }
) )
@@ -121,7 +124,7 @@ fun AppContent(
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimaryContainer) { CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimaryContainer) {
if (viewModel.currentDestination.showBackButton) { if (viewModel.currentDestination.showBackButton) {
IconButton(onClick = { navController.popBackStack() }) { IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
} }
} else { } else {
IconButton(onClick = { scope.launch { drawerState.open() } }) { IconButton(onClick = { scope.launch { drawerState.open() } }) {
@@ -134,10 +137,18 @@ fun AppContent(
} }
}, },
actions = { actions = {
if (viewModel.currentDestination is AppDestination.ManageLists) { when (viewModel.currentDestination) {
IconButton(onClick = { viewModel.showAddListSheet = true }) { is AppDestination.ManageLists -> {
Icon(Icons.Default.Add, contentDescription = "Add List") IconButton(onClick = { viewModel.showAddListSheet = true }) {
Icon(Icons.Default.Add, contentDescription = "Add List")
}
} }
is AppDestination.RecycleBin -> {
TextButton(onClick = { viewModel.emptyRecycleBin() }) {
Text(text = "Empty Recycle Bin", color = MaterialTheme.colorScheme.onPrimary)
}
}
else -> null
} }
} }
) )
@@ -177,23 +188,21 @@ fun AppContent(
slideOutHorizontally(targetOffsetX = { fullWidth -> fullWidth }, animationSpec = tween(300)) slideOutHorizontally(targetOffsetX = { fullWidth -> fullWidth }, animationSpec = tween(300))
} }
) { ) {
//viewModel.destinations.forEach { destination -> composable(
composable( route = "taskList/{taskListId}",
route = "taskList/{taskListId}", arguments = listOf(navArgument("taskListId") {
arguments = listOf(navArgument("taskListId") { type = NavType.LongType
type = NavType.LongType })
}) ) { navBackStackEntry ->
) { navBackStackEntry -> val taskListViewModel: TaskListViewModel = hiltViewModel(navBackStackEntry)
val taskListViewModel: TaskListViewModel = hiltViewModel(navBackStackEntry) TaskListScreen(
TaskListScreen( viewModel = taskListViewModel,
viewModel = taskListViewModel, onTaskClick = { task ->
onTaskClick = { task -> taskViewModel.startEditTask(task)
taskViewModel.startEditTask(task) viewModel.showTaskSheet = true
viewModel.showTaskSheet = true }
} )
) }
}
//}
composable(AppDestination.ManageLists.route) { composable(AppDestination.ManageLists.route) {
ManageListsScreen( ManageListsScreen(
@@ -201,6 +210,24 @@ fun AppContent(
showAddListSheet = {viewModel.showAddListSheet = true} showAddListSheet = {viewModel.showAddListSheet = true}
) )
} }
composable(AppDestination.DueTodayList.route) {
DueTodayTasksScreen (
modifier = Modifier,
onTaskClick = { task ->
taskViewModel.startEditTask(task)
viewModel.showTaskSheet = true
}
)
}
composable(AppDestination.RecycleBin.route) {
RecycleBinScreen(
modifier = Modifier,
onTaskClick = { task ->
taskViewModel.startEditTask(task)
viewModel.showTaskSheet = true
}
)
}
} }
} }
} }

View File

@@ -183,7 +183,7 @@ fun ManageListsScreen(
IconButton(onClick = { isInEditMode = true }) { IconButton(onClick = { isInEditMode = true }) {
Icon(Icons.Default.Edit, contentDescription = "Edit") Icon(Icons.Default.Edit, contentDescription = "Edit")
} }
IconButton(onClick = { viewModel.deleteTaskList(list.id!!) }) { IconButton(onClick = { viewModel.deleteTaskList(list.id) }) {
Icon( Icon(
Icons.Default.Delete, Icons.Default.Delete,
contentDescription = "Delete" contentDescription = "Delete"

View File

@@ -2,11 +2,15 @@ package com.wismna.geoffroy.donext.presentation.screen
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.List
import androidx.compose.material3.Badge import androidx.compose.material3.Badge
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -45,6 +49,19 @@ fun MenuScreen(
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) )
NavigationDrawerItem(
label = {
Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("Due Today")
Text(viewModel.dueTodayTasksCount.toString())
}
},
icon = { Icon(Icons.Default.DateRange, contentDescription = "Due Today") },
selected = currentDestination is AppDestination.DueTodayList,
onClick = { onNavigate(AppDestination.DueTodayList.route) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
HorizontalDivider()
viewModel.taskLists.forEach { list -> viewModel.taskLists.forEach { list ->
NavigationDrawerItem( NavigationDrawerItem(
label = { label = {
@@ -54,7 +71,7 @@ fun MenuScreen(
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
}, },
icon = { Icon(Icons.Default.List, contentDescription = list.name) }, icon = { Icon(Icons.AutoMirrored.Default.List, contentDescription = list.name) },
selected = currentDestination is AppDestination.TaskList && selected = currentDestination is AppDestination.TaskList &&
currentDestination.taskListId == list.id, currentDestination.taskListId == list.id,
onClick = { onNavigate("taskList/${list.id}") }, onClick = { onNavigate("taskList/${list.id}") },
@@ -70,6 +87,13 @@ fun MenuScreen(
Column { Column {
HorizontalDivider() HorizontalDivider()
NavigationDrawerItem(
label = { Text("Recycle Bin") },
icon = { Icon(Icons.Default.Delete, contentDescription = "Recycle Bin") },
selected = currentDestination is AppDestination.RecycleBin,
onClick = { onNavigate(AppDestination.RecycleBin.route) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
NavigationDrawerItem( NavigationDrawerItem(
label = { Text("Edit Lists") }, label = { Text("Edit Lists") },
icon = { Icon(Icons.Default.Edit, contentDescription = "Edit Lists") }, icon = { Icon(Icons.Default.Edit, contentDescription = "Edit Lists") },

View File

@@ -0,0 +1,60 @@
package com.wismna.geoffroy.donext.presentation.screen
import android.widget.Toast
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.presentation.viewmodel.RecycleBinViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
@Composable
fun RecycleBinScreen(
modifier: Modifier = Modifier,
viewModel: RecycleBinViewModel = hiltViewModel(),
onTaskClick: (task: Task) -> Unit
) {
val tasks = viewModel.deletedTasks
if (tasks.isEmpty()) {
// Placeholder when recycle bin is empty
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Recycle Bin is empty")
}
} else {
val context = LocalContext.current
LazyColumn(
modifier = modifier.padding(8.dp)
) {
items(tasks, key = { it.id!! }) { task ->
TaskItemScreen(
modifier = Modifier.animateItem(),
viewModel = TaskItemViewModel(task),
onTaskClick = { onTaskClick(task) },
onSwipeLeft = {
viewModel.restore(task.id!!)
Toast.makeText(context, "Task restored", Toast.LENGTH_SHORT).show()
},
onSwipeRight = {
viewModel.deleteForever(task.id!!)
Toast.makeText(context, "Task deleted", Toast.LENGTH_SHORT).show()
}
)
}
}
}
}

View File

@@ -1,6 +1,5 @@
package com.wismna.geoffroy.donext.presentation.screen package com.wismna.geoffroy.donext.presentation.screen
import android.widget.Toast
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -12,10 +11,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.Done
import androidx.compose.material3.Badge import androidx.compose.material3.Badge
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SwipeToDismissBox import androidx.compose.material3.SwipeToDismissBox
@@ -28,12 +30,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.wismna.geoffroy.donext.domain.model.Priority import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
@@ -41,23 +43,15 @@ import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
fun TaskItemScreen( fun TaskItemScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: TaskItemViewModel, viewModel: TaskItemViewModel,
onSwipeDone: () -> Unit, onTaskClick: (taskId: Long) -> Unit,
onSwipeDelete: () -> Unit onSwipeLeft: () -> Unit,
onSwipeRight: () -> Unit
) { ) {
val context = LocalContext.current
val dismissState = rememberSwipeToDismissBoxState( val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = { confirmValueChange = {
when (it) { when (it) {
SwipeToDismissBoxValue.StartToEnd -> { SwipeToDismissBoxValue.StartToEnd -> { onSwipeRight() }
onSwipeDelete() SwipeToDismissBoxValue.EndToStart -> { onSwipeLeft() }
Toast.makeText(context, "Task deleted", Toast.LENGTH_SHORT).show()
}
SwipeToDismissBoxValue.EndToStart -> {
onSwipeDone()
Toast.makeText(context, "Task done", Toast.LENGTH_SHORT).show()
}
SwipeToDismissBoxValue.Settled -> return@rememberSwipeToDismissBoxState false SwipeToDismissBoxValue.Settled -> return@rememberSwipeToDismissBoxState false
} }
return@rememberSwipeToDismissBoxState true return@rememberSwipeToDismissBoxState true
@@ -78,85 +72,99 @@ fun TaskItemScreen(
}, },
textDecoration = if (viewModel.isDone) TextDecoration.LineThrough else TextDecoration.None textDecoration = if (viewModel.isDone) TextDecoration.LineThrough else TextDecoration.None
) )
Card(
SwipeToDismissBox(
state = dismissState,
modifier = modifier, modifier = modifier,
backgroundContent = { DismissBackground(dismissState, viewModel.isDone) }, onClick = { onTaskClick(viewModel.id) },
content = { colors = CardDefaults.cardColors(
Row( containerColor = MaterialTheme.colorScheme.surfaceContainer,
modifier = modifier ),
.fillMaxWidth() ) {
.background(MaterialTheme.colorScheme.surfaceContainer) SwipeToDismissBox(
.padding(8.dp) state = dismissState,
.alpha(if (viewModel.isDone || viewModel.priority == Priority.LOW) 0.5f else 1f), backgroundContent = {
verticalAlignment = Alignment.CenterVertically // centers checkbox + content DismissBackground(
) { dismissState,
Box( viewModel.isDone,
viewModel.isDeleted
)
},
content = {
Row(
modifier = Modifier modifier = Modifier
.weight(1f) .fillMaxWidth()
.padding(start = 8.dp) .background(MaterialTheme.colorScheme.surfaceContainer)
.height(IntrinsicSize.Min) // shrink to fit title/description .padding(8.dp)
.alpha(if (viewModel.isDone || viewModel.priority == Priority.LOW) 0.5f else 1f),
verticalAlignment = Alignment.CenterVertically // centers checkbox + content
) { ) {
// Title
Text(
text = viewModel.name,
style = baseStyle,
modifier = Modifier
.align(
if (viewModel.description.isNullOrBlank()) Alignment.CenterStart
else Alignment.TopStart
),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
// Due date badge
viewModel.dueDateText?.let { dueMillis ->
Badge(
modifier = Modifier
.align(
if (viewModel.description.isNullOrBlank()) Alignment.CenterEnd
else Alignment.TopEnd
),
containerColor = if (viewModel.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,
style = MaterialTheme.typography.bodySmall
)
}
}
// Optional description
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .weight(1f)
.height(40.dp) // 👈 adjust to the typical description height .padding(start = 8.dp)
.padding(top = 20.dp), .height(IntrinsicSize.Min) // shrink to fit title/description
contentAlignment = Alignment.TopStart
) { ) {
if (!viewModel.description.isNullOrBlank()) { // Title
Text( Text(
text = viewModel.description, text = viewModel.name,
style = baseStyle.copy( fontSize = 18.sp,
fontSize = MaterialTheme.typography.bodyMedium.fontSize, style = baseStyle,
fontStyle = FontStyle.Italic modifier = Modifier
.align(
if (viewModel.description.isNullOrBlank()) Alignment.CenterStart
else Alignment.TopStart
), ),
maxLines = 2, overflow = TextOverflow.Ellipsis,
overflow = TextOverflow.Ellipsis maxLines = 1,
) )
// Due date badge
viewModel.dueDateText?.let { dueMillis ->
Badge(
modifier = Modifier
.align(
if (viewModel.description.isNullOrBlank()) Alignment.CenterEnd
else Alignment.TopEnd
),
containerColor = if (viewModel.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,
style = MaterialTheme.typography.bodySmall
)
}
}
// Optional description
Box(
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
.padding(top = 24.dp),
contentAlignment = Alignment.TopStart
) {
if (!viewModel.description.isNullOrBlank()) {
Text(
text = viewModel.description,
color = MaterialTheme.colorScheme.tertiary,
style = baseStyle.copy(
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
fontStyle = FontStyle.Italic
),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
} }
} }
} }
} })
}) }
} }
@Composable @Composable
fun DismissBackground(dismissState: SwipeToDismissBoxState, isDone: Boolean) { fun DismissBackground(dismissState: SwipeToDismissBoxState, isDone: Boolean, isDeleted: Boolean) {
val color = when (dismissState.dismissDirection) { val color = when (dismissState.dismissDirection) {
SwipeToDismissBoxValue.StartToEnd -> MaterialTheme.colorScheme.error SwipeToDismissBoxValue.StartToEnd -> MaterialTheme.colorScheme.error
SwipeToDismissBoxValue.EndToStart -> Color(0xFF18590D) SwipeToDismissBoxValue.EndToStart -> Color(0xFF18590D)
@@ -172,7 +180,7 @@ fun DismissBackground(dismissState: SwipeToDismissBoxState, isDone: Boolean) {
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Icon( Icon(
Icons.Default.Delete, if (isDeleted) Icons.Default.Clear else Icons.Default.Delete,
tint = Color.LightGray, tint = Color.LightGray,
contentDescription = "Delete" contentDescription = "Delete"
) )

View File

@@ -1,5 +1,6 @@
package com.wismna.geoffroy.donext.presentation.screen package com.wismna.geoffroy.donext.presentation.screen
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -9,16 +10,14 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.wismna.geoffroy.donext.domain.model.Task import com.wismna.geoffroy.donext.domain.model.Task
@@ -38,6 +37,7 @@ fun TaskListScreen(
tasks.partition { !it.isDone } tasks.partition { !it.isDone }
} }
val context = LocalContext.current
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(8.dp), contentPadding = PaddingValues(8.dp),
@@ -48,24 +48,19 @@ fun TaskListScreen(
items = active, items = active,
key = { it.id!! } key = { it.id!! }
) { task -> ) { task ->
Card( TaskItemScreen(
onClick = { onTaskClick(task) }, modifier = Modifier.animateItem(),
//elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp), viewModel = TaskItemViewModel(task),
colors = CardDefaults.cardColors( onTaskClick = { onTaskClick(task) },
containerColor = MaterialTheme.colorScheme.surfaceContainer, onSwipeLeft = {
), viewModel.updateTaskDone(task.id!!, true)
) { Toast.makeText(context, "Task done", Toast.LENGTH_SHORT).show()
TaskItemScreen( },
modifier = Modifier.animateItem(), onSwipeRight = {
viewModel = TaskItemViewModel(task), viewModel.deleteTask(task.id!!)
onSwipeDone = { Toast.makeText(context, "Task moved to recycle bin", Toast.LENGTH_SHORT).show()
viewModel.updateTaskDone(task.id!!, true) }
}, )
onSwipeDelete = {
viewModel.deleteTask(task.id!!)
}
)
}
} }
// Divider between active and done (optional) // Divider between active and done (optional)
@@ -84,25 +79,21 @@ fun TaskListScreen(
items = done, items = done,
key = { it.id!! } key = { it.id!! }
) { task -> ) { task ->
Card( TaskItemScreen(
onClick = { onTaskClick(task) }, modifier = Modifier.animateItem(),
//elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp), viewModel = TaskItemViewModel(task),
colors = CardDefaults.cardColors( onTaskClick = { onTaskClick(task) },
containerColor = MaterialTheme.colorScheme.surfaceContainer, onSwipeLeft = {
), viewModel.updateTaskDone(task.id!!, false)
) { Toast.makeText(context, "Task in progress", Toast.LENGTH_SHORT).show()
TaskItemScreen( },
modifier = Modifier.animateItem(), onSwipeRight = {
viewModel = TaskItemViewModel(task), viewModel.deleteTask(task.id!!)
onSwipeDone = { Toast.makeText(context, "Task moved to recycle bin", Toast.LENGTH_SHORT).show()
viewModel.updateTaskDone(task.id!!, false) },
}, )
onSwipeDelete = {
viewModel.deleteTask(task.id!!)
},
)
}
} }
} }
} }

View File

@@ -42,6 +42,7 @@ import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.ZoneId
import java.time.ZoneOffset import java.time.ZoneOffset
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
@@ -108,7 +109,7 @@ fun TaskBottomSheet(
var showDatePicker by remember { mutableStateOf(false) } var showDatePicker by remember { mutableStateOf(false) }
val formattedDate = viewModel.dueDate?.let { val formattedDate = viewModel.dueDate?.let {
Instant.ofEpochMilli(it) Instant.ofEpochMilli(it)
.atZone(ZoneOffset.UTC) .atZone(ZoneId.systemDefault())
.toLocalDate() .toLocalDate()
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)) .format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM))
} ?: "" } ?: ""

View File

@@ -25,48 +25,48 @@ fun DoNextTheme(
darkTheme -> darkColorScheme( darkTheme -> darkColorScheme(
primary = Purple40, primary = Purple40,
onPrimary = Color.White, onPrimary = LightSurfaceContainer,
primaryContainer = Purple40Container, primaryContainer = Purple40Container,
onPrimaryContainer = Color.White, onPrimaryContainer = LightSurfaceContainer,
secondary = PurpleGrey40, secondary = PurpleGrey40,
onSecondary = Color.White, onSecondary = LightSurfaceContainer,
secondaryContainer = PurpleGrey40Container, secondaryContainer = PurpleGrey40Container,
onSecondaryContainer = Color.White, onSecondaryContainer = LightSurfaceContainer,
tertiary = Pink40, tertiary = Pink80,
onTertiary = Color.White, onTertiary = DarkSurfaceContainer,
tertiaryContainer = Pink40Container, tertiaryContainer = Pink40Container,
onTertiaryContainer = Color.White, onTertiaryContainer = LightSurfaceContainer,
background = Color(0xFF121212), background = Color(0xFF121212),
onBackground = Color.White, onBackground = LightSurfaceContainer,
surface = Color(0xFF121212), surface = Color(0xFF121212),
onSurface = Color.White, onSurface = LightSurfaceContainer,
surfaceVariant = DarkSurfaceContainer, surfaceVariant = DarkSurfaceContainer,
onSurfaceVariant = Color.White, onSurfaceVariant = LightSurfaceContainer,
error = Color(0xFFCF6679), error = Color(0xFFCF6679),
onError = Color.Black onError = DarkSurfaceContainer
) )
else -> lightColorScheme( else -> lightColorScheme(
primary = Purple80, primary = Purple80,
onPrimary = Color.Black, onPrimary = DarkSurfaceContainer,
primaryContainer = Purple80Container, primaryContainer = Purple80Container,
onPrimaryContainer = Color.Black, onPrimaryContainer = DarkSurfaceContainer,
secondary = PurpleGrey80, secondary = PurpleGrey80,
onSecondary = Color.Black, onSecondary = DarkSurfaceContainer,
secondaryContainer = PurpleGrey80Container, secondaryContainer = PurpleGrey80Container,
onSecondaryContainer = Color.Black, onSecondaryContainer = DarkSurfaceContainer,
tertiary = Pink80, tertiary = Pink40,
onTertiary = Color.Black, onTertiary = LightSurfaceContainer,
tertiaryContainer = Pink80Container, tertiaryContainer = Pink80Container,
onTertiaryContainer = Color.Black, onTertiaryContainer = DarkSurfaceContainer,
background = Color(0xFFFFFBFE), background = Color(0xFFFFFBFE),
onBackground = Color.Black, onBackground = DarkSurfaceContainer,
surface = Color(0xFFFFFBFE), surface = Color(0xFFFFFBFE),
onSurface = Color.Black, onSurface = DarkSurfaceContainer,
surfaceVariant = LightSurfaceContainer, surfaceVariant = LightSurfaceContainer,
onSurfaceVariant = Color.Black, onSurfaceVariant = DarkSurfaceContainer,
error = Color(0xFFB00020), error = Color(0xFFB00020),
onError = Color.White onError = LightSurfaceContainer
) )
} }

View File

@@ -0,0 +1,46 @@
package com.wismna.geoffroy.donext.presentation.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.usecase.GetDueTodayTasksUseCase
import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDeletedUseCase
import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDoneUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class DueTodayViewModel @Inject constructor(
getDueTodayTasks: GetDueTodayTasksUseCase,
private val toggleTaskDeleted: ToggleTaskDeletedUseCase,
private val toggleTaskDone: ToggleTaskDoneUseCase
) : ViewModel() {
var dueTodayTasks by mutableStateOf<List<Task>>(emptyList())
private set
init {
getDueTodayTasks()
.onEach { tasks ->
dueTodayTasks = tasks
}
.launchIn(viewModelScope)
}
fun updateTaskDone(taskId: Long) {
viewModelScope.launch {
toggleTaskDone(taskId, true)
}
}
fun deleteTask(taskId: Long) {
viewModelScope.launch {
toggleTaskDeleted(taskId, true)
}
}
}

View File

@@ -7,15 +7,18 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
import com.wismna.geoffroy.donext.domain.model.AppDestination import com.wismna.geoffroy.donext.domain.model.AppDestination
import com.wismna.geoffroy.donext.domain.usecase.EmptyRecycleBinUseCase
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MainViewModel @Inject constructor( class MainViewModel @Inject constructor(
getTaskLists: GetTaskListsUseCase getTaskListsUseCase: GetTaskListsUseCase,
private val emptyRecycleBinUseCase: EmptyRecycleBinUseCase
) : ViewModel() { ) : ViewModel() {
var isLoading by mutableStateOf(true) var isLoading by mutableStateOf(true)
@@ -34,11 +37,14 @@ class MainViewModel @Inject constructor(
var showAddListSheet by mutableStateOf(false) var showAddListSheet by mutableStateOf(false)
init { init {
getTaskLists() getTaskListsUseCase()
.onEach { lists -> .onEach { lists ->
destinations = lists.map { taskList -> destinations = lists.map { taskList ->
AppDestination.TaskList(taskList.id!!, taskList.name) AppDestination.TaskList(taskList.id!!, taskList.name)
} + AppDestination.ManageLists } +
AppDestination.ManageLists +
AppDestination.RecycleBin +
AppDestination.DueTodayList
isLoading = false isLoading = false
if (startDestination == AppDestination.ManageLists && destinations.isNotEmpty()) { if (startDestination == AppDestination.ManageLists && destinations.isNotEmpty()) {
startDestination = destinations.first() startDestination = destinations.first()
@@ -58,4 +64,10 @@ class MainViewModel @Inject constructor(
} }
} ?: startDestination } ?: startDestination
} }
fun emptyRecycleBin() {
viewModelScope.launch {
emptyRecycleBinUseCase()
}
}
} }

View File

@@ -1,11 +1,13 @@
package com.wismna.geoffroy.donext.presentation.viewmodel package com.wismna.geoffroy.donext.presentation.viewmodel
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
import com.wismna.geoffroy.donext.domain.usecase.GetDueTodayTasksUseCase
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsWithOverdueUseCase import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsWithOverdueUseCase
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@@ -14,17 +16,26 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MenuViewModel @Inject constructor( class MenuViewModel @Inject constructor(
getTaskListsWithOverdue: GetTaskListsWithOverdueUseCase getTaskListsWithOverdue: GetTaskListsWithOverdueUseCase,
getDueTodayTasks: GetDueTodayTasksUseCase
) : ViewModel() { ) : ViewModel() {
var taskLists by mutableStateOf<List<TaskListWithOverdue>>(emptyList()) var taskLists by mutableStateOf<List<TaskListWithOverdue>>(emptyList())
private set private set
var dueTodayTasksCount by mutableIntStateOf(0)
private set
init { init {
getTaskListsWithOverdue() getTaskListsWithOverdue()
.onEach { lists -> .onEach { lists ->
taskLists = lists taskLists = lists
} }
.launchIn(viewModelScope) .launchIn(viewModelScope)
getDueTodayTasks()
.onEach { tasks ->
dueTodayTasksCount = tasks.count()
}
.launchIn(viewModelScope)
} }
} }

View File

@@ -0,0 +1,53 @@
package com.wismna.geoffroy.donext.presentation.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.usecase.GetDeletedTasksUseCase
import com.wismna.geoffroy.donext.domain.usecase.PermanentlyDeleteTaskUseCase
import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDeletedUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class RecycleBinViewModel @Inject constructor(
private val getDeletedTasks: GetDeletedTasksUseCase,
private val restoreTask: ToggleTaskDeletedUseCase,
private val permanentlyDeleteTask: PermanentlyDeleteTaskUseCase
) : ViewModel() {
var deletedTasks by mutableStateOf<List<Task>>(emptyList())
private set
init {
loadDeletedTasks()
}
fun loadDeletedTasks() {
getDeletedTasks()
.onEach { tasks ->
deletedTasks = tasks
}
.launchIn(viewModelScope)
}
fun restore(taskId: Long) {
viewModelScope.launch {
restoreTask(taskId, false)
loadDeletedTasks()
}
}
fun deleteForever(taskId: Long) {
viewModelScope.launch {
permanentlyDeleteTask(taskId)
loadDeletedTasks()
}
}
}

View File

@@ -4,7 +4,7 @@ import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.domain.model.Task import com.wismna.geoffroy.donext.domain.model.Task
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.ZoneOffset import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.time.format.TextStyle import java.time.format.TextStyle
@@ -15,13 +15,14 @@ class TaskItemViewModel(task: Task) {
val name: String = task.name val name: String = task.name
val description: String? = task.description val description: String? = task.description
val isDone: Boolean = task.isDone val isDone: Boolean = task.isDone
val isDeleted: Boolean = task.isDeleted
val priority: Priority = task.priority val priority: Priority = task.priority
val today: LocalDate = LocalDate.now(ZoneOffset.UTC) val today: LocalDate = LocalDate.now(ZoneId.systemDefault())
val isOverdue: Boolean = task.dueDate?.let { millis -> val isOverdue: Boolean = task.dueDate?.let { millis ->
val dueDate = Instant.ofEpochMilli(millis) val dueDate = Instant.ofEpochMilli(millis)
.atZone(ZoneOffset.UTC) .atZone(ZoneId.systemDefault())
.toLocalDate() .toLocalDate()
dueDate.isBefore(today) dueDate.isBefore(today)
} ?: false } ?: false
@@ -29,7 +30,7 @@ class TaskItemViewModel(task: Task) {
val dueDateText: String? = task.dueDate?.let { formatDueDate(it) } val dueDateText: String? = task.dueDate?.let { formatDueDate(it) }
private fun formatDueDate(dueMillis: Long): String { private fun formatDueDate(dueMillis: Long): String {
val dueDate = Instant.ofEpochMilli(dueMillis).atZone(ZoneOffset.UTC).toLocalDate() val dueDate = Instant.ofEpochMilli(dueMillis).atZone(ZoneId.systemDefault()).toLocalDate()
return when { return when {
dueDate.isEqual(today) -> "Today" dueDate.isEqual(today) -> "Today"

View File

@@ -7,8 +7,8 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wismna.geoffroy.donext.domain.model.Task import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.usecase.DeleteTaskListUseCase
import com.wismna.geoffroy.donext.domain.usecase.GetTasksForListUseCase import com.wismna.geoffroy.donext.domain.usecase.GetTasksForListUseCase
import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDeletedUseCase
import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDoneUseCase import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDoneUseCase
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@@ -19,8 +19,8 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class TaskListViewModel @Inject constructor( class TaskListViewModel @Inject constructor(
getTasks: GetTasksForListUseCase, getTasks: GetTasksForListUseCase,
private val toggleTaskDoneUseCase: ToggleTaskDoneUseCase, private val toggleTaskDone: ToggleTaskDoneUseCase,
private val deleteTaskListUseCase: DeleteTaskListUseCase, private val toggleTaskDeleted: ToggleTaskDeletedUseCase,
savedStateHandle: SavedStateHandle savedStateHandle: SavedStateHandle
) : ViewModel() { ) : ViewModel() {
@@ -42,12 +42,12 @@ class TaskListViewModel @Inject constructor(
fun updateTaskDone(taskId: Long, isDone: Boolean) { fun updateTaskDone(taskId: Long, isDone: Boolean) {
viewModelScope.launch { viewModelScope.launch {
toggleTaskDoneUseCase(taskId, isDone) toggleTaskDone(taskId, isDone)
} }
} }
fun deleteTask(taskId: Long) { fun deleteTask(taskId: Long) {
viewModelScope.launch { viewModelScope.launch {
deleteTaskListUseCase(taskId) toggleTaskDeleted(taskId, true)
} }
} }
} }

View File

@@ -8,7 +8,7 @@ import androidx.lifecycle.viewModelScope
import com.wismna.geoffroy.donext.domain.model.Priority import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.domain.model.Task import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.usecase.AddTaskUseCase import com.wismna.geoffroy.donext.domain.usecase.AddTaskUseCase
import com.wismna.geoffroy.donext.domain.usecase.DeleteTaskUseCase import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDeletedUseCase
import com.wismna.geoffroy.donext.domain.usecase.UpdateTaskUseCase import com.wismna.geoffroy.donext.domain.usecase.UpdateTaskUseCase
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -18,7 +18,7 @@ import javax.inject.Inject
class TaskViewModel @Inject constructor( class TaskViewModel @Inject constructor(
private val createTaskUseCase: AddTaskUseCase, private val createTaskUseCase: AddTaskUseCase,
private val updateTaskUseCase: UpdateTaskUseCase, private val updateTaskUseCase: UpdateTaskUseCase,
private val deleteTaskUseCase: DeleteTaskUseCase private val toggleTaskDeletedUseCase: ToggleTaskDeletedUseCase
) : ViewModel() { ) : ViewModel() {
var title by mutableStateOf("") var title by mutableStateOf("")
@@ -79,7 +79,7 @@ class TaskViewModel @Inject constructor(
fun delete() { fun delete() {
editingTaskId?.let { id -> editingTaskId?.let { id ->
viewModelScope.launch { viewModelScope.launch {
deleteTaskUseCase(id) toggleTaskDeletedUseCase(id, true)
reset() reset()
} }
} }