mirror of
https://github.com/wismna/DoNext.git
synced 2025-10-03 07:30:13 -04:00
Add an Empty Recycle Bin action button
Refactor Task Item Screen to include the Card WIP on fix overdue dates calculation on task items
This commit is contained in:
@@ -21,7 +21,7 @@ interface TaskDao {
|
||||
fun getDueTodayTasks(todayStart: Long, todayEnd: Long): Flow<List<TaskEntity>>
|
||||
|
||||
@Query("SELECT * FROM tasks WHERE deleted = 1")
|
||||
suspend fun getDeletedTasks(): List<TaskEntity>
|
||||
fun getDeletedTasks(): Flow<List<TaskEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertTask(task: TaskEntity)
|
||||
@@ -40,4 +40,7 @@ interface TaskDao {
|
||||
|
||||
@Query("DELETE FROM tasks WHERE id = :taskId")
|
||||
suspend fun permanentDeleteTask(taskId: Long)
|
||||
|
||||
@Query("DELETE FROM tasks WHERE deleted = 1")
|
||||
suspend fun permanentDeleteAllDeletedTasks()
|
||||
}
|
@@ -11,6 +11,7 @@ import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.map
|
||||
|
||||
class TaskRepositoryImpl @Inject constructor(
|
||||
private val taskDao: TaskDao,
|
||||
@@ -24,8 +25,8 @@ class TaskRepositoryImpl @Inject constructor(
|
||||
return taskDao.getDueTodayTasks(todayStart, todayEnd).map {entity -> entity.map { it.toDomain() }}
|
||||
}
|
||||
|
||||
override suspend fun getDeletedTasks(): List<Task> {
|
||||
return taskDao.getDeletedTasks().map {entity -> entity.toDomain() }
|
||||
override fun getDeletedTasks(): Flow<List<Task>> {
|
||||
return taskDao.getDeletedTasks().map {entity -> entity.map { it.toDomain() }}
|
||||
}
|
||||
|
||||
override suspend fun insertTask(task: Task) {
|
||||
@@ -48,6 +49,10 @@ class TaskRepositoryImpl @Inject constructor(
|
||||
taskDao.permanentDeleteTask(taskId)
|
||||
}
|
||||
|
||||
override suspend fun permanentlyDeleteAllDeletedTask() {
|
||||
taskDao.permanentDeleteAllDeletedTasks()
|
||||
}
|
||||
|
||||
override fun getTaskLists(): Flow<List<TaskList>> {
|
||||
return taskListDao.getTaskLists().map {entities -> entities.map { it.toDomain() }}
|
||||
}
|
||||
|
@@ -8,12 +8,13 @@ import kotlinx.coroutines.flow.Flow
|
||||
interface TaskRepository {
|
||||
fun getTasksForList(listId: Long): Flow<List<Task>>
|
||||
fun getDueTodayTasks(todayStart: Long, todayEnd: Long): Flow<List<Task>>
|
||||
suspend fun getDeletedTasks(): List<Task>
|
||||
fun getDeletedTasks(): Flow<List<Task>>
|
||||
suspend fun insertTask(task: Task)
|
||||
suspend fun updateTask(task: Task)
|
||||
suspend fun toggleTaskDeleted(taskId: Long, isDeleted: Boolean)
|
||||
suspend fun toggleTaskDone(taskId: Long, isDone: Boolean)
|
||||
suspend fun permanentlyDeleteTask(taskId: Long)
|
||||
suspend fun permanentlyDeleteAllDeletedTask()
|
||||
|
||||
fun getTaskLists(): Flow<List<TaskList>>
|
||||
suspend fun insertTaskList(taskList: TaskList)
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -2,8 +2,9 @@ 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) {
|
||||
suspend operator fun invoke(): List<Task> = repository.getDeletedTasks()
|
||||
operator fun invoke(): Flow<List<Task>> = repository.getDeletedTasks()
|
||||
}
|
@@ -6,9 +6,6 @@ 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.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -42,26 +39,21 @@ fun DueTodayTasksScreen(
|
||||
modifier = modifier.padding(8.dp)
|
||||
) {
|
||||
items(tasks, 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),
|
||||
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()
|
||||
Toast.makeText(context, "Task moved to recycle bin", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -26,6 +26,7 @@ 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
|
||||
@@ -136,11 +137,19 @@ fun AppContent(
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (viewModel.currentDestination is AppDestination.ManageLists) {
|
||||
when (viewModel.currentDestination) {
|
||||
is AppDestination.ManageLists -> {
|
||||
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
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -179,7 +188,6 @@ fun AppContent(
|
||||
slideOutHorizontally(targetOffsetX = { fullWidth -> fullWidth }, animationSpec = tween(300))
|
||||
}
|
||||
) {
|
||||
//viewModel.destinations.forEach { destination ->
|
||||
composable(
|
||||
route = "taskList/{taskListId}",
|
||||
arguments = listOf(navArgument("taskListId") {
|
||||
@@ -195,7 +203,6 @@ fun AppContent(
|
||||
}
|
||||
)
|
||||
}
|
||||
//}
|
||||
|
||||
composable(AppDestination.ManageLists.route) {
|
||||
ManageListsScreen(
|
||||
@@ -214,7 +221,11 @@ fun AppContent(
|
||||
}
|
||||
composable(AppDestination.RecycleBin.route) {
|
||||
RecycleBinScreen(
|
||||
modifier = Modifier
|
||||
modifier = Modifier,
|
||||
onTaskClick = { task ->
|
||||
taskViewModel.startEditTask(task)
|
||||
viewModel.showTaskSheet = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -6,9 +6,6 @@ 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.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -16,6 +13,7 @@ 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
|
||||
|
||||
@@ -23,6 +21,7 @@ import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
|
||||
fun RecycleBinScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: RecycleBinViewModel = hiltViewModel(),
|
||||
onTaskClick: (task: Task) -> Unit
|
||||
) {
|
||||
val tasks = viewModel.deletedTasks
|
||||
|
||||
@@ -40,15 +39,11 @@ fun RecycleBinScreen(
|
||||
modifier = modifier.padding(8.dp)
|
||||
) {
|
||||
items(tasks, 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),
|
||||
onTaskClick = { onTaskClick(task) },
|
||||
onSwipeLeft = {
|
||||
viewModel.restore(task.id!!)
|
||||
Toast.makeText(context, "Task restored", Toast.LENGTH_SHORT).show()
|
||||
@@ -58,7 +53,7 @@ fun RecycleBinScreen(
|
||||
Toast.makeText(context, "Task deleted", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -16,6 +16,8 @@ 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.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SwipeToDismissBox
|
||||
@@ -41,6 +43,7 @@ import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
|
||||
fun TaskItemScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: TaskItemViewModel,
|
||||
onTaskClick: (taskId: Long) -> Unit,
|
||||
onSwipeLeft: () -> Unit,
|
||||
onSwipeRight: () -> Unit
|
||||
) {
|
||||
@@ -69,14 +72,25 @@ fun TaskItemScreen(
|
||||
},
|
||||
textDecoration = if (viewModel.isDone) TextDecoration.LineThrough else TextDecoration.None
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = modifier,
|
||||
onClick = { onTaskClick(viewModel.id) },
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
) {
|
||||
SwipeToDismissBox(
|
||||
state = dismissState,
|
||||
modifier = modifier,
|
||||
backgroundContent = { DismissBackground(dismissState, viewModel.isDone, viewModel.isDeleted) },
|
||||
backgroundContent = {
|
||||
DismissBackground(
|
||||
dismissState,
|
||||
viewModel.isDone,
|
||||
viewModel.isDeleted
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Row(
|
||||
modifier = modifier
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(8.dp)
|
||||
@@ -147,6 +161,7 @@ fun TaskItemScreen(
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DismissBackground(dismissState: SwipeToDismissBoxState, isDone: Boolean, isDeleted: Boolean) {
|
||||
|
@@ -10,12 +10,9 @@ 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
|
||||
@@ -51,16 +48,10 @@ 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),
|
||||
onTaskClick = { onTaskClick(task) },
|
||||
onSwipeLeft = {
|
||||
viewModel.updateTaskDone(task.id!!, true)
|
||||
Toast.makeText(context, "Task done", Toast.LENGTH_SHORT).show()
|
||||
@@ -71,7 +62,6 @@ fun TaskListScreen(
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Divider between active and done (optional)
|
||||
if (done.isNotEmpty() && active.isNotEmpty()) {
|
||||
@@ -89,16 +79,10 @@ 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),
|
||||
onTaskClick = { onTaskClick(task) },
|
||||
onSwipeLeft = {
|
||||
viewModel.updateTaskDone(task.id!!, false)
|
||||
Toast.makeText(context, "Task in progress", Toast.LENGTH_SHORT).show()
|
||||
@@ -109,7 +93,7 @@ fun TaskListScreen(
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -42,6 +42,7 @@ import com.wismna.geoffroy.donext.domain.model.Priority
|
||||
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
@@ -108,7 +109,7 @@ fun TaskBottomSheet(
|
||||
var showDatePicker by remember { mutableStateOf(false) }
|
||||
val formattedDate = viewModel.dueDate?.let {
|
||||
Instant.ofEpochMilli(it)
|
||||
.atZone(ZoneOffset.UTC)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDate()
|
||||
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM))
|
||||
} ?: ""
|
||||
|
@@ -7,15 +7,18 @@ 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(
|
||||
getTaskLists: GetTaskListsUseCase
|
||||
getTaskListsUseCase: GetTaskListsUseCase,
|
||||
private val emptyRecycleBinUseCase: EmptyRecycleBinUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
var isLoading by mutableStateOf(true)
|
||||
@@ -34,7 +37,7 @@ class MainViewModel @Inject constructor(
|
||||
var showAddListSheet by mutableStateOf(false)
|
||||
|
||||
init {
|
||||
getTaskLists()
|
||||
getTaskListsUseCase()
|
||||
.onEach { lists ->
|
||||
destinations = lists.map { taskList ->
|
||||
AppDestination.TaskList(taskList.id!!, taskList.name)
|
||||
@@ -61,4 +64,10 @@ class MainViewModel @Inject constructor(
|
||||
}
|
||||
} ?: startDestination
|
||||
}
|
||||
|
||||
fun emptyRecycleBin() {
|
||||
viewModelScope.launch {
|
||||
emptyRecycleBinUseCase()
|
||||
}
|
||||
}
|
||||
}
|
@@ -10,6 +10,8 @@ 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
|
||||
|
||||
@@ -28,9 +30,11 @@ class RecycleBinViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun loadDeletedTasks() {
|
||||
viewModelScope.launch {
|
||||
deletedTasks = getDeletedTasks()
|
||||
getDeletedTasks()
|
||||
.onEach { tasks ->
|
||||
deletedTasks = tasks
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun restore(taskId: Long) {
|
||||
|
@@ -4,7 +4,7 @@ import com.wismna.geoffroy.donext.domain.model.Priority
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.time.format.TextStyle
|
||||
@@ -18,11 +18,11 @@ class TaskItemViewModel(task: Task) {
|
||||
val isDeleted: Boolean = task.isDeleted
|
||||
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 dueDate = Instant.ofEpochMilli(millis)
|
||||
.atZone(ZoneOffset.UTC)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDate()
|
||||
dueDate.isBefore(today)
|
||||
} ?: false
|
||||
@@ -30,7 +30,7 @@ class TaskItemViewModel(task: Task) {
|
||||
val dueDateText: String? = task.dueDate?.let { formatDueDate(it) }
|
||||
|
||||
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 {
|
||||
dueDate.isEqual(today) -> "Today"
|
||||
|
Reference in New Issue
Block a user