diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncProgress.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncProgress.kt index f2f25c0..5483509 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncProgress.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncProgress.kt @@ -52,9 +52,10 @@ data class SyncProgress( /** * Ob das Banner sichtbar sein soll * Silent syncs zeigen nie ein Banner + * πŸ†• v1.8.1 (IMPL_12): INFO ist immer sichtbar (nicht vom silent-Flag betroffen) */ val isVisible: Boolean - get() = !silent && phase != SyncPhase.IDLE + get() = phase == SyncPhase.INFO || (!silent && phase != SyncPhase.IDLE) /** * Ob gerade ein aktiver Sync lΓ€uft (mit Spinner) @@ -95,5 +96,8 @@ enum class SyncPhase { COMPLETED, /** Sync mit Fehler abgebrochen */ - ERROR + ERROR, + + /** πŸ†• v1.8.1 (IMPL_12): Kurzfristige Info-Meldung (nicht sync-bezogen) */ + INFO } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt index 7439a27..a98297d 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt @@ -245,4 +245,60 @@ object SyncStateManager { fun markGlobalSyncStarted(prefs: android.content.SharedPreferences) { prefs.edit().putLong(dev.dettmer.simplenotes.utils.Constants.KEY_LAST_GLOBAL_SYNC_TIME, System.currentTimeMillis()).apply() } + + // ═══════════════════════════════════════════════════════════════════════ + // πŸ†• v1.8.1 (IMPL_12): Info-Meldungen ΓΌber das Banner-System + // ═══════════════════════════════════════════════════════════════════════ + + /** + * Zeigt eine kurzfristige Info-Meldung im Banner an. + * Wird fΓΌr nicht-sync-bezogene Benachrichtigungen verwendet + * (z.B. Server-Delete-Ergebnisse). + * + * ACHTUNG: Wenn gerade ein Sync lΓ€uft (isSyncing), wird die Meldung + * ignoriert β€” der Sync-Progress hat Vorrang. + * + * Auto-Hide erfolgt ΓΌber ComposeMainActivity (2.5s). + */ + fun showInfo(message: String) { + synchronized(lock) { + // Nicht wΓ€hrend aktivem Sync anzeigen β€” Sync-Fortschritt hat Vorrang + if (isSyncing) { + Logger.d(TAG, "ℹ️ Info suppressed during sync: $message") + return + } + + _syncProgress.value = SyncProgress( + phase = SyncPhase.INFO, + resultMessage = message, + silent = false // INFO ist nie silent + ) + + Logger.d(TAG, "ℹ️ Showing info: $message") + } + } + + /** + * Zeigt eine Fehlermeldung im Banner an, auch außerhalb eines Syncs. + * FΓΌr nicht-sync-bezogene Fehler (z.B. Server-Delete fehlgeschlagen). + * + * Auto-Hide erfolgt ΓΌber ComposeMainActivity (4s). + */ + fun showError(message: String?) { + synchronized(lock) { + // Nicht wΓ€hrend aktivem Sync anzeigen + if (isSyncing) { + Logger.d(TAG, "❌ Error suppressed during sync: $message") + return + } + + _syncProgress.value = SyncProgress( + phase = SyncPhase.ERROR, + resultMessage = message, + silent = false + ) + + Logger.e(TAG, "❌ Showing error: $message") + } + } } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt index 7aac05d..9e31e71 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt @@ -13,6 +13,7 @@ import dev.dettmer.simplenotes.models.Note import dev.dettmer.simplenotes.models.NoteType import dev.dettmer.simplenotes.models.SyncStatus import dev.dettmer.simplenotes.storage.NotesStorage +import dev.dettmer.simplenotes.sync.SyncStateManager import dev.dettmer.simplenotes.sync.SyncWorker import dev.dettmer.simplenotes.sync.WebDavSyncService import dev.dettmer.simplenotes.utils.Constants @@ -398,7 +399,7 @@ class NoteEditorViewModel( } } - _events.emit(NoteEditorEvent.ShowToast(ToastMessage.NOTE_SAVED)) + // πŸ†• v1.8.1 (IMPL_12): NOTE_SAVED Toast entfernt β€” NavigateBack ist ausreichend // 🌟 v1.6.0: Trigger onSave Sync triggerOnSaveSync() @@ -438,17 +439,33 @@ class NoteEditorViewModel( val success = withContext(Dispatchers.IO) { webdavService.deleteNoteFromServer(noteId) } + // πŸ†• v1.8.1 (IMPL_12): Banner-Feedback statt stiller Log-EintrΓ€ge if (success) { Logger.d(TAG, "Note $noteId deleted from server") + SyncStateManager.showInfo( + getApplication().getString( + dev.dettmer.simplenotes.R.string.snackbar_deleted_from_server + ) + ) } else { Logger.w(TAG, "Failed to delete note $noteId from server") + SyncStateManager.showError( + getApplication().getString( + dev.dettmer.simplenotes.R.string.snackbar_server_delete_failed + ) + ) } } catch (e: Exception) { Logger.e(TAG, "Error deleting note from server: ${e.message}") + SyncStateManager.showError( + getApplication().getString( + dev.dettmer.simplenotes.R.string.snackbar_server_error, + e.message ?: "" + ) + ) } } - _events.emit(NoteEditorEvent.ShowToast(ToastMessage.NOTE_DELETED)) _events.emit(NoteEditorEvent.NavigateBack) } } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt index 3ba829c..e965120 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt @@ -236,6 +236,11 @@ class ComposeMainActivity : ComponentActivity() { kotlinx.coroutines.delay(2000L) SyncStateManager.reset() } + // πŸ†• v1.8.1 (IMPL_12): INFO-Meldungen nach 2.5s ausblenden + dev.dettmer.simplenotes.sync.SyncPhase.INFO -> { + kotlinx.coroutines.delay(2500L) + SyncStateManager.reset() + } dev.dettmer.simplenotes.sync.SyncPhase.ERROR -> { kotlinx.coroutines.delay(4000L) SyncStateManager.reset() diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt index 360e43a..a850d16 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt @@ -470,12 +470,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } if (success) { - _showToast.emit(getString(R.string.snackbar_deleted_from_server)) + // πŸ†• v1.8.1 (IMPL_12): Toast β†’ Banner INFO + SyncStateManager.showInfo(getString(R.string.snackbar_deleted_from_server)) } else { - _showToast.emit(getString(R.string.snackbar_server_delete_failed)) + SyncStateManager.showError(getString(R.string.snackbar_server_delete_failed)) } } catch (e: Exception) { - _showToast.emit(getString(R.string.snackbar_server_error, e.message ?: "")) + SyncStateManager.showError(getString(R.string.snackbar_server_error, e.message ?: "")) } finally { // Remove from pending deletions _pendingDeletions.value = _pendingDeletions.value - noteId @@ -507,7 +508,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } } - // Show aggregated toast + // πŸ†• v1.8.1 (IMPL_12): Toast β†’ Banner INFO/ERROR val message = when { failCount == 0 -> getString(R.string.snackbar_notes_deleted_from_server, successCount) successCount == 0 -> getString(R.string.snackbar_server_delete_failed) @@ -517,7 +518,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { successCount + failCount ) } - _showToast.emit(message) + if (failCount > 0) { + SyncStateManager.showError(message) + } else { + SyncStateManager.showInfo(message) + } } } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncProgressBanner.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncProgressBanner.kt index 2f397d8..ad66218 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncProgressBanner.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncProgressBanner.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.ErrorOutline +import androidx.compose.material.icons.outlined.Info import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator @@ -51,11 +52,13 @@ fun SyncProgressBanner( // Farbe animiert wechseln je nach State val isError = progress.phase == SyncPhase.ERROR val isCompleted = progress.phase == SyncPhase.COMPLETED - val isResult = isError || isCompleted + val isInfo = progress.phase == SyncPhase.INFO // πŸ†• v1.8.1 (IMPL_12) + val isResult = isError || isCompleted || isInfo val backgroundColor by animateColorAsState( targetValue = when { isError -> MaterialTheme.colorScheme.errorContainer + isInfo -> MaterialTheme.colorScheme.secondaryContainer // πŸ†• v1.8.1 (IMPL_12) else -> MaterialTheme.colorScheme.primaryContainer }, label = "bannerColor" @@ -64,6 +67,7 @@ fun SyncProgressBanner( val contentColor by animateColorAsState( targetValue = when { isError -> MaterialTheme.colorScheme.onErrorContainer + isInfo -> MaterialTheme.colorScheme.onSecondaryContainer // πŸ†• v1.8.1 (IMPL_12) else -> MaterialTheme.colorScheme.onPrimaryContainer }, label = "bannerContentColor" @@ -89,7 +93,7 @@ fun SyncProgressBanner( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp) ) { - // Icon: Spinner (aktiv), Checkmark (completed), Error (error) + // Icon: Spinner (aktiv), Checkmark (completed), Error (error), Info (info) when { isCompleted -> { Icon( @@ -99,6 +103,14 @@ fun SyncProgressBanner( tint = contentColor ) } + isInfo -> { // πŸ†• v1.8.1 (IMPL_12) + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = contentColor + ) + } isError -> { Icon( imageVector = Icons.Filled.ErrorOutline, @@ -187,5 +199,6 @@ private fun phaseToString(phase: SyncPhase): String { SyncPhase.IMPORTING_MARKDOWN -> stringResource(R.string.sync_phase_importing_markdown) SyncPhase.COMPLETED -> stringResource(R.string.sync_phase_completed) SyncPhase.ERROR -> stringResource(R.string.sync_phase_error) + SyncPhase.INFO -> "" // πŸ†• v1.8.1 (IMPL_12): INFO nutzt immer resultMessage } }