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 f91da92..ab8053f 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 @@ -20,11 +20,11 @@ interface TaskDao { suspend fun updateTask(task: TaskEntity) @Query("UPDATE tasks SET done = :done WHERE id = :taskId") - suspend fun markTaskDone(taskId: Long, done: Boolean) + suspend fun toggleTaskDone(taskId: Long, done: Boolean) @Query("UPDATE tasks SET deleted = :deleted WHERE id = :taskId") - suspend fun markTaskDeleted(taskId: Long, deleted: Boolean) + suspend fun toggleTaskDeleted(taskId: Long, deleted: Boolean) @Query("UPDATE tasks SET deleted = :deleted WHERE task_list_id = :taskListId") - suspend fun deleteAllTasksFromList(taskListId: Long, deleted: Boolean) + suspend fun toggleAllTasksFromListDeleted(taskListId: Long, deleted: Boolean) } \ No newline at end of file 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 dfa860b..2d31b5f 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 @@ -29,11 +29,11 @@ class TaskRepositoryImpl @Inject constructor( } override suspend fun deleteTask(taskId: Long, isDeleted: Boolean) { - taskDao.markTaskDeleted(taskId, isDeleted) + taskDao.toggleTaskDeleted(taskId, isDeleted) } override suspend fun toggleTaskDone(taskId: Long, isDone: Boolean) { - taskDao.markTaskDone(taskId, isDone) + taskDao.toggleTaskDone(taskId, isDone) } override fun getTaskLists(): Flow> { @@ -49,7 +49,7 @@ class TaskRepositoryImpl @Inject constructor( } override suspend fun deleteTaskList(taskListId: Long, isDeleted: Boolean) { - taskDao.deleteAllTasksFromList(taskListId, isDeleted) + taskDao.toggleAllTasksFromListDeleted(taskListId, isDeleted) taskListDao.deleteTaskList(taskListId, isDeleted) } diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/ManageListsScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/ManageListsScreen.kt index db7b2c3..0c74eac 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/ManageListsScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/ManageListsScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.animation.togetherWith import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -45,6 +46,7 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.customActions +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.ManageListsViewModel @@ -67,7 +69,12 @@ fun ManageListsScreen( } ) - LazyColumn(modifier = modifier.fillMaxWidth().padding(), state = lazyListState) { + LazyColumn( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(vertical = 8.dp), + state = lazyListState + ) { itemsIndexed(lists, key = { _, list -> list.id!! }) { index, list -> var isInEditMode by remember { mutableStateOf(false) } @@ -81,7 +88,7 @@ fun ManageListsScreen( onClick = {}, elevation = CardDefaults.elevatedCardElevation(defaultElevation = 5.dp), colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer, + containerColor = MaterialTheme.colorScheme.secondaryContainer, ), modifier = Modifier.draggableHandle( onDragStopped = { @@ -127,8 +134,8 @@ fun ManageListsScreen( verticalAlignment = Alignment.CenterVertically ) { AnimatedContent( - //modifier = Modifier.padding(start = 12.dp), targetState = isInEditMode, + modifier = Modifier.weight(1f), transitionSpec = { fadeIn() togetherWith fadeOut() }, @@ -141,7 +148,12 @@ fun ManageListsScreen( singleLine = true ) } else { - Text(list.name) + Text( + modifier = Modifier.padding(start = 8.dp), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = list.name + ) } } AnimatedContent( diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MenuScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MenuScreen.kt index b432975..76c67b8 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MenuScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MenuScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.material3.NavigationDrawerItemDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.wismna.geoffroy.donext.domain.model.AppDestination @@ -46,7 +47,13 @@ fun MenuScreen( ) viewModel.taskLists.forEach { list -> NavigationDrawerItem( - label = { Text(list.name) }, + label = { + Text( + text = list.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, icon = { Icon(Icons.Default.List, contentDescription = list.name) }, selected = currentDestination is AppDestination.TaskList && currentDestination.taskListId == list.id, 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 efb7c91..43b3f20 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 @@ -1,29 +1,35 @@ package com.wismna.geoffroy.donext.presentation.screen -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.clickable +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Done import androidx.compose.material3.Badge -import androidx.compose.material3.Checkbox +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxState +import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text +import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip 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.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow @@ -35,9 +41,30 @@ import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel fun TaskItemScreen( modifier: Modifier = Modifier, viewModel: TaskItemViewModel, - onClick: () -> Unit, - onToggleDone: (Boolean) -> Unit + onSwipeDone: () -> Unit, + onSwipeDelete: () -> Unit ) { + val context = LocalContext.current + val dismissState = rememberSwipeToDismissBoxState( + confirmValueChange = { + when (it) { + SwipeToDismissBoxValue.StartToEnd -> { + onSwipeDelete() + 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 + } + return@rememberSwipeToDismissBoxState true + }, + // positional threshold of 25% + positionalThreshold = { it * .25f } + ) val baseStyle = MaterialTheme.typography.bodyLarge.copy( fontWeight = when (viewModel.priority) { Priority.HIGH -> FontWeight.Bold @@ -49,79 +76,111 @@ fun TaskItemScreen( Priority.NORMAL -> MaterialTheme.colorScheme.onSurface Priority.LOW -> MaterialTheme.colorScheme.onSurfaceVariant }, - textDecoration = if (viewModel.isDone) TextDecoration.LineThrough else TextDecoration.None) + textDecoration = if (viewModel.isDone) TextDecoration.LineThrough else TextDecoration.None + ) - Row( - modifier = modifier - .fillMaxWidth() - .clickable { onClick() } - .padding(8.dp) - .alpha(if (viewModel.isDone || viewModel.priority == Priority.LOW) 0.5f else 1f), - verticalAlignment = Alignment.CenterVertically // centers checkbox + content - ) { - // Done checkbox - Checkbox( - checked = viewModel.isDone, - onCheckedChange = onToggleDone, - modifier = Modifier - .size(40.dp) - .clip(CircleShape) - ) - - Box( - modifier = Modifier - .weight(1f) - .padding(start = 8.dp) - .height(IntrinsicSize.Min) // shrink to fit title/description - ) { - // Title - Text( - text = viewModel.name, - style = baseStyle, - modifier = Modifier - .align( - if (viewModel.description.isNullOrBlank()) Alignment.CenterStart - else Alignment.TopStart - ) - ) - - // Due date badge - viewModel.dueDateText?.let { dueMillis -> - Badge( + SwipeToDismissBox( + state = dismissState, + modifier = modifier, + backgroundContent = { DismissBackground(dismissState, viewModel.isDone) }, + content = { + Row( + modifier = modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(8.dp) + .alpha(if (viewModel.isDone || viewModel.priority == Priority.LOW) 0.5f else 1f), + verticalAlignment = Alignment.CenterVertically // centers checkbox + content + ) { + Box( modifier = Modifier - .align( - if (viewModel.description.isNullOrBlank()) Alignment.CenterEnd - else Alignment.TopEnd - ), - containerColor = if (viewModel.isOverdue) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primaryContainer + .weight(1f) + .padding(start = 8.dp) + .height(IntrinsicSize.Min) // shrink to fit title/description ) { + // Title 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 + 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( + modifier = Modifier + .fillMaxWidth() + .height(40.dp) // 👈 adjust to the typical description height + .padding(top = 20.dp), + contentAlignment = Alignment.TopStart + ) { + if (!viewModel.description.isNullOrBlank()) { + Text( + text = viewModel.description, + style = baseStyle.copy( + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + fontStyle = FontStyle.Italic + ), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } } } + }) +} - // Optional description - this@Row.AnimatedVisibility( - visible = !viewModel.description.isNullOrBlank(), - modifier = Modifier.align(Alignment.BottomStart), - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - Text( - text = viewModel.description!!, - style = baseStyle.copy(fontSize = MaterialTheme.typography.bodyMedium.fontSize), - maxLines = 3, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .align(Alignment.BottomStart) - .padding(top = 20.dp) // spacing below title - ) - } +@Composable +fun DismissBackground(dismissState: SwipeToDismissBoxState, isDone: Boolean) { + val color = when (dismissState.dismissDirection) { + SwipeToDismissBoxValue.StartToEnd -> MaterialTheme.colorScheme.error + SwipeToDismissBoxValue.EndToStart -> Color(0xFF18590D) + SwipeToDismissBoxValue.Settled -> Color.Transparent + } - } + Row( + modifier = Modifier + .fillMaxSize() + .background(color) + .padding(12.dp, 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Icon( + Icons.Default.Delete, + tint = Color.LightGray, + contentDescription = "Delete" + ) + Spacer(modifier = Modifier) + Icon( + if (isDone) Icons.Default.Close else Icons.Default.Done, + tint = Color.LightGray, + contentDescription = "Archive" + ) } } \ No newline at end of file diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskListScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskListScreen.kt index e442e10..c1d9e0c 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskListScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskListScreen.kt @@ -9,9 +9,12 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons 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.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -45,14 +48,24 @@ fun TaskListScreen( items = active, key = { it.id!! } ) { task -> - TaskItemScreen( - modifier = Modifier.animateItem(), - viewModel = TaskItemViewModel(task), + Card( onClick = { onTaskClick(task) }, - onToggleDone = { checked -> - viewModel.updateTaskDone(task.id!!, checked) - } - ) + //elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), + ) { + TaskItemScreen( + modifier = Modifier.animateItem(), + viewModel = TaskItemViewModel(task), + onSwipeDone = { + viewModel.updateTaskDone(task.id!!, true) + }, + onSwipeDelete = { + viewModel.deleteTask(task.id!!) + } + ) + } } // Divider between active and done (optional) @@ -71,14 +84,24 @@ fun TaskListScreen( items = done, key = { it.id!! } ) { task -> - TaskItemScreen( - modifier = Modifier.animateItem(), - viewModel = TaskItemViewModel(task), + Card( onClick = { onTaskClick(task) }, - onToggleDone = { checked -> - viewModel.updateTaskDone(task.id!!, checked) - } - ) + //elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), + ) { + TaskItemScreen( + modifier = Modifier.animateItem(), + viewModel = TaskItemViewModel(task), + onSwipeDone = { + viewModel.updateTaskDone(task.id!!, false) + }, + onSwipeDelete = { + viewModel.deleteTask(task.id!!) + }, + ) + } } } } diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskListViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskListViewModel.kt index d46b2a2..078a354 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskListViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskListViewModel.kt @@ -7,8 +7,9 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wismna.geoffroy.donext.domain.model.Task -import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDoneUseCase +import com.wismna.geoffroy.donext.domain.usecase.DeleteTaskListUseCase import com.wismna.geoffroy.donext.domain.usecase.GetTasksForListUseCase +import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDoneUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -18,7 +19,8 @@ import javax.inject.Inject @HiltViewModel class TaskListViewModel @Inject constructor( getTasks: GetTasksForListUseCase, - private val toggleTaskDone: ToggleTaskDoneUseCase, + private val toggleTaskDoneUseCase: ToggleTaskDoneUseCase, + private val deleteTaskListUseCase: DeleteTaskListUseCase, savedStateHandle: SavedStateHandle ) : ViewModel() { @@ -40,7 +42,12 @@ class TaskListViewModel @Inject constructor( fun updateTaskDone(taskId: Long, isDone: Boolean) { viewModelScope.launch { - toggleTaskDone(taskId, isDone) + toggleTaskDoneUseCase(taskId, isDone) + } + } + fun deleteTask(taskId: Long) { + viewModelScope.launch { + deleteTaskListUseCase(taskId) } } } \ No newline at end of file