diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/components/MemoCard.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/components/MemoCard.kt index 8d20bec..eb5b95d 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/components/MemoCard.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/components/MemoCard.kt @@ -273,27 +273,26 @@ private fun InlineEditor( TextField( value = content, onValueChange = { newText -> - // Backspace on empty auto-inserted line: remove it - if (newText.length < content.length && content.endsWith("- [ ] ") && newText == content.dropLast(6).trimEnd() + "\n") { - onContentChange(newText.trimEnd('\n')) + val oldLines = content.lines() + val lastLine = oldLines.lastOrNull() ?: "" + + // Backspace on empty auto-inserted task line: remove it + if (newText.length < content.length && lastLine.trim() == "- [ ]" && oldLines.size > 1) { + val withoutLast = oldLines.dropLast(1).joinToString("\n") + if (newText.trimEnd() == withoutLast.trimEnd()) { + onContentChange(withoutLast) + return@TextField + } + } + // Enter on empty auto-inserted task line: remove it + if (newText.length > content.length && newText.endsWith("\n") && lastLine.trim() == "- [ ]" && oldLines.size > 1) { + onContentChange(oldLines.dropLast(1).joinToString("\n") + "\n") return@TextField } - // Enter on empty auto-inserted line: remove it - if (newText.length > content.length && newText.endsWith("\n") && content.endsWith("- [ ] ")) { - val lastLine = content.lines().last() - if (lastLine.trim() == "- [ ]") { - onContentChange(content.dropLast(lastLine.length + 1).trimEnd('\n') + "\n") - return@TextField - } - } // Auto-checklist: continue task list on enter - if (newText.length > content.length && newText.endsWith("\n")) { - val beforeNewline = newText.dropLast(1) - val lastLine = beforeNewline.lines().lastOrNull() ?: "" - if (lastLine.trimStart().startsWith("- [")) { - onContentChange(newText + "- [ ] ") - return@TextField - } + if (newText.length > content.length && newText.endsWith("\n") && lastLine.trimStart().startsWith("- [") && lastLine.trim() != "- [ ]") { + onContentChange(newText + "- [ ] ") + return@TextField } onContentChange(newText) }, diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListScreen.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListScreen.kt index c66de58..a1e02cc 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListScreen.kt @@ -53,6 +53,8 @@ import com.avinal.memos.domain.MemoVisibility import com.avinal.memos.ui.components.MemoCard import com.avinal.memos.ui.theme.LocalAccentColor import com.avinal.memos.util.rememberFilePicker +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import kotlinx.coroutines.flow.first @@ -66,6 +68,7 @@ import kotlinx.datetime.todayIn @Composable fun MemoListScreen( deps: AppDependencies, + sharedText: String? = null, onMemoClick: (String) -> Unit, onCreateMemo: () -> Unit, dateFilter: String? = null, @@ -130,7 +133,7 @@ fun MemoListScreen( val textColor = MaterialTheme.colorScheme.onBackground val subtleColor = MaterialTheme.colorScheme.onSurfaceVariant - var composeField by remember { mutableStateOf(TextFieldValue("")) } + var composeField by remember { mutableStateOf(TextFieldValue(sharedText ?: "")) } val defaultVis by produceState(MemoVisibility.PRIVATE) { deps.tokenStore.defaultVisibility.first().let { value = MemoVisibility.fromApiString(it) } } @@ -139,6 +142,7 @@ fun MemoListScreen( var uploadedAttachmentNames by remember { mutableStateOf>(emptyList()) } var isUploading by remember { mutableStateOf(false) } val uploadScope = rememberCoroutineScope() + val haptics = LocalHapticFeedback.current val launchFilePicker = rememberFilePicker { pickedFile -> isUploading = true @@ -254,30 +258,28 @@ fun MemoListScreen( onValueChange = { newField -> val newText = newField.text val oldText = composeField.text - // Backspace on empty auto-inserted line: remove it - if (newText.length < oldText.length && oldText.endsWith("- [ ] ") && newText == oldText.dropLast(6).trimEnd() + "\n") { - val cleaned = newText.trimEnd('\n') - composeField = TextFieldValue(cleaned, TextRange(cleaned.length)) + val oldLines = oldText.lines() + val lastLine = oldLines.lastOrNull() ?: "" + + // Backspace on empty auto-inserted task line: remove it + if (newText.length < oldText.length && lastLine.trim() == "- [ ]" && oldLines.size > 1) { + val withoutLast = oldLines.dropLast(1).joinToString("\n") + if (newText.trimEnd() == withoutLast.trimEnd()) { + composeField = TextFieldValue(withoutLast, TextRange(withoutLast.length)) + return@TextField + } + } + // Enter on empty auto-inserted task line: remove it and exit task mode + if (newText.length > oldText.length && newText.endsWith("\n") && lastLine.trim() == "- [ ]" && oldLines.size > 1) { + val withoutLast = oldLines.dropLast(1).joinToString("\n") + "\n" + composeField = TextFieldValue(withoutLast, TextRange(withoutLast.length)) return@TextField } - // Enter on empty auto-inserted line: remove it - if (newText.length > oldText.length && newText.endsWith("\n") && oldText.endsWith("- [ ] ")) { - val lastLine = oldText.lines().last() - if (lastLine.trim() == "- [ ]") { - val cleaned = oldText.dropLast(lastLine.length + 1).trimEnd('\n') + "\n" - composeField = TextFieldValue(cleaned, TextRange(cleaned.length)) - return@TextField - } - } - // Auto-checklist: if user pressed enter after a task line, auto-insert "- [ ] " - if (newText.length > oldText.length && newText.endsWith("\n")) { - val beforeNewline = newText.dropLast(1) - val lastLine = beforeNewline.lines().lastOrNull() ?: "" - if (lastLine.trimStart().startsWith("- [")) { - val result = newText + "- [ ] " - composeField = TextFieldValue(result, TextRange(result.length)) - return@TextField - } + // Auto-checklist: continue task list on enter + if (newText.length > oldText.length && newText.endsWith("\n") && lastLine.trimStart().startsWith("- [") && lastLine.trim() != "- [ ]") { + val result = newText + "- [ ] " + composeField = TextFieldValue(result, TextRange(result.length)) + return@TextField } composeField = newField }, @@ -444,9 +446,11 @@ fun MemoListScreen( modifier = Modifier .then( if (composeField.text.isNotBlank()) Modifier.clickable { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) viewModel.createMemo(composeField.text, composeVisibility, uploadedAttachmentNames) composeField = TextFieldValue("") uploadedAttachmentNames = emptyList() + uploadScope.launch { listState.animateScrollToItem(0) } } else Modifier ) .padding(horizontal = 4.dp, vertical = 4.dp), @@ -489,7 +493,16 @@ fun MemoListScreen( } else if (memos.isEmpty() && !uiState.isRefreshing) { item { Box(Modifier.fillMaxWidth().padding(top = 48.dp), contentAlignment = Alignment.Center) { - Text(if (showArchived) "no archived memos" else "no memos yet", fontSize = 15.sp, color = subtleColor) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + if (showArchived) "no archived memos" else "nothing here yet", + fontSize = 17.sp, fontWeight = FontWeight.Light, color = MaterialTheme.colorScheme.onBackground, + ) + if (!showArchived) { + Spacer(Modifier.height(4.dp)) + Text("tap above to write your first memo", fontSize = 13.sp, color = subtleColor) + } + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/tasks/TaskListScreen.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/tasks/TaskListScreen.kt index 0a27c14..bf8e259 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/tasks/TaskListScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/tasks/TaskListScreen.kt @@ -20,18 +20,28 @@ import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp @@ -45,10 +55,12 @@ import com.avinal.memos.ui.theme.PriorityP1 import com.avinal.memos.ui.theme.PriorityP2 import com.avinal.memos.ui.theme.PriorityP3 import kotlin.time.Clock +import kotlinx.coroutines.launch import kotlinx.datetime.TimeZone import kotlinx.datetime.todayIn import kotlinx.datetime.daysUntil +@OptIn(ExperimentalMaterial3Api::class) @Composable fun TaskListScreen( deps: AppDependencies, @@ -61,6 +73,10 @@ fun TaskListScreen( val textColor = MaterialTheme.colorScheme.onBackground val subtleColor = MaterialTheme.colorScheme.onSurfaceVariant var selectedTask by remember { mutableStateOf(null) } + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + val haptics = LocalHapticFeedback.current + var isRefreshing by remember { mutableStateOf(false) } selectedTask?.let { task -> TaskDetailSheet( @@ -71,6 +87,30 @@ fun TaskListScreen( ) } + Scaffold( + snackbarHost = { + SnackbarHost(snackbarHostState) { data -> + Snackbar( + snackbarData = data, + containerColor = MaterialTheme.colorScheme.surfaceContainer, + contentColor = textColor, + actionColor = accent, + ) + } + }, + containerColor = Color.Transparent, + ) { padding -> + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { + isRefreshing = true + scope.launch { + deps.memoRepository.refreshMemos() + isRefreshing = false + } + }, + modifier = Modifier.padding(padding), + ) { Column(modifier = Modifier.fillMaxSize()) { Row( modifier = Modifier @@ -187,7 +227,17 @@ fun TaskListScreen( dotColor = dotColor, textColor = textColor, subtleColor = subtleColor, - onToggle = { viewModel.toggleTask(task) }, + onToggle = { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + viewModel.toggleTask(task) + scope.launch { + val label = if (task.isCompleted) "task reopened" else "task completed" + val result = snackbarHostState.showSnackbar(label, actionLabel = "undo", withDismissAction = true) + if (result == SnackbarResult.ActionPerformed) { + viewModel.toggleTask(task) + } + } + }, onClick = { selectedTask = task }, ) } @@ -197,12 +247,18 @@ fun TaskListScreen( if (grouped.groups.isEmpty() || grouped.groups.all { it.tasks.isEmpty() }) { item { Box(Modifier.fillMaxWidth().padding(top = 48.dp), contentAlignment = Alignment.Center) { - Text("no tasks", fontSize = 15.sp, color = subtleColor) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text("all clear", fontSize = 17.sp, fontWeight = FontWeight.Light, color = textColor) + Spacer(Modifier.height(4.dp)) + Text("tasks from your memos will appear here", fontSize = 13.sp, color = subtleColor) + } } } } } } + } + } } @Composable