fix: champ is_deleted manquant dans TaskEntity, ZoneOffset.UTC, sérialisation des labels, toolchain Kotlin 2.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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? {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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<String>()), labels),
|
||||
pinned = pinned,
|
||||
collapsed = collapsed,
|
||||
childOrder = childOrder,
|
||||
|
||||
@@ -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<com.planify.mobile.domain.model.Source?>(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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user