mirror of
https://github.com/wismna/DoNext.git
synced 2025-12-06 00:02:40 -05:00
Finally fix the double navigation issue event
Dialogs now survive screen rotation Improve empty screens placeholders
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
package com.wismna.geoffroy.donext.presentation.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CalendarToday
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -26,7 +32,14 @@ fun DueTodayTasksScreen(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Nothing due today !")
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
Icons.Default.CalendarToday,
|
||||
contentDescription = "Due today background icon",
|
||||
modifier = Modifier.size(60.dp),
|
||||
tint = MaterialTheme.colorScheme.secondary)
|
||||
Text("Nothing due today !", color = MaterialTheme.colorScheme.secondary)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
|
||||
@@ -206,7 +206,6 @@ fun AppContent(
|
||||
navController.navigate(event.route)
|
||||
}
|
||||
is UiEvent.NavigateBack -> navController.popBackStack()
|
||||
is UiEvent.EditTask -> { viewModel.showTaskSheet = true }
|
||||
is UiEvent.ShowUndoSnackbar -> {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = event.message,
|
||||
@@ -221,6 +220,15 @@ fun AppContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.uiEventBus.stickyEvents.collect { event ->
|
||||
when (event) {
|
||||
is UiEvent.EditTask -> { viewModel.showTaskSheet = true }
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
containerColor = Color.Transparent,
|
||||
|
||||
@@ -2,12 +2,15 @@ package com.wismna.geoffroy.donext.presentation.screen
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.DeleteOutline
|
||||
import androidx.compose.material.icons.filled.DeleteSweep
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
@@ -19,9 +22,6 @@ 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
|
||||
@@ -29,6 +29,7 @@ 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 androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.wismna.geoffroy.donext.presentation.viewmodel.RecycleBinViewModel
|
||||
|
||||
@Composable
|
||||
@@ -37,6 +38,7 @@ fun RecycleBinScreen(
|
||||
viewModel: RecycleBinViewModel = hiltViewModel(),
|
||||
) {
|
||||
val tasks = viewModel.deletedTasks
|
||||
val taskToDelete by viewModel.taskToDeleteFlow.collectAsStateWithLifecycle()
|
||||
|
||||
if (tasks.isEmpty()) {
|
||||
// Placeholder when recycle bin is empty
|
||||
@@ -44,7 +46,15 @@ fun RecycleBinScreen(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Recycle Bin is empty")
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
Icons.Default.DeleteOutline,
|
||||
contentDescription = "Recycle bin background icon",
|
||||
modifier = Modifier.size(60.dp),
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
Text("Recycle Bin is empty", color = MaterialTheme.colorScheme.secondary)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -52,7 +62,7 @@ fun RecycleBinScreen(
|
||||
val grouped = tasks.groupBy { it.listName }
|
||||
val context = LocalContext.current
|
||||
|
||||
if (viewModel.taskToDelete != null) {
|
||||
if (taskToDelete != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.onCancelDelete() },
|
||||
title = { Text("Delete task") },
|
||||
@@ -114,10 +124,10 @@ fun RecycleBinScreen(
|
||||
@Composable
|
||||
fun EmptyRecycleBinAction(viewModel: RecycleBinViewModel = hiltViewModel()) {
|
||||
val isEmpty = viewModel.deletedTasks.isEmpty()
|
||||
var showConfirmDialog by remember { mutableStateOf(false) }
|
||||
val emptyRecycleBin by viewModel.emptyRecycleBinFlow.collectAsStateWithLifecycle()
|
||||
|
||||
IconButton(
|
||||
onClick = { showConfirmDialog = true },
|
||||
onClick = { viewModel.onEmptyRecycleBinRequest() },
|
||||
enabled = !isEmpty) {
|
||||
Icon(
|
||||
Icons.Default.DeleteSweep,
|
||||
@@ -127,9 +137,9 @@ fun EmptyRecycleBinAction(viewModel: RecycleBinViewModel = hiltViewModel()) {
|
||||
)
|
||||
}
|
||||
|
||||
if (showConfirmDialog) {
|
||||
if (emptyRecycleBin) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showConfirmDialog = false },
|
||||
onDismissRequest = { viewModel.onCancelEmptyRecycleBinRequest() },
|
||||
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.")
|
||||
@@ -138,7 +148,6 @@ fun EmptyRecycleBinAction(viewModel: RecycleBinViewModel = hiltViewModel()) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.emptyRecycleBin()
|
||||
showConfirmDialog = false
|
||||
},
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
@@ -148,7 +157,7 @@ fun EmptyRecycleBinAction(viewModel: RecycleBinViewModel = hiltViewModel()) {
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showConfirmDialog = false }) {
|
||||
TextButton(onClick = { viewModel.onCancelEmptyRecycleBinRequest() }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.wismna.geoffroy.donext.presentation.ui.events
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import javax.inject.Inject
|
||||
@@ -7,10 +8,24 @@ import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class UiEventBus @Inject constructor() {
|
||||
private val _events = MutableSharedFlow<UiEvent>(replay = 1)
|
||||
// Non-replayable (e.g. navigation, snackbar)
|
||||
private val _events = MutableSharedFlow<UiEvent>(replay = 0, extraBufferCapacity = 1)
|
||||
val events = _events.asSharedFlow()
|
||||
|
||||
// Replayable (e.g. edit/create task)
|
||||
private val _stickyEvents = MutableSharedFlow<UiEvent>(replay = 1, extraBufferCapacity = 1)
|
||||
val stickyEvents = _stickyEvents.asSharedFlow()
|
||||
|
||||
suspend fun send(event: UiEvent) {
|
||||
_events.emit(event)
|
||||
when (event) {
|
||||
is UiEvent.EditTask,
|
||||
is UiEvent.CreateNewTask -> _stickyEvents.emit(event)
|
||||
else -> _events.emit(event)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun clearSticky() {
|
||||
_stickyEvents.resetReplayCache()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ class MainViewModel @Inject constructor(
|
||||
showTaskSheet = false
|
||||
viewModelScope.launch {
|
||||
uiEventBus.send(UiEvent.CloseTask)
|
||||
uiEventBus.clearSticky()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,11 +30,15 @@ class RecycleBinViewModel @Inject constructor(
|
||||
private val savedStateHandle: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private const val TASK_TO_DELETE = "taskToDeleteId"
|
||||
private const val EMPTY_RECYCLE_BIN = "emptyRecycleBin"
|
||||
}
|
||||
var deletedTasks by mutableStateOf<List<TaskWithListName>>(emptyList())
|
||||
private set
|
||||
|
||||
var taskToDelete by mutableStateOf(savedStateHandle.get<Long>("taskToDelete"))
|
||||
private set
|
||||
val taskToDeleteFlow = savedStateHandle.getStateFlow<Long?>(TASK_TO_DELETE, null)
|
||||
val emptyRecycleBinFlow = savedStateHandle.getStateFlow<Boolean>(EMPTY_RECYCLE_BIN, false)
|
||||
|
||||
init {
|
||||
loadDeletedTasks()
|
||||
@@ -70,29 +74,36 @@ class RecycleBinViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
fun onTaskDeleteRequest(taskId: Long) {
|
||||
taskToDelete = taskId
|
||||
savedStateHandle["taskToDelete"] = taskId
|
||||
|
||||
fun onEmptyRecycleBinRequest() {
|
||||
savedStateHandle[EMPTY_RECYCLE_BIN] = true
|
||||
}
|
||||
|
||||
fun onConfirmDelete() {
|
||||
taskToDelete?.let {
|
||||
viewModelScope.launch {
|
||||
permanentlyDeleteTaskUseCase(it)
|
||||
}
|
||||
}
|
||||
taskToDelete = null
|
||||
savedStateHandle["taskToDelete"] = null
|
||||
}
|
||||
|
||||
fun onCancelDelete() {
|
||||
taskToDelete = null
|
||||
savedStateHandle["taskToDelete"] = null
|
||||
fun onCancelEmptyRecycleBinRequest() {
|
||||
savedStateHandle[EMPTY_RECYCLE_BIN] = false
|
||||
}
|
||||
|
||||
fun emptyRecycleBin() {
|
||||
viewModelScope.launch {
|
||||
emptyRecycleBinUseCase()
|
||||
savedStateHandle[EMPTY_RECYCLE_BIN] = false
|
||||
}
|
||||
}
|
||||
|
||||
fun onTaskDeleteRequest(taskId: Long) {
|
||||
savedStateHandle[TASK_TO_DELETE] = taskId
|
||||
}
|
||||
|
||||
fun onConfirmDelete() {
|
||||
taskToDeleteFlow.value?.let {
|
||||
viewModelScope.launch {
|
||||
permanentlyDeleteTaskUseCase(it)
|
||||
}
|
||||
}
|
||||
savedStateHandle[TASK_TO_DELETE] = null
|
||||
}
|
||||
|
||||
fun onCancelDelete() {
|
||||
savedStateHandle[TASK_TO_DELETE] = null
|
||||
}
|
||||
}
|
||||
@@ -37,13 +37,14 @@ class TaskViewModel @Inject constructor(
|
||||
private set
|
||||
var isDeleted by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
private var editingTaskId: Long? = null
|
||||
private var taskListId: Long? = null
|
||||
var taskListId by mutableStateOf<Long?>(null)
|
||||
private set
|
||||
var editingTaskId by mutableStateOf<Long?>(null)
|
||||
private set
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
uiEventBus.events.collect { event ->
|
||||
uiEventBus.stickyEvents.collect { event ->
|
||||
when (event) {
|
||||
is UiEvent.CreateNewTask -> startNewTask(event.taskListId)
|
||||
is UiEvent.EditTask -> startEditTask(event.task)
|
||||
|
||||
Reference in New Issue
Block a user