From 40d7c83c847ba17fc375f7edc576eb79e06efdd5 Mon Sep 17 00:00:00 2001 From: inventory69 Date: Sun, 8 Feb 2026 23:27:10 +0100 Subject: [PATCH] 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 --- .../simplenotes/adapters/NotesAdapter.kt | 1 + .../dettmer/simplenotes/models/SyncStatus.kt | 15 ++-- .../dettmer/simplenotes/sync/SyncResult.kt | 11 ++- .../simplenotes/sync/WebDavSyncService.kt | 68 +++++++++++++++++-- .../ui/main/components/NoteCard.kt | 10 ++- .../ui/main/components/NoteCardCompact.kt | 2 + .../ui/main/components/NoteCardGrid.kt | 2 + android/app/src/main/res/values/strings.xml | 7 ++ 8 files changed, 105 insertions(+), 11 deletions(-) diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/adapters/NotesAdapter.kt b/android/app/src/main/java/dev/dettmer/simplenotes/adapters/NotesAdapter.kt index 6161d61..14efaad 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/adapters/NotesAdapter.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/adapters/NotesAdapter.kt @@ -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 diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/models/SyncStatus.kt b/android/app/src/main/java/dev/dettmer/simplenotes/models/SyncStatus.kt index c1aea44..042d026 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/models/SyncStatus.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/models/SyncStatus.kt @@ -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 + LOCAL_ONLY, // Noch nie gesynct + SYNCED, // Erfolgreich gesynct + PENDING, // Wartet auf Sync + CONFLICT, // Konflikt erkannt + DELETED_ON_SERVER // πŸ†• v1.8.0: Server hat gelΓΆscht, lokal noch vorhanden } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncResult.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncResult.kt index 3aa986a..dc711af 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncResult.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncResult.kt @@ -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 } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt index 20efa08..851db3b 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt @@ -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, + localNotes: List + ): 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() + 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() { diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCard.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCard.kt index e6ce88b..1cc0baf 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCard.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCard.kt @@ -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) diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCardCompact.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCardCompact.kt index c51ebd8..d04a2f3 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCardCompact.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCardCompact.kt @@ -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) diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCardGrid.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCardGrid.kt index 159be46..606b0e7 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCardGrid.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCardGrid.kt @@ -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) diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index e51bbf0..8c70906 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -58,6 +58,13 @@ Sync failed Sync already in progress + + Synced with server + Waiting for sync + Sync conflict detected + Not yet synced + Deleted on server +