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:
Geoffroy Bonneville
2025-09-24 21:29:23 -04:00
parent cf770ddb83
commit b71fa4fdb7
14 changed files with 224 additions and 191 deletions

View File

@@ -21,7 +21,7 @@ interface TaskDao {
fun getDueTodayTasks(todayStart: Long, todayEnd: Long): Flow<List<TaskEntity>> fun getDueTodayTasks(todayStart: Long, todayEnd: Long): Flow<List<TaskEntity>>
@Query("SELECT * FROM tasks WHERE deleted = 1") @Query("SELECT * FROM tasks WHERE deleted = 1")
suspend fun getDeletedTasks(): List<TaskEntity> fun getDeletedTasks(): Flow<List<TaskEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: TaskEntity) suspend fun insertTask(task: TaskEntity)
@@ -40,4 +40,7 @@ interface TaskDao {
@Query("DELETE FROM tasks WHERE id = :taskId") @Query("DELETE FROM tasks WHERE id = :taskId")
suspend fun permanentDeleteTask(taskId: Long) 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,
@@ -21,11 +22,11 @@ class TaskRepositoryImpl @Inject constructor(
} }
override fun getDueTodayTasks(todayStart: Long, todayEnd: Long): Flow<List<Task>> { override fun getDueTodayTasks(todayStart: Long, todayEnd: Long): Flow<List<Task>> {
return taskDao.getDueTodayTasks(todayStart, todayEnd).map {entity -> entity.map { it.toDomain() }} return taskDao.getDueTodayTasks(todayStart, todayEnd).map {entity -> entity.map { it.toDomain() }}
} }
override suspend fun getDeletedTasks(): List<Task> { override fun getDeletedTasks(): Flow<List<Task>> {
return taskDao.getDeletedTasks().map {entity -> entity.toDomain() } return taskDao.getDeletedTasks().map {entity -> entity.map { it.toDomain() }}
} }
override suspend fun insertTask(task: Task) { override suspend fun insertTask(task: Task) {
@@ -48,6 +49,10 @@ class TaskRepositoryImpl @Inject constructor(
taskDao.permanentDeleteTask(taskId) 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

@@ -8,12 +8,13 @@ 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 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 insertTask(task: Task)
suspend fun updateTask(task: Task) suspend fun updateTask(task: Task)
suspend fun toggleTaskDeleted(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 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

@@ -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.model.Task
import com.wismna.geoffroy.donext.domain.repository.TaskRepository import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject import javax.inject.Inject
class GetDeletedTasksUseCase @Inject constructor(private val repository: TaskRepository) { class GetDeletedTasksUseCase @Inject constructor(private val repository: TaskRepository) {
suspend operator fun invoke(): List<Task> = repository.getDeletedTasks() operator fun invoke(): Flow<List<Task>> = repository.getDeletedTasks()
} }

View File

@@ -6,9 +6,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -42,25 +39,20 @@ fun DueTodayTasksScreen(
modifier = modifier.padding(8.dp) modifier = modifier.padding(8.dp)
) { ) {
items(tasks, key = { it.id!! }) { task -> items(tasks, key = { it.id!! }) { 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!!)
) { Toast.makeText(context, "Task done", Toast.LENGTH_SHORT).show()
TaskItemScreen( },
viewModel = TaskItemViewModel(task), onSwipeRight = {
onSwipeLeft = { viewModel.deleteTask(task.id!!)
viewModel.updateTaskDone(task.id!!) Toast.makeText(context, "Task moved to recycle bin", Toast.LENGTH_SHORT)
Toast.makeText(context, "Task done", Toast.LENGTH_SHORT).show() .show()
}, }
onSwipeRight = { )
viewModel.deleteTask(task.id!!)
Toast.makeText(context, "Task moved to recycle bin", Toast.LENGTH_SHORT).show()
}
)
}
} }
} }
} }

View File

@@ -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
@@ -136,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
} }
} }
) )
@@ -179,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(
@@ -214,7 +221,11 @@ fun AppContent(
} }
composable(AppDestination.RecycleBin.route) { composable(AppDestination.RecycleBin.route) {
RecycleBinScreen( RecycleBinScreen(
modifier = Modifier modifier = Modifier,
onTaskClick = { task ->
taskViewModel.startEditTask(task)
viewModel.showTaskSheet = true
}
) )
} }
} }

View File

@@ -6,9 +6,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -16,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext 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.presentation.viewmodel.RecycleBinViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.RecycleBinViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
@@ -23,6 +21,7 @@ import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
fun RecycleBinScreen( fun RecycleBinScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: RecycleBinViewModel = hiltViewModel(), viewModel: RecycleBinViewModel = hiltViewModel(),
onTaskClick: (task: Task) -> Unit
) { ) {
val tasks = viewModel.deletedTasks val tasks = viewModel.deletedTasks
@@ -40,25 +39,21 @@ fun RecycleBinScreen(
modifier = modifier.padding(8.dp) modifier = modifier.padding(8.dp)
) { ) {
items(tasks, key = { it.id!! }) { task -> items(tasks, key = { it.id!! }) { task ->
Card(
//onClick = { onTaskClick(task) }, TaskItemScreen(
//elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp), modifier = Modifier.animateItem(),
colors = CardDefaults.cardColors( viewModel = TaskItemViewModel(task),
containerColor = MaterialTheme.colorScheme.surfaceContainer, onTaskClick = { onTaskClick(task) },
), onSwipeLeft = {
) { viewModel.restore(task.id!!)
TaskItemScreen( Toast.makeText(context, "Task restored", Toast.LENGTH_SHORT).show()
viewModel = TaskItemViewModel(task), },
onSwipeLeft = { onSwipeRight = {
viewModel.restore(task.id!!) viewModel.deleteForever(task.id!!)
Toast.makeText(context, "Task restored", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Task deleted", Toast.LENGTH_SHORT).show()
}, }
onSwipeRight = { )
viewModel.deleteForever(task.id!!)
Toast.makeText(context, "Task deleted", Toast.LENGTH_SHORT).show()
}
)
}
} }
} }
} }

View File

@@ -16,6 +16,8 @@ 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
@@ -41,6 +43,7 @@ import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
fun TaskItemScreen( fun TaskItemScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: TaskItemViewModel, viewModel: TaskItemViewModel,
onTaskClick: (taskId: Long) -> Unit,
onSwipeLeft: () -> Unit, onSwipeLeft: () -> Unit,
onSwipeRight: () -> Unit onSwipeRight: () -> Unit
) { ) {
@@ -69,83 +72,95 @@ 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, viewModel.isDeleted) }, 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,
fontSize = 18.sp,
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) .padding(start = 8.dp)
.padding(top = 24.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,
color = MaterialTheme.colorScheme.tertiary, fontSize = 18.sp,
style = baseStyle.copy( style = baseStyle,
fontSize = MaterialTheme.typography.bodyMedium.fontSize, modifier = Modifier
fontStyle = FontStyle.Italic .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

View File

@@ -10,12 +10,9 @@ 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
@@ -51,26 +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!!)
onSwipeLeft = { Toast.makeText(context, "Task moved to recycle bin", Toast.LENGTH_SHORT).show()
viewModel.updateTaskDone(task.id!!, true) }
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()
}
)
}
} }
// Divider between active and done (optional) // Divider between active and done (optional)
@@ -89,27 +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!!)
onSwipeLeft = { Toast.makeText(context, "Task moved to recycle bin", Toast.LENGTH_SHORT).show()
viewModel.updateTaskDone(task.id!!, false) },
Toast.makeText(context, "Task in progress", Toast.LENGTH_SHORT).show() )
},
onSwipeRight = {
viewModel.deleteTask(task.id!!)
Toast.makeText(context, "Task moved to recycle bin", Toast.LENGTH_SHORT).show()
},
)
}
} }
} }
} }

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

@@ -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,7 +37,7 @@ 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)
@@ -61,4 +64,10 @@ class MainViewModel @Inject constructor(
} }
} ?: startDestination } ?: startDestination
} }
fun emptyRecycleBin() {
viewModelScope.launch {
emptyRecycleBinUseCase()
}
}
} }

View File

@@ -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.PermanentlyDeleteTaskUseCase
import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDeletedUseCase import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDeletedUseCase
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@@ -28,9 +30,11 @@ class RecycleBinViewModel @Inject constructor(
} }
fun loadDeletedTasks() { fun loadDeletedTasks() {
viewModelScope.launch { getDeletedTasks()
deletedTasks = getDeletedTasks() .onEach { tasks ->
} deletedTasks = tasks
}
.launchIn(viewModelScope)
} }
fun restore(taskId: Long) { fun restore(taskId: Long) {

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
@@ -18,11 +18,11 @@ class TaskItemViewModel(task: Task) {
val isDeleted: Boolean = task.isDeleted 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
@@ -30,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"