From 07607fc095955d52731bec7927e0e09d6608924b Mon Sep 17 00:00:00 2001 From: inventory69 Date: Mon, 9 Feb 2026 09:24:04 +0100 Subject: [PATCH] feat(v1.8.0): IMPL_021 Sync Status Legend - New SyncStatusLegendDialog.kt showing all 5 sync status icons with descriptions - Help button (?) in MainScreen TopAppBar (only visible when sync available) - Localized strings (English + German) for all 5 status explanations - Material You design with consistent colors matching NoteCard icons - Dialog shows: Synced, Pending, Conflict, Local only, Deleted on server IMPL_021_SYNC_STATUS_LEGEND.md --- .../dettmer/simplenotes/ui/main/MainScreen.kt | 25 +++ .../main/components/SyncStatusLegendDialog.kt | 148 ++++++++++++++++++ .../app/src/main/res/values-de/strings.xml | 15 ++ android/app/src/main/res/values/strings.xml | 15 ++ 4 files changed, 203 insertions(+) create mode 100644 android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncStatusLegendDialog.kt diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainScreen.kt index ff9f437..99602aa 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainScreen.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.SelectAll import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material3.ExperimentalMaterial3Api // FabPosition nicht mehr benötigt - FAB wird manuell platziert import androidx.compose.material3.Icon @@ -53,6 +54,7 @@ import dev.dettmer.simplenotes.ui.main.components.NoteTypeFAB import dev.dettmer.simplenotes.ui.main.components.NotesList import dev.dettmer.simplenotes.ui.main.components.NotesStaggeredGrid import dev.dettmer.simplenotes.ui.main.components.SyncStatusBanner +import dev.dettmer.simplenotes.ui.main.components.SyncStatusLegendDialog import kotlinx.coroutines.launch private const val TIMESTAMP_UPDATE_INTERVAL_MS = 30_000L @@ -92,6 +94,9 @@ fun MainScreen( // Delete confirmation dialog state var showBatchDeleteDialog by remember { mutableStateOf(false) } + // 🆕 v1.8.0: Sync status legend dialog + var showSyncLegend by remember { mutableStateOf(false) } + val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() val listState = rememberLazyListState() @@ -170,6 +175,8 @@ fun MainScreen( ) { MainTopBar( syncEnabled = canSync, + showSyncLegend = isSyncAvailable, // 🆕 v1.8.0: Nur wenn Sync verfügbar + onSyncLegendClick = { showSyncLegend = true }, // 🆕 v1.8.0 onSyncClick = { viewModel.triggerManualSync("toolbar") }, onSettingsClick = onOpenSettings ) @@ -276,6 +283,13 @@ fun MainScreen( } ) } + + // 🆕 v1.8.0: Sync Status Legend Dialog + if (showSyncLegend) { + SyncStatusLegendDialog( + onDismiss = { showSyncLegend = false } + ) + } } } @@ -283,6 +297,8 @@ fun MainScreen( @Composable private fun MainTopBar( syncEnabled: Boolean, + showSyncLegend: Boolean, // 🆕 v1.8.0: Ob der Hilfe-Button sichtbar sein soll + onSyncLegendClick: () -> Unit, // 🆕 v1.8.0 onSyncClick: () -> Unit, onSettingsClick: () -> Unit ) { @@ -294,6 +310,15 @@ private fun MainTopBar( ) }, actions = { + // 🆕 v1.8.0: Sync Status Legend Button (nur wenn Sync verfügbar) + if (showSyncLegend) { + IconButton(onClick = onSyncLegendClick) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.HelpOutline, + contentDescription = stringResource(R.string.sync_legend_button) + ) + } + } IconButton( onClick = onSyncClick, enabled = syncEnabled diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncStatusLegendDialog.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncStatusLegendDialog.kt new file mode 100644 index 0000000..9db62d8 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncStatusLegendDialog.kt @@ -0,0 +1,148 @@ +package dev.dettmer.simplenotes.ui.main.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material.icons.outlined.CloudDone +import androidx.compose.material.icons.outlined.CloudOff +import androidx.compose.material.icons.outlined.CloudSync +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.dettmer.simplenotes.R + +/** + * 🆕 v1.8.0: Dialog showing the sync status icon legend + * + * Displays all 5 SyncStatus values with their icons, colors, + * and descriptions. Helps users understand what each icon means. + */ +@Composable +fun SyncStatusLegendDialog( + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + text = stringResource(R.string.sync_legend_title), + style = MaterialTheme.typography.headlineSmall + ) + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Optional: Kurze Einleitung + Text( + text = stringResource(R.string.sync_legend_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + HorizontalDivider() + + // ☁️✓ SYNCED + LegendRow( + icon = Icons.Outlined.CloudDone, + tint = MaterialTheme.colorScheme.primary, + label = stringResource(R.string.sync_legend_synced_label), + description = stringResource(R.string.sync_legend_synced_desc) + ) + + // ☁️↻ PENDING + LegendRow( + icon = Icons.Outlined.CloudSync, + tint = MaterialTheme.colorScheme.outline, + label = stringResource(R.string.sync_legend_pending_label), + description = stringResource(R.string.sync_legend_pending_desc) + ) + + // ⚠️ CONFLICT + LegendRow( + icon = Icons.Default.Warning, + tint = MaterialTheme.colorScheme.error, + label = stringResource(R.string.sync_legend_conflict_label), + description = stringResource(R.string.sync_legend_conflict_desc) + ) + + // ☁️✗ LOCAL_ONLY + LegendRow( + icon = Icons.Outlined.CloudOff, + tint = MaterialTheme.colorScheme.outline, + label = stringResource(R.string.sync_legend_local_only_label), + description = stringResource(R.string.sync_legend_local_only_desc) + ) + + // ☁️✗ DELETED_ON_SERVER + LegendRow( + icon = Icons.Outlined.CloudOff, + tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f), + label = stringResource(R.string.sync_legend_deleted_label), + description = stringResource(R.string.sync_legend_deleted_desc) + ) + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.ok)) + } + } + ) +} + +/** + * Single row in the sync status legend + * Shows icon + label + description + */ +@Composable +private fun LegendRow( + icon: ImageVector, + tint: Color, + label: String, + description: String +) { + Row( + verticalAlignment = Alignment.Top, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + imageVector = icon, + contentDescription = null, // Dekorativ, Label reicht + tint = tint, + modifier = Modifier + .size(20.dp) + .padding(top = 2.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml index f773ecd..9e99138 100644 --- a/android/app/src/main/res/values-de/strings.xml +++ b/android/app/src/main/res/values-de/strings.xml @@ -58,6 +58,21 @@ Synchronisierung fehlgeschlagen Synchronisierung läuft bereits + + Sync-Status Hilfe + Sync-Status Icons + Jede Notiz zeigt ein kleines Icon, das den Sync-Status anzeigt: + Synchronisiert + Diese Notiz ist auf allen Geräten aktuell. + Ausstehend + Diese Notiz hat lokale Änderungen, die noch synchronisiert werden müssen. + Konflikt + Diese Notiz wurde auf mehreren Geräten gleichzeitig geändert. Die neueste Version wurde beibehalten. + Nur lokal + Diese Notiz wurde noch nie mit dem Server synchronisiert. + Auf Server gelöscht + Diese Notiz wurde auf einem anderen Gerät oder direkt auf dem Server gelöscht. Sie existiert noch lokal. + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 8c70906..11b1a41 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -65,6 +65,21 @@ Not yet synced Deleted on server + + Sync status help + Sync Status Icons + Each note shows a small icon indicating its sync status: + Synced + This note is up to date on all devices. + Pending + This note has local changes waiting to be synced. + Conflict + This note was changed on multiple devices simultaneously. The latest version was kept. + Local only + This note has never been synced to the server yet. + Deleted on server + This note was deleted on another device or directly on the server. It still exists locally. +