feat(v1.8.0): IMPL_016 - Server Deletion Detection
- Add DELETED_ON_SERVER to SyncStatus enum - Add deletedOnServerCount to SyncResult - Implement detectServerDeletions() function in WebDavSyncService - Integrate server deletion detection in downloadRemoteNotes() - Update UI icons in NoteCard, NoteCardGrid, NoteCardCompact - Add string resources for deleted_on_server status - No additional HTTP requests (uses existing PROPFIND data) - Zero performance impact Closes #IMPL_016
This commit is contained in:
@@ -89,6 +89,7 @@ class NotesAdapter(
|
||||
SyncStatus.PENDING -> android.R.drawable.ic_popup_sync
|
||||
SyncStatus.CONFLICT -> android.R.drawable.ic_dialog_alert
|
||||
SyncStatus.LOCAL_ONLY -> android.R.drawable.ic_menu_save
|
||||
SyncStatus.DELETED_ON_SERVER -> android.R.drawable.ic_menu_delete // 🆕 v1.8.0
|
||||
}
|
||||
imageViewSyncStatus.setImageResource(syncIcon)
|
||||
imageViewSyncStatus.visibility = View.VISIBLE
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
package dev.dettmer.simplenotes.models
|
||||
|
||||
/**
|
||||
* Sync-Status einer Notiz
|
||||
*
|
||||
* v1.4.0: Initial (LOCAL_ONLY, SYNCED, PENDING, CONFLICT)
|
||||
* v1.8.0: DELETED_ON_SERVER hinzugefügt
|
||||
*/
|
||||
enum class SyncStatus {
|
||||
LOCAL_ONLY, // Noch nie gesynct
|
||||
SYNCED, // Erfolgreich gesynct
|
||||
PENDING, // Wartet auf Sync
|
||||
CONFLICT // Konflikt erkannt
|
||||
CONFLICT, // Konflikt erkannt
|
||||
DELETED_ON_SERVER // 🆕 v1.8.0: Server hat gelöscht, lokal noch vorhanden
|
||||
}
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
package dev.dettmer.simplenotes.sync
|
||||
|
||||
/**
|
||||
* Ergebnis eines Sync-Vorgangs
|
||||
*
|
||||
* v1.7.0: Initial
|
||||
* v1.8.0: deletedOnServerCount hinzugefügt
|
||||
*/
|
||||
data class SyncResult(
|
||||
val isSuccess: Boolean,
|
||||
val syncedCount: Int = 0,
|
||||
val conflictCount: Int = 0,
|
||||
val deletedOnServerCount: Int = 0, // 🆕 v1.8.0
|
||||
val errorMessage: String? = null
|
||||
) {
|
||||
val hasConflicts: Boolean
|
||||
get() = conflictCount > 0
|
||||
val hasConflicts: Boolean get() = conflictCount > 0
|
||||
val hasServerDeletions: Boolean get() = deletedOnServerCount > 0 // 🆕 v1.8.0
|
||||
}
|
||||
|
||||
@@ -655,6 +655,7 @@ class WebDavSyncService(private val context: Context) {
|
||||
|
||||
Logger.d(TAG, "📍 Step 5: Downloading remote notes")
|
||||
// Download remote notes
|
||||
var deletedOnServerCount = 0 // 🆕 v1.8.0
|
||||
try {
|
||||
Logger.d(TAG, "⬇️ Downloading remote notes...")
|
||||
val downloadResult = downloadRemoteNotes(
|
||||
@@ -664,10 +665,12 @@ class WebDavSyncService(private val context: Context) {
|
||||
)
|
||||
syncedCount += downloadResult.downloadedCount
|
||||
conflictCount += downloadResult.conflictCount
|
||||
deletedOnServerCount = downloadResult.deletedOnServerCount // 🆕 v1.8.0
|
||||
Logger.d(
|
||||
TAG,
|
||||
"✅ Downloaded: ${downloadResult.downloadedCount} notes, " +
|
||||
"Conflicts: ${downloadResult.conflictCount}"
|
||||
"Conflicts: ${downloadResult.conflictCount}, " +
|
||||
"Deleted on server: ${downloadResult.deletedOnServerCount}" // 🆕 v1.8.0
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "💥 CRASH in downloadRemoteNotes()!", e)
|
||||
@@ -724,12 +727,16 @@ class WebDavSyncService(private val context: Context) {
|
||||
if (markdownImportedCount > 0 && syncedCount > 0) {
|
||||
Logger.d(TAG, "📝 Including $markdownImportedCount Markdown file updates")
|
||||
}
|
||||
if (deletedOnServerCount > 0) { // 🆕 v1.8.0
|
||||
Logger.d(TAG, "🗑️ Detected $deletedOnServerCount notes deleted on server")
|
||||
}
|
||||
Logger.d(TAG, "═══════════════════════════════════════")
|
||||
|
||||
SyncResult(
|
||||
isSuccess = true,
|
||||
syncedCount = effectiveSyncedCount,
|
||||
conflictCount = conflictCount
|
||||
conflictCount = conflictCount,
|
||||
deletedOnServerCount = deletedOnServerCount // 🆕 v1.8.0
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -1038,9 +1045,45 @@ class WebDavSyncService(private val context: Context) {
|
||||
|
||||
private data class DownloadResult(
|
||||
val downloadedCount: Int,
|
||||
val conflictCount: Int
|
||||
val conflictCount: Int,
|
||||
val deletedOnServerCount: Int = 0 // 🆕 v1.8.0
|
||||
)
|
||||
|
||||
/**
|
||||
* 🆕 v1.8.0: Erkennt Notizen, die auf dem Server gelöscht wurden
|
||||
*
|
||||
* Keine zusätzlichen HTTP-Requests! Nutzt die bereits geladene
|
||||
* serverNoteIds-Liste aus dem PROPFIND-Request.
|
||||
*
|
||||
* @param serverNoteIds Set aller Note-IDs auf dem Server (aus PROPFIND)
|
||||
* @param localNotes Alle lokalen Notizen
|
||||
* @return Anzahl der als DELETED_ON_SERVER markierten Notizen
|
||||
*/
|
||||
private fun detectServerDeletions(
|
||||
serverNoteIds: Set<String>,
|
||||
localNotes: List<Note>
|
||||
): Int {
|
||||
var deletedCount = 0
|
||||
|
||||
localNotes.forEach { note ->
|
||||
// Nur SYNCED-Notizen prüfen:
|
||||
// - LOCAL_ONLY: War nie auf Server → irrelevant
|
||||
// - PENDING: Soll hochgeladen werden → nicht überschreiben
|
||||
// - CONFLICT: Wird separat behandelt
|
||||
// - DELETED_ON_SERVER: Bereits markiert
|
||||
if (note.syncStatus == SyncStatus.SYNCED && note.id !in serverNoteIds) {
|
||||
val updatedNote = note.copy(syncStatus = SyncStatus.DELETED_ON_SERVER)
|
||||
storage.saveNote(updatedNote)
|
||||
deletedCount++
|
||||
|
||||
Logger.d(TAG, "Note '${note.title}' (${note.id}) " +
|
||||
"was deleted on server, marked as DELETED_ON_SERVER")
|
||||
}
|
||||
}
|
||||
|
||||
return deletedCount
|
||||
}
|
||||
|
||||
@Suppress("NestedBlockDepth", "LoopWithTooManyJumpStatements")
|
||||
// Sync logic requires nested conditions for comprehensive error handling and conflict resolution
|
||||
private fun downloadRemoteNotes(
|
||||
@@ -1062,6 +1105,9 @@ class WebDavSyncService(private val context: Context) {
|
||||
// Use provided deletion tracker (allows fresh tracker from restore)
|
||||
var trackerModified = false
|
||||
|
||||
// 🆕 v1.8.0: Collect server note IDs for deletion detection
|
||||
val serverNoteIds = mutableSetOf<String>()
|
||||
|
||||
try {
|
||||
// 🆕 PHASE 1: Download from /notes/ (new structure v1.2.1+)
|
||||
val notesUrl = getNotesUrl(serverUrl)
|
||||
@@ -1077,6 +1123,12 @@ class WebDavSyncService(private val context: Context) {
|
||||
val jsonFiles = resources.filter { !it.isDirectory && it.name.endsWith(".json") }
|
||||
Logger.d(TAG, " 📊 Found ${jsonFiles.size} JSON files on server")
|
||||
|
||||
// 🆕 v1.8.0: Extract server note IDs
|
||||
jsonFiles.forEach { resource ->
|
||||
val noteId = resource.name.removeSuffix(".json")
|
||||
serverNoteIds.add(noteId)
|
||||
}
|
||||
|
||||
for (resource in jsonFiles) {
|
||||
|
||||
val noteId = resource.name.removeSuffix(".json")
|
||||
@@ -1317,8 +1369,16 @@ class WebDavSyncService(private val context: Context) {
|
||||
Logger.d(TAG, "💾 Deletion tracker updated")
|
||||
}
|
||||
|
||||
// 🆕 v1.8.0: Server-Deletions erkennen (nach Downloads)
|
||||
val allLocalNotes = storage.loadAllNotes()
|
||||
val deletedOnServerCount = detectServerDeletions(serverNoteIds, allLocalNotes)
|
||||
|
||||
if (deletedOnServerCount > 0) {
|
||||
Logger.d(TAG, "$deletedOnServerCount note(s) detected as deleted on server")
|
||||
}
|
||||
|
||||
Logger.d(TAG, "📊 Total: $downloadedCount downloaded, $conflictCount conflicts, $skippedDeleted deleted")
|
||||
return DownloadResult(downloadedCount, conflictCount)
|
||||
return DownloadResult(downloadedCount, conflictCount, deletedOnServerCount)
|
||||
}
|
||||
|
||||
private fun saveLastSyncTimestamp() {
|
||||
|
||||
@@ -186,11 +186,19 @@ fun NoteCard(
|
||||
SyncStatus.PENDING -> Icons.Outlined.CloudSync
|
||||
SyncStatus.CONFLICT -> Icons.Default.Warning
|
||||
SyncStatus.LOCAL_ONLY -> Icons.Outlined.CloudOff
|
||||
SyncStatus.DELETED_ON_SERVER -> Icons.Outlined.CloudOff // 🆕 v1.8.0
|
||||
},
|
||||
contentDescription = when (note.syncStatus) {
|
||||
SyncStatus.SYNCED -> stringResource(R.string.sync_status_synced)
|
||||
SyncStatus.PENDING -> stringResource(R.string.sync_status_pending)
|
||||
SyncStatus.CONFLICT -> stringResource(R.string.sync_status_conflict)
|
||||
SyncStatus.LOCAL_ONLY -> stringResource(R.string.sync_status_local_only)
|
||||
SyncStatus.DELETED_ON_SERVER -> stringResource(R.string.sync_status_deleted_on_server) // 🆕 v1.8.0
|
||||
},
|
||||
contentDescription = null,
|
||||
tint = when (note.syncStatus) {
|
||||
SyncStatus.SYNCED -> MaterialTheme.colorScheme.primary
|
||||
SyncStatus.CONFLICT -> MaterialTheme.colorScheme.error
|
||||
SyncStatus.DELETED_ON_SERVER -> MaterialTheme.colorScheme.outline.copy(alpha = 0.5f) // 🆕 v1.8.0
|
||||
else -> MaterialTheme.colorScheme.outline
|
||||
},
|
||||
modifier = Modifier.size(16.dp)
|
||||
|
||||
@@ -187,11 +187,13 @@ fun NoteCardCompact(
|
||||
SyncStatus.PENDING -> Icons.Outlined.CloudSync
|
||||
SyncStatus.CONFLICT -> Icons.Default.Warning
|
||||
SyncStatus.LOCAL_ONLY -> Icons.Outlined.CloudOff
|
||||
SyncStatus.DELETED_ON_SERVER -> Icons.Outlined.CloudOff // 🆕 v1.8.0
|
||||
},
|
||||
contentDescription = null,
|
||||
tint = when (note.syncStatus) {
|
||||
SyncStatus.SYNCED -> MaterialTheme.colorScheme.primary
|
||||
SyncStatus.CONFLICT -> MaterialTheme.colorScheme.error
|
||||
SyncStatus.DELETED_ON_SERVER -> MaterialTheme.colorScheme.outline.copy(alpha = 0.5f) // 🆕 v1.8.0
|
||||
else -> MaterialTheme.colorScheme.outline
|
||||
},
|
||||
modifier = Modifier.size(14.dp)
|
||||
|
||||
@@ -199,11 +199,13 @@ fun NoteCardGrid(
|
||||
SyncStatus.PENDING -> Icons.Outlined.CloudSync
|
||||
SyncStatus.CONFLICT -> Icons.Default.Warning
|
||||
SyncStatus.LOCAL_ONLY -> Icons.Outlined.CloudOff
|
||||
SyncStatus.DELETED_ON_SERVER -> Icons.Outlined.CloudOff // 🆕 v1.8.0
|
||||
},
|
||||
contentDescription = null,
|
||||
tint = when (note.syncStatus) {
|
||||
SyncStatus.SYNCED -> MaterialTheme.colorScheme.primary
|
||||
SyncStatus.CONFLICT -> MaterialTheme.colorScheme.error
|
||||
SyncStatus.DELETED_ON_SERVER -> MaterialTheme.colorScheme.outline.copy(alpha = 0.5f) // 🆕 v1.8.0
|
||||
else -> MaterialTheme.colorScheme.outline
|
||||
},
|
||||
modifier = Modifier.size(14.dp)
|
||||
|
||||
@@ -58,6 +58,13 @@
|
||||
<string name="sync_status_error">Sync failed</string>
|
||||
<string name="sync_already_running">Sync already in progress</string>
|
||||
|
||||
<!-- 🆕 v1.8.0: SyncStatus enum values -->
|
||||
<string name="sync_status_synced">Synced with server</string>
|
||||
<string name="sync_status_pending">Waiting for sync</string>
|
||||
<string name="sync_status_conflict">Sync conflict detected</string>
|
||||
<string name="sync_status_local_only">Not yet synced</string>
|
||||
<string name="sync_status_deleted_on_server">Deleted on server</string>
|
||||
|
||||
<!-- ============================= -->
|
||||
<!-- DELETE DIALOGS -->
|
||||
<!-- ============================= -->
|
||||
|
||||
Reference in New Issue
Block a user