From 0fd300ffdc0066542f86929a28e6afb62a2cfa0f Mon Sep 17 00:00:00 2001 From: Gato Date: Sat, 6 Jun 2026 07:45:48 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20champ=20is=5Fdeleted=20manquant=20dans?= =?UTF-8?q?=20TaskEntity,=20ZoneOffset.UTC,=20s=C3=A9rialisation=20des=20l?= =?UTF-8?q?abels,=20toolchain=20Kotlin=202.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../mobile/data/caldav/CalDavDiscovery.kt | 10 ++- .../planify/mobile/data/local/AppDatabase.kt | 2 +- .../mobile/data/local/entity/TaskEntity.kt | 1 + .../data/repository/TaskRepositoryImpl.kt | 2 +- .../mobile/ui/settings/SettingsScreen.kt | 84 ++++++++++++++++++- .../mobile/ui/settings/SettingsViewModel.kt | 34 ++++++++ .../mobile/ui/task/DueDatePickerSheet.kt | 7 +- build.gradle.kts | 1 + gradle/libs.versions.toml | 24 +++--- 9 files changed, 141 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/planify/mobile/data/caldav/CalDavDiscovery.kt b/app/src/main/java/com/planify/mobile/data/caldav/CalDavDiscovery.kt index a074afb..0d5f4bf 100644 --- a/app/src/main/java/com/planify/mobile/data/caldav/CalDavDiscovery.kt +++ b/app/src/main/java/com/planify/mobile/data/caldav/CalDavDiscovery.kt @@ -4,6 +4,8 @@ import com.planify.mobile.domain.model.CalDavType import com.planify.mobile.domain.model.Source import com.planify.mobile.domain.model.SourceCalDavData import com.planify.mobile.domain.model.SourceType +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserFactory import java.io.StringReader @@ -21,17 +23,17 @@ sealed class DiscoveryResult { @Singleton class CalDavDiscovery @Inject constructor(private val client: CalDavClient) { - suspend fun discover(baseUrl: String, username: String, password: String): DiscoveryResult { + suspend fun discover(baseUrl: String, username: String, password: String): DiscoveryResult = withContext(Dispatchers.IO) { val credentials = CalDavClient.basicCredentials(username, password) val normalizedBase = baseUrl.trimEnd('/') // Step 1: resolve principal URL val principalUrl = resolvePrincipalUrl(normalizedBase, credentials, username) - ?: return DiscoveryResult.Failure("Impossible de trouver le principal CalDAV") + ?: return@withContext DiscoveryResult.Failure("Impossible de trouver le principal CalDAV") // Step 2: find calendar home val calendarHome = resolveCalendarHome(principalUrl, credentials) - ?: return DiscoveryResult.Failure("Impossible de trouver le calendar home") + ?: return@withContext DiscoveryResult.Failure("Impossible de trouver le calendar home") // Step 3: list VTODO-capable calendars val calendars = listCalendars(calendarHome, credentials, username, baseUrl) @@ -54,7 +56,7 @@ class CalDavDiscovery @Inject constructor(private val client: CalDavClient) { ), ) } - return DiscoveryResult.Success(sources) + DiscoveryResult.Success(sources) } private fun resolvePrincipalUrl(baseUrl: String, credentials: String, username: String): String? { diff --git a/app/src/main/java/com/planify/mobile/data/local/AppDatabase.kt b/app/src/main/java/com/planify/mobile/data/local/AppDatabase.kt index 0190df0..4688203 100644 --- a/app/src/main/java/com/planify/mobile/data/local/AppDatabase.kt +++ b/app/src/main/java/com/planify/mobile/data/local/AppDatabase.kt @@ -24,7 +24,7 @@ import com.planify.mobile.data.local.entity.TaskEntity ReminderEntity::class, SourceEntity::class, ], - version = 1, + version = 2, exportSchema = true, ) abstract class AppDatabase : RoomDatabase() { diff --git a/app/src/main/java/com/planify/mobile/data/local/entity/TaskEntity.kt b/app/src/main/java/com/planify/mobile/data/local/entity/TaskEntity.kt index 868c8ff..91efd2d 100644 --- a/app/src/main/java/com/planify/mobile/data/local/entity/TaskEntity.kt +++ b/app/src/main/java/com/planify/mobile/data/local/entity/TaskEntity.kt @@ -45,4 +45,5 @@ data class TaskEntity( @ColumnInfo(name = "ical_url") val icalUrl: String? = null, val etag: String? = null, @ColumnInfo(name = "responsible_uid") val responsibleUid: String? = null, + @ColumnInfo(name = "is_deleted") val isDeleted: Boolean = false, ) diff --git a/app/src/main/java/com/planify/mobile/data/repository/TaskRepositoryImpl.kt b/app/src/main/java/com/planify/mobile/data/repository/TaskRepositoryImpl.kt index 13b84d8..c6cee91 100644 --- a/app/src/main/java/com/planify/mobile/data/repository/TaskRepositoryImpl.kt +++ b/app/src/main/java/com/planify/mobile/data/repository/TaskRepositoryImpl.kt @@ -117,7 +117,7 @@ class TaskRepositoryImpl @Inject constructor( checked = checked, dueDate = dueDate?.let { Json.encodeToString(DueDate.serializer(), it) }, deadlineDate = deadlineDate, - labels = Json.encodeToString(kotlinx.serialization.builtins.ListSerializer(kotlinx.serialization.builtins.serializer()), labels), + labels = Json.encodeToString(kotlinx.serialization.builtins.ListSerializer(kotlinx.serialization.serializer()), labels), pinned = pinned, collapsed = collapsed, childOrder = childOrder, diff --git a/app/src/main/java/com/planify/mobile/ui/settings/SettingsScreen.kt b/app/src/main/java/com/planify/mobile/ui/settings/SettingsScreen.kt index 3c7a37b..3d15816 100644 --- a/app/src/main/java/com/planify/mobile/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/planify/mobile/ui/settings/SettingsScreen.kt @@ -3,6 +3,7 @@ package com.planify.mobile.ui.settings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -14,6 +15,7 @@ import androidx.compose.material.icons.outlined.AccountCircle import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Download +import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Sync import androidx.compose.material.icons.outlined.Warning import androidx.compose.material3.AlertDialog @@ -58,6 +60,7 @@ fun SettingsScreen( val discovery by viewModel.discoveryInProgress.collectAsState() val exportUri by viewModel.exportUri.collectAsState() var showAddAccount by remember { mutableStateOf(false) } + var editingSource by remember { mutableStateOf(null) } val context = LocalContext.current LaunchedEffect(exportUri) { @@ -156,7 +159,11 @@ fun SettingsScreen( // ── Comptes CalDAV ──────────────────────────────────────────────────── SectionTitle("Comptes CalDAV") state.caldavSources.forEach { source -> - CalDavSourceRow(source = source, onDelete = { viewModel.removeCalDavAccount(source) }) + CalDavSourceRow( + source = source, + onEdit = { editingSource = source }, + onDelete = { viewModel.removeCalDavAccount(source) }, + ) } ListItem( headlineContent = { Text("Ajouter un compte") }, @@ -229,6 +236,17 @@ fun SettingsScreen( }, ) } + + editingSource?.let { source -> + EditCalDavAccountDialog( + source = source, + onDismiss = { editingSource = null }, + onConfirm = { url, user, pwd -> + viewModel.updateCalDavAccount(source, url, user, pwd) + editingSource = null + }, + ) + } } @Composable @@ -242,7 +260,7 @@ private fun SectionTitle(text: String) { } @Composable -private fun CalDavSourceRow(source: Source, onDelete: () -> Unit) { +private fun CalDavSourceRow(source: Source, onEdit: () -> Unit, onDelete: () -> Unit) { val connectionFailed = source.caldavData?.calendarHomeUrl == null ListItem( leadingContent = { @@ -266,13 +284,71 @@ private fun CalDavSourceRow(source: Source, onDelete: () -> Unit) { } }, trailingContent = { - IconButton(onClick = onDelete) { - Icon(Icons.Outlined.Delete, contentDescription = "Supprimer", tint = MaterialTheme.colorScheme.error) + Row { + IconButton(onClick = onEdit) { + Icon(Icons.Outlined.Edit, contentDescription = "Modifier") + } + Spacer(Modifier.width(4.dp)) + IconButton(onClick = onDelete) { + Icon(Icons.Outlined.Delete, contentDescription = "Supprimer", tint = MaterialTheme.colorScheme.error) + } } }, ) } +@Composable +private fun EditCalDavAccountDialog( + source: Source, + onDismiss: () -> Unit, + onConfirm: (url: String, username: String, password: String) -> Unit, +) { + var url by remember { mutableStateOf(source.caldavData?.serverUrl ?: "") } + var username by remember { mutableStateOf(source.caldavData?.username ?: "") } + var password by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Modifier le compte CalDAV") }, + text = { + Column { + OutlinedTextField( + value = url, + onValueChange = { url = it }, + label = { Text("URL du serveur") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + Spacer(Modifier.height(8.dp)) + OutlinedTextField( + value = username, + onValueChange = { username = it }, + label = { Text("Nom d'utilisateur") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + Spacer(Modifier.height(8.dp)) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text("Nouveau mot de passe") }, + placeholder = { Text("Laisser vide pour conserver") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + visualTransformation = androidx.compose.ui.text.input.PasswordVisualTransformation(), + ) + } + }, + confirmButton = { + Button( + onClick = { onConfirm(url.trim(), username.trim(), password) }, + enabled = url.isNotBlank() && username.isNotBlank(), + ) { Text("Enregistrer") } + }, + dismissButton = { TextButton(onClick = onDismiss) { Text("Annuler") } }, + ) +} + @Composable private fun AddCalDavAccountDialog( onDismiss: () -> Unit, diff --git a/app/src/main/java/com/planify/mobile/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/planify/mobile/ui/settings/SettingsViewModel.kt index e4d850d..c9d8fbe 100644 --- a/app/src/main/java/com/planify/mobile/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/planify/mobile/ui/settings/SettingsViewModel.kt @@ -126,6 +126,40 @@ class SettingsViewModel @Inject constructor( } } + fun updateCalDavAccount(source: Source, newUrl: String, newUsername: String, newPassword: String) { + _discoveryState.update { true to null } + val effectivePassword = newPassword.ifBlank { credentialStore.getPassword(source.id) } + viewModelScope.launch { + when (val result = discovery.discover(newUrl, newUsername, effectivePassword)) { + is DiscoveryResult.Success -> { + credentialStore.deletePassword(source.id) + sourceRepository.deleteSource(source.id) + result.sources.forEach { s -> + credentialStore.savePassword(s.id, effectivePassword) + sourceRepository.insertSource(s) + } + _discoveryState.update { false to null } + if (uiState.value.syncEnabled) syncScheduler.schedule() + } + is DiscoveryResult.Failure -> { + val now = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + val updated = source.copy( + displayName = newUsername, + updatedAt = now, + caldavData = source.caldavData?.copy( + serverUrl = newUrl, + username = newUsername, + calendarHomeUrl = null, + ), + ) + credentialStore.savePassword(source.id, effectivePassword) + sourceRepository.updateSource(updated) + _discoveryState.update { false to result.message } + } + } + } + } + fun removeCalDavAccount(source: Source) = viewModelScope.launch { credentialStore.deletePassword(source.id) sourceRepository.deleteSource(source.id) diff --git a/app/src/main/java/com/planify/mobile/ui/task/DueDatePickerSheet.kt b/app/src/main/java/com/planify/mobile/ui/task/DueDatePickerSheet.kt index 427958e..88d412f 100644 --- a/app/src/main/java/com/planify/mobile/ui/task/DueDatePickerSheet.kt +++ b/app/src/main/java/com/planify/mobile/ui/task/DueDatePickerSheet.kt @@ -34,6 +34,7 @@ import com.planify.mobile.domain.model.DueDate import java.time.Instant import java.time.LocalDate import java.time.ZoneId +import java.time.ZoneOffset import java.time.format.DateTimeFormatter @OptIn(ExperimentalMaterial3Api::class) @@ -46,7 +47,7 @@ fun DueDatePickerSheet( val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val datePickerState = rememberDatePickerState( initialSelectedDateMillis = currentDueDate?.date - ?.let { runCatching { LocalDate.parse(it).atStartOfDay(ZoneId.UTC).toInstant().toEpochMilli() }.getOrNull() } + ?.let { runCatching { LocalDate.parse(it).atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli() }.getOrNull() } ) var showRecurrence by remember { mutableStateOf(false) } @@ -103,7 +104,7 @@ fun DueDatePickerSheet( TextButton(onClick = { val millis = datePickerState.selectedDateMillis val date = millis?.let { - Instant.ofEpochMilli(it).atZone(ZoneId.UTC).toLocalDate().toString() + Instant.ofEpochMilli(it).atZone(ZoneOffset.UTC).toLocalDate().toString() } ?: return@TextButton onConfirm(DueDate(date = date, isRecurring = currentDueDate?.isRecurring ?: false, recurrencyType = currentDueDate?.recurrencyType ?: com.planify.mobile.domain.model.RecurrencyType.NONE)) @@ -119,7 +120,7 @@ fun DueDatePickerSheet( showRecurrence = false val millis = datePickerState.selectedDateMillis val date = millis?.let { - Instant.ofEpochMilli(it).atZone(ZoneId.UTC).toLocalDate().toString() + Instant.ofEpochMilli(it).atZone(ZoneOffset.UTC).toLocalDate().toString() } ?: currentDueDate?.date ?: LocalDate.now().toString() onConfirm(recDueDate?.copy(date = date)) }, diff --git a/build.gradle.kts b/build.gradle.kts index c848b72..d5c9ca8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.jetbrains.kotlin.android) apply false + alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.hilt) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.kotlin.serialization) apply false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 23fc40d..f8f7aad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,20 +1,21 @@ [versions] -agp = "8.4.0" -kotlin = "1.9.24" +agp = "8.7.3" +kotlin = "2.0.21" +ksp = "2.0.21-1.0.28" coreKtx = "1.13.1" -lifecycleRuntimeKtx = "2.8.3" -activityCompose = "1.9.0" -composeBom = "2024.06.00" -hilt = "2.51.1" +lifecycleRuntimeKtx = "2.8.7" +activityCompose = "1.9.3" +composeBom = "2024.12.01" +hilt = "2.52" hiltNavigationCompose = "1.2.0" -navigationCompose = "2.7.7" +navigationCompose = "2.8.5" room = "2.6.1" -coroutines = "1.8.1" +coroutines = "1.9.0" okhttp = "4.12.0" datastore = "1.1.1" securityCrypto = "1.1.0-alpha06" -workManager = "2.9.0" -serialization = "1.6.3" +workManager = "2.10.0" +serialization = "1.7.3" junit = "4.13.2" junitExt = "1.2.1" espressoCore = "3.6.1" @@ -56,6 +57,7 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co [plugins] android-application = { id = "com.android.application", version.ref = "agp" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } -ksp = { id = "com.google.devtools.ksp", version = "1.9.24-1.0.20" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }