mirror of
https://github.com/wismna/DoNext.git
synced 2025-10-03 07:30:13 -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.Query
|
||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
import com.wismna.geoffroy.donext.data.entities.TaskListEntity
|
import com.wismna.geoffroy.donext.data.entities.TaskListEntity
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@@ -13,6 +14,25 @@ interface TaskListDao {
|
|||||||
@Query("SELECT * FROM task_lists WHERE deleted = 0 ORDER BY display_order ASC")
|
@Query("SELECT * FROM task_lists WHERE deleted = 0 ORDER BY display_order ASC")
|
||||||
fun getTaskLists(): Flow<List<TaskListEntity>>
|
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)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertTaskList(taskList: TaskListEntity)
|
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.data.toEntity
|
||||||
import com.wismna.geoffroy.donext.domain.model.Task
|
import com.wismna.geoffroy.donext.domain.model.Task
|
||||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
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 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 java.time.LocalDate
|
||||||
|
import java.time.ZoneOffset
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TaskRepositoryImpl @Inject constructor(
|
class TaskRepositoryImpl @Inject constructor(
|
||||||
@@ -47,4 +50,14 @@ class TaskRepositoryImpl @Inject constructor(
|
|||||||
taskDao.deleteAllTasksFromList(taskListId, isDeleted)
|
taskDao.deleteAllTasksFromList(taskListId, isDeleted)
|
||||||
taskListDao.deleteTaskList(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.Task
|
||||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface TaskRepository {
|
interface TaskRepository {
|
||||||
@@ -14,4 +15,5 @@ interface TaskRepository {
|
|||||||
fun getTaskLists(): Flow<List<TaskList>>
|
fun getTaskLists(): Flow<List<TaskList>>
|
||||||
suspend fun insertTaskList(taskList: TaskList)
|
suspend fun insertTaskList(taskList: TaskList)
|
||||||
suspend fun deleteTaskList(taskListId: Long, isDeleted: Boolean)
|
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.foundation.layout.padding
|
||||||
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.material.icons.filled.Menu
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.material3.BadgedBox
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.DrawerValue
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
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.Scaffold
|
||||||
import androidx.compose.material3.SegmentedButton
|
import androidx.compose.material3.SegmentedButton
|
||||||
import androidx.compose.material3.SegmentedButtonDefaults
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||||
import androidx.compose.material3.Tab
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
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.text.style.TextOverflow
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.NavType
|
import androidx.navigation.NavType
|
||||||
import androidx.navigation.compose.NavHost
|
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.domain.model.Priority
|
||||||
import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel
|
import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel
|
||||||
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel
|
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
@@ -57,11 +66,42 @@ fun MainScreen(
|
|||||||
val startDestination = viewModel.taskLists[0]
|
val startDestination = viewModel.taskLists[0]
|
||||||
// TODO: get last opened tab from saved settings
|
// TODO: get last opened tab from saved settings
|
||||||
var selectedDestination by rememberSaveable { mutableIntStateOf(0) }
|
var selectedDestination by rememberSaveable { mutableIntStateOf(0) }
|
||||||
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
TaskBottomSheet(taskViewModel, { showBottomSheet = false })
|
TaskBottomSheet(taskViewModel, { showBottomSheet = false })
|
||||||
}
|
}
|
||||||
|
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 = {
|
||||||
|
selectedDestination = index
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
|
},
|
||||||
|
badge = {
|
||||||
|
if (list.overdueCount > 0) {
|
||||||
|
Badge {
|
||||||
|
Text(list.overdueCount.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drawerState = drawerState
|
||||||
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
@@ -71,37 +111,17 @@ fun MainScreen(
|
|||||||
showBottomSheet = true
|
showBottomSheet = true
|
||||||
}
|
}
|
||||||
}, topBar = {
|
}, topBar = {
|
||||||
PrimaryTabRow(selectedTabIndex = selectedDestination) {
|
// TODO: add list title
|
||||||
viewModel.taskLists.forEachIndexed { index, destination ->
|
// TODO: add button such as edit and delete
|
||||||
Tab(
|
TopAppBar(
|
||||||
selected = selectedDestination == index,
|
title = { Text(viewModel.taskLists.getOrNull(selectedDestination)?.name ?: "Tasks") },
|
||||||
onClick = {
|
navigationIcon = {
|
||||||
navController.navigate(route = "taskList/${destination.id}")
|
IconButton(onClick = { scope.launch { drawerState.open() } }) {
|
||||||
selectedDestination = index
|
Icon(Icons.Default.Menu, contentDescription = "Open navigation drawer")
|
||||||
},
|
|
||||||
text = {
|
|
||||||
/*BadgedBox(
|
|
||||||
badge = {
|
|
||||||
if (overdueCount > 0) {
|
|
||||||
Badge { Text(overdueCount.toString()) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {*/
|
|
||||||
Text(
|
|
||||||
text = destination.name,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}) { contentPadding ->
|
}) { contentPadding ->
|
||||||
Box(modifier = Modifier
|
|
||||||
.padding(contentPadding)
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController,
|
navController,
|
||||||
startDestination = "taskList/${startDestination.id}"
|
startDestination = "taskList/${startDestination.id}"
|
||||||
@@ -114,6 +134,7 @@ fun MainScreen(
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
TaskListScreen(
|
TaskListScreen(
|
||||||
|
modifier = Modifier.padding(contentPadding),
|
||||||
onTaskClick = { task ->
|
onTaskClick = { task ->
|
||||||
taskViewModel.startEditTask(task)
|
taskViewModel.startEditTask(task)
|
||||||
showBottomSheet = true
|
showBottomSheet = true
|
||||||
|
@@ -4,7 +4,6 @@ import androidx.compose.foundation.clickable
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -34,11 +33,11 @@ import java.time.LocalDate
|
|||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TaskListScreen(viewModel: TaskListViewModel = hiltViewModel(), onTaskClick: (Task) -> Unit) {
|
fun TaskListScreen(modifier: Modifier = Modifier, viewModel: TaskListViewModel = hiltViewModel(), onTaskClick: (Task) -> Unit) {
|
||||||
val tasks = viewModel.tasks
|
val tasks = viewModel.tasks
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize()
|
//modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
itemsIndexed(tasks, key = { _, task -> task.id!! }) { index, task ->
|
itemsIndexed(tasks, key = { _, task -> task.id!! }) { index, task ->
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
|
@@ -5,8 +5,8 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||||
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
|
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsWithOverdueUseCase
|
||||||
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
|
||||||
@@ -14,16 +14,16 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MainViewModel @Inject constructor(
|
class MainViewModel @Inject constructor(
|
||||||
getTaskLists: GetTaskListsUseCase
|
getTaskListsWithOverdue: GetTaskListsWithOverdueUseCase
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
var taskLists by mutableStateOf<List<TaskList>>(emptyList())
|
var taskLists by mutableStateOf<List<TaskListWithOverdue>>(emptyList())
|
||||||
private set
|
private set
|
||||||
var isLoading by mutableStateOf(true)
|
var isLoading by mutableStateOf(true)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
init {
|
init {
|
||||||
getTaskLists()
|
getTaskListsWithOverdue()
|
||||||
.onEach { lists ->
|
.onEach { lists ->
|
||||||
taskLists = lists
|
taskLists = lists
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
Reference in New Issue
Block a user