From 5652e14352cdd27e5885c223f17029bc903b4582 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Thu, 23 Apr 2026 19:05:52 -0400 Subject: [PATCH] feat: implement TodoScreen UI with AI-powered brain dump functionality and notification permissions --- .../shaarit/presentation/todo/TodoScreen.kt | 790 +++++++++++++----- patch.py | 608 ++++++++++++++ temp.txt | Bin 0 -> 25162 bytes version.properties | 6 +- 4 files changed, 1169 insertions(+), 235 deletions(-) create mode 100644 patch.py create mode 100644 temp.txt diff --git a/app/src/main/java/com/shaarit/presentation/todo/TodoScreen.kt b/app/src/main/java/com/shaarit/presentation/todo/TodoScreen.kt index 9de0fc7..1283e99 100644 --- a/app/src/main/java/com/shaarit/presentation/todo/TodoScreen.kt +++ b/app/src/main/java/com/shaarit/presentation/todo/TodoScreen.kt @@ -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, - 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, + 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, + 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) } } } + + 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() + } + ) + } } + } + } + } +} - // Due date +@Composable +private fun TaskSubtasksSection( + subtasks: List, + 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 = { - pickDateTime(context, state.dueDate, timezoneId) { millis -> onDueDateChanged(millis) } - }) { - Text("Date") - } - if (state.dueDate != null) { - TextButton(onClick = { onDueDateChanged(null) }) { - Text("Effacer") - } - } + TextButton(onClick = onDismiss) { Text("Annuler") } + Spacer(modifier = Modifier.width(8.dp)) + TextButton(onClick = { + 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 ====== diff --git a/patch.py b/patch.py new file mode 100644 index 0000000..4d0008e --- /dev/null +++ b/patch.py @@ -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, + 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 tche", 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'chance" + 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, + 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, + 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, + 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-tche", 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-tche...", 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'chance") { } + ActionItem(Icons.Default.Schedule, "Heure d'chance") { 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') diff --git a/temp.txt b/temp.txt new file mode 100644 index 0000000000000000000000000000000000000000..8486752fd36e39508d98912e29582e519c9a9029 GIT binary patch literal 25162 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