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.PENDING -> android.R.drawable.ic_popup_sync
|
||||||
SyncStatus.CONFLICT -> android.R.drawable.ic_dialog_alert
|
SyncStatus.CONFLICT -> android.R.drawable.ic_dialog_alert
|
||||||
SyncStatus.LOCAL_ONLY -> android.R.drawable.ic_menu_save
|
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.setImageResource(syncIcon)
|
||||||
imageViewSyncStatus.visibility = View.VISIBLE
|
imageViewSyncStatus.visibility = View.VISIBLE
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
package dev.dettmer.simplenotes.models
|
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 {
|
enum class SyncStatus {
|
||||||
LOCAL_ONLY, // Noch nie gesynct
|
LOCAL_ONLY, // Noch nie gesynct
|
||||||
SYNCED, // Erfolgreich gesynct
|
SYNCED, // Erfolgreich gesynct
|
||||||
PENDING, // Wartet auf Sync
|
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
|
package dev.dettmer.simplenotes.sync
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ergebnis eines Sync-Vorgangs
|
||||||
|
*
|
||||||
|
* v1.7.0: Initial
|
||||||
|
* v1.8.0: deletedOnServerCount hinzugefügt
|
||||||
|
*/
|
||||||
data class SyncResult(
|
data class SyncResult(
|
||||||
val isSuccess: Boolean,
|
val isSuccess: Boolean,
|
||||||
val syncedCount: Int = 0,
|
val syncedCount: Int = 0,
|
||||||
val conflictCount: Int = 0,
|
val conflictCount: Int = 0,
|
||||||
|
val deletedOnServerCount: Int = 0, // 🆕 v1.8.0
|
||||||
val errorMessage: String? = null
|
val errorMessage: String? = null
|
||||||
) {
|
) {
|
||||||
val hasConflicts: Boolean
|
val hasConflicts: Boolean get() = conflictCount > 0
|
||||||
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")
|
Logger.d(TAG, "📍 Step 5: Downloading remote notes")
|
||||||
// Download remote notes
|
// Download remote notes
|
||||||
|
var deletedOnServerCount = 0 // 🆕 v1.8.0
|
||||||
try {
|
try {
|
||||||
Logger.d(TAG, "⬇️ Downloading remote notes...")
|
Logger.d(TAG, "⬇️ Downloading remote notes...")
|
||||||
val downloadResult = downloadRemoteNotes(
|
val downloadResult = downloadRemoteNotes(
|
||||||
@@ -664,10 +665,12 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
)
|
)
|
||||||
syncedCount += downloadResult.downloadedCount
|
syncedCount += downloadResult.downloadedCount
|
||||||
conflictCount += downloadResult.conflictCount
|
conflictCount += downloadResult.conflictCount
|
||||||
|
deletedOnServerCount = downloadResult.deletedOnServerCount // 🆕 v1.8.0
|
||||||
Logger.d(
|
Logger.d(
|
||||||
TAG,
|
TAG,
|
||||||
"✅ Downloaded: ${downloadResult.downloadedCount} notes, " +
|
"✅ Downloaded: ${downloadResult.downloadedCount} notes, " +
|
||||||
"Conflicts: ${downloadResult.conflictCount}"
|
"Conflicts: ${downloadResult.conflictCount}, " +
|
||||||
|
"Deleted on server: ${downloadResult.deletedOnServerCount}" // 🆕 v1.8.0
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.e(TAG, "💥 CRASH in downloadRemoteNotes()!", e)
|
Logger.e(TAG, "💥 CRASH in downloadRemoteNotes()!", e)
|
||||||
@@ -724,12 +727,16 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
if (markdownImportedCount > 0 && syncedCount > 0) {
|
if (markdownImportedCount > 0 && syncedCount > 0) {
|
||||||
Logger.d(TAG, "📝 Including $markdownImportedCount Markdown file updates")
|
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, "═══════════════════════════════════════")
|
Logger.d(TAG, "═══════════════════════════════════════")
|
||||||
|
|
||||||
SyncResult(
|
SyncResult(
|
||||||
isSuccess = true,
|
isSuccess = true,
|
||||||
syncedCount = effectiveSyncedCount,
|
syncedCount = effectiveSyncedCount,
|
||||||
conflictCount = conflictCount
|
conflictCount = conflictCount,
|
||||||
|
deletedOnServerCount = deletedOnServerCount // 🆕 v1.8.0
|
||||||
)
|
)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -1038,9 +1045,45 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
|
|
||||||
private data class DownloadResult(
|
private data class DownloadResult(
|
||||||
val downloadedCount: Int,
|
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")
|
@Suppress("NestedBlockDepth", "LoopWithTooManyJumpStatements")
|
||||||
// Sync logic requires nested conditions for comprehensive error handling and conflict resolution
|
// Sync logic requires nested conditions for comprehensive error handling and conflict resolution
|
||||||
private fun downloadRemoteNotes(
|
private fun downloadRemoteNotes(
|
||||||
@@ -1062,6 +1105,9 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
// Use provided deletion tracker (allows fresh tracker from restore)
|
// Use provided deletion tracker (allows fresh tracker from restore)
|
||||||
var trackerModified = false
|
var trackerModified = false
|
||||||
|
|
||||||
|
// 🆕 v1.8.0: Collect server note IDs for deletion detection
|
||||||
|
val serverNoteIds = mutableSetOf<String>()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 🆕 PHASE 1: Download from /notes/ (new structure v1.2.1+)
|
// 🆕 PHASE 1: Download from /notes/ (new structure v1.2.1+)
|
||||||
val notesUrl = getNotesUrl(serverUrl)
|
val notesUrl = getNotesUrl(serverUrl)
|
||||||
@@ -1077,6 +1123,12 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
val jsonFiles = resources.filter { !it.isDirectory && it.name.endsWith(".json") }
|
val jsonFiles = resources.filter { !it.isDirectory && it.name.endsWith(".json") }
|
||||||
Logger.d(TAG, " 📊 Found ${jsonFiles.size} JSON files on server")
|
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) {
|
for (resource in jsonFiles) {
|
||||||
|
|
||||||
val noteId = resource.name.removeSuffix(".json")
|
val noteId = resource.name.removeSuffix(".json")
|
||||||
@@ -1317,8 +1369,16 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
Logger.d(TAG, "💾 Deletion tracker updated")
|
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")
|
Logger.d(TAG, "📊 Total: $downloadedCount downloaded, $conflictCount conflicts, $skippedDeleted deleted")
|
||||||
return DownloadResult(downloadedCount, conflictCount)
|
return DownloadResult(downloadedCount, conflictCount, deletedOnServerCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveLastSyncTimestamp() {
|
private fun saveLastSyncTimestamp() {
|
||||||
|
|||||||
@@ -186,11 +186,19 @@ fun NoteCard(
|
|||||||
SyncStatus.PENDING -> Icons.Outlined.CloudSync
|
SyncStatus.PENDING -> Icons.Outlined.CloudSync
|
||||||
SyncStatus.CONFLICT -> Icons.Default.Warning
|
SyncStatus.CONFLICT -> Icons.Default.Warning
|
||||||
SyncStatus.LOCAL_ONLY -> Icons.Outlined.CloudOff
|
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) {
|
tint = when (note.syncStatus) {
|
||||||
SyncStatus.SYNCED -> MaterialTheme.colorScheme.primary
|
SyncStatus.SYNCED -> MaterialTheme.colorScheme.primary
|
||||||
SyncStatus.CONFLICT -> MaterialTheme.colorScheme.error
|
SyncStatus.CONFLICT -> MaterialTheme.colorScheme.error
|
||||||
|
SyncStatus.DELETED_ON_SERVER -> MaterialTheme.colorScheme.outline.copy(alpha = 0.5f) // 🆕 v1.8.0
|
||||||
else -> MaterialTheme.colorScheme.outline
|
else -> MaterialTheme.colorScheme.outline
|
||||||
},
|
},
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
|
|||||||
@@ -187,11 +187,13 @@ fun NoteCardCompact(
|
|||||||
SyncStatus.PENDING -> Icons.Outlined.CloudSync
|
SyncStatus.PENDING -> Icons.Outlined.CloudSync
|
||||||
SyncStatus.CONFLICT -> Icons.Default.Warning
|
SyncStatus.CONFLICT -> Icons.Default.Warning
|
||||||
SyncStatus.LOCAL_ONLY -> Icons.Outlined.CloudOff
|
SyncStatus.LOCAL_ONLY -> Icons.Outlined.CloudOff
|
||||||
|
SyncStatus.DELETED_ON_SERVER -> Icons.Outlined.CloudOff // 🆕 v1.8.0
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = when (note.syncStatus) {
|
tint = when (note.syncStatus) {
|
||||||
SyncStatus.SYNCED -> MaterialTheme.colorScheme.primary
|
SyncStatus.SYNCED -> MaterialTheme.colorScheme.primary
|
||||||
SyncStatus.CONFLICT -> MaterialTheme.colorScheme.error
|
SyncStatus.CONFLICT -> MaterialTheme.colorScheme.error
|
||||||
|
SyncStatus.DELETED_ON_SERVER -> MaterialTheme.colorScheme.outline.copy(alpha = 0.5f) // 🆕 v1.8.0
|
||||||
else -> MaterialTheme.colorScheme.outline
|
else -> MaterialTheme.colorScheme.outline
|
||||||
},
|
},
|
||||||
modifier = Modifier.size(14.dp)
|
modifier = Modifier.size(14.dp)
|
||||||
|
|||||||
@@ -199,11 +199,13 @@ fun NoteCardGrid(
|
|||||||
SyncStatus.PENDING -> Icons.Outlined.CloudSync
|
SyncStatus.PENDING -> Icons.Outlined.CloudSync
|
||||||
SyncStatus.CONFLICT -> Icons.Default.Warning
|
SyncStatus.CONFLICT -> Icons.Default.Warning
|
||||||
SyncStatus.LOCAL_ONLY -> Icons.Outlined.CloudOff
|
SyncStatus.LOCAL_ONLY -> Icons.Outlined.CloudOff
|
||||||
|
SyncStatus.DELETED_ON_SERVER -> Icons.Outlined.CloudOff // 🆕 v1.8.0
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = when (note.syncStatus) {
|
tint = when (note.syncStatus) {
|
||||||
SyncStatus.SYNCED -> MaterialTheme.colorScheme.primary
|
SyncStatus.SYNCED -> MaterialTheme.colorScheme.primary
|
||||||
SyncStatus.CONFLICT -> MaterialTheme.colorScheme.error
|
SyncStatus.CONFLICT -> MaterialTheme.colorScheme.error
|
||||||
|
SyncStatus.DELETED_ON_SERVER -> MaterialTheme.colorScheme.outline.copy(alpha = 0.5f) // 🆕 v1.8.0
|
||||||
else -> MaterialTheme.colorScheme.outline
|
else -> MaterialTheme.colorScheme.outline
|
||||||
},
|
},
|
||||||
modifier = Modifier.size(14.dp)
|
modifier = Modifier.size(14.dp)
|
||||||
|
|||||||
@@ -58,6 +58,13 @@
|
|||||||
<string name="sync_status_error">Sync failed</string>
|
<string name="sync_status_error">Sync failed</string>
|
||||||
<string name="sync_already_running">Sync already in progress</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 -->
|
<!-- DELETE DIALOGS -->
|
||||||
<!-- ============================= -->
|
<!-- ============================= -->
|
||||||
|
|||||||
Reference in New Issue
Block a user