feat: implement TodoScreen UI with AI-powered brain dump functionality and notification permissions

This commit is contained in:
Bruno Charest 2026-04-23 19:05:52 -04:00
parent 3b0cb915f0
commit 5652e14352
4 changed files with 1169 additions and 235 deletions

View File

@ -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,185 +668,274 @@ 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
properties = androidx.compose.ui.window.DialogProperties(
usePlatformDefaultWidth = false,
decorFitsSystemWindows = true
)
) {
Text(dialogTitle)
Scaffold(
topBar = {
TopAppBar(
title = { },
navigationIcon = {
IconButton(onClick = onSave, enabled = !state.isSaving && state.content.isNotBlank()) {
if (state.isSaving) {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
} else {
Icon(Icons.Default.Check, contentDescription = "Enregistrer")
}
}
},
actions = {
IconButton(onClick = onDismiss) {
Icon(Icons.Default.Close, contentDescription = "Fermer")
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
)
},
containerColor = MaterialTheme.colorScheme.surface
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) {
// Task Title
Row(
verticalAlignment = Alignment.Top,
modifier = Modifier.padding(16.dp).fillMaxWidth()
) {
Icon(
Icons.Outlined.CheckBoxOutlineBlank,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 4.dp)
)
Spacer(Modifier.width(16.dp))
androidx.compose.foundation.text.BasicTextField(
value = state.content,
onValueChange = onContentChanged,
textStyle = MaterialTheme.typography.titleLarge.copy(color = MaterialTheme.colorScheme.onSurface),
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
decorationBox = { innerTextField ->
if (state.content.isEmpty()) {
Text("Nom de la tâche", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.titleLarge)
}
innerTextField()
}
)
}
androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
// Due Date
val dateText = state.dueDate?.let { formatDateTime(it, timezoneId) } ?: "Aucune date d'échéance"
TaskActionRow(
icon = Icons.Default.Schedule,
text = dateText,
onClick = { showDatePicker = true },
isSet = state.dueDate != null
)
androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
// Group (Famille)
TaskGroupRow(
groupName = state.groupName,
groupNames = groupNames,
onGroupChanged = onGroupChanged
)
androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
// Tags
TaskTagsRow(
tags = state.tags,
newTagText = state.newTagText,
onNewTagTextChanged = onNewTagTextChanged,
onAddTag = onAddTag,
onRemoveTag = onRemoveTag
)
androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
// Subtasks
TaskSubtasksSection(
subtasks = state.subtasks,
newSubtaskText = state.newSubtaskText,
onNewSubtaskTextChanged = onNewSubtaskTextChanged,
onAddSubtask = onAddSubtask,
onRemoveSubtask = onRemoveSubtask,
onToggleSubtask = onToggleSubtask,
onUpdateSubtaskContent = onUpdateSubtaskContent
)
Spacer(modifier = Modifier.height(32.dp))
}
}
if (showDatePicker) {
DateTimePickerBottomSheet(
initialDate = state.dueDate,
timezoneId = timezoneId,
onDismiss = { showDatePicker = false },
onDateSelected = onDueDateChanged
)
}
}
}
@Composable
private fun TaskActionRow(
icon: androidx.compose.ui.graphics.vector.ImageVector,
text: String,
onClick: () -> Unit = {},
isSet: Boolean = false
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
icon,
contentDescription = null,
tint = if (isSet) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text,
color = if (isSet) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyLarge
)
}
}
@Composable
private fun TaskGroupRow(
groupName: String,
groupNames: List<String>,
onGroupChanged: (String) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
var isEditing by remember { mutableStateOf(false) }
var customGroup by remember { mutableStateOf("") }
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { expanded = true }
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.List, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
Spacer(modifier = Modifier.width(16.dp))
if (groupName.isNotBlank()) {
Surface(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.small,
onClick = { expanded = true }
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)) {
Icon(Icons.Default.List, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(6.dp))
Text(groupName.uppercase(), style = MaterialTheme.typography.labelMedium)
}
}
} else {
Text("Aucune liste", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyLarge)
}
androidx.compose.material3.DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
androidx.compose.material3.DropdownMenuItem(
text = { Text("Aucune liste") },
onClick = { onGroupChanged(""); expanded = false }
)
groupNames.forEach { group ->
androidx.compose.material3.DropdownMenuItem(
text = { Text(group) },
onClick = { onGroupChanged(group); expanded = false }
)
}
androidx.compose.material3.Divider()
androidx.compose.material3.DropdownMenuItem(
text = { Text("Nouvelle liste...") },
onClick = { isEditing = true; expanded = false }
)
}
}
if (isEditing) {
AlertDialog(
onDismissRequest = { isEditing = false },
title = { Text("Nouvelle liste") },
text = {
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()
.wrapContentHeight()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp)
.clickable { isEditing = true }
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
// Content
OutlinedTextField(
value = state.content,
onValueChange = onContentChanged,
modifier = Modifier.fillMaxWidth(),
label = { Text("Contenu") },
singleLine = false,
maxLines = 4,
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences)
)
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.Folder, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
Spacer(modifier = Modifier.width(16.dp))
// Group
OutlinedTextField(
value = state.groupName,
onValueChange = onGroupChanged,
modifier = Modifier.fillMaxWidth(),
label = { Text("Groupe") },
placeholder = { Text("Ex: Famille, Personnel, Projet X...") },
singleLine = true,
leadingIcon = { Icon(Icons.Default.Folder, contentDescription = null) }
)
if (groupNames.isNotEmpty()) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
groupNames.forEach { group ->
Surface(
color = if (state.groupName == group) {
MaterialTheme.colorScheme.primaryContainer
if (tags.isEmpty() && !isEditing) {
Text("Ajouter des étiquettes", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyLarge)
} else {
MaterialTheme.colorScheme.surfaceVariant
},
shape = MaterialTheme.shapes.small,
onClick = { onGroupChanged(group) }
) {
Text(
text = group,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
color = if (state.groupName == group) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
)
}
}
}
}
// Due date
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = state.dueDate?.let { formatDateTime(it, timezoneId) } ?: "Sans échéance",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Row {
TextButton(onClick = {
pickDateTime(context, state.dueDate, timezoneId) { millis -> onDueDateChanged(millis) }
}) {
Text("Date")
}
if (state.dueDate != null) {
TextButton(onClick = { onDueDateChanged(null) }) {
Text("Effacer")
}
}
}
}
// Subtasks
Text(
text = "Sous-tâches",
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
state.subtasks.forEachIndexed { index, sub ->
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = sub.isDone,
onCheckedChange = { onToggleSubtask(index) },
modifier = Modifier.size(32.dp)
)
OutlinedTextField(
value = sub.content,
onValueChange = { onUpdateSubtaskContent(index, it) },
modifier = Modifier.weight(1f),
singleLine = true,
textStyle = MaterialTheme.typography.bodySmall.copy(
textDecoration = if (sub.isDone) TextDecoration.LineThrough else TextDecoration.None
)
)
IconButton(
onClick = { onRemoveSubtask(index) },
modifier = Modifier.size(32.dp)
) {
Icon(
Icons.Default.Close,
contentDescription = "Supprimer",
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.error
)
}
}
}
// Add subtask
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = state.newSubtaskText,
onValueChange = onNewSubtaskTextChanged,
modifier = Modifier.weight(1f),
placeholder = { Text("Nouvelle sous-tâche...") },
singleLine = true,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
capitalization = KeyboardCapitalization.Sentences
),
keyboardActions = KeyboardActions(onDone = { onAddSubtask() })
)
IconButton(onClick = onAddSubtask) {
Icon(Icons.Default.Add, contentDescription = "Ajouter")
}
}
// Tags section with add capability
Text(
text = "Tags",
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
if (state.tags.isNotEmpty()) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
state.tags.forEach { tag ->
tags.forEach { tag ->
Surface(
color = MaterialTheme.colorScheme.secondaryContainer,
shape = MaterialTheme.shapes.small,
@ -846,75 +945,302 @@ private fun EditTodoDialog(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "#$tag",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
Text("#$tag", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSecondaryContainer)
Spacer(modifier = Modifier.width(4.dp))
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Supprimer le tag",
modifier = Modifier.size(12.dp),
tint = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
Icon(Icons.Default.Close, contentDescription = null, modifier = Modifier.size(12.dp), tint = MaterialTheme.colorScheme.onSecondaryContainer)
}
}
}
// Add tag
if (isEditing) {
androidx.compose.foundation.text.BasicTextField(
value = newTagText,
onValueChange = onNewTagTextChanged,
textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface),
modifier = Modifier.width(100.dp).align(Alignment.CenterVertically),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { onAddTag(); isEditing = false }),
decorationBox = { inner ->
if (newTagText.isEmpty()) Text("Nouveau tag...", color = MaterialTheme.colorScheme.onSurfaceVariant)
inner()
}
)
}
}
}
}
}
}
@Composable
private fun TaskSubtasksSection(
subtasks: List<SubTask>,
newSubtaskText: String,
onNewSubtaskTextChanged: (String) -> Unit,
onAddSubtask: () -> Unit,
onRemoveSubtask: (Int) -> Unit,
onToggleSubtask: (Int) -> Unit,
onUpdateSubtaskContent: (Int, String) -> Unit
) {
Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
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() })
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)
)
IconButton(onClick = onAddTag) {
Icon(Icons.Default.Add, contentDescription = "Ajouter tag")
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)
}
}
}
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()
Row(
modifier = Modifier.fillMaxWidth().padding(start = 48.dp, end = 16.dp, top = 4.dp, bottom = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
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")
}
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
) {
IconButton(onClick = {
isDialMode = !isDialMode
prefs.edit().putBoolean("is_dial_mode", isDialMode).apply()
}) {
Icon(
if (isDialMode) Icons.Outlined.Keyboard else Icons.Default.Schedule,
contentDescription = "Changer de mode"
)
}
Row {
TextButton(onClick = onDismiss) { Text("Annuler") }
Spacer(modifier = Modifier.width(8.dp))
TextButton(onClick = {
onTimeSelected(timePickerState.hour, timePickerState.minute)
}) { Text("Valider") }
}
}
}
}
}
}
// ====== Brain Dump Dialog ======

608
patch.py Normal file
View 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')

BIN
temp.txt Normal file

Binary file not shown.

View File

@ -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