feat: [#6] navigation principale (NavigationDrawer, NavHost, routes, DrawerViewModel)

This commit is contained in:
2026-06-06 06:03:53 +02:00
parent 4dfc224eb6
commit 38b96c0c72
7 changed files with 311 additions and 1 deletions
@@ -0,0 +1,61 @@
package com.planify.mobile.data.repository
import com.planify.mobile.data.local.dao.ProjectDao
import com.planify.mobile.data.local.entity.ProjectEntity
import com.planify.mobile.domain.model.BackendType
import com.planify.mobile.domain.model.Project
import com.planify.mobile.domain.model.SortBy
import com.planify.mobile.domain.model.ViewStyle
import com.planify.mobile.domain.repository.ProjectRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
class ProjectRepositoryImpl @Inject constructor(
private val dao: ProjectDao,
) : ProjectRepository {
override fun getAllProjects(): Flow<List<Project>> =
dao.getAllProjects().map { list -> list.map { it.toDomain() } }
override fun getFavoriteProjects(): Flow<List<Project>> =
dao.getFavoriteProjects().map { list -> list.map { it.toDomain() } }
override fun getSubProjects(parentId: String): Flow<List<Project>> =
dao.getSubProjects(parentId).map { list -> list.map { it.toDomain() } }
override suspend fun getProjectById(id: String): Project? =
dao.getById(id)?.toDomain()
override suspend fun getInboxProject(): Project? =
dao.getInboxProject()?.toDomain()
override suspend fun insertProject(project: Project) =
dao.insert(project.toEntity())
override suspend fun updateProject(project: Project) =
dao.update(project.toEntity())
override suspend fun deleteProject(id: String) =
dao.softDelete(id)
private fun ProjectEntity.toDomain() = Project(
id = id, name = name, color = color, emoji = emoji,
parentId = parentId, sourceId = sourceId,
backendType = runCatching { BackendType.valueOf(backendType) }.getOrDefault(BackendType.LOCAL),
isInbox = isInbox, isFavorite = isFavorite, isArchived = isArchived, isDeleted = isDeleted,
viewStyle = runCatching { ViewStyle.valueOf(viewStyle) }.getOrDefault(ViewStyle.LIST),
sortedBy = runCatching { SortBy.valueOf(sortedBy) }.getOrDefault(SortBy.MANUAL),
sortAscending = sortAscending, childOrder = childOrder,
calendarUrl = calendarUrl, syncId = syncId,
)
private fun Project.toEntity() = ProjectEntity(
id = id, name = name, color = color, emoji = emoji,
parentId = parentId, sourceId = sourceId, backendType = backendType.name,
isInbox = isInbox, isFavorite = isFavorite, isArchived = isArchived, isDeleted = isDeleted,
viewStyle = viewStyle.name, sortedBy = sortedBy.name,
sortAscending = sortAscending, childOrder = childOrder,
calendarUrl = calendarUrl, syncId = syncId,
)
}
@@ -0,0 +1,22 @@
package com.planify.mobile.di
import com.planify.mobile.data.repository.ProjectRepositoryImpl
import com.planify.mobile.data.repository.TaskRepositoryImpl
import com.planify.mobile.domain.repository.ProjectRepository
import com.planify.mobile.domain.repository.TaskRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds @Singleton
abstract fun bindProjectRepository(impl: ProjectRepositoryImpl): ProjectRepository
@Binds @Singleton
abstract fun bindTaskRepository(impl: TaskRepositoryImpl): TaskRepository
}
@@ -15,7 +15,7 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
PlanifyTheme { PlanifyTheme {
// TODO #6 : PlanifyNavHost() MainScreen()
} }
} }
} }
@@ -0,0 +1,141 @@
package com.planify.mobile.ui
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.outlined.Inbox
import androidx.compose.material.icons.outlined.Menu
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Today
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.planify.mobile.ui.navigation.DrawerViewModel
import com.planify.mobile.ui.navigation.PlanifyNavHost
import com.planify.mobile.ui.navigation.Route
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(viewModel: DrawerViewModel = hiltViewModel()) {
val navController = rememberNavController()
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
val projects by viewModel.projects.collectAsState()
val navBackStack by navController.currentBackStackEntryAsState()
val currentRoute = navBackStack?.destination?.route
val drawerTitles = mapOf(
Route.Inbox.path to "Inbox",
Route.Today.path to "Aujourd'hui",
Route.Scheduled.path to "Planifié",
)
val title = drawerTitles[currentRoute]
?: projects.find { "project/${it.id}" == currentRoute }?.name
?: "Planify"
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet {
Text(
text = "Planify",
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 20.dp),
)
NavigationDrawerItem(
icon = { Icon(Icons.Outlined.Inbox, null) },
label = { Text("Inbox") },
selected = currentRoute == Route.Inbox.path,
onClick = {
navController.navigate(Route.Inbox.path)
scope.launch { drawerState.close() }
},
)
NavigationDrawerItem(
icon = { Icon(Icons.Outlined.Today, null) },
label = { Text("Aujourd'hui") },
selected = currentRoute == Route.Today.path,
onClick = {
navController.navigate(Route.Today.path)
scope.launch { drawerState.close() }
},
)
HorizontalDivider(Modifier.padding(vertical = 8.dp))
Text(
text = "Projets",
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
)
LazyColumn {
items(projects) { project ->
NavigationDrawerItem(
icon = { Icon(Icons.Default.FolderOpen, null) },
label = { Text(project.name) },
selected = currentRoute == "project/${project.id}",
onClick = {
navController.navigate(Route.Project().buildRoute(project.id))
scope.launch { drawerState.close() }
},
)
}
}
HorizontalDivider(Modifier.padding(vertical = 8.dp))
NavigationDrawerItem(
icon = { Icon(Icons.Outlined.Settings, null) },
label = { Text("Paramètres") },
selected = false,
onClick = { scope.launch { drawerState.close() } },
)
Spacer(Modifier.height(8.dp))
}
}
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(title) },
navigationIcon = {
IconButton(onClick = { scope.launch { drawerState.open() } }) {
Icon(Icons.Outlined.Menu, contentDescription = "Menu")
}
},
)
},
) { padding ->
PlanifyNavHost(
navController = navController,
modifier = Modifier.padding(padding),
)
}
}
}
@@ -0,0 +1,22 @@
package com.planify.mobile.ui.navigation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.planify.mobile.domain.model.Project
import com.planify.mobile.domain.repository.ProjectRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel
class DrawerViewModel @Inject constructor(
projectRepository: ProjectRepository,
) : ViewModel() {
val projects = projectRepository.getAllProjects()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
val favorites = projectRepository.getFavoriteProjects()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList<Project>())
}
@@ -0,0 +1,48 @@
package com.planify.mobile.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.planify.mobile.ui.inbox.InboxScreen
import com.planify.mobile.ui.project.ProjectScreen
import com.planify.mobile.ui.today.TodayScreen
@Composable
fun PlanifyNavHost(
navController: NavHostController,
modifier: Modifier = Modifier,
) {
NavHost(
navController = navController,
startDestination = Route.Inbox.path,
modifier = modifier,
) {
composable(Route.Inbox.path) {
InboxScreen(
onTaskClick = { /* TODO #11 : ouvrir édition */ }
)
}
composable(Route.Today.path) {
TodayScreen(
onTaskClick = { /* TODO #11 : ouvrir édition */ }
)
}
composable(
route = Route.Project().path,
arguments = listOf(navArgument("projectId") { type = NavType.StringType })
) { backStack ->
val projectId = backStack.arguments?.getString("projectId") ?: return@composable
ProjectScreen(
projectId = projectId,
onTaskClick = { /* TODO #11 : ouvrir édition */ },
onBack = { navController.popBackStack() },
)
}
}
}
@@ -0,0 +1,16 @@
package com.planify.mobile.ui.navigation
sealed class Route(val path: String) {
data object Inbox : Route("inbox")
data object Today : Route("today")
data object Scheduled : Route("scheduled")
data class Project(val projectId: String = "{projectId}") :
Route("project/{projectId}") {
fun buildRoute(id: String) = "project/$id"
}
data class Label(val labelId: String = "{labelId}") :
Route("label/{labelId}") {
fun buildRoute(id: String) = "label/$id"
}
data object Settings : Route("settings")
}