1
0
mirror of https://github.com/avinal/nikki.git synced 2026-07-04 05:50:10 +05:30

Add compose toolbar, per-task preview, monospace editor

Compose card toolbar:
- "add task" button inserts - [ ] on new line
- "due" and "at" buttons appear when editing a task line
- Date/time pickers insert formatted values at cursor
- "+" opens existing insert menu (media, code block)

Per-task live preview:
- Each task shown as a row: checkbox + text + metadata chips
- Replaces flat bag of unassociated chips
- Same preview in both compose card and inline memo editor

Editor improvements:
- Monospace font in both compose and inline editors
- TextFieldValue for cursor control (cursor moves to end)
- Auto-continue: enter after task line inserts new - [ ]
- Backspace on empty auto-inserted line removes it
- Enter on empty task line exits task mode

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context)
This commit is contained in:
2026-06-01 15:25:11 +05:30
parent ad536d1e3d
commit f788cb8162
2 changed files with 328 additions and 92 deletions
@@ -15,11 +15,17 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TimePicker
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -38,6 +44,8 @@ import com.avinal.memos.domain.MemoVisibility
import com.avinal.memos.ui.theme.LocalAccentColor import com.avinal.memos.ui.theme.LocalAccentColor
import com.avinal.memos.util.sharePlainText import com.avinal.memos.util.sharePlainText
import kotlin.time.Instant import kotlin.time.Instant
import kotlinx.datetime.todayIn
import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
@@ -201,6 +209,7 @@ private fun MetroMenuItem(text: String, color: Color, onClick: () -> Unit) {
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun InlineEditor( private fun InlineEditor(
content: String, visibility: MemoVisibility, accent: Color, content: String, visibility: MemoVisibility, accent: Color,
@@ -209,33 +218,145 @@ private fun InlineEditor(
onSave: () -> Unit, onCancel: () -> Unit, onSave: () -> Unit, onCancel: () -> Unit,
) { ) {
var showVisibilityMenu by remember { mutableStateOf(false) } var showVisibilityMenu by remember { mutableStateOf(false) }
var showDatePicker by remember { mutableStateOf(false) }
var showTimePicker by remember { mutableStateOf(false) }
val isEditingTask = remember(content) {
val lastLine = content.lines().lastOrNull { it.isNotBlank() } ?: ""
lastLine.trimStart().startsWith("- [")
}
if (showDatePicker) {
val today = kotlin.time.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault())
val dateState = rememberDatePickerState(
initialSelectedDateMillis = today.toEpochDays().toLong() * 86400000L,
)
DatePickerDialog(
onDismissRequest = { showDatePicker = false },
confirmButton = {
TextButton(onClick = {
dateState.selectedDateMillis?.let { ms ->
val d = kotlinx.datetime.Instant.fromEpochMilliseconds(ms)
.toLocalDateTime(kotlinx.datetime.TimeZone.UTC).date
onContentChange(content.trimEnd() + " $d")
}
showDatePicker = false
}) { Text("ok", color = accent) }
},
dismissButton = {
TextButton(onClick = { showDatePicker = false }) { Text("cancel") }
},
) { DatePicker(state = dateState) }
}
if (showTimePicker) {
val timeState = rememberTimePickerState()
AlertDialog(
onDismissRequest = { showTimePicker = false },
confirmButton = {
TextButton(onClick = {
val h = timeState.hour; val m = timeState.minute
val timeStr = if (m == 0) {
if (h == 0) "12am" else if (h < 12) "${h}am" else if (h == 12) "12pm" else "${h - 12}pm"
} else "${h}:${m.toString().padStart(2, '0')}"
onContentChange(content.trimEnd() + " $timeStr")
showTimePicker = false
}) { Text("ok", color = accent) }
},
dismissButton = {
TextButton(onClick = { showTimePicker = false }) { Text("cancel") }
},
text = { TimePicker(state = timeState) },
)
}
TextField( TextField(
value = content, onValueChange = onContentChange, 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'))
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
}
}
onContentChange(newText)
},
modifier = Modifier.fillMaxWidth().height(180.dp), modifier = Modifier.fillMaxWidth().height(180.dp),
textStyle = MaterialTheme.typography.bodyMedium.copy(color = textColor), textStyle = MaterialTheme.typography.bodyMedium.copy(
color = textColor,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
),
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = accent, unfocusedIndicatorColor = subtleColor.copy(alpha = 0.3f), cursorColor = accent, focusedIndicatorColor = accent, unfocusedIndicatorColor = subtleColor.copy(alpha = 0.3f), cursorColor = accent,
), ),
) )
val previewTasks = remember(content) {
com.avinal.memos.parser.TaskParser.extractTasks("preview", content)
}
if (previewTasks.isNotEmpty()) {
Column(modifier = Modifier.padding(top = 4.dp)) {
previewTasks.forEach { task ->
Row(modifier = Modifier.padding(vertical = 2.dp), verticalAlignment = Alignment.CenterVertically) {
Text(if (task.isCompleted) "" else "", fontSize = 12.sp, color = if (task.isCompleted) accent else subtleColor)
Spacer(Modifier.width(6.dp))
Text(task.text, fontSize = 12.sp, color = if (task.isCompleted) subtleColor else textColor, modifier = Modifier.weight(1f))
task.dueDate?.let { EditorChip("$it", accent) }
task.dueTime?.let { EditorChip("$it", accent) }
task.reminder?.let { EditorChip("!$it", subtleColor) }
task.priority?.let { p ->
val c = when (p) { 1 -> com.avinal.memos.ui.theme.PriorityP1; 2 -> com.avinal.memos.ui.theme.PriorityP2; else -> com.avinal.memos.ui.theme.PriorityP3 }
EditorChip("p$p", c)
}
task.lists.forEach { EditorChip("#$it", accent) }
}
}
}
}
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
Box { Row(horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.CenterVertically) {
Text(visibility.name.lowercase(), fontSize = 13.sp, color = subtleColor, modifier = Modifier.clickable { showVisibilityMenu = true }) Box {
if (showVisibilityMenu) { Text(visibility.name.lowercase(), fontSize = 13.sp, color = subtleColor, modifier = Modifier.clickable { showVisibilityMenu = true })
AlertDialog( if (showVisibilityMenu) {
onDismissRequest = { showVisibilityMenu = false }, containerColor = MaterialTheme.colorScheme.surfaceContainer, title = null, AlertDialog(
text = { onDismissRequest = { showVisibilityMenu = false }, containerColor = MaterialTheme.colorScheme.surfaceContainer, title = null,
Column { text = {
MemoVisibility.entries.forEach { vis -> Column {
Text(vis.name.lowercase(), fontSize = 17.sp, color = if (vis == visibility) accent else textColor, MemoVisibility.entries.forEach { vis ->
modifier = Modifier.fillMaxWidth().clickable { onVisibilityChange(vis); showVisibilityMenu = false }.padding(vertical = 10.dp)) Text(vis.name.lowercase(), fontSize = 17.sp, color = if (vis == visibility) accent else textColor,
modifier = Modifier.fillMaxWidth().clickable { onVisibilityChange(vis); showVisibilityMenu = false }.padding(vertical = 10.dp))
}
} }
} },
}, confirmButton = {},
confirmButton = {}, )
) }
}
Text("add task", fontSize = 13.sp, color = subtleColor, modifier = Modifier.clickable {
onContentChange(content.let { if (it.isEmpty() || it.endsWith("\n")) it else "$it\n" } + "- [ ] ")
})
if (isEditingTask) {
Text("due", fontSize = 13.sp, color = subtleColor, modifier = Modifier.clickable { showDatePicker = true })
Text("at", fontSize = 13.sp, color = subtleColor, modifier = Modifier.clickable { showTimePicker = true })
} }
} }
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
@@ -246,6 +367,16 @@ private fun InlineEditor(
} }
} }
@Composable
private fun EditorChip(label: String, color: Color) {
Text(
label, fontSize = 10.sp, color = color,
modifier = Modifier.padding(start = 4.dp)
.background(color.copy(alpha = 0.1f), androidx.compose.foundation.shape.RoundedCornerShape(3.dp))
.padding(horizontal = 4.dp, vertical = 1.dp),
)
}
private val monthNames = listOf("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") private val monthNames = listOf("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
private val dayNames = listOf("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") private val dayNames = listOf("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
@@ -16,12 +16,18 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TimePicker
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -47,11 +53,14 @@ import com.avinal.memos.domain.MemoVisibility
import com.avinal.memos.ui.components.MemoCard import com.avinal.memos.ui.components.MemoCard
import com.avinal.memos.ui.theme.LocalAccentColor import com.avinal.memos.ui.theme.LocalAccentColor
import com.avinal.memos.util.rememberFilePicker import com.avinal.memos.util.rememberFilePicker
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.io.encoding.Base64 import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.todayIn
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
@Composable @Composable
@@ -121,7 +130,7 @@ fun MemoListScreen(
val textColor = MaterialTheme.colorScheme.onBackground val textColor = MaterialTheme.colorScheme.onBackground
val subtleColor = MaterialTheme.colorScheme.onSurfaceVariant val subtleColor = MaterialTheme.colorScheme.onSurfaceVariant
var composeText by remember { mutableStateOf("") } var composeField by remember { mutableStateOf(TextFieldValue("")) }
val defaultVis by produceState(MemoVisibility.PRIVATE) { val defaultVis by produceState(MemoVisibility.PRIVATE) {
deps.tokenStore.defaultVisibility.first().let { value = MemoVisibility.fromApiString(it) } deps.tokenStore.defaultVisibility.first().let { value = MemoVisibility.fromApiString(it) }
} }
@@ -225,8 +234,8 @@ fun MemoListScreen(
.clickable { .clickable {
showInsertMenu = false showInsertMenu = false
when (item) { when (item) {
"code block" -> composeText += "\n```\n\n```" "code block" -> { val t = composeField.text + "\n```\n\n```"; composeField = TextFieldValue(t, TextRange(t.length)) }
"link memo" -> composeText += "\n[memo]()" "link memo" -> { val t = composeField.text + "\n[memo]()"; composeField = TextFieldValue(t, TextRange(t.length)) }
"media", "file" -> launchFilePicker() "media", "file" -> launchFilePicker()
} }
} }
@@ -241,18 +250,36 @@ fun MemoListScreen(
Column(modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 10.dp, bottom = 10.dp)) { Column(modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 10.dp, bottom = 10.dp)) {
TextField( TextField(
value = composeText, value = composeField,
onValueChange = { newText -> onValueChange = { newField ->
// Auto-checklist: if user pressed enter after a task line, auto-insert "- [ ] " val newText = newField.text
if (newText.length > composeText.length && newText.endsWith("\n")) { val oldText = composeField.text
val beforeNewline = newText.dropLast(1) // Backspace on empty auto-inserted line: remove it
val lastLine = beforeNewline.lines().lastOrNull() ?: "" if (newText.length < oldText.length && oldText.endsWith("- [ ] ") && newText == oldText.dropLast(6).trimEnd() + "\n") {
if (lastLine.trimStart().startsWith("- [")) { val cleaned = newText.trimEnd('\n')
composeText = newText + "- [ ] " composeField = TextFieldValue(cleaned, TextRange(cleaned.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 return@TextField
} }
} }
composeText = newText // 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
}
}
composeField = newField
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeholder = { placeholder = {
@@ -261,7 +288,7 @@ fun MemoListScreen(
singleLine = false, singleLine = false,
minLines = 1, minLines = 1,
maxLines = 10, maxLines = 10,
textStyle = MaterialTheme.typography.bodyMedium.copy(color = textColor), textStyle = MaterialTheme.typography.bodyMedium.copy(color = textColor, fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace),
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent,
@@ -271,53 +298,48 @@ fun MemoListScreen(
), ),
) )
// Live metadata preview // Per-task live preview
val previewChips = remember(composeText) { val previewTasks = remember(composeField.text) {
val parser = com.avinal.memos.parser.TaskParser com.avinal.memos.parser.TaskParser.extractTasks("preview", composeField.text)
val tasks = parser.extractTasks("preview", composeText)
if (tasks.isEmpty()) emptyList()
else tasks.flatMap { task ->
buildList {
task.dueDate?.let { add("due: $it" to accent) }
task.dueTime?.let { add("at: $it" to accent) }
task.reminder?.let { add("!$it" to accent) }
task.priority?.let {
val color = when (it) {
1 -> com.avinal.memos.ui.theme.PriorityP1
2 -> com.avinal.memos.ui.theme.PriorityP2
else -> com.avinal.memos.ui.theme.PriorityP3
}
add("p$it" to color)
}
task.lists.forEach { add("#$it" to accent) }
}
}.distinct()
} }
if (previewChips.isNotEmpty()) { if (previewTasks.isNotEmpty()) {
androidx.compose.foundation.layout.FlowRow( Column(modifier = Modifier.padding(top = 6.dp)) {
modifier = Modifier.padding(top = 4.dp), previewTasks.forEach { task ->
horizontalArrangement = Arrangement.spacedBy(6.dp), Row(
verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.padding(vertical = 2.dp),
) { verticalAlignment = Alignment.CenterVertically,
previewChips.forEach { (label, color) -> ) {
Text( Text(
label, if (task.isCompleted) "" else "",
fontSize = 11.sp, fontSize = 12.sp,
color = color, color = if (task.isCompleted) accent else subtleColor,
modifier = Modifier )
.background(color.copy(alpha = 0.1f), androidx.compose.foundation.shape.RoundedCornerShape(4.dp)) Spacer(Modifier.width(6.dp))
.padding(horizontal = 6.dp, vertical = 2.dp), Text(
) task.text,
fontSize = 12.sp,
color = if (task.isCompleted) subtleColor else textColor,
modifier = Modifier.weight(1f),
)
task.dueDate?.let { MetadataChip("$it", accent) }
task.dueTime?.let { MetadataChip("$it", accent) }
task.reminder?.let { MetadataChip("!$it", subtleColor) }
task.priority?.let { p ->
val c = when (p) { 1 -> com.avinal.memos.ui.theme.PriorityP1; 2 -> com.avinal.memos.ui.theme.PriorityP2; else -> com.avinal.memos.ui.theme.PriorityP3 }
MetadataChip("p$p", c)
}
task.lists.forEach { MetadataChip("#$it", accent) }
}
} }
} }
} }
val parseWarnings = remember(composeText) { com.avinal.memos.parser.TaskParser.validateContent(composeText) } val parseWarnings = remember(composeField.text) { com.avinal.memos.parser.TaskParser.validateContent(composeField.text) }
if (parseWarnings.isNotEmpty()) { if (parseWarnings.isNotEmpty()) {
parseWarnings.forEach { warning -> parseWarnings.forEach { warning ->
Text( Text(
"${warning.taskText}: ${warning.issue}", "${warning.taskText}: ${warning.issue}",
fontSize = 11.sp, color = MaterialTheme.colorScheme.error, fontSize = 11.sp, color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(top = 2.dp), modifier = Modifier.padding(top = 2.dp),
) )
@@ -337,42 +359,99 @@ fun MemoListScreen(
Spacer(Modifier.height(6.dp)) Spacer(Modifier.height(6.dp))
// Compose toolbar
var showDatePicker by remember { mutableStateOf(false) }
var showTimePicker by remember { mutableStateOf(false) }
if (showDatePicker) {
val today = kotlin.time.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault())
val dateState = rememberDatePickerState(
initialSelectedDateMillis = today.toEpochDays().toLong() * 86400000L,
)
DatePickerDialog(
onDismissRequest = { showDatePicker = false },
confirmButton = {
TextButton(onClick = {
dateState.selectedDateMillis?.let { ms ->
val d = kotlinx.datetime.Instant.fromEpochMilliseconds(ms)
.toLocalDateTime(kotlinx.datetime.TimeZone.UTC).date
val r = composeField.text.trimEnd() + " $d"; composeField = TextFieldValue(r, TextRange(r.length))
}
showDatePicker = false
}) { Text("ok", color = accent) }
},
dismissButton = {
TextButton(onClick = { showDatePicker = false }) { Text("cancel") }
},
) { DatePicker(state = dateState) }
}
if (showTimePicker) {
val timeState = rememberTimePickerState()
AlertDialog(
onDismissRequest = { showTimePicker = false },
confirmButton = {
TextButton(onClick = {
val h = timeState.hour
val m = timeState.minute
val timeStr = if (m == 0) {
if (h == 0) "12am" else if (h < 12) "${h}am" else if (h == 12) "12pm" else "${h - 12}pm"
} else {
"${h}:${m.toString().padStart(2, '0')}"
}
val r = composeField.text.trimEnd() + " $timeStr"; composeField = TextFieldValue(r, TextRange(r.length))
showTimePicker = false
}) { Text("ok", color = accent) }
},
dismissButton = {
TextButton(onClick = { showTimePicker = false }) { Text("cancel") }
},
text = { TimePicker(state = timeState) },
)
}
val isEditingTask = remember(composeField.text) {
val lastLine = composeField.text.lines().lastOrNull { it.isNotBlank() } ?: ""
lastLine.trimStart().startsWith("- [")
}
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Row(horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.CenterVertically) { Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
Text( ToolbarButton("add task", subtleColor) { val t = composeField.text.let { if (it.isEmpty() || it.endsWith("\n")) it else "$it\n" } + "- [ ] "; composeField = TextFieldValue(t, TextRange(t.length)) }
"+", if (isEditingTask) {
fontSize = 18.sp, ToolbarButton("due", subtleColor) { showDatePicker = true }
fontWeight = FontWeight.Light, ToolbarButton("at", subtleColor) { showTimePicker = true }
color = subtleColor, }
modifier = Modifier.clickable { showInsertMenu = true }, ToolbarButton("+", subtleColor) { showInsertMenu = true }
) }
Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) {
Text( Text(
composeVisibility.name.lowercase(), composeVisibility.name.lowercase(),
fontSize = 12.sp, fontSize = 11.sp,
color = subtleColor, color = subtleColor,
modifier = Modifier.clickable { showVisibilityPicker = true }, modifier = Modifier.clickable { showVisibilityPicker = true },
) )
Text(
"post",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = if (composeField.text.isNotBlank()) accent else subtleColor.copy(alpha = 0.3f),
modifier = Modifier
.then(
if (composeField.text.isNotBlank()) Modifier.clickable {
viewModel.createMemo(composeField.text, composeVisibility, uploadedAttachmentNames)
composeField = TextFieldValue("")
uploadedAttachmentNames = emptyList()
} else Modifier
)
.padding(horizontal = 4.dp, vertical = 4.dp),
)
} }
Text(
"post",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = if (composeText.isNotBlank()) accent else subtleColor.copy(alpha = 0.3f),
modifier = Modifier
.then(
if (composeText.isNotBlank()) Modifier.clickable {
viewModel.createMemo(composeText, composeVisibility, uploadedAttachmentNames)
composeText = ""
uploadedAttachmentNames = emptyList()
} else Modifier
)
.padding(horizontal = 4.dp, vertical = 4.dp),
)
} }
} }
@@ -454,3 +533,29 @@ fun MemoListScreen(
} }
} }
} }
@Composable
private fun ToolbarButton(label: String, color: Color, onClick: () -> Unit) {
Text(
label,
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
color = color,
modifier = Modifier
.clickable(onClick = onClick)
.padding(vertical = 4.dp),
)
}
@Composable
private fun MetadataChip(label: String, color: Color) {
Text(
label,
fontSize = 10.sp,
color = color,
modifier = Modifier
.padding(start = 4.dp)
.background(color.copy(alpha = 0.1f), androidx.compose.foundation.shape.RoundedCornerShape(3.dp))
.padding(horizontal = 4.dp, vertical = 1.dp),
)
}