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.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
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.Delete
|
||||
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.List
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
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.Checkbox
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
@ -638,12 +648,12 @@ private fun DueDateChip(
|
||||
|
||||
// ====== Edit / Create Todo Dialog ======
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun EditTodoDialog(
|
||||
state: EditTodoDialogUiState,
|
||||
groupNames: List<String>,
|
||||
timezoneId: String = TimeZone.getDefault().id,
|
||||
timezoneId: String = java.util.TimeZone.getDefault().id,
|
||||
onDismiss: () -> Unit,
|
||||
onContentChanged: (String) -> Unit,
|
||||
onDueDateChanged: (Long?) -> Unit,
|
||||
@ -658,263 +668,579 @@ private fun EditTodoDialog(
|
||||
onRemoveTag: (String) -> Unit,
|
||||
onSave: () -> Unit
|
||||
) {
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
val dialogTitle = if (state.isCreateMode) "➕ Nouvelle tâche" else "✏️ Modifier la tâche"
|
||||
var showDatePicker by remember { mutableStateOf(false) }
|
||||
|
||||
AlertDialog(
|
||||
androidx.compose.ui.window.Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(dialogTitle)
|
||||
IconButton(onClick = onDismiss) {
|
||||
Icon(Icons.Default.Close, contentDescription = "Fermer")
|
||||
}
|
||||
}
|
||||
},
|
||||
text = {
|
||||
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
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// Content
|
||||
OutlinedTextField(
|
||||
value = state.content,
|
||||
onValueChange = onContentChanged,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = { Text("Contenu") },
|
||||
singleLine = false,
|
||||
maxLines = 4,
|
||||
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences)
|
||||
// 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
|
||||
)
|
||||
|
||||
// Group
|
||||
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 = {
|
||||
OutlinedTextField(
|
||||
value = state.groupName,
|
||||
onValueChange = onGroupChanged,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = { Text("Groupe") },
|
||||
placeholder = { Text("Ex: Famille, Personnel, Projet X...") },
|
||||
value = customGroup,
|
||||
onValueChange = { customGroup = it },
|
||||
singleLine = true,
|
||||
leadingIcon = { Icon(Icons.Default.Folder, contentDescription = null) }
|
||||
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") }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
MaterialTheme.colorScheme.surfaceVariant
|
||||
},
|
||||
shape = MaterialTheme.shapes.small,
|
||||
onClick = { onGroupChanged(group) }
|
||||
@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(
|
||||
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
|
||||
}
|
||||
)
|
||||
Text("#$tag", 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Due date
|
||||
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 = 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
|
||||
) {
|
||||
Text(
|
||||
text = state.dueDate?.let { formatDateTime(it, timezoneId) } ?: "Sans échéance",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
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 = {
|
||||
pickDateTime(context, state.dueDate, timezoneId) { millis -> onDueDateChanged(millis) }
|
||||
}) {
|
||||
Text("Date")
|
||||
}
|
||||
if (state.dueDate != null) {
|
||||
TextButton(onClick = { onDueDateChanged(null) }) {
|
||||
Text("Effacer")
|
||||
}
|
||||
}
|
||||
onTimeSelected(timePickerState.hour, timePickerState.minute)
|
||||
}) { Text("Valider") }
|
||||
}
|
||||
}
|
||||
|
||||
// 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(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
state.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(
|
||||
text = "#$tag",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Supprimer le tag",
|
||||
modifier = Modifier.size(12.dp),
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add tag
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = state.newTagText,
|
||||
onValueChange = onNewTagTextChanged,
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = { Text("Nouveau tag...") },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
capitalization = KeyboardCapitalization.None
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { onAddTag() })
|
||||
)
|
||||
IconButton(onClick = onAddTag) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Ajouter tag")
|
||||
}
|
||||
}
|
||||
|
||||
state.errorMessage?.let { message ->
|
||||
Text(
|
||||
text = message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = onSave,
|
||||
enabled = !state.isSaving && state.content.isNotBlank()
|
||||
) {
|
||||
if (state.isSaving) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
Icon(Icons.Default.Check, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(if (state.isCreateMode) "Créer" else "Enregistrer")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Annuler")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 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
|
||||
VERSION_NAME=2.12.1
|
||||
VERSION_CODE=39
|
||||
#Thu Apr 23 19:02:26 2026
|
||||
VERSION_NAME=2.13.0
|
||||
VERSION_CODE=40
|
||||
Loading…
x
Reference in New Issue
Block a user