mirror of
https://github.com/wismna/DoNext.git
synced 2025-10-03 15:40:14 -04:00
Replace tabs with navigation drawer
WIP on overdue task count on list names
This commit is contained in:
@@ -6,6 +6,7 @@ import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import com.wismna.geoffroy.donext.data.entities.TaskListEntity
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
@@ -13,6 +14,25 @@ interface TaskListDao {
|
||||
@Query("SELECT * FROM task_lists WHERE deleted = 0 ORDER BY display_order ASC")
|
||||
fun getTaskLists(): Flow<List<TaskListEntity>>
|
||||
|
||||
@Query("""
|
||||
SELECT
|
||||
tl.id AS id,
|
||||
tl.name AS name,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN t.done = 0
|
||||
AND t.due_date IS NOT NULL
|
||||
AND t.due_date < :today
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
), 0) AS overdueCount
|
||||
FROM task_lists tl
|
||||
LEFT JOIN tasks t ON t.task_list_id = tl.id
|
||||
GROUP BY tl.id
|
||||
""")
|
||||
fun getTaskListsWithOverdue(today: Long): Flow<List<TaskListWithOverdue>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertTaskList(taskList: TaskListEntity)
|
||||
|
||||
|
@@ -6,9 +6,12 @@ import com.wismna.geoffroy.donext.data.toDomain
|
||||
import com.wismna.geoffroy.donext.data.toEntity
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import javax.inject.Inject
|
||||
|
||||
class TaskRepositoryImpl @Inject constructor(
|
||||
@@ -47,4 +50,14 @@ class TaskRepositoryImpl @Inject constructor(
|
||||
taskDao.deleteAllTasksFromList(taskListId, isDeleted)
|
||||
taskListDao.deleteTaskList(taskListId, isDeleted)
|
||||
}
|
||||
|
||||
override fun getTaskListsWithOverdue(): Flow<List<TaskListWithOverdue>> {
|
||||
val todayMillis = LocalDate.now()
|
||||
.atStartOfDay(ZoneOffset.UTC)
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
|
||||
return taskListDao.getTaskListsWithOverdue(todayMillis)
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.wismna.geoffroy.donext.domain.model
|
||||
|
||||
data class TaskListWithOverdue(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val overdueCount: Int
|
||||
)
|
@@ -2,6 +2,7 @@ package com.wismna.geoffroy.donext.domain.repository
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TaskRepository {
|
||||
@@ -14,4 +15,5 @@ interface TaskRepository {
|
||||
fun getTaskLists(): Flow<List<TaskList>>
|
||||
suspend fun insertTaskList(taskList: TaskList)
|
||||
suspend fun deleteTaskList(taskListId: Long, isDeleted: Boolean)
|
||||
fun getTaskListsWithOverdue(): Flow<List<TaskListWithOverdue>>
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetTaskListsWithOverdueUseCase @Inject constructor(
|
||||
private val taskRepository: TaskRepository
|
||||
) {
|
||||
operator fun invoke(): Flow<List<TaskListWithOverdue>> {
|
||||
return taskRepository.getTaskListsWithOverdue()
|
||||
}
|
||||
}
|
@@ -7,29 +7,37 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.NavigationDrawerItem
|
||||
import androidx.compose.material3.NavigationDrawerItemDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
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 androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
@@ -39,6 +47,7 @@ import androidx.navigation.navArgument
|
||||
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||
import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel
|
||||
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
@@ -57,70 +66,82 @@ fun MainScreen(
|
||||
val startDestination = viewModel.taskLists[0]
|
||||
// TODO: get last opened tab from saved settings
|
||||
var selectedDestination by rememberSaveable { mutableIntStateOf(0) }
|
||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
if (showBottomSheet) {
|
||||
TaskBottomSheet(taskViewModel, { showBottomSheet = false })
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
floatingActionButton = {
|
||||
AddNewTaskButton {
|
||||
val currentListId = viewModel.taskLists[selectedDestination].id
|
||||
taskViewModel.startNewTask(currentListId)
|
||||
showBottomSheet = true
|
||||
}
|
||||
}, topBar = {
|
||||
PrimaryTabRow(selectedTabIndex = selectedDestination) {
|
||||
viewModel.taskLists.forEachIndexed { index, destination ->
|
||||
Tab(
|
||||
ModalNavigationDrawer(
|
||||
drawerContent = {
|
||||
ModalDrawerSheet {
|
||||
Text(
|
||||
text = "Task Lists",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
viewModel.taskLists.forEachIndexed { index, list ->
|
||||
NavigationDrawerItem(
|
||||
label = { Text(list.name) },
|
||||
selected = selectedDestination == index,
|
||||
onClick = {
|
||||
navController.navigate(route = "taskList/${destination.id}")
|
||||
selectedDestination = index
|
||||
scope.launch { drawerState.close() }
|
||||
},
|
||||
text = {
|
||||
/*BadgedBox(
|
||||
badge = {
|
||||
if (overdueCount > 0) {
|
||||
Badge { Text(overdueCount.toString()) }
|
||||
}
|
||||
badge = {
|
||||
if (list.overdueCount > 0) {
|
||||
Badge {
|
||||
Text(list.overdueCount.toString())
|
||||
}
|
||||
) {*/
|
||||
Text(
|
||||
text = destination.name,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
//}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
}) { contentPadding ->
|
||||
Box(modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination = "taskList/${startDestination.id}"
|
||||
) {
|
||||
viewModel.taskLists.forEach { destination ->
|
||||
composable(
|
||||
route = "taskList/{taskListId}",
|
||||
arguments = listOf(navArgument("taskListId") {
|
||||
type = NavType.LongType
|
||||
})
|
||||
) {
|
||||
TaskListScreen(
|
||||
onTaskClick = { task ->
|
||||
taskViewModel.startEditTask(task)
|
||||
showBottomSheet = true
|
||||
},
|
||||
drawerState = drawerState
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
floatingActionButton = {
|
||||
AddNewTaskButton {
|
||||
val currentListId = viewModel.taskLists[selectedDestination].id
|
||||
taskViewModel.startNewTask(currentListId)
|
||||
showBottomSheet = true
|
||||
}
|
||||
}, topBar = {
|
||||
// TODO: add list title
|
||||
// TODO: add button such as edit and delete
|
||||
TopAppBar(
|
||||
title = { Text(viewModel.taskLists.getOrNull(selectedDestination)?.name ?: "Tasks") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { scope.launch { drawerState.open() } }) {
|
||||
Icon(Icons.Default.Menu, contentDescription = "Open navigation drawer")
|
||||
}
|
||||
}
|
||||
)
|
||||
}) { contentPadding ->
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination = "taskList/${startDestination.id}"
|
||||
) {
|
||||
viewModel.taskLists.forEach { destination ->
|
||||
composable(
|
||||
route = "taskList/{taskListId}",
|
||||
arguments = listOf(navArgument("taskListId") {
|
||||
type = NavType.LongType
|
||||
})
|
||||
) {
|
||||
TaskListScreen(
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
onTaskClick = { task ->
|
||||
taskViewModel.startEditTask(task)
|
||||
showBottomSheet = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,6 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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
|
||||
@@ -34,11 +33,11 @@ import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
|
||||
@Composable
|
||||
fun TaskListScreen(viewModel: TaskListViewModel = hiltViewModel(), onTaskClick: (Task) -> Unit) {
|
||||
fun TaskListScreen(modifier: Modifier = Modifier, viewModel: TaskListViewModel = hiltViewModel(), onTaskClick: (Task) -> Unit) {
|
||||
val tasks = viewModel.tasks
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
//modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
itemsIndexed(tasks, key = { _, task -> task.id!! }) { index, task ->
|
||||
if (index > 0) {
|
||||
|
@@ -5,8 +5,8 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsWithOverdueUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -14,16 +14,16 @@ import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MainViewModel @Inject constructor(
|
||||
getTaskLists: GetTaskListsUseCase
|
||||
getTaskListsWithOverdue: GetTaskListsWithOverdueUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
var taskLists by mutableStateOf<List<TaskList>>(emptyList())
|
||||
var taskLists by mutableStateOf<List<TaskListWithOverdue>>(emptyList())
|
||||
private set
|
||||
var isLoading by mutableStateOf(true)
|
||||
private set
|
||||
|
||||
init {
|
||||
getTaskLists()
|
||||
getTaskListsWithOverdue()
|
||||
.onEach { lists ->
|
||||
taskLists = lists
|
||||
isLoading = false
|
||||
|
Reference in New Issue
Block a user