diff --git a/app/src/main/java/com/planify/mobile/data/repository/ProjectRepositoryImpl.kt b/app/src/main/java/com/planify/mobile/data/repository/ProjectRepositoryImpl.kt new file mode 100644 index 0000000..19a81a0 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/repository/ProjectRepositoryImpl.kt @@ -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> = + dao.getAllProjects().map { list -> list.map { it.toDomain() } } + + override fun getFavoriteProjects(): Flow> = + dao.getFavoriteProjects().map { list -> list.map { it.toDomain() } } + + override fun getSubProjects(parentId: String): Flow> = + 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, + ) +} diff --git a/app/src/main/java/com/planify/mobile/di/RepositoryModule.kt b/app/src/main/java/com/planify/mobile/di/RepositoryModule.kt new file mode 100644 index 0000000..56d0f50 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/di/RepositoryModule.kt @@ -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 +} diff --git a/app/src/main/java/com/planify/mobile/ui/MainActivity.kt b/app/src/main/java/com/planify/mobile/ui/MainActivity.kt index f866341..b2fe2d3 100644 --- a/app/src/main/java/com/planify/mobile/ui/MainActivity.kt +++ b/app/src/main/java/com/planify/mobile/ui/MainActivity.kt @@ -15,7 +15,7 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { PlanifyTheme { - // TODO #6 : PlanifyNavHost() + MainScreen() } } } diff --git a/app/src/main/java/com/planify/mobile/ui/MainScreen.kt b/app/src/main/java/com/planify/mobile/ui/MainScreen.kt new file mode 100644 index 0000000..48ded9f --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/MainScreen.kt @@ -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), + ) + } + } +} diff --git a/app/src/main/java/com/planify/mobile/ui/navigation/DrawerViewModel.kt b/app/src/main/java/com/planify/mobile/ui/navigation/DrawerViewModel.kt new file mode 100644 index 0000000..f3d870f --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/navigation/DrawerViewModel.kt @@ -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()) +} diff --git a/app/src/main/java/com/planify/mobile/ui/navigation/PlanifyNavHost.kt b/app/src/main/java/com/planify/mobile/ui/navigation/PlanifyNavHost.kt new file mode 100644 index 0000000..bbd9e2e --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/navigation/PlanifyNavHost.kt @@ -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() }, + ) + } + } +} diff --git a/app/src/main/java/com/planify/mobile/ui/navigation/Route.kt b/app/src/main/java/com/planify/mobile/ui/navigation/Route.kt new file mode 100644 index 0000000..aae7359 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/navigation/Route.kt @@ -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") +}