diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt index c1c5f56..fa56658 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource @@ -37,14 +38,16 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import dev.dettmer.simplenotes.R import dev.dettmer.simplenotes.ui.editor.ChecklistItemState /** * A single row in the checklist editor with drag handle, checkbox, text input, and delete button. - * + * * v1.5.0: Jetpack Compose NoteEditor Redesign + * v1.8.0: Long text UX improvements (gradient fade, auto-expand on focus) */ @Composable fun ChecklistItemRow( @@ -61,6 +64,15 @@ fun ChecklistItemRow( var textFieldValue by remember(item.id) { mutableStateOf(TextFieldValue(text = item.text, selection = TextRange(item.text.length))) } + + // πŸ†• v1.8.0: Focus-State tracken fΓΌr Expand/Collapse + var isFocused by remember { mutableStateOf(false) } + + // πŸ†• v1.8.0: Overflow erkennen (Text lΓ€nger als maxLines) + var hasOverflow by remember { mutableStateOf(false) } + + // πŸ†• v1.8.0: Dynamische maxLines basierend auf Focus + val currentMaxLines = if (isFocused) Int.MAX_VALUE else COLLAPSED_MAX_LINES // v1.5.0: Auto-focus AND show keyboard when requestFocus is true (new items) LaunchedEffect(requestFocus) { @@ -88,7 +100,7 @@ fun ChecklistItemRow( modifier = modifier .fillMaxWidth() .padding(horizontal = 8.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.Top // πŸ†• v1.8.0: Top statt CenterVertically fΓΌr lange Texte ) { // Drag Handle Icon( @@ -96,6 +108,7 @@ fun ChecklistItemRow( contentDescription = stringResource(R.string.drag_to_reorder), modifier = Modifier .size(24.dp) + .padding(top = 12.dp) // πŸ†• v1.8.0: Visuell am oberen Rand ausrichten .alpha(0.5f), tint = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -110,63 +123,85 @@ fun ChecklistItemRow( ) Spacer(modifier = Modifier.width(4.dp)) - - // Text Input with placeholder - BasicTextField( - value = textFieldValue, - onValueChange = { newValue -> - // Check for newline (Enter key) - if (newValue.text.contains("\n")) { - val cleanText = newValue.text.replace("\n", "") - textFieldValue = TextFieldValue( - text = cleanText, - selection = TextRange(cleanText.length) - ) - onTextChange(cleanText) - onAddNewItem() - } else { - textFieldValue = newValue - onTextChange(newValue.text) - } - }, - modifier = Modifier - .weight(1f) - .focusRequester(focusRequester) - .alpha(alpha), - textStyle = LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.onSurface, - textDecoration = textDecoration - ), - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Next - ), - keyboardActions = KeyboardActions( - onNext = { onAddNewItem() } - ), - singleLine = false, - maxLines = 5, - cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), - decorationBox = { innerTextField -> - Box { - if (textFieldValue.text.isEmpty()) { - Text( - text = stringResource(R.string.item_placeholder), - style = LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) - ) + + // πŸ†• v1.8.0: Text Input mit Overflow-Gradient + Box(modifier = Modifier.weight(1f)) { + BasicTextField( + value = textFieldValue, + onValueChange = { newValue -> + // Check for newline (Enter key) + if (newValue.text.contains("\n")) { + val cleanText = newValue.text.replace("\n", "") + textFieldValue = TextFieldValue( + text = cleanText, + selection = TextRange(cleanText.length) ) + onTextChange(cleanText) + onAddNewItem() + } else { + textFieldValue = newValue + onTextChange(newValue.text) + } + }, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester) + .onFocusChanged { focusState -> + isFocused = focusState.isFocused + } + .alpha(alpha), + textStyle = LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.onSurface, + textDecoration = textDecoration + ), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next + ), + keyboardActions = KeyboardActions( + onNext = { onAddNewItem() } + ), + singleLine = false, + maxLines = currentMaxLines, // πŸ†• v1.8.0: Dynamisch + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + onTextLayout = { textLayoutResult -> + // πŸ†• v1.8.0: Overflow erkennen + hasOverflow = textLayoutResult.hasVisualOverflow || + textLayoutResult.lineCount > COLLAPSED_MAX_LINES + }, + decorationBox = { innerTextField -> + Box { + if (textFieldValue.text.isEmpty()) { + Text( + text = stringResource(R.string.item_placeholder), + style = LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + ) + } + innerTextField() } - innerTextField() } + ) + + // πŸ†• v1.8.0: Gradient-Fade Overlay wenn Text ΓΌberlΓ€uft + // Zeige nur Gradient oben, da man am unteren Rand startet und nach oben scrollt + if (hasOverflow && !isFocused) { + // Gradient oben (zeigt: es gibt Text oberhalb der sichtbaren Zeilen) + OverflowGradient( + modifier = Modifier.align(Alignment.TopCenter), + isTopGradient = true + ) } - ) + } Spacer(modifier = Modifier.width(4.dp)) // Delete Button IconButton( onClick = onDelete, - modifier = Modifier.size(36.dp) + modifier = Modifier + .size(36.dp) + .padding(top = 4.dp) // πŸ†• v1.8.0: Ausrichtung mit Top-aligned Text ) { Icon( imageVector = Icons.Default.Close, @@ -177,3 +212,63 @@ fun ChecklistItemRow( } } } + +// πŸ†• v1.8.0: Maximum lines when collapsed (not focused) +private const val COLLAPSED_MAX_LINES = 5 + +// ════════════════════════════════════════════════════════════════ +// πŸ†• v1.8.0: Preview Composables for Manual Testing +// ════════════════════════════════════════════════════════════════ + +@Preview(showBackground = true) +@Composable +private fun ChecklistItemRowShortTextPreview() { + ChecklistItemRow( + item = ChecklistItemState( + id = "preview-1", + text = "Kurzer Text", + isChecked = false + ), + onTextChange = {}, + onCheckedChange = {}, + onDelete = {}, + onAddNewItem = {} + ) +} + +@Preview(showBackground = true) +@Composable +private fun ChecklistItemRowLongTextPreview() { + ChecklistItemRow( + item = ChecklistItemState( + id = "preview-2", + text = "Dies ist ein sehr langer Text der sich ΓΌber viele Zeilen erstreckt " + + "und dazu dient den Overflow-Gradient zu demonstrieren. Er hat deutlich " + + "mehr als fΓΌnf Zeilen wenn er in der normalen Breite eines Smartphones " + + "angezeigt wird und sollte einen schΓΆnen Fade-Effekt am unteren Rand zeigen. " + + "Dieser zusΓ€tzliche Text sorgt dafΓΌr, dass wir wirklich genug Zeilen haben " + + "um den Gradient sichtbar zu machen.", + isChecked = false + ), + onTextChange = {}, + onCheckedChange = {}, + onDelete = {}, + onAddNewItem = {} + ) +} + +@Preview(showBackground = true) +@Composable +private fun ChecklistItemRowCheckedPreview() { + ChecklistItemRow( + item = ChecklistItemState( + id = "preview-3", + text = "Erledigte Aufgabe mit durchgestrichenem Text", + isChecked = true + ), + onTextChange = {}, + onCheckedChange = {}, + onDelete = {}, + onAddNewItem = {} + ) +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/OverflowGradient.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/OverflowGradient.kt new file mode 100644 index 0000000..88b1cfd --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/OverflowGradient.kt @@ -0,0 +1,62 @@ +package dev.dettmer.simplenotes.ui.editor.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +/** + * πŸ†• v1.8.0: Dezenter Gradient-Overlay der anzeigt, dass mehr Text + * vorhanden ist als aktuell sichtbar. + * + * Features: + * - Top gradient: surface β†’ transparent (zeigt Text oberhalb) + * - Bottom gradient: transparent β†’ surface (zeigt Text unterhalb) + * - HΓΆhe: 24dp fΓΌr subtilen, aber erkennbaren Effekt + * - Material You kompatibel: nutzt dynamische surface-Farbe + * - Dark Mode Support: automatisch durch MaterialTheme + * + * Verwendet in: ChecklistItemRow fΓΌr lange TexteintrΓ€ge + * + * @param isTopGradient true = Gradient von surfaceβ†’transparent (oben), false = transparentβ†’surface (unten) + */ +@Composable +fun OverflowGradient( + modifier: Modifier = Modifier, + isTopGradient: Boolean = false +) { + val surfaceColor = MaterialTheme.colorScheme.surface + + val gradientColors = if (isTopGradient) { + // Oben: surface β†’ transparent (zeigt dass Text OBERHALB existiert) + listOf( + surfaceColor.copy(alpha = 0.95f), + surfaceColor.copy(alpha = 0.7f), + Color.Transparent + ) + } else { + // Unten: transparent β†’ surface (zeigt dass Text UNTERHALB existiert) + listOf( + Color.Transparent, + surfaceColor.copy(alpha = 0.7f), + surfaceColor.copy(alpha = 0.95f) + ) + } + + Box( + modifier = modifier + .fillMaxWidth() + .height(GRADIENT_HEIGHT) + .background( + brush = Brush.verticalGradient(colors = gradientColors) + ) + ) +} + +private val GRADIENT_HEIGHT = 24.dp