feat: implement TodoScreen UI with AI-powered brain dump functionality and notification permissions
This commit is contained in:
parent
3b0cb915f0
commit
5652e14352
@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
@ -47,8 +48,17 @@ import androidx.compose.material.icons.filled.Check
|
|||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.Event
|
||||||
import androidx.compose.material.icons.filled.Folder
|
import androidx.compose.material.icons.filled.Folder
|
||||||
|
import androidx.compose.material.icons.filled.List
|
||||||
|
import androidx.compose.material.icons.filled.Schedule
|
||||||
import androidx.compose.material.icons.filled.Snooze
|
import androidx.compose.material.icons.filled.Snooze
|
||||||
|
import androidx.compose.material.icons.outlined.CheckBoxOutlineBlank
|
||||||
|
import androidx.compose.material.icons.outlined.Keyboard
|
||||||
|
import androidx.compose.material.icons.outlined.LocalCafe
|
||||||
|
import androidx.compose.material.icons.outlined.NightsStay
|
||||||
|
import androidx.compose.material.icons.outlined.WbSunny
|
||||||
|
import androidx.compose.material.icons.outlined.WbTwilight
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
@ -638,12 +648,12 @@ private fun DueDateChip(
|
|||||||
|
|
||||||
// ====== Edit / Create Todo Dialog ======
|
// ====== Edit / Create Todo Dialog ======
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun EditTodoDialog(
|
private fun EditTodoDialog(
|
||||||
state: EditTodoDialogUiState,
|
state: EditTodoDialogUiState,
|
||||||
groupNames: List<String>,
|
groupNames: List<String>,
|
||||||
timezoneId: String = TimeZone.getDefault().id,
|
timezoneId: String = java.util.TimeZone.getDefault().id,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onContentChanged: (String) -> Unit,
|
onContentChanged: (String) -> Unit,
|
||||||
onDueDateChanged: (Long?) -> Unit,
|
onDueDateChanged: (Long?) -> Unit,
|
||||||
@ -658,185 +668,274 @@ private fun EditTodoDialog(
|
|||||||
onRemoveTag: (String) -> Unit,
|
onRemoveTag: (String) -> Unit,
|
||||||
onSave: () -> Unit
|
onSave: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = androidx.compose.ui.platform.LocalContext.current
|
var showDatePicker by remember { mutableStateOf(false) }
|
||||||
val dialogTitle = if (state.isCreateMode) "➕ Nouvelle tâche" else "✏️ Modifier la tâche"
|
|
||||||
|
|
||||||
AlertDialog(
|
androidx.compose.ui.window.Dialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = {
|
properties = androidx.compose.ui.window.DialogProperties(
|
||||||
Row(
|
usePlatformDefaultWidth = false,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
decorFitsSystemWindows = true
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
)
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
) {
|
||||||
Text(dialogTitle)
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onSave, enabled = !state.isSaving && state.content.isNotBlank()) {
|
||||||
|
if (state.isSaving) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Default.Check, contentDescription = "Enregistrer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
IconButton(onClick = onDismiss) {
|
IconButton(onClick = onDismiss) {
|
||||||
Icon(Icons.Default.Close, contentDescription = "Fermer")
|
Icon(Icons.Default.Close, contentDescription = "Fermer")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
// Task Title
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
modifier = Modifier.padding(16.dp).fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.CheckBoxOutlineBlank,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(16.dp))
|
||||||
|
androidx.compose.foundation.text.BasicTextField(
|
||||||
|
value = state.content,
|
||||||
|
onValueChange = onContentChanged,
|
||||||
|
textStyle = MaterialTheme.typography.titleLarge.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
if (state.content.isEmpty()) {
|
||||||
|
Text("Nom de la tâche", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.titleLarge)
|
||||||
|
}
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||||
|
|
||||||
|
// Due Date
|
||||||
|
val dateText = state.dueDate?.let { formatDateTime(it, timezoneId) } ?: "Aucune date d'échéance"
|
||||||
|
TaskActionRow(
|
||||||
|
icon = Icons.Default.Schedule,
|
||||||
|
text = dateText,
|
||||||
|
onClick = { showDatePicker = true },
|
||||||
|
isSet = state.dueDate != null
|
||||||
|
)
|
||||||
|
|
||||||
|
androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||||
|
|
||||||
|
// Group (Famille)
|
||||||
|
TaskGroupRow(
|
||||||
|
groupName = state.groupName,
|
||||||
|
groupNames = groupNames,
|
||||||
|
onGroupChanged = onGroupChanged
|
||||||
|
)
|
||||||
|
|
||||||
|
androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
TaskTagsRow(
|
||||||
|
tags = state.tags,
|
||||||
|
newTagText = state.newTagText,
|
||||||
|
onNewTagTextChanged = onNewTagTextChanged,
|
||||||
|
onAddTag = onAddTag,
|
||||||
|
onRemoveTag = onRemoveTag
|
||||||
|
)
|
||||||
|
|
||||||
|
androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||||
|
|
||||||
|
// Subtasks
|
||||||
|
TaskSubtasksSection(
|
||||||
|
subtasks = state.subtasks,
|
||||||
|
newSubtaskText = state.newSubtaskText,
|
||||||
|
onNewSubtaskTextChanged = onNewSubtaskTextChanged,
|
||||||
|
onAddSubtask = onAddSubtask,
|
||||||
|
onRemoveSubtask = onRemoveSubtask,
|
||||||
|
onToggleSubtask = onToggleSubtask,
|
||||||
|
onUpdateSubtaskContent = onUpdateSubtaskContent
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDatePicker) {
|
||||||
|
DateTimePickerBottomSheet(
|
||||||
|
initialDate = state.dueDate,
|
||||||
|
timezoneId = timezoneId,
|
||||||
|
onDismiss = { showDatePicker = false },
|
||||||
|
onDateSelected = onDueDateChanged
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TaskActionRow(
|
||||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
isSet: Boolean = false
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (isSet) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
color = if (isSet) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TaskGroupRow(
|
||||||
|
groupName: String,
|
||||||
|
groupNames: List<String>,
|
||||||
|
onGroupChanged: (String) -> Unit
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
var isEditing by remember { mutableStateOf(false) }
|
||||||
|
var customGroup by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { expanded = true }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.List, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
if (groupName.isNotBlank()) {
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
shape = MaterialTheme.shapes.small,
|
||||||
|
onClick = { expanded = true }
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)) {
|
||||||
|
Icon(Icons.Default.List, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||||
|
Spacer(Modifier.width(6.dp))
|
||||||
|
Text(groupName.uppercase(), style = MaterialTheme.typography.labelMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Aucune liste", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
androidx.compose.material3.DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false }
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.DropdownMenuItem(
|
||||||
|
text = { Text("Aucune liste") },
|
||||||
|
onClick = { onGroupChanged(""); expanded = false }
|
||||||
|
)
|
||||||
|
groupNames.forEach { group ->
|
||||||
|
androidx.compose.material3.DropdownMenuItem(
|
||||||
|
text = { Text(group) },
|
||||||
|
onClick = { onGroupChanged(group); expanded = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
androidx.compose.material3.Divider()
|
||||||
|
androidx.compose.material3.DropdownMenuItem(
|
||||||
|
text = { Text("Nouvelle liste...") },
|
||||||
|
onClick = { isEditing = true; expanded = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { isEditing = false },
|
||||||
|
title = { Text("Nouvelle liste") },
|
||||||
text = {
|
text = {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = customGroup,
|
||||||
|
onValueChange = { customGroup = it },
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = { Text("Nom de la liste") }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
if (customGroup.isNotBlank()) {
|
||||||
|
onGroupChanged(customGroup)
|
||||||
|
}
|
||||||
|
isEditing = false
|
||||||
|
}) { Text("Ajouter") }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { isEditing = false }) { Text("Annuler") }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun TaskTagsRow(
|
||||||
|
tags: List<String>,
|
||||||
|
newTagText: String,
|
||||||
|
onNewTagTextChanged: (String) -> Unit,
|
||||||
|
onAddTag: () -> Unit,
|
||||||
|
onRemoveTag: (String) -> Unit
|
||||||
|
) {
|
||||||
|
var isEditing by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.clickable { isEditing = true }
|
||||||
.verticalScroll(rememberScrollState()),
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
) {
|
||||||
// Content
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
OutlinedTextField(
|
Icon(Icons.Default.Folder, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
value = state.content,
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
onValueChange = onContentChanged,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
label = { Text("Contenu") },
|
|
||||||
singleLine = false,
|
|
||||||
maxLines = 4,
|
|
||||||
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Group
|
if (tags.isEmpty() && !isEditing) {
|
||||||
OutlinedTextField(
|
Text("Ajouter des étiquettes", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyLarge)
|
||||||
value = state.groupName,
|
|
||||||
onValueChange = onGroupChanged,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
label = { Text("Groupe") },
|
|
||||||
placeholder = { Text("Ex: Famille, Personnel, Projet X...") },
|
|
||||||
singleLine = true,
|
|
||||||
leadingIcon = { Icon(Icons.Default.Folder, contentDescription = null) }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (groupNames.isNotEmpty()) {
|
|
||||||
FlowRow(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
|
||||||
) {
|
|
||||||
groupNames.forEach { group ->
|
|
||||||
Surface(
|
|
||||||
color = if (state.groupName == group) {
|
|
||||||
MaterialTheme.colorScheme.primaryContainer
|
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
},
|
|
||||||
shape = MaterialTheme.shapes.small,
|
|
||||||
onClick = { onGroupChanged(group) }
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = group,
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
|
||||||
color = if (state.groupName == group) {
|
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Due date
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = state.dueDate?.let { formatDateTime(it, timezoneId) } ?: "Sans échéance",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Row {
|
|
||||||
TextButton(onClick = {
|
|
||||||
pickDateTime(context, state.dueDate, timezoneId) { millis -> onDueDateChanged(millis) }
|
|
||||||
}) {
|
|
||||||
Text("Date")
|
|
||||||
}
|
|
||||||
if (state.dueDate != null) {
|
|
||||||
TextButton(onClick = { onDueDateChanged(null) }) {
|
|
||||||
Text("Effacer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subtasks
|
|
||||||
Text(
|
|
||||||
text = "Sous-tâches",
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
|
|
||||||
state.subtasks.forEachIndexed { index, sub ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Checkbox(
|
|
||||||
checked = sub.isDone,
|
|
||||||
onCheckedChange = { onToggleSubtask(index) },
|
|
||||||
modifier = Modifier.size(32.dp)
|
|
||||||
)
|
|
||||||
OutlinedTextField(
|
|
||||||
value = sub.content,
|
|
||||||
onValueChange = { onUpdateSubtaskContent(index, it) },
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
singleLine = true,
|
|
||||||
textStyle = MaterialTheme.typography.bodySmall.copy(
|
|
||||||
textDecoration = if (sub.isDone) TextDecoration.LineThrough else TextDecoration.None
|
|
||||||
)
|
|
||||||
)
|
|
||||||
IconButton(
|
|
||||||
onClick = { onRemoveSubtask(index) },
|
|
||||||
modifier = Modifier.size(32.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Close,
|
|
||||||
contentDescription = "Supprimer",
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add subtask
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.newSubtaskText,
|
|
||||||
onValueChange = onNewSubtaskTextChanged,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
placeholder = { Text("Nouvelle sous-tâche...") },
|
|
||||||
singleLine = true,
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
imeAction = ImeAction.Done,
|
|
||||||
capitalization = KeyboardCapitalization.Sentences
|
|
||||||
),
|
|
||||||
keyboardActions = KeyboardActions(onDone = { onAddSubtask() })
|
|
||||||
)
|
|
||||||
IconButton(onClick = onAddSubtask) {
|
|
||||||
Icon(Icons.Default.Add, contentDescription = "Ajouter")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tags section with add capability
|
|
||||||
Text(
|
|
||||||
text = "Tags",
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
|
|
||||||
if (state.tags.isNotEmpty()) {
|
|
||||||
FlowRow(
|
FlowRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
state.tags.forEach { tag ->
|
tags.forEach { tag ->
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
shape = MaterialTheme.shapes.small,
|
shape = MaterialTheme.shapes.small,
|
||||||
@ -846,75 +945,302 @@ private fun EditTodoDialog(
|
|||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text("#$tag", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSecondaryContainer)
|
||||||
text = "#$tag",
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Icon(
|
Icon(Icons.Default.Close, contentDescription = null, modifier = Modifier.size(12.dp), tint = MaterialTheme.colorScheme.onSecondaryContainer)
|
||||||
imageVector = Icons.Default.Close,
|
|
||||||
contentDescription = "Supprimer le tag",
|
|
||||||
modifier = Modifier.size(12.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add tag
|
if (isEditing) {
|
||||||
|
androidx.compose.foundation.text.BasicTextField(
|
||||||
|
value = newTagText,
|
||||||
|
onValueChange = onNewTagTextChanged,
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||||
|
modifier = Modifier.width(100.dp).align(Alignment.CenterVertically),
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
|
keyboardActions = KeyboardActions(onDone = { onAddTag(); isEditing = false }),
|
||||||
|
decorationBox = { inner ->
|
||||||
|
if (newTagText.isEmpty()) Text("Nouveau tag...", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
inner()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TaskSubtasksSection(
|
||||||
|
subtasks: List<SubTask>,
|
||||||
|
newSubtaskText: String,
|
||||||
|
onNewSubtaskTextChanged: (String) -> Unit,
|
||||||
|
onAddSubtask: () -> Unit,
|
||||||
|
onRemoveSubtask: (Int) -> Unit,
|
||||||
|
onToggleSubtask: (Int) -> Unit,
|
||||||
|
onUpdateSubtaskContent: (Int, String) -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
Icon(Icons.Default.Add, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
value = state.newTagText,
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
onValueChange = onNewTagTextChanged,
|
Text("Ajouter une sous-tâche", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyLarge)
|
||||||
modifier = Modifier.weight(1f),
|
}
|
||||||
placeholder = { Text("Nouveau tag...") },
|
|
||||||
singleLine = true,
|
subtasks.forEachIndexed { index, sub ->
|
||||||
keyboardOptions = KeyboardOptions(
|
Row(
|
||||||
imeAction = ImeAction.Done,
|
modifier = Modifier.fillMaxWidth().padding(start = 48.dp, end = 16.dp, top = 4.dp, bottom = 4.dp),
|
||||||
capitalization = KeyboardCapitalization.None
|
verticalAlignment = Alignment.CenterVertically
|
||||||
),
|
) {
|
||||||
keyboardActions = KeyboardActions(onDone = { onAddTag() })
|
Checkbox(
|
||||||
|
checked = sub.isDone,
|
||||||
|
onCheckedChange = { onToggleSubtask(index) },
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
)
|
)
|
||||||
IconButton(onClick = onAddTag) {
|
androidx.compose.foundation.text.BasicTextField(
|
||||||
Icon(Icons.Default.Add, contentDescription = "Ajouter tag")
|
value = sub.content,
|
||||||
|
onValueChange = { onUpdateSubtaskContent(index, it) },
|
||||||
|
modifier = Modifier.weight(1f).padding(horizontal = 8.dp),
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
textDecoration = if (sub.isDone) TextDecoration.LineThrough else TextDecoration.None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
IconButton(onClick = { onRemoveSubtask(index) }, modifier = Modifier.size(32.dp)) {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = "Supprimer", modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.errorMessage?.let { message ->
|
Row(
|
||||||
Text(
|
modifier = Modifier.fillMaxWidth().padding(start = 48.dp, end = 16.dp, top = 4.dp, bottom = 12.dp),
|
||||||
text = message,
|
verticalAlignment = Alignment.CenterVertically
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = onSave,
|
|
||||||
enabled = !state.isSaving && state.content.isNotBlank()
|
|
||||||
) {
|
) {
|
||||||
if (state.isSaving) {
|
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(24.dp).padding(4.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
|
androidx.compose.foundation.text.BasicTextField(
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
value = newSubtaskText,
|
||||||
}
|
onValueChange = onNewSubtaskTextChanged,
|
||||||
Icon(Icons.Default.Check, contentDescription = null)
|
modifier = Modifier.weight(1f).padding(horizontal = 8.dp),
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||||
Text(if (state.isCreateMode) "Créer" else "Enregistrer")
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
}
|
keyboardActions = KeyboardActions(onDone = { onAddSubtask() }),
|
||||||
},
|
decorationBox = { inner ->
|
||||||
dismissButton = {
|
if (newSubtaskText.isEmpty()) Text("Nouvelle sous-tâche...", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
TextButton(onClick = onDismiss) {
|
inner()
|
||||||
Text("Annuler")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun DateTimePickerBottomSheet(
|
||||||
|
initialDate: Long?,
|
||||||
|
timezoneId: String,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onDateSelected: (Long?) -> Unit
|
||||||
|
) {
|
||||||
|
val sheetState = androidx.compose.material3.rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
var showTimePicker by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val tz = java.util.TimeZone.getTimeZone(timezoneId)
|
||||||
|
val calendar = remember {
|
||||||
|
java.util.Calendar.getInstance(tz).apply {
|
||||||
|
if (initialDate != null) timeInMillis = initialDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedDateMillis by remember { mutableStateOf(initialDate) }
|
||||||
|
val datePickerState = androidx.compose.material3.rememberDatePickerState(
|
||||||
|
initialSelectedDateMillis = initialDate
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(datePickerState.selectedDateMillis) {
|
||||||
|
if (datePickerState.selectedDateMillis != null) {
|
||||||
|
val newDateCal = java.util.Calendar.getInstance(tz).apply {
|
||||||
|
timeInMillis = datePickerState.selectedDateMillis!!
|
||||||
|
}
|
||||||
|
calendar.set(java.util.Calendar.YEAR, newDateCal.get(java.util.Calendar.YEAR))
|
||||||
|
calendar.set(java.util.Calendar.MONTH, newDateCal.get(java.util.Calendar.MONTH))
|
||||||
|
calendar.set(java.util.Calendar.DAY_OF_MONTH, newDateCal.get(java.util.Calendar.DAY_OF_MONTH))
|
||||||
|
selectedDateMillis = calendar.timeInMillis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showTimePicker) {
|
||||||
|
CustomTimePickerDialog(
|
||||||
|
initialHour = calendar.get(java.util.Calendar.HOUR_OF_DAY),
|
||||||
|
initialMinute = calendar.get(java.util.Calendar.MINUTE),
|
||||||
|
onDismiss = { showTimePicker = false },
|
||||||
|
onTimeSelected = { hour, minute ->
|
||||||
|
calendar.set(java.util.Calendar.HOUR_OF_DAY, hour)
|
||||||
|
calendar.set(java.util.Calendar.MINUTE, minute)
|
||||||
|
selectedDateMillis = calendar.timeInMillis
|
||||||
|
showTimePicker = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
androidx.compose.material3.ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = sheetState,
|
||||||
|
dragHandle = { androidx.compose.material3.BottomSheetDefaults.DragHandle() },
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
ActionItem(Icons.Default.Event, "Date d'échéance") { }
|
||||||
|
ActionItem(Icons.Default.Schedule, "Heure d'échéance") { showTimePicker = true }
|
||||||
|
ActionItem(Icons.Default.Close, "Aucune date", tint = MaterialTheme.colorScheme.primary) {
|
||||||
|
onDateSelected(null)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
ActionItem(Icons.Outlined.LocalCafe, "9 h 00 a.m.") {
|
||||||
|
calendar.set(java.util.Calendar.HOUR_OF_DAY, 9)
|
||||||
|
calendar.set(java.util.Calendar.MINUTE, 0)
|
||||||
|
selectedDateMillis = calendar.timeInMillis
|
||||||
|
}
|
||||||
|
ActionItem(Icons.Outlined.WbSunny, "1 h 00 p.m.") {
|
||||||
|
calendar.set(java.util.Calendar.HOUR_OF_DAY, 13)
|
||||||
|
calendar.set(java.util.Calendar.MINUTE, 0)
|
||||||
|
selectedDateMillis = calendar.timeInMillis
|
||||||
|
}
|
||||||
|
ActionItem(Icons.Outlined.WbTwilight, "5 h 00 p.m.") {
|
||||||
|
calendar.set(java.util.Calendar.HOUR_OF_DAY, 17)
|
||||||
|
calendar.set(java.util.Calendar.MINUTE, 0)
|
||||||
|
selectedDateMillis = calendar.timeInMillis
|
||||||
|
}
|
||||||
|
ActionItem(Icons.Outlined.NightsStay, "8 h 00 p.m.") {
|
||||||
|
calendar.set(java.util.Calendar.HOUR_OF_DAY, 20)
|
||||||
|
calendar.set(java.util.Calendar.MINUTE, 0)
|
||||||
|
selectedDateMillis = calendar.timeInMillis
|
||||||
|
}
|
||||||
|
ActionItem(Icons.Default.Schedule, "Choisir l'heure") {
|
||||||
|
showTimePicker = true
|
||||||
|
}
|
||||||
|
ActionItem(Icons.Default.Close, "Aucune heure", tint = MaterialTheme.colorScheme.primary) {
|
||||||
|
calendar.set(java.util.Calendar.HOUR_OF_DAY, 0)
|
||||||
|
calendar.set(java.util.Calendar.MINUTE, 0)
|
||||||
|
selectedDateMillis = calendar.timeInMillis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
androidx.compose.material3.DatePicker(
|
||||||
|
state = datePickerState,
|
||||||
|
showModeToggle = false,
|
||||||
|
title = null,
|
||||||
|
headline = null,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp, top = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onDismiss) { Text("Annuler") }
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
TextButton(onClick = {
|
||||||
|
onDateSelected(selectedDateMillis)
|
||||||
|
onDismiss()
|
||||||
|
}) { Text("Valider") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ActionItem(
|
||||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
|
text: String,
|
||||||
|
tint: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(vertical = 12.dp, horizontal = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(icon, contentDescription = null, modifier = Modifier.size(20.dp), tint = tint)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(text, style = MaterialTheme.typography.bodyMedium, color = tint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun CustomTimePickerDialog(
|
||||||
|
initialHour: Int,
|
||||||
|
initialMinute: Int,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onTimeSelected: (Int, Int) -> Unit
|
||||||
|
) {
|
||||||
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
|
val prefs = context.getSharedPreferences("time_picker_prefs", android.content.Context.MODE_PRIVATE)
|
||||||
|
var isDialMode by remember { mutableStateOf(prefs.getBoolean("is_dial_mode", true)) }
|
||||||
|
|
||||||
|
val timePickerState = androidx.compose.material3.rememberTimePickerState(
|
||||||
|
initialHour = initialHour,
|
||||||
|
initialMinute = initialMinute,
|
||||||
|
is24Hour = false
|
||||||
|
)
|
||||||
|
|
||||||
|
androidx.compose.ui.window.Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Surface(
|
||||||
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
if (isDialMode) {
|
||||||
|
androidx.compose.material3.TimePicker(state = timePickerState)
|
||||||
|
} else {
|
||||||
|
androidx.compose.material3.TimeInput(state = timePickerState)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
IconButton(onClick = {
|
||||||
|
isDialMode = !isDialMode
|
||||||
|
prefs.edit().putBoolean("is_dial_mode", isDialMode).apply()
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
if (isDialMode) Icons.Outlined.Keyboard else Icons.Default.Schedule,
|
||||||
|
contentDescription = "Changer de mode"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
TextButton(onClick = onDismiss) { Text("Annuler") }
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
TextButton(onClick = {
|
||||||
|
onTimeSelected(timePickerState.hour, timePickerState.minute)
|
||||||
|
}) { Text("Valider") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== Brain Dump Dialog ======
|
// ====== Brain Dump Dialog ======
|
||||||
|
|||||||
608
patch.py
Normal file
608
patch.py
Normal file
@ -0,0 +1,608 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
with open('c:/dev/git/Android/ShaarIt/app/src/main/java/com/shaarit/presentation/todo/TodoScreen.kt', 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
replacement = \"\"\"// ====== Edit / Create Todo Screen ======
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun EditTodoDialog(
|
||||||
|
state: EditTodoDialogUiState,
|
||||||
|
groupNames: List<String>,
|
||||||
|
timezoneId: String = java.util.TimeZone.getDefault().id,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onContentChanged: (String) -> Unit,
|
||||||
|
onDueDateChanged: (Long?) -> Unit,
|
||||||
|
onGroupChanged: (String) -> Unit,
|
||||||
|
onNewSubtaskTextChanged: (String) -> Unit,
|
||||||
|
onAddSubtask: () -> Unit,
|
||||||
|
onRemoveSubtask: (Int) -> Unit,
|
||||||
|
onToggleSubtask: (Int) -> Unit,
|
||||||
|
onUpdateSubtaskContent: (Int, String) -> Unit,
|
||||||
|
onNewTagTextChanged: (String) -> Unit,
|
||||||
|
onAddTag: () -> Unit,
|
||||||
|
onRemoveTag: (String) -> Unit,
|
||||||
|
onSave: () -> Unit
|
||||||
|
) {
|
||||||
|
var showDatePicker by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
androidx.compose.ui.window.Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = androidx.compose.ui.window.DialogProperties(
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
decorFitsSystemWindows = true
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onSave, enabled = !state.isSaving && state.content.isNotBlank()) {
|
||||||
|
if (state.isSaving) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Default.Check, contentDescription = "Enregistrer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = onDismiss) {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = "Fermer")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
// Task Title
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
modifier = Modifier.padding(16.dp).fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
androidx.compose.material.icons.outlined.CheckBoxOutlineBlank,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(16.dp))
|
||||||
|
androidx.compose.foundation.text.BasicTextField(
|
||||||
|
value = state.content,
|
||||||
|
onValueChange = onContentChanged,
|
||||||
|
textStyle = MaterialTheme.typography.titleLarge.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
if (state.content.isEmpty()) {
|
||||||
|
Text("Nom de la tâche", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.titleLarge)
|
||||||
|
}
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
androidx.compose.material3.HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||||
|
|
||||||
|
// Due Date
|
||||||
|
val dateText = state.dueDate?.let { formatDateTime(it, timezoneId) } ?: "Aucune date d'échéance"
|
||||||
|
TaskActionRow(
|
||||||
|
icon = Icons.Default.Schedule,
|
||||||
|
text = dateText,
|
||||||
|
onClick = { showDatePicker = true },
|
||||||
|
isSet = state.dueDate != null
|
||||||
|
)
|
||||||
|
|
||||||
|
androidx.compose.material3.HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||||
|
|
||||||
|
// Group (Famille)
|
||||||
|
TaskGroupRow(
|
||||||
|
groupName = state.groupName,
|
||||||
|
groupNames = groupNames,
|
||||||
|
onGroupChanged = onGroupChanged
|
||||||
|
)
|
||||||
|
|
||||||
|
androidx.compose.material3.HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
TaskTagsRow(
|
||||||
|
tags = state.tags,
|
||||||
|
newTagText = state.newTagText,
|
||||||
|
onNewTagTextChanged = onNewTagTextChanged,
|
||||||
|
onAddTag = onAddTag,
|
||||||
|
onRemoveTag = onRemoveTag
|
||||||
|
)
|
||||||
|
|
||||||
|
androidx.compose.material3.HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||||
|
|
||||||
|
// Subtasks
|
||||||
|
TaskSubtasksSection(
|
||||||
|
subtasks = state.subtasks,
|
||||||
|
newSubtaskText = state.newSubtaskText,
|
||||||
|
onNewSubtaskTextChanged = onNewSubtaskTextChanged,
|
||||||
|
onAddSubtask = onAddSubtask,
|
||||||
|
onRemoveSubtask = onRemoveSubtask,
|
||||||
|
onToggleSubtask = onToggleSubtask,
|
||||||
|
onUpdateSubtaskContent = onUpdateSubtaskContent
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDatePicker) {
|
||||||
|
DateTimePickerBottomSheet(
|
||||||
|
initialDate = state.dueDate,
|
||||||
|
timezoneId = timezoneId,
|
||||||
|
onDismiss = { showDatePicker = false },
|
||||||
|
onDateSelected = onDueDateChanged
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TaskActionRow(
|
||||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
isSet: Boolean = false
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (isSet) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
color = if (isSet) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TaskGroupRow(
|
||||||
|
groupName: String,
|
||||||
|
groupNames: List<String>,
|
||||||
|
onGroupChanged: (String) -> Unit
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
var isEditing by remember { mutableStateOf(false) }
|
||||||
|
var customGroup by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { expanded = true }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.List, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
if (groupName.isNotBlank()) {
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
shape = MaterialTheme.shapes.small,
|
||||||
|
onClick = { expanded = true }
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)) {
|
||||||
|
Icon(Icons.Default.List, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||||
|
Spacer(Modifier.width(6.dp))
|
||||||
|
Text(groupName.uppercase(), style = MaterialTheme.typography.labelMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Aucune liste", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
androidx.compose.material3.DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false }
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.DropdownMenuItem(
|
||||||
|
text = { Text("Aucune liste") },
|
||||||
|
onClick = { onGroupChanged(""); expanded = false }
|
||||||
|
)
|
||||||
|
groupNames.forEach { group ->
|
||||||
|
androidx.compose.material3.DropdownMenuItem(
|
||||||
|
text = { Text(group) },
|
||||||
|
onClick = { onGroupChanged(group); expanded = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
androidx.compose.material3.HorizontalDivider()
|
||||||
|
androidx.compose.material3.DropdownMenuItem(
|
||||||
|
text = { Text("Nouvelle liste...") },
|
||||||
|
onClick = { isEditing = true; expanded = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { isEditing = false },
|
||||||
|
title = { Text("Nouvelle liste") },
|
||||||
|
text = {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = customGroup,
|
||||||
|
onValueChange = { customGroup = it },
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = { Text("Nom de la liste") }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
if (customGroup.isNotBlank()) {
|
||||||
|
onGroupChanged(customGroup)
|
||||||
|
}
|
||||||
|
isEditing = false
|
||||||
|
}) { Text("Ajouter") }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { isEditing = false }) { Text("Annuler") }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun TaskTagsRow(
|
||||||
|
tags: List<String>,
|
||||||
|
newTagText: String,
|
||||||
|
onNewTagTextChanged: (String) -> Unit,
|
||||||
|
onAddTag: () -> Unit,
|
||||||
|
onRemoveTag: (String) -> Unit
|
||||||
|
) {
|
||||||
|
var isEditing by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { isEditing = true }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(Icons.Default.Folder, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
if (tags.isEmpty() && !isEditing) {
|
||||||
|
Text("Ajouter des étiquettes", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
} else {
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
tags.forEach { tag ->
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
shape = MaterialTheme.shapes.small,
|
||||||
|
onClick = { onRemoveTag(tag) }
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text("#\", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSecondaryContainer)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Icon(Icons.Default.Close, contentDescription = null, modifier = Modifier.size(12.dp), tint = MaterialTheme.colorScheme.onSecondaryContainer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
androidx.compose.foundation.text.BasicTextField(
|
||||||
|
value = newTagText,
|
||||||
|
onValueChange = onNewTagTextChanged,
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||||
|
modifier = Modifier.width(100.dp).align(Alignment.CenterVertically),
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
|
keyboardActions = KeyboardActions(onDone = { onAddTag(); isEditing = false }),
|
||||||
|
decorationBox = { inner ->
|
||||||
|
if (newTagText.isEmpty()) Text("Nouveau tag...", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
inner()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TaskSubtasksSection(
|
||||||
|
subtasks: List<SubTask>,
|
||||||
|
newSubtaskText: String,
|
||||||
|
onNewSubtaskTextChanged: (String) -> Unit,
|
||||||
|
onAddSubtask: () -> Unit,
|
||||||
|
onRemoveSubtask: (Int) -> Unit,
|
||||||
|
onToggleSubtask: (Int) -> Unit,
|
||||||
|
onUpdateSubtaskContent: (Int, String) -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Add, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Text("Ajouter une sous-tâche", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
subtasks.forEachIndexed { index, sub ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(start = 48.dp, end = 16.dp, top = 4.dp, bottom = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = sub.isDone,
|
||||||
|
onCheckedChange = { onToggleSubtask(index) },
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
androidx.compose.foundation.text.BasicTextField(
|
||||||
|
value = sub.content,
|
||||||
|
onValueChange = { onUpdateSubtaskContent(index, it) },
|
||||||
|
modifier = Modifier.weight(1f).padding(horizontal = 8.dp),
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
textDecoration = if (sub.isDone) TextDecoration.LineThrough else TextDecoration.None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
IconButton(onClick = { onRemoveSubtask(index) }, modifier = Modifier.size(32.dp)) {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = "Supprimer", modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(start = 48.dp, end = 16.dp, top = 4.dp, bottom = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(24.dp).padding(4.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
androidx.compose.foundation.text.BasicTextField(
|
||||||
|
value = newSubtaskText,
|
||||||
|
onValueChange = onNewSubtaskTextChanged,
|
||||||
|
modifier = Modifier.weight(1f).padding(horizontal = 8.dp),
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
|
keyboardActions = KeyboardActions(onDone = onAddSubtask),
|
||||||
|
decorationBox = { inner ->
|
||||||
|
if (newSubtaskText.isEmpty()) Text("Nouvelle sous-tâche...", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
inner()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun DateTimePickerBottomSheet(
|
||||||
|
initialDate: Long?,
|
||||||
|
timezoneId: String,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onDateSelected: (Long?) -> Unit
|
||||||
|
) {
|
||||||
|
val sheetState = androidx.compose.material3.rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
var showTimePicker by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val tz = TimeZone.getTimeZone(timezoneId)
|
||||||
|
val calendar = remember {
|
||||||
|
Calendar.getInstance(tz).apply {
|
||||||
|
if (initialDate != null) timeInMillis = initialDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedDateMillis by remember { mutableStateOf(initialDate) }
|
||||||
|
val datePickerState = androidx.compose.material3.rememberDatePickerState(
|
||||||
|
initialSelectedDateMillis = initialDate
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(datePickerState.selectedDateMillis) {
|
||||||
|
if (datePickerState.selectedDateMillis != null) {
|
||||||
|
val newDateCal = Calendar.getInstance(tz).apply {
|
||||||
|
timeInMillis = datePickerState.selectedDateMillis!!
|
||||||
|
}
|
||||||
|
calendar.set(Calendar.YEAR, newDateCal.get(Calendar.YEAR))
|
||||||
|
calendar.set(Calendar.MONTH, newDateCal.get(Calendar.MONTH))
|
||||||
|
calendar.set(Calendar.DAY_OF_MONTH, newDateCal.get(Calendar.DAY_OF_MONTH))
|
||||||
|
selectedDateMillis = calendar.timeInMillis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showTimePicker) {
|
||||||
|
CustomTimePickerDialog(
|
||||||
|
initialHour = calendar.get(Calendar.HOUR_OF_DAY),
|
||||||
|
initialMinute = calendar.get(Calendar.MINUTE),
|
||||||
|
onDismiss = { showTimePicker = false },
|
||||||
|
onTimeSelected = { hour, minute ->
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, hour)
|
||||||
|
calendar.set(Calendar.MINUTE, minute)
|
||||||
|
selectedDateMillis = calendar.timeInMillis
|
||||||
|
showTimePicker = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
androidx.compose.material3.ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = sheetState,
|
||||||
|
dragHandle = { androidx.compose.material3.BottomSheetDefaults.DragHandle() },
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
ActionItem(Icons.Default.Event, "Date d'échéance") { }
|
||||||
|
ActionItem(Icons.Default.Schedule, "Heure d'échéance") { showTimePicker = true }
|
||||||
|
ActionItem(Icons.Default.Close, "Aucune date", tint = MaterialTheme.colorScheme.primary) {
|
||||||
|
onDateSelected(null)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
ActionItem(androidx.compose.material.icons.outlined.Coffee, "9 h 00 a.m.") {
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, 9)
|
||||||
|
calendar.set(Calendar.MINUTE, 0)
|
||||||
|
selectedDateMillis = calendar.timeInMillis
|
||||||
|
}
|
||||||
|
ActionItem(androidx.compose.material.icons.outlined.WbSunny, "1 h 00 p.m.") {
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, 13)
|
||||||
|
calendar.set(Calendar.MINUTE, 0)
|
||||||
|
selectedDateMillis = calendar.timeInMillis
|
||||||
|
}
|
||||||
|
ActionItem(androidx.compose.material.icons.outlined.WbTwilight, "5 h 00 p.m.") {
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, 17)
|
||||||
|
calendar.set(Calendar.MINUTE, 0)
|
||||||
|
selectedDateMillis = calendar.timeInMillis
|
||||||
|
}
|
||||||
|
ActionItem(androidx.compose.material.icons.outlined.NightsStay, "8 h 00 p.m.") {
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, 20)
|
||||||
|
calendar.set(Calendar.MINUTE, 0)
|
||||||
|
selectedDateMillis = calendar.timeInMillis
|
||||||
|
}
|
||||||
|
ActionItem(Icons.Default.Schedule, "Choisir l'heure") {
|
||||||
|
showTimePicker = true
|
||||||
|
}
|
||||||
|
ActionItem(Icons.Default.Close, "Aucune heure", tint = MaterialTheme.colorScheme.primary) {
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, 0)
|
||||||
|
calendar.set(Calendar.MINUTE, 0)
|
||||||
|
selectedDateMillis = calendar.timeInMillis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
androidx.compose.material3.DatePicker(
|
||||||
|
state = datePickerState,
|
||||||
|
showModeToggle = false,
|
||||||
|
title = null,
|
||||||
|
headline = null,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp, top = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onDismiss) { Text("Annuler") }
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
TextButton(onClick = {
|
||||||
|
onDateSelected(selectedDateMillis)
|
||||||
|
onDismiss()
|
||||||
|
}) { Text("Valider") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ActionItem(
|
||||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
|
text: String,
|
||||||
|
tint: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(vertical = 12.dp, horizontal = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(icon, contentDescription = null, modifier = Modifier.size(20.dp), tint = tint)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(text, style = MaterialTheme.typography.bodyMedium, color = tint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun CustomTimePickerDialog(
|
||||||
|
initialHour: Int,
|
||||||
|
initialMinute: Int,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onTimeSelected: (Int, Int) -> Unit
|
||||||
|
) {
|
||||||
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
|
val prefs = context.getSharedPreferences("time_picker_prefs", android.content.Context.MODE_PRIVATE)
|
||||||
|
var isDialMode by remember { mutableStateOf(prefs.getBoolean("is_dial_mode", true)) }
|
||||||
|
|
||||||
|
val timePickerState = androidx.compose.material3.rememberTimePickerState(
|
||||||
|
initialHour = initialHour,
|
||||||
|
initialMinute = initialMinute,
|
||||||
|
is24Hour = false
|
||||||
|
)
|
||||||
|
|
||||||
|
androidx.compose.ui.window.Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Surface(
|
||||||
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
if (isDialMode) {
|
||||||
|
androidx.compose.material3.TimePicker(state = timePickerState)
|
||||||
|
} else {
|
||||||
|
androidx.compose.material3.TimeInput(state = timePickerState)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
IconButton(onClick = {
|
||||||
|
isDialMode = !isDialMode
|
||||||
|
prefs.edit().putBoolean("is_dial_mode", isDialMode).apply()
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
if (isDialMode) androidx.compose.material.icons.outlined.Keyboard else Icons.Default.Schedule,
|
||||||
|
contentDescription = "Changer de mode"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
TextButton(onClick = onDismiss) { Text("Annuler") }
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
TextButton(onClick = {
|
||||||
|
onTimeSelected(timePickerState.hour, timePickerState.minute)
|
||||||
|
}) { Text("Valider") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\"\"\"
|
||||||
|
new_lines = lines[:640] + [replacement + \"\\n\"] + lines[918:]
|
||||||
|
|
||||||
|
with open('c:/dev/git/Android/ShaarIt/app/src/main/java/com/shaarit/presentation/todo/TodoScreen.kt', 'w', encoding='utf-8') as f:
|
||||||
|
f.writelines(new_lines)
|
||||||
|
|
||||||
|
print('Success')
|
||||||
@ -1,3 +1,3 @@
|
|||||||
#Thu Apr 23 16:12:34 2026
|
#Thu Apr 23 19:02:26 2026
|
||||||
VERSION_NAME=2.12.1
|
VERSION_NAME=2.13.0
|
||||||
VERSION_CODE=39
|
VERSION_CODE=40
|
||||||
Loading…
x
Reference in New Issue
Block a user