feat: Introduce local link data access with LinkDao and SyncManager for synchronization.
This commit is contained in:
parent
2358ed68b2
commit
34964f7b8c
@ -178,6 +178,12 @@ interface LinkDao {
|
||||
@Query("UPDATE links SET sync_status = 'SYNCED', local_modified_at = :timestamp WHERE id = :id")
|
||||
suspend fun markAsSynced(id: Int, timestamp: Long = System.currentTimeMillis())
|
||||
|
||||
@Query("SELECT id FROM links WHERE sync_status = 'SYNCED'")
|
||||
suspend fun getAllSyncedLinkIds(): List<Int>
|
||||
|
||||
@Query("DELETE FROM links WHERE id IN (:ids) AND sync_status = 'SYNCED'")
|
||||
suspend fun deleteSyncedLinksByIds(ids: List<Int>)
|
||||
|
||||
// ====== Insert / Update / Delete ======
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
|
||||
@ -371,7 +371,14 @@ class SyncManager @Inject constructor(
|
||||
api.deleteLink(link.id)
|
||||
linkDao.deleteLink(link.id)
|
||||
} catch (e: HttpException) {
|
||||
Log.e(TAG, "Échec suppression lien ${link.id}: ${e.code()}", e)
|
||||
val code = e.code()
|
||||
if (code == 404) {
|
||||
// Lien déjà supprimé sur le serveur — nettoyer localement
|
||||
linkDao.deleteLink(link.id)
|
||||
Log.d(TAG, "Lien ${link.id} déjà supprimé sur serveur (404), nettoyé localement")
|
||||
} else {
|
||||
Log.e(TAG, "Échec suppression lien ${link.id}: $code", e)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception lors de la suppression du lien ${link.id}", e)
|
||||
}
|
||||
@ -382,20 +389,20 @@ class SyncManager @Inject constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les données depuis le serveur (sync incrémentale)
|
||||
* S'arrête quand on rencontre des liens déjà synchronisés (non modifiés depuis la dernière sync)
|
||||
* Récupère les données depuis le serveur (sync complète).
|
||||
* Parcourt TOUTES les pages du serveur, met à jour les liens locaux,
|
||||
* et supprime les liens locaux SYNCED qui n'existent plus sur le serveur.
|
||||
*/
|
||||
private suspend fun pullFromServer() {
|
||||
var offset = 0
|
||||
val limit = 100
|
||||
var hasMore = true
|
||||
val lastSyncTimestamp = tokenManager.getLastSyncTimestamp()
|
||||
val syncStartTime = System.currentTimeMillis()
|
||||
val isFirstSync = lastSyncTimestamp == 0L
|
||||
var unchangedStreakCount = 0
|
||||
val unchangedStreakThreshold = 2 // Stop after 2 consecutive pages of unchanged links
|
||||
|
||||
Log.d(TAG, "Sync incrémentale: lastSync=${if (isFirstSync) "jamais" else java.time.Instant.ofEpochMilli(lastSyncTimestamp)}")
|
||||
// Collecter tous les IDs vus sur le serveur pour détecter les suppressions côté serveur
|
||||
val serverIds = mutableSetOf<Int>()
|
||||
|
||||
Log.d(TAG, "Démarrage du pull complet depuis le serveur")
|
||||
|
||||
while (hasMore) {
|
||||
try {
|
||||
@ -405,7 +412,7 @@ class SyncManager @Inject constructor(
|
||||
if (links.isEmpty()) {
|
||||
hasMore = false
|
||||
} else {
|
||||
// Filtrer les liens invalides (sans ID ou URL) et convertir en entités
|
||||
// Filtrer les liens invalides (sans ID ou URL) et exclure la config collections
|
||||
val validLinks = links.filter { dto ->
|
||||
val isValid = dto.id != null && !dto.url.isNullOrBlank()
|
||||
val isCollectionsConfig =
|
||||
@ -415,6 +422,9 @@ class SyncManager @Inject constructor(
|
||||
}
|
||||
Log.d(TAG, "${validLinks.size}/${links.size} liens valides")
|
||||
|
||||
// Enregistrer tous les IDs serveur
|
||||
validLinks.forEach { dto -> dto.id?.let { serverIds.add(it) } }
|
||||
|
||||
var newOrUpdatedCount = 0
|
||||
|
||||
val entities = validLinks.mapNotNull { dto ->
|
||||
@ -427,9 +437,8 @@ class SyncManager @Inject constructor(
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
// Check if this link has been modified since last sync
|
||||
if (!isFirstSync && existing != null && existing.updatedAt >= serverUpdatedAt && existing.syncStatus == SyncStatus.SYNCED) {
|
||||
// Link unchanged since last sync — skip
|
||||
// Si le lien existe déjà et n'a pas changé, on le skip
|
||||
if (existing != null && serverUpdatedAt > 0L && existing.updatedAt == serverUpdatedAt) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
@ -444,7 +453,7 @@ class SyncManager @Inject constructor(
|
||||
isPrivate = dto.isPrivate ?: false,
|
||||
isPinned = existing?.isPinned ?: false,
|
||||
createdAt = parseDate(dto.created),
|
||||
updatedAt = serverUpdatedAt,
|
||||
updatedAt = if (serverUpdatedAt > 0L) serverUpdatedAt else existing?.updatedAt ?: syncStartTime,
|
||||
syncStatus = SyncStatus.SYNCED,
|
||||
thumbnailUrl = existing?.thumbnailUrl,
|
||||
readingTimeMinutes = existing?.readingTimeMinutes,
|
||||
@ -469,17 +478,6 @@ class SyncManager @Inject constructor(
|
||||
|
||||
Log.d(TAG, "Page offset=$offset: $newOrUpdatedCount nouveaux/modifiés sur ${validLinks.size} valides")
|
||||
|
||||
// Incremental sync: stop early if we encounter pages with no changes
|
||||
if (!isFirstSync && newOrUpdatedCount == 0) {
|
||||
unchangedStreakCount++
|
||||
if (unchangedStreakCount >= unchangedStreakThreshold) {
|
||||
Log.d(TAG, "Sync incrémentale: $unchangedStreakThreshold pages consécutives sans changement, arrêt anticipé")
|
||||
hasMore = false
|
||||
}
|
||||
} else {
|
||||
unchangedStreakCount = 0
|
||||
}
|
||||
|
||||
offset += links.size
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@ -488,6 +486,21 @@ class SyncManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// Détecter et supprimer les liens locaux SYNCED qui n'existent plus sur le serveur
|
||||
try {
|
||||
val localSyncedIds = linkDao.getAllSyncedLinkIds()
|
||||
val deletedOnServer = localSyncedIds.filter { it !in serverIds }
|
||||
if (deletedOnServer.isNotEmpty()) {
|
||||
Log.d(TAG, "${deletedOnServer.size} liens supprimés côté serveur, nettoyage local")
|
||||
// Supprimer par lots pour éviter les limites SQLite
|
||||
deletedOnServer.chunked(500).forEach { batch ->
|
||||
linkDao.deleteSyncedLinksByIds(batch)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Erreur lors de la détection des suppressions serveur", e)
|
||||
}
|
||||
|
||||
// Save sync timestamp on success
|
||||
tokenManager.saveLastSyncTimestamp(syncStartTime)
|
||||
|
||||
@ -590,11 +603,17 @@ class SyncManager @Inject constructor(
|
||||
}
|
||||
|
||||
private fun parseDate(dateString: String?): Long {
|
||||
if (dateString.isNullOrBlank()) return System.currentTimeMillis()
|
||||
if (dateString.isNullOrBlank()) return 0L
|
||||
return try {
|
||||
java.time.Instant.parse(dateString).toEpochMilli()
|
||||
} catch (e: Exception) {
|
||||
System.currentTimeMillis()
|
||||
try {
|
||||
// Fallback: essayer le format ISO sans 'Z' (ex: "2024-01-15T10:30:00+01:00")
|
||||
java.time.OffsetDateTime.parse(dateString).toInstant().toEpochMilli()
|
||||
} catch (e2: Exception) {
|
||||
Log.w(TAG, "Impossible de parser la date: $dateString")
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
#Sat Feb 14 15:58:10 2026
|
||||
VERSION_NAME=2.1.7
|
||||
VERSION_CODE=22
|
||||
#Sun Feb 15 10:38:54 2026
|
||||
VERSION_NAME=2.1.8
|
||||
VERSION_CODE=23
|
||||
Loading…
x
Reference in New Issue
Block a user