From 7dddc62377b24a37d749c5e53349292e229ce7b9 Mon Sep 17 00:00:00 2001 From: Geoffroy Bonneville <24917789+wismna@users.noreply.github.com> Date: Thu, 2 Oct 2025 21:09:51 -0400 Subject: [PATCH] Recycle Bin displays tasks grouped by list Restoring a task from a deleted list restores the list Removed Delete button from task sheet Added Cancel button in task sheet Task sheet is read-only in the Recycle Bin only Empty Recycle Bin displays a confirmation Empty Recycle Bin is now an IconButton --- .../wismna/geoffroy/donext/data/Mappers.kt | 8 ++ .../data/entities/TaskWithListNameEntity.kt | 8 ++ .../geoffroy/donext/data/local/dao/TaskDao.kt | 14 ++- .../donext/data/local/dao/TaskListDao.kt | 3 + .../local/repository/TaskRepositoryImpl.kt | 13 ++- .../donext/domain/model/TaskWithListName.kt | 6 ++ .../domain/repository/TaskRepository.kt | 5 +- .../domain/usecase/GetDeletedTasksUseCase.kt | 4 +- .../usecase/ToggleTaskDeletedUseCase.kt | 11 ++ .../donext/presentation/screen/MainScreen.kt | 7 +- .../presentation/screen/RecycleBinScreen.kt | 101 ++++++++++++++++-- .../presentation/screen/TaskItemScreen.kt | 10 +- .../donext/presentation/screen/TaskScreen.kt | 76 ++++++++----- .../presentation/viewmodel/MainViewModel.kt | 11 +- .../viewmodel/ManageListsViewModel.kt | 4 +- .../viewmodel/RecycleBinViewModel.kt | 15 ++- .../presentation/viewmodel/TaskViewModel.kt | 18 ++-- 17 files changed, 229 insertions(+), 85 deletions(-) create mode 100644 donextv2/src/main/java/com/wismna/geoffroy/donext/data/entities/TaskWithListNameEntity.kt create mode 100644 donextv2/src/main/java/com/wismna/geoffroy/donext/domain/model/TaskWithListName.kt diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/Mappers.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/Mappers.kt index 7b6dc41..e8471ad 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/Mappers.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/Mappers.kt @@ -2,8 +2,10 @@ package com.wismna.geoffroy.donext.data import com.wismna.geoffroy.donext.data.entities.TaskEntity import com.wismna.geoffroy.donext.data.entities.TaskListEntity +import com.wismna.geoffroy.donext.data.entities.TaskWithListNameEntity import com.wismna.geoffroy.donext.domain.model.Task import com.wismna.geoffroy.donext.domain.model.TaskList +import com.wismna.geoffroy.donext.domain.model.TaskWithListName fun TaskEntity.toDomain() = Task( id = id, @@ -15,6 +17,12 @@ fun TaskEntity.toDomain() = Task( dueDate = dueDate, priority = priority, ) +fun TaskWithListNameEntity.toDomain(): TaskWithListName { + return TaskWithListName( + task = task.toDomain(), + listName = listName + ) +} fun Task.toEntity() = TaskEntity( id = id ?: 0, diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/entities/TaskWithListNameEntity.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/entities/TaskWithListNameEntity.kt new file mode 100644 index 0000000..7338d1c --- /dev/null +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/entities/TaskWithListNameEntity.kt @@ -0,0 +1,8 @@ +package com.wismna.geoffroy.donext.data.entities + +import androidx.room.Embedded + +data class TaskWithListNameEntity( + @Embedded val task: TaskEntity, + val listName: String +) \ No newline at end of file diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskDao.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskDao.kt index ecc2b1a..2a16661 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskDao.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskDao.kt @@ -6,6 +6,7 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Update import com.wismna.geoffroy.donext.data.entities.TaskEntity +import com.wismna.geoffroy.donext.data.entities.TaskWithListNameEntity import kotlinx.coroutines.flow.Flow @Dao @@ -20,8 +21,17 @@ interface TaskDao { """) fun getDueTodayTasks(todayStart: Long, todayEnd: Long): Flow> - @Query("SELECT * FROM tasks WHERE deleted = 1") - fun getDeletedTasks(): Flow> + @Query(""" + SELECT t.*, l.name AS listName + FROM tasks t + INNER JOIN task_lists l ON t.task_list_id = l.id + WHERE t.deleted = 1 + ORDER BY l.name + """) + fun getDeletedTasksWithListName(): Flow> + + @Query("SELECT * FROM tasks WHERE id = :taskId") + suspend fun getTaskById(taskId: Long): TaskEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertTask(task: TaskEntity) diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskListDao.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskListDao.kt index 1a9b715..41ec713 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskListDao.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskListDao.kt @@ -35,6 +35,9 @@ interface TaskListDao { """) fun getTaskListsWithOverdue(nowMillis: Long): Flow> + @Query("SELECT * FROM task_lists WHERE id = :taskListId") + suspend fun getTaskListById(taskListId: Long): TaskListEntity? + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertTaskList(taskList: TaskListEntity) diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/repository/TaskRepositoryImpl.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/repository/TaskRepositoryImpl.kt index 8c9d8aa..d4ea58c 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/repository/TaskRepositoryImpl.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/repository/TaskRepositoryImpl.kt @@ -7,6 +7,7 @@ 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 @@ -25,8 +26,12 @@ class TaskRepositoryImpl @Inject constructor( return taskDao.getDueTodayTasks(todayStart, todayEnd).map {entity -> entity.map { it.toDomain() }} } - override fun getDeletedTasks(): Flow> { - return taskDao.getDeletedTasks().map {entity -> entity.map { it.toDomain() }} + override fun getDeletedTasks(): Flow> { + return taskDao.getDeletedTasksWithListName().map {entity -> entity.map { it.toDomain() }} + } + + override suspend fun getTaskById(taskId: Long): Task? { + return taskDao.getTaskById(taskId)?.toDomain() } override suspend fun insertTask(task: Task) { @@ -57,6 +62,10 @@ class TaskRepositoryImpl @Inject constructor( 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()) } diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/model/TaskWithListName.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/model/TaskWithListName.kt new file mode 100644 index 0000000..80e7456 --- /dev/null +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/model/TaskWithListName.kt @@ -0,0 +1,6 @@ +package com.wismna.geoffroy.donext.domain.model + +data class TaskWithListName ( + val task: Task, + val listName: String +) \ No newline at end of file diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/repository/TaskRepository.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/repository/TaskRepository.kt index 1694fba..8f6db99 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/repository/TaskRepository.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/repository/TaskRepository.kt @@ -3,12 +3,14 @@ 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 interface TaskRepository { fun getTasksForList(listId: Long): Flow> fun getDueTodayTasks(todayStart: Long, todayEnd: Long): Flow> - fun getDeletedTasks(): Flow> + fun getDeletedTasks(): Flow> + suspend fun getTaskById(taskId: Long): Task? suspend fun insertTask(task: Task) suspend fun updateTask(task: Task) suspend fun toggleTaskDeleted(taskId: Long, isDeleted: Boolean) @@ -17,6 +19,7 @@ interface TaskRepository { suspend fun permanentlyDeleteAllDeletedTask() fun getTaskLists(): Flow> + suspend fun getTaskListById(taskListId: Long): TaskList? suspend fun insertTaskList(taskList: TaskList) suspend fun updateTaskList(taskList: TaskList) suspend fun deleteTaskList(taskListId: Long, isDeleted: Boolean) diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/GetDeletedTasksUseCase.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/GetDeletedTasksUseCase.kt index 258e110..07e2ad7 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/GetDeletedTasksUseCase.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/GetDeletedTasksUseCase.kt @@ -1,10 +1,10 @@ package com.wismna.geoffroy.donext.domain.usecase -import com.wismna.geoffroy.donext.domain.model.Task +import com.wismna.geoffroy.donext.domain.model.TaskWithListName 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> = repository.getDeletedTasks() + operator fun invoke(): Flow> = repository.getDeletedTasks() } \ No newline at end of file diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/ToggleTaskDeletedUseCase.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/ToggleTaskDeletedUseCase.kt index 5bdbaef..1754d96 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/ToggleTaskDeletedUseCase.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/ToggleTaskDeletedUseCase.kt @@ -7,6 +7,17 @@ class ToggleTaskDeletedUseCase @Inject constructor( private val repository: TaskRepository ) { suspend operator fun invoke(taskId: Long, isDeleted: Boolean) { + if (!isDeleted) { + val task = repository.getTaskById(taskId) + if (task != null) { + // If task list was soft-deleted, restore it as well + val taskList = repository.getTaskListById(task.taskListId) + if (taskList != null && taskList.isDeleted) { + repository.updateTaskList(taskList.copy(isDeleted = false)) + } + } + } + repository.toggleTaskDeleted(taskId, isDeleted) } } \ No newline at end of file diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MainScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MainScreen.kt index ce7d339..ec8ce91 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MainScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MainScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberDrawerState @@ -61,6 +60,7 @@ fun MainScreen( val navController = rememberNavController() val drawerState = rememberDrawerState(DrawerValue.Closed) val scope = rememberCoroutineScope() + // TODO: find a way to get rid of this val taskViewModel: TaskViewModel = hiltViewModel() if (viewModel.isLoading) { @@ -144,9 +144,7 @@ fun AppContent( } } is AppDestination.RecycleBin -> { - TextButton(onClick = { viewModel.emptyRecycleBin() }) { - Text(text = "Empty Recycle Bin", color = MaterialTheme.colorScheme.onPrimaryContainer) - } + EmptyRecycleBinAction() } else -> null } @@ -194,6 +192,7 @@ fun AppContent( type = NavType.LongType }) ) { navBackStackEntry -> + // TODO: when task list has been deleted, we should not navigate to it event if in the stack val taskListViewModel: TaskListViewModel = hiltViewModel(navBackStackEntry) TaskListScreen( viewModel = taskListViewModel, diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/RecycleBinScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/RecycleBinScreen.kt index be23fd5..e9c1a44 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/RecycleBinScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/RecycleBinScreen.kt @@ -3,14 +3,30 @@ 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.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DeleteSweep +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.wismna.geoffroy.donext.domain.model.Task @@ -33,28 +49,91 @@ fun RecycleBinScreen( ) { Text("Recycle Bin is empty") } - } else { - val context = LocalContext.current - LazyColumn( - modifier = modifier.padding(8.dp) - ) { - items(tasks, key = { it.id!! }) { task -> + return + } + val grouped = tasks.groupBy { it.listName } + + val context = LocalContext.current + LazyColumn( + modifier = modifier.padding(8.dp) + ) { + // Deleted tasks are grouped by list name + grouped.forEach { (listName, items) -> + stickyHeader { + Surface( + tonalElevation = 2.dp, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = listName, + modifier = Modifier.padding(12.dp), + style = MaterialTheme.typography.titleMedium.copy( + fontStyle = FontStyle.Italic + ), + ) + } + } + items(items, key = { it.task.id!! }) { item -> TaskItemScreen( modifier = Modifier.animateItem(), - viewModel = TaskItemViewModel(task), - onTaskClick = { onTaskClick(task) }, + viewModel = TaskItemViewModel(item.task), + onTaskClick = { onTaskClick(item.task) }, onSwipeLeft = { - viewModel.restore(task.id!!) + viewModel.restore(item.task.id!!) Toast.makeText(context, "Task restored", Toast.LENGTH_SHORT).show() }, onSwipeRight = { - viewModel.deleteForever(task.id!!) + viewModel.deleteForever(item.task.id!!) Toast.makeText(context, "Task deleted", Toast.LENGTH_SHORT).show() } ) - } } } } + +@Composable +fun EmptyRecycleBinAction(viewModel: RecycleBinViewModel = hiltViewModel()) { + val isEmpty = viewModel.deletedTasks.isEmpty() + var showConfirmDialog by remember { mutableStateOf(false) } + + IconButton( + onClick = { showConfirmDialog = true }, + enabled = !isEmpty) { + Icon( + Icons.Default.DeleteSweep, + modifier = Modifier.alpha(if (isEmpty) 0.5f else 1.0f), + contentDescription = "Empty Recycle Bin", + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + + if (showConfirmDialog) { + AlertDialog( + onDismissRequest = { showConfirmDialog = false }, + title = { Text("Empty Recycle Bin") }, + text = { + Text("Are you sure you want to permanently delete all tasks in the recycle bin? This cannot be undone.") + }, + confirmButton = { + TextButton( + onClick = { + viewModel.emptyRecycleBin() + showConfirmDialog = false + }, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error + ) + ) { + Text("Delete") + } + }, + dismissButton = { + TextButton(onClick = { showConfirmDialog = false }) { + Text("Cancel") + } + } + ) + } +} diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskItemScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskItemScreen.kt index 78c7a45..2bab4f1 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskItemScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskItemScreen.kt @@ -12,11 +12,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.DeleteForever import androidx.compose.material.icons.filled.DeleteOutline import androidx.compose.material.icons.filled.RestoreFromTrash -import androidx.compose.material.icons.filled.Unpublished +import androidx.compose.material.icons.outlined.CheckCircle +import androidx.compose.material.icons.outlined.Unpublished import androidx.compose.material3.Badge import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -32,7 +32,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow @@ -153,7 +152,6 @@ fun TaskItemScreen( color = MaterialTheme.colorScheme.tertiary, style = baseStyle.copy( fontSize = MaterialTheme.typography.bodyMedium.fontSize, - fontStyle = FontStyle.Italic ), maxLines = 2, overflow = TextOverflow.Ellipsis @@ -191,14 +189,14 @@ fun DismissBackground(dismissState: SwipeToDismissBoxState, isDone: Boolean, isD Text( color = MaterialTheme.colorScheme.onPrimary, fontSize = 10.sp, - text = if (isDeleted) "Delete" else "Trash" + text = if (isDeleted) "Delete" else "Recycle" ) } Spacer(modifier = Modifier) Column (horizontalAlignment = Alignment.CenterHorizontally) { Icon( if (isDeleted) Icons.Default.RestoreFromTrash else - if (isDone) Icons.Default.Unpublished else Icons.Default.CheckCircle, + if (isDone) Icons.Outlined.Unpublished else Icons.Outlined.CheckCircle, tint = Color.LightGray, contentDescription = "Archive" ) diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskScreen.kt index 2080d5e..24f2d5e 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskScreen.kt @@ -27,11 +27,13 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberDatePickerState +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -41,6 +43,7 @@ import androidx.compose.ui.unit.dp import com.wismna.geoffroy.donext.domain.extension.toLocalDate import com.wismna.geoffroy.donext.domain.model.Priority import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel +import kotlinx.coroutines.launch import java.time.LocalDate import java.time.ZoneId import java.time.ZoneOffset @@ -54,15 +57,19 @@ fun TaskBottomSheet( onDismiss: () -> Unit ) { val titleFocusRequester = remember { FocusRequester() } + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val scope = rememberCoroutineScope() LaunchedEffect(Unit) { titleFocusRequester.requestFocus() } - ModalBottomSheet(onDismissRequest = onDismiss) { + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState) { Column(Modifier.padding(16.dp)) { Text( - if (viewModel.isEditing()) "Edit Task" else "New Task", + viewModel.screenTitle(), style = MaterialTheme.typography.titleLarge ) Spacer(Modifier.height(8.dp)) @@ -71,7 +78,7 @@ fun TaskBottomSheet( OutlinedTextField( value = viewModel.title, singleLine = true, - readOnly = viewModel.isDone, + readOnly = viewModel.isDeleted, onValueChange = { viewModel.onTitleChanged(it) }, label = { Text("Title") }, modifier = Modifier @@ -83,7 +90,7 @@ fun TaskBottomSheet( // --- Description --- OutlinedTextField( value = viewModel.description, - readOnly = viewModel.isDone, + readOnly = viewModel.isDeleted, onValueChange = { viewModel.onDescriptionChanged(it) }, label = { Text("Description") }, maxLines = 3, @@ -99,6 +106,7 @@ fun TaskBottomSheet( Text("Priority", style = MaterialTheme.typography.labelLarge) SingleChoiceSegmentedButton( value = viewModel.priority, + isEnabled = !viewModel.isDeleted, onValueChange = { viewModel.onPriorityChanged(it) } ) } @@ -120,13 +128,13 @@ fun TaskBottomSheet( if (viewModel.dueDate != null) { IconButton( onClick = { viewModel.onDueDateChanged(null) }, - enabled = !viewModel.isDone) { + enabled = !viewModel.isDeleted) { Icon(Icons.Default.Clear, contentDescription = "Clear due date") } } IconButton( onClick = { showDatePicker = true }, - enabled = !viewModel.isDone) { + enabled = !viewModel.isDeleted) { Icon(Icons.Default.CalendarMonth, contentDescription = "Pick due date") } } @@ -164,31 +172,39 @@ fun TaskBottomSheet( DatePicker(state = datePickerState) } } + if (!viewModel.isDeleted) { + Spacer(Modifier.height(16.dp)) - Spacer(Modifier.height(16.dp)) - - Row ( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = if (viewModel.isEditing()) Arrangement.SpaceBetween else Arrangement.End) { - - // --- Delete Button --- - if (viewModel.isEditing()) { - Button( - onClick = { viewModel.delete(); onDismiss() }, - colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.error - ) - ) { Text("Delete") } - } - // --- Save Button --- - Button( - onClick = { - viewModel.save() - onDismiss() - }, - enabled = viewModel.title.isNotBlank() && !viewModel.isDone, + Row ( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween ) { - Text(if (viewModel.isEditing()) "Save" else "Create") + // --- Cancel Button --- + Button( + onClick = { + scope.launch { + sheetState.hide() + onDismiss() + } + }, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.primary + ) + ) { Text("Cancel") } + + // --- Save Button --- + Button( + onClick = { + scope.launch { + viewModel.save() + sheetState.hide() + onDismiss() + } + }, + enabled = viewModel.title.isNotBlank() && !viewModel.isDeleted, + ) { + Text(if (viewModel.isEditing()) "Save" else "Create") + } } } } @@ -198,6 +214,7 @@ fun TaskBottomSheet( @Composable fun SingleChoiceSegmentedButton( value: Priority, + isEnabled: Boolean, onValueChange: (Priority) -> Unit) { val options = listOf(Priority.LOW.label, Priority.NORMAL.label, Priority.HIGH.label) @@ -208,6 +225,7 @@ fun SingleChoiceSegmentedButton( index = index, count = options.size ), + enabled = isEnabled, onClick = { onValueChange(Priority.fromValue(index)) }, selected = index == value.value, label = { Text(label) } diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt index 37ea138..0419ff3 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt @@ -7,18 +7,15 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavBackStackEntry 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 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 MainViewModel @Inject constructor( - getTaskListsUseCase: GetTaskListsUseCase, - private val emptyRecycleBinUseCase: EmptyRecycleBinUseCase + getTaskListsUseCase: GetTaskListsUseCase ) : ViewModel() { var isLoading by mutableStateOf(true) @@ -64,10 +61,4 @@ class MainViewModel @Inject constructor( } } ?: startDestination } - - fun emptyRecycleBin() { - viewModelScope.launch { - emptyRecycleBinUseCase() - } - } } \ No newline at end of file diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/ManageListsViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/ManageListsViewModel.kt index c638699..cbeb10c 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/ManageListsViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/ManageListsViewModel.kt @@ -49,9 +49,9 @@ class ManageListsViewModel @Inject constructor( updateTaskListUseCase(taskList.id!!, taskList.name, taskList.order) } } - fun deleteTaskList(taskId: Long) { + fun deleteTaskList(taskListId: Long) { viewModelScope.launch { - deleteTaskListUseCase(taskId) + deleteTaskListUseCase(taskListId) } } diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/RecycleBinViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/RecycleBinViewModel.kt index cc9952f..0e7defd 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/RecycleBinViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/RecycleBinViewModel.kt @@ -5,7 +5,8 @@ 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.model.TaskWithListName +import com.wismna.geoffroy.donext.domain.usecase.EmptyRecycleBinUseCase import com.wismna.geoffroy.donext.domain.usecase.GetDeletedTasksUseCase import com.wismna.geoffroy.donext.domain.usecase.PermanentlyDeleteTaskUseCase import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDeletedUseCase @@ -19,10 +20,11 @@ import javax.inject.Inject class RecycleBinViewModel @Inject constructor( private val getDeletedTasks: GetDeletedTasksUseCase, private val restoreTask: ToggleTaskDeletedUseCase, - private val permanentlyDeleteTask: PermanentlyDeleteTaskUseCase + private val permanentlyDeleteTask: PermanentlyDeleteTaskUseCase, + private val emptyRecycleBinUseCase: EmptyRecycleBinUseCase ) : ViewModel() { - var deletedTasks by mutableStateOf>(emptyList()) + var deletedTasks by mutableStateOf>(emptyList()) private set init { @@ -50,4 +52,9 @@ class RecycleBinViewModel @Inject constructor( loadDeletedTasks() } } -} + fun emptyRecycleBin() { + viewModelScope.launch { + emptyRecycleBinUseCase() + } + } +} \ No newline at end of file diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskViewModel.kt index d8596b5..61a131d 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskViewModel.kt @@ -8,7 +8,6 @@ import androidx.lifecycle.viewModelScope import com.wismna.geoffroy.donext.domain.model.Priority import com.wismna.geoffroy.donext.domain.model.Task import com.wismna.geoffroy.donext.domain.usecase.AddTaskUseCase -import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDeletedUseCase import com.wismna.geoffroy.donext.domain.usecase.UpdateTaskUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -20,8 +19,7 @@ import javax.inject.Inject @HiltViewModel class TaskViewModel @Inject constructor( private val createTaskUseCase: AddTaskUseCase, - private val updateTaskUseCase: UpdateTaskUseCase, - private val toggleTaskDeletedUseCase: ToggleTaskDeletedUseCase + private val updateTaskUseCase: UpdateTaskUseCase ) : ViewModel() { var title by mutableStateOf("") @@ -34,10 +32,13 @@ class TaskViewModel @Inject constructor( private set var isDone by mutableStateOf(false) private set + var isDeleted by mutableStateOf(false) + private set private var editingTaskId: Long? = null private var taskListId: Long? = null + fun screenTitle(): String = if (isDeleted) "Task details" else if (isEditing()) "Edit Task" else "New Task" fun isEditing(): Boolean = editingTaskId != null fun startNewTask(selectedListId: Long) { @@ -47,6 +48,7 @@ class TaskViewModel @Inject constructor( description = "" priority = Priority.NORMAL dueDate = null + isDeleted = false } fun startEditTask(task: Task) { @@ -57,6 +59,7 @@ class TaskViewModel @Inject constructor( priority = task.priority dueDate = task.dueDate isDone = task.isDone + isDeleted = task.isDeleted } fun onTitleChanged(value: String) { title = value } @@ -87,15 +90,6 @@ class TaskViewModel @Inject constructor( } } - fun delete() { - editingTaskId?.let { id -> - viewModelScope.launch { - toggleTaskDeletedUseCase(id, true) - reset() - } - } - } - fun reset() { editingTaskId = null taskListId = null