mirror of
https://github.com/wismna/DoNext.git
synced 2025-10-03 07:30:13 -04:00
Added swiping moves to the tasks: left for done, right for delete
Rename some DAO functions
This commit is contained in:
@@ -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)
|
||||
}
|
@@ -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<List<TaskList>> {
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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(
|
||||
|
@@ -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,
|
||||
|
@@ -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,25 +76,22 @@ 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
|
||||
)
|
||||
|
||||
SwipeToDismissBox(
|
||||
state = dismissState,
|
||||
modifier = modifier,
|
||||
backgroundContent = { DismissBackground(dismissState, viewModel.isDone) },
|
||||
content = {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.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
|
||||
) {
|
||||
// Done checkbox
|
||||
Checkbox(
|
||||
checked = viewModel.isDone,
|
||||
onCheckedChange = onToggleDone,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
@@ -82,7 +106,9 @@ fun TaskItemScreen(
|
||||
.align(
|
||||
if (viewModel.description.isNullOrBlank()) Alignment.CenterStart
|
||||
else Alignment.TopStart
|
||||
)
|
||||
),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
// Due date badge
|
||||
@@ -105,23 +131,56 @@ fun TaskItemScreen(
|
||||
}
|
||||
|
||||
// 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,
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(top = 20.dp) // spacing below title
|
||||
.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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@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"
|
||||
)
|
||||
}
|
||||
}
|
@@ -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,15 +48,25 @@ fun TaskListScreen(
|
||||
items = active,
|
||||
key = { it.id!! }
|
||||
) { task ->
|
||||
Card(
|
||||
onClick = { onTaskClick(task) },
|
||||
//elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
) {
|
||||
TaskItemScreen(
|
||||
modifier = Modifier.animateItem(),
|
||||
viewModel = TaskItemViewModel(task),
|
||||
onClick = { onTaskClick(task) },
|
||||
onToggleDone = { checked ->
|
||||
viewModel.updateTaskDone(task.id!!, checked)
|
||||
onSwipeDone = {
|
||||
viewModel.updateTaskDone(task.id!!, true)
|
||||
},
|
||||
onSwipeDelete = {
|
||||
viewModel.deleteTask(task.id!!)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Divider between active and done (optional)
|
||||
if (done.isNotEmpty() && active.isNotEmpty()) {
|
||||
@@ -71,16 +84,26 @@ fun TaskListScreen(
|
||||
items = done,
|
||||
key = { it.id!! }
|
||||
) { task ->
|
||||
Card(
|
||||
onClick = { onTaskClick(task) },
|
||||
//elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
) {
|
||||
TaskItemScreen(
|
||||
modifier = Modifier.animateItem(),
|
||||
viewModel = TaskItemViewModel(task),
|
||||
onClick = { onTaskClick(task) },
|
||||
onToggleDone = { checked ->
|
||||
viewModel.updateTaskDone(task.id!!, checked)
|
||||
}
|
||||
onSwipeDone = {
|
||||
viewModel.updateTaskDone(task.id!!, false)
|
||||
},
|
||||
onSwipeDelete = {
|
||||
viewModel.deleteTask(task.id!!)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user