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.Source
|
||||||
import com.planify.mobile.domain.model.SourceCalDavData
|
import com.planify.mobile.domain.model.SourceCalDavData
|
||||||
import com.planify.mobile.domain.model.SourceType
|
import com.planify.mobile.domain.model.SourceType
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.xmlpull.v1.XmlPullParser
|
import org.xmlpull.v1.XmlPullParser
|
||||||
import org.xmlpull.v1.XmlPullParserFactory
|
import org.xmlpull.v1.XmlPullParserFactory
|
||||||
import java.io.StringReader
|
import java.io.StringReader
|
||||||
@@ -21,17 +23,17 @@ sealed class DiscoveryResult {
|
|||||||
@Singleton
|
@Singleton
|
||||||
class CalDavDiscovery @Inject constructor(private val client: CalDavClient) {
|
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 credentials = CalDavClient.basicCredentials(username, password)
|
||||||
val normalizedBase = baseUrl.trimEnd('/')
|
val normalizedBase = baseUrl.trimEnd('/')
|
||||||
|
|
||||||
// Step 1: resolve principal URL
|
// Step 1: resolve principal URL
|
||||||
val principalUrl = resolvePrincipalUrl(normalizedBase, credentials, username)
|
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
|
// Step 2: find calendar home
|
||||||
val calendarHome = resolveCalendarHome(principalUrl, credentials)
|
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
|
// Step 3: list VTODO-capable calendars
|
||||||
val calendars = listCalendars(calendarHome, credentials, username, baseUrl)
|
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? {
|
private fun resolvePrincipalUrl(baseUrl: String, credentials: String, username: String): String? {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import com.planify.mobile.data.local.entity.TaskEntity
|
|||||||
ReminderEntity::class,
|
ReminderEntity::class,
|
||||||
SourceEntity::class,
|
SourceEntity::class,
|
||||||
],
|
],
|
||||||
version = 1,
|
version = 2,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|||||||
@@ -45,4 +45,5 @@ data class TaskEntity(
|
|||||||
@ColumnInfo(name = "ical_url") val icalUrl: String? = null,
|
@ColumnInfo(name = "ical_url") val icalUrl: String? = null,
|
||||||
val etag: String? = null,
|
val etag: String? = null,
|
||||||
@ColumnInfo(name = "responsible_uid") val responsibleUid: 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,
|
checked = checked,
|
||||||
dueDate = dueDate?.let { Json.encodeToString(DueDate.serializer(), it) },
|
dueDate = dueDate?.let { Json.encodeToString(DueDate.serializer(), it) },
|
||||||
deadlineDate = deadlineDate,
|
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,
|
pinned = pinned,
|
||||||
collapsed = collapsed,
|
collapsed = collapsed,
|
||||||
childOrder = childOrder,
|
childOrder = childOrder,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.planify.mobile.ui.settings
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
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.Add
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material.icons.outlined.Download
|
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.Sync
|
||||||
import androidx.compose.material.icons.outlined.Warning
|
import androidx.compose.material.icons.outlined.Warning
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
@@ -58,6 +60,7 @@ fun SettingsScreen(
|
|||||||
val discovery by viewModel.discoveryInProgress.collectAsState()
|
val discovery by viewModel.discoveryInProgress.collectAsState()
|
||||||
val exportUri by viewModel.exportUri.collectAsState()
|
val exportUri by viewModel.exportUri.collectAsState()
|
||||||
var showAddAccount by remember { mutableStateOf(false) }
|
var showAddAccount by remember { mutableStateOf(false) }
|
||||||
|
var editingSource by remember { mutableStateOf<com.planify.mobile.domain.model.Source?>(null) }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
LaunchedEffect(exportUri) {
|
LaunchedEffect(exportUri) {
|
||||||
@@ -156,7 +159,11 @@ fun SettingsScreen(
|
|||||||
// ── Comptes CalDAV ────────────────────────────────────────────────────
|
// ── Comptes CalDAV ────────────────────────────────────────────────────
|
||||||
SectionTitle("Comptes CalDAV")
|
SectionTitle("Comptes CalDAV")
|
||||||
state.caldavSources.forEach { source ->
|
state.caldavSources.forEach { source ->
|
||||||
CalDavSourceRow(source = source, onDelete = { viewModel.removeCalDavAccount(source) })
|
CalDavSourceRow(
|
||||||
|
source = source,
|
||||||
|
onEdit = { editingSource = source },
|
||||||
|
onDelete = { viewModel.removeCalDavAccount(source) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text("Ajouter un compte") },
|
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
|
@Composable
|
||||||
@@ -242,7 +260,7 @@ private fun SectionTitle(text: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CalDavSourceRow(source: Source, onDelete: () -> Unit) {
|
private fun CalDavSourceRow(source: Source, onEdit: () -> Unit, onDelete: () -> Unit) {
|
||||||
val connectionFailed = source.caldavData?.calendarHomeUrl == null
|
val connectionFailed = source.caldavData?.calendarHomeUrl == null
|
||||||
ListItem(
|
ListItem(
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
@@ -266,13 +284,71 @@ private fun CalDavSourceRow(source: Source, onDelete: () -> Unit) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
IconButton(onClick = onDelete) {
|
Row {
|
||||||
Icon(Icons.Outlined.Delete, contentDescription = "Supprimer", tint = MaterialTheme.colorScheme.error)
|
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
|
@Composable
|
||||||
private fun AddCalDavAccountDialog(
|
private fun AddCalDavAccountDialog(
|
||||||
onDismiss: () -> Unit,
|
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 {
|
fun removeCalDavAccount(source: Source) = viewModelScope.launch {
|
||||||
credentialStore.deletePassword(source.id)
|
credentialStore.deletePassword(source.id)
|
||||||
sourceRepository.deleteSource(source.id)
|
sourceRepository.deleteSource(source.id)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import com.planify.mobile.domain.model.DueDate
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
|
import java.time.ZoneOffset
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -46,7 +47,7 @@ fun DueDatePickerSheet(
|
|||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
val datePickerState = rememberDatePickerState(
|
val datePickerState = rememberDatePickerState(
|
||||||
initialSelectedDateMillis = currentDueDate?.date
|
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) }
|
var showRecurrence by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -103,7 +104,7 @@ fun DueDatePickerSheet(
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
val millis = datePickerState.selectedDateMillis
|
val millis = datePickerState.selectedDateMillis
|
||||||
val date = millis?.let {
|
val date = millis?.let {
|
||||||
Instant.ofEpochMilli(it).atZone(ZoneId.UTC).toLocalDate().toString()
|
Instant.ofEpochMilli(it).atZone(ZoneOffset.UTC).toLocalDate().toString()
|
||||||
} ?: return@TextButton
|
} ?: return@TextButton
|
||||||
onConfirm(DueDate(date = date, isRecurring = currentDueDate?.isRecurring ?: false,
|
onConfirm(DueDate(date = date, isRecurring = currentDueDate?.isRecurring ?: false,
|
||||||
recurrencyType = currentDueDate?.recurrencyType ?: com.planify.mobile.domain.model.RecurrencyType.NONE))
|
recurrencyType = currentDueDate?.recurrencyType ?: com.planify.mobile.domain.model.RecurrencyType.NONE))
|
||||||
@@ -119,7 +120,7 @@ fun DueDatePickerSheet(
|
|||||||
showRecurrence = false
|
showRecurrence = false
|
||||||
val millis = datePickerState.selectedDateMillis
|
val millis = datePickerState.selectedDateMillis
|
||||||
val date = millis?.let {
|
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()
|
} ?: currentDueDate?.date ?: LocalDate.now().toString()
|
||||||
onConfirm(recDueDate?.copy(date = date))
|
onConfirm(recDueDate?.copy(date = date))
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.jetbrains.kotlin.android) 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.hilt) apply false
|
||||||
alias(libs.plugins.ksp) apply false
|
alias(libs.plugins.ksp) apply false
|
||||||
alias(libs.plugins.kotlin.serialization) apply false
|
alias(libs.plugins.kotlin.serialization) apply false
|
||||||
|
|||||||
+13
-11
@@ -1,20 +1,21 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.4.0"
|
agp = "8.7.3"
|
||||||
kotlin = "1.9.24"
|
kotlin = "2.0.21"
|
||||||
|
ksp = "2.0.21-1.0.28"
|
||||||
coreKtx = "1.13.1"
|
coreKtx = "1.13.1"
|
||||||
lifecycleRuntimeKtx = "2.8.3"
|
lifecycleRuntimeKtx = "2.8.7"
|
||||||
activityCompose = "1.9.0"
|
activityCompose = "1.9.3"
|
||||||
composeBom = "2024.06.00"
|
composeBom = "2024.12.01"
|
||||||
hilt = "2.51.1"
|
hilt = "2.52"
|
||||||
hiltNavigationCompose = "1.2.0"
|
hiltNavigationCompose = "1.2.0"
|
||||||
navigationCompose = "2.7.7"
|
navigationCompose = "2.8.5"
|
||||||
room = "2.6.1"
|
room = "2.6.1"
|
||||||
coroutines = "1.8.1"
|
coroutines = "1.9.0"
|
||||||
okhttp = "4.12.0"
|
okhttp = "4.12.0"
|
||||||
datastore = "1.1.1"
|
datastore = "1.1.1"
|
||||||
securityCrypto = "1.1.0-alpha06"
|
securityCrypto = "1.1.0-alpha06"
|
||||||
workManager = "2.9.0"
|
workManager = "2.10.0"
|
||||||
serialization = "1.6.3"
|
serialization = "1.7.3"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitExt = "1.2.1"
|
junitExt = "1.2.1"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
@@ -56,6 +57,7 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co
|
|||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
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" }
|
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" }
|
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
|
|||||||
Reference in New Issue
Block a user