mirror of
https://github.com/avinal/nikki.git
synced 2026-07-03 21:40:09 +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:
@@ -15,11 +15,17 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
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.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
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.getValue
|
||||
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.util.sharePlainText
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.datetime.todayIn
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
|
||||
@@ -201,6 +209,7 @@ private fun MetroMenuItem(text: String, color: Color, onClick: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun InlineEditor(
|
||||
content: String, visibility: MemoVisibility, accent: Color,
|
||||
@@ -209,33 +218,145 @@ private fun InlineEditor(
|
||||
onSave: () -> Unit, onCancel: () -> Unit,
|
||||
) {
|
||||
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(
|
||||
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),
|
||||
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(
|
||||
focusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent,
|
||||
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))
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
Box {
|
||||
Text(visibility.name.lowercase(), fontSize = 13.sp, color = subtleColor, modifier = Modifier.clickable { showVisibilityMenu = true })
|
||||
if (showVisibilityMenu) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showVisibilityMenu = false }, containerColor = MaterialTheme.colorScheme.surfaceContainer, title = null,
|
||||
text = {
|
||||
Column {
|
||||
MemoVisibility.entries.forEach { vis ->
|
||||
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))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box {
|
||||
Text(visibility.name.lowercase(), fontSize = 13.sp, color = subtleColor, modifier = Modifier.clickable { showVisibilityMenu = true })
|
||||
if (showVisibilityMenu) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showVisibilityMenu = false }, containerColor = MaterialTheme.colorScheme.surfaceContainer, title = null,
|
||||
text = {
|
||||
Column {
|
||||
MemoVisibility.entries.forEach { vis ->
|
||||
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)) {
|
||||
@@ -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 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.rememberLazyListState
|
||||
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.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TimePicker
|
||||
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.LaunchedEffect
|
||||
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.theme.LocalAccentColor
|
||||
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.launch
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlinx.datetime.todayIn
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
||||
@Composable
|
||||
@@ -121,7 +130,7 @@ fun MemoListScreen(
|
||||
val textColor = MaterialTheme.colorScheme.onBackground
|
||||
val subtleColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
||||
var composeText by remember { mutableStateOf("") }
|
||||
var composeField by remember { mutableStateOf(TextFieldValue("")) }
|
||||
val defaultVis by produceState(MemoVisibility.PRIVATE) {
|
||||
deps.tokenStore.defaultVisibility.first().let { value = MemoVisibility.fromApiString(it) }
|
||||
}
|
||||
@@ -225,8 +234,8 @@ fun MemoListScreen(
|
||||
.clickable {
|
||||
showInsertMenu = false
|
||||
when (item) {
|
||||
"code block" -> composeText += "\n```\n\n```"
|
||||
"link memo" -> composeText += "\n[memo]()"
|
||||
"code block" -> { val t = composeField.text + "\n```\n\n```"; composeField = TextFieldValue(t, TextRange(t.length)) }
|
||||
"link memo" -> { val t = composeField.text + "\n[memo]()"; composeField = TextFieldValue(t, TextRange(t.length)) }
|
||||
"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)) {
|
||||
TextField(
|
||||
value = composeText,
|
||||
onValueChange = { newText ->
|
||||
// Auto-checklist: if user pressed enter after a task line, auto-insert "- [ ] "
|
||||
if (newText.length > composeText.length && newText.endsWith("\n")) {
|
||||
val beforeNewline = newText.dropLast(1)
|
||||
val lastLine = beforeNewline.lines().lastOrNull() ?: ""
|
||||
if (lastLine.trimStart().startsWith("- [")) {
|
||||
composeText = newText + "- [ ] "
|
||||
value = composeField,
|
||||
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))
|
||||
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
|
||||
}
|
||||
}
|
||||
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(),
|
||||
placeholder = {
|
||||
@@ -261,7 +288,7 @@ fun MemoListScreen(
|
||||
singleLine = false,
|
||||
minLines = 1,
|
||||
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(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
@@ -271,53 +298,48 @@ fun MemoListScreen(
|
||||
),
|
||||
)
|
||||
|
||||
// Live metadata preview
|
||||
val previewChips = remember(composeText) {
|
||||
val parser = com.avinal.memos.parser.TaskParser
|
||||
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()
|
||||
// Per-task live preview
|
||||
val previewTasks = remember(composeField.text) {
|
||||
com.avinal.memos.parser.TaskParser.extractTasks("preview", composeField.text)
|
||||
}
|
||||
|
||||
if (previewChips.isNotEmpty()) {
|
||||
androidx.compose.foundation.layout.FlowRow(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
previewChips.forEach { (label, color) ->
|
||||
Text(
|
||||
label,
|
||||
fontSize = 11.sp,
|
||||
color = color,
|
||||
modifier = Modifier
|
||||
.background(color.copy(alpha = 0.1f), androidx.compose.foundation.shape.RoundedCornerShape(4.dp))
|
||||
.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
)
|
||||
if (previewTasks.isNotEmpty()) {
|
||||
Column(modifier = Modifier.padding(top = 6.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 { 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()) {
|
||||
parseWarnings.forEach { warning ->
|
||||
Text(
|
||||
"⚠ ${warning.taskText}: ${warning.issue}",
|
||||
"${warning.taskText}: ${warning.issue}",
|
||||
fontSize = 11.sp, color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(top = 2.dp),
|
||||
)
|
||||
@@ -337,42 +359,99 @@ fun MemoListScreen(
|
||||
|
||||
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(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
"+",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = subtleColor,
|
||||
modifier = Modifier.clickable { showInsertMenu = true },
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
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) {
|
||||
ToolbarButton("due", subtleColor) { showDatePicker = true }
|
||||
ToolbarButton("at", subtleColor) { showTimePicker = true }
|
||||
}
|
||||
ToolbarButton("+", subtleColor) { showInsertMenu = true }
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
composeVisibility.name.lowercase(),
|
||||
fontSize = 12.sp,
|
||||
fontSize = 11.sp,
|
||||
color = subtleColor,
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user