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
|
package com.wismna.geoffroy.donext.presentation.screen
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
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.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.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -26,7 +32,14 @@ fun DueTodayTasksScreen(
|
|||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
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 {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|||||||
@@ -206,7 +206,6 @@ fun AppContent(
|
|||||||
navController.navigate(event.route)
|
navController.navigate(event.route)
|
||||||
}
|
}
|
||||||
is UiEvent.NavigateBack -> navController.popBackStack()
|
is UiEvent.NavigateBack -> navController.popBackStack()
|
||||||
is UiEvent.EditTask -> { viewModel.showTaskSheet = true }
|
|
||||||
is UiEvent.ShowUndoSnackbar -> {
|
is UiEvent.ShowUndoSnackbar -> {
|
||||||
val result = snackbarHostState.showSnackbar(
|
val result = snackbarHostState.showSnackbar(
|
||||||
message = event.message,
|
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(
|
Scaffold(
|
||||||
modifier = modifier.background(MaterialTheme.colorScheme.primaryContainer),
|
modifier = modifier.background(MaterialTheme.colorScheme.primaryContainer),
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ package com.wismna.geoffroy.donext.presentation.screen
|
|||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
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.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.DeleteOutline
|
||||||
import androidx.compose.material.icons.filled.DeleteSweep
|
import androidx.compose.material.icons.filled.DeleteSweep
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
@@ -19,9 +22,6 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
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.text.font.FontStyle
|
||||||
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 androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.wismna.geoffroy.donext.presentation.viewmodel.RecycleBinViewModel
|
import com.wismna.geoffroy.donext.presentation.viewmodel.RecycleBinViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -37,6 +38,7 @@ fun RecycleBinScreen(
|
|||||||
viewModel: RecycleBinViewModel = hiltViewModel(),
|
viewModel: RecycleBinViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val tasks = viewModel.deletedTasks
|
val tasks = viewModel.deletedTasks
|
||||||
|
val taskToDelete by viewModel.taskToDeleteFlow.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
if (tasks.isEmpty()) {
|
if (tasks.isEmpty()) {
|
||||||
// Placeholder when recycle bin is empty
|
// Placeholder when recycle bin is empty
|
||||||
@@ -44,7 +46,15 @@ fun RecycleBinScreen(
|
|||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
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
|
return
|
||||||
}
|
}
|
||||||
@@ -52,7 +62,7 @@ fun RecycleBinScreen(
|
|||||||
val grouped = tasks.groupBy { it.listName }
|
val grouped = tasks.groupBy { it.listName }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
if (viewModel.taskToDelete != null) {
|
if (taskToDelete != null) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { viewModel.onCancelDelete() },
|
onDismissRequest = { viewModel.onCancelDelete() },
|
||||||
title = { Text("Delete task") },
|
title = { Text("Delete task") },
|
||||||
@@ -114,10 +124,10 @@ fun RecycleBinScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
fun EmptyRecycleBinAction(viewModel: RecycleBinViewModel = hiltViewModel()) {
|
fun EmptyRecycleBinAction(viewModel: RecycleBinViewModel = hiltViewModel()) {
|
||||||
val isEmpty = viewModel.deletedTasks.isEmpty()
|
val isEmpty = viewModel.deletedTasks.isEmpty()
|
||||||
var showConfirmDialog by remember { mutableStateOf(false) }
|
val emptyRecycleBin by viewModel.emptyRecycleBinFlow.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { showConfirmDialog = true },
|
onClick = { viewModel.onEmptyRecycleBinRequest() },
|
||||||
enabled = !isEmpty) {
|
enabled = !isEmpty) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.DeleteSweep,
|
Icons.Default.DeleteSweep,
|
||||||
@@ -127,9 +137,9 @@ fun EmptyRecycleBinAction(viewModel: RecycleBinViewModel = hiltViewModel()) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showConfirmDialog) {
|
if (emptyRecycleBin) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showConfirmDialog = false },
|
onDismissRequest = { viewModel.onCancelEmptyRecycleBinRequest() },
|
||||||
title = { Text("Empty Recycle Bin") },
|
title = { Text("Empty Recycle Bin") },
|
||||||
text = {
|
text = {
|
||||||
Text("Are you sure you want to permanently delete all tasks in the recycle bin? This cannot be undone.")
|
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(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.emptyRecycleBin()
|
viewModel.emptyRecycleBin()
|
||||||
showConfirmDialog = false
|
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.textButtonColors(
|
colors = ButtonDefaults.textButtonColors(
|
||||||
contentColor = MaterialTheme.colorScheme.error
|
contentColor = MaterialTheme.colorScheme.error
|
||||||
@@ -148,7 +157,7 @@ fun EmptyRecycleBinAction(viewModel: RecycleBinViewModel = hiltViewModel()) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showConfirmDialog = false }) {
|
TextButton(onClick = { viewModel.onCancelEmptyRecycleBinRequest() }) {
|
||||||
Text("Cancel")
|
Text("Cancel")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.wismna.geoffroy.donext.presentation.ui.events
|
package com.wismna.geoffroy.donext.presentation.ui.events
|
||||||
|
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -7,10 +8,24 @@ import javax.inject.Singleton
|
|||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class UiEventBus @Inject constructor() {
|
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()
|
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) {
|
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
|
showTaskSheet = false
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
uiEventBus.send(UiEvent.CloseTask)
|
uiEventBus.send(UiEvent.CloseTask)
|
||||||
|
uiEventBus.clearSticky()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,11 +30,15 @@ class RecycleBinViewModel @Inject constructor(
|
|||||||
private val savedStateHandle: SavedStateHandle
|
private val savedStateHandle: SavedStateHandle
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TASK_TO_DELETE = "taskToDeleteId"
|
||||||
|
private const val EMPTY_RECYCLE_BIN = "emptyRecycleBin"
|
||||||
|
}
|
||||||
var deletedTasks by mutableStateOf<List<TaskWithListName>>(emptyList())
|
var deletedTasks by mutableStateOf<List<TaskWithListName>>(emptyList())
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var taskToDelete by mutableStateOf(savedStateHandle.get<Long>("taskToDelete"))
|
val taskToDeleteFlow = savedStateHandle.getStateFlow<Long?>(TASK_TO_DELETE, null)
|
||||||
private set
|
val emptyRecycleBinFlow = savedStateHandle.getStateFlow<Boolean>(EMPTY_RECYCLE_BIN, false)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadDeletedTasks()
|
loadDeletedTasks()
|
||||||
@@ -70,29 +74,36 @@ class RecycleBinViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun onTaskDeleteRequest(taskId: Long) {
|
|
||||||
taskToDelete = taskId
|
fun onEmptyRecycleBinRequest() {
|
||||||
savedStateHandle["taskToDelete"] = taskId
|
savedStateHandle[EMPTY_RECYCLE_BIN] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onConfirmDelete() {
|
fun onCancelEmptyRecycleBinRequest() {
|
||||||
taskToDelete?.let {
|
savedStateHandle[EMPTY_RECYCLE_BIN] = false
|
||||||
viewModelScope.launch {
|
|
||||||
permanentlyDeleteTaskUseCase(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
taskToDelete = null
|
|
||||||
savedStateHandle["taskToDelete"] = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onCancelDelete() {
|
|
||||||
taskToDelete = null
|
|
||||||
savedStateHandle["taskToDelete"] = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun emptyRecycleBin() {
|
fun emptyRecycleBin() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
emptyRecycleBinUseCase()
|
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
|
private set
|
||||||
var isDeleted by mutableStateOf(false)
|
var isDeleted by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
var taskListId by mutableStateOf<Long?>(null)
|
||||||
private var editingTaskId: Long? = null
|
private set
|
||||||
private var taskListId: Long? = null
|
var editingTaskId by mutableStateOf<Long?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
uiEventBus.events.collect { event ->
|
uiEventBus.stickyEvents.collect { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
is UiEvent.CreateNewTask -> startNewTask(event.taskListId)
|
is UiEvent.CreateNewTask -> startNewTask(event.taskListId)
|
||||||
is UiEvent.EditTask -> startEditTask(event.task)
|
is UiEvent.EditTask -> startEditTask(event.task)
|
||||||
|
|||||||
Reference in New Issue
Block a user