feat: [#15] fiche projet (ProjectEditSheet + ProjectEditViewModel, couleurs, vue liste/tableau)
This commit is contained in:
@@ -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)
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user