diff --git a/app/src/main/java/com/planify/mobile/ui/project/ProjectEditSheet.kt b/app/src/main/java/com/planify/mobile/ui/project/ProjectEditSheet.kt new file mode 100644 index 0000000..86f8f57 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/project/ProjectEditSheet.kt @@ -0,0 +1,167 @@ +package com.planify.mobile.ui.project + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Star +import androidx.compose.material.icons.outlined.StarBorder +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.planify.mobile.domain.model.ViewStyle + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProjectEditSheet( + projectId: String? = null, + onDismiss: () -> Unit, + onSaved: (projectId: String) -> Unit = {}, + onDeleted: () -> Unit = {}, + viewModel: ProjectEditViewModel = hiltViewModel(), +) { + LaunchedEffect(projectId) { viewModel.init(projectId) } + + val state by viewModel.state.collectAsState() + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) { + Column(Modifier.padding(horizontal = 16.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = if (projectId == null) "Nouveau projet" else "Modifier le projet", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.weight(1f), + ) + IconButton(onClick = { viewModel.toggleFavorite() }) { + Icon( + imageVector = if (state.isFavorite) Icons.Outlined.Star else Icons.Outlined.StarBorder, + contentDescription = "Favori", + tint = if (state.isFavorite) Color(0xFFFFC107) else MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + if (projectId != null) { + IconButton(onClick = { viewModel.delete { onDeleted(); onDismiss() } }) { + Icon(Icons.Outlined.Delete, contentDescription = "Supprimer", tint = MaterialTheme.colorScheme.error) + } + } + } + + Spacer(Modifier.height(12.dp)) + + OutlinedTextField( + value = state.name, + onValueChange = viewModel::setName, + placeholder = { Text("Nom du projet") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + leadingIcon = { + Box( + modifier = Modifier + .size(20.dp) + .clip(CircleShape) + .background(parseColor(state.color)), + ) + }, + ) + + Spacer(Modifier.height(16.dp)) + Text("Couleur", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + Spacer(Modifier.height(8.dp)) + + LazyVerticalGrid( + columns = GridCells.Fixed(6), + modifier = Modifier.height(80.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(viewModel.colors) { color -> + Box( + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .background(parseColor(color)) + .then( + if (color == state.color) Modifier.border(2.dp, MaterialTheme.colorScheme.onSurface, CircleShape) + else Modifier + ) + .clickable { viewModel.setColor(color) }, + ) + } + } + + Spacer(Modifier.height(16.dp)) + HorizontalDivider() + Spacer(Modifier.height(12.dp)) + + Text("Vue par défaut", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + Spacer(Modifier.height(8.dp)) + + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + ViewStyle.entries.forEachIndexed { index, style -> + SegmentedButton( + selected = state.viewStyle == style, + onClick = { viewModel.setViewStyle(style) }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = ViewStyle.entries.size), + label = { Text(if (style == ViewStyle.LIST) "Liste" else "Tableau") }, + ) + } + } + + Spacer(Modifier.height(16.dp)) + + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + Button( + onClick = { viewModel.save(onDone = { id -> onSaved(id); onDismiss() }) }, + enabled = state.name.isNotBlank() && !state.isSaving, + ) { + Text(if (projectId == null) "Créer" else "Enregistrer") + } + } + + Spacer(Modifier.height(16.dp)) + } + } +} + +private fun parseColor(hex: String): Color = runCatching { + Color(android.graphics.Color.parseColor(hex)) +}.getOrDefault(Color.Gray) diff --git a/app/src/main/java/com/planify/mobile/ui/project/ProjectEditViewModel.kt b/app/src/main/java/com/planify/mobile/ui/project/ProjectEditViewModel.kt new file mode 100644 index 0000000..593648c --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/project/ProjectEditViewModel.kt @@ -0,0 +1,113 @@ +package com.planify.mobile.ui.project + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +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 dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import java.util.UUID +import javax.inject.Inject + +data class ProjectEditState( + val projectId: String? = null, + val name: String = "", + val color: String = "#4CAF50", + val emoji: String? = null, + val isFavorite: Boolean = false, + val viewStyle: ViewStyle = ViewStyle.LIST, + val isSaving: Boolean = false, +) + +private val defaultColors = listOf( + "#F44336", "#E91E63", "#9C27B0", "#3F51B5", + "#2196F3", "#00BCD4", "#4CAF50", "#8BC34A", + "#FFEB3B", "#FF9800", "#795548", "#607D8B", +) + +@HiltViewModel +class ProjectEditViewModel @Inject constructor( + private val projectRepository: ProjectRepository, +) : ViewModel() { + + val colors = defaultColors + + private val _state = MutableStateFlow(ProjectEditState()) + val state = _state.asStateFlow() + + fun init(projectId: String?) { + if (projectId != null) { + viewModelScope.launch { + val project = projectRepository.getProjectById(projectId) ?: return@launch + _state.update { + it.copy( + projectId = projectId, + name = project.name, + color = project.color, + emoji = project.emoji, + isFavorite = project.isFavorite, + viewStyle = project.viewStyle, + ) + } + } + } + } + + fun setName(name: String) = _state.update { it.copy(name = name) } + fun setColor(color: String) = _state.update { it.copy(color = color) } + fun setEmoji(emoji: String?) = _state.update { it.copy(emoji = emoji) } + fun toggleFavorite() = _state.update { it.copy(isFavorite = !it.isFavorite) } + fun setViewStyle(style: ViewStyle) = _state.update { it.copy(viewStyle = style) } + + fun save(onDone: (projectId: String) -> Unit) { + val st = _state.value + if (st.name.isBlank()) return + _state.update { it.copy(isSaving = true) } + viewModelScope.launch { + val id = st.projectId ?: UUID.randomUUID().toString() + if (st.projectId != null) { + val existing = projectRepository.getProjectById(id) + if (existing != null) { + projectRepository.updateProject( + existing.copy( + name = st.name, + color = st.color, + emoji = st.emoji, + isFavorite = st.isFavorite, + viewStyle = st.viewStyle, + ) + ) + } + } else { + projectRepository.insertProject( + Project( + id = id, + name = st.name, + color = st.color, + emoji = st.emoji, + isFavorite = st.isFavorite, + viewStyle = st.viewStyle, + backendType = BackendType.LOCAL, + sortedBy = SortBy.MANUAL, + ) + ) + } + _state.update { it.copy(isSaving = false) } + onDone(id) + } + } + + fun delete(onDone: () -> Unit) { + val id = _state.value.projectId ?: return + viewModelScope.launch { + projectRepository.deleteProject(id) + onDone() + } + } +}