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:
@@ -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,18 +218,122 @@ 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) {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Box {
|
Box {
|
||||||
Text(visibility.name.lowercase(), fontSize = 13.sp, color = subtleColor, modifier = Modifier.clickable { showVisibilityMenu = true })
|
Text(visibility.name.lowercase(), fontSize = 13.sp, color = subtleColor, modifier = Modifier.clickable { showVisibilityMenu = true })
|
||||||
if (showVisibilityMenu) {
|
if (showVisibilityMenu) {
|
||||||
@@ -238,6 +351,14 @@ private fun InlineEditor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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)) {
|
||||||
Text("cancel", fontSize = 14.sp, color = subtleColor, modifier = Modifier.clickable(onClick = onCancel))
|
Text("cancel", fontSize = 14.sp, color = subtleColor, modifier = Modifier.clickable(onClick = onCancel))
|
||||||
Text("save", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = if (content.isNotBlank()) accent else subtleColor,
|
Text("save", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = if (content.isNotBlank()) accent else subtleColor,
|
||||||
@@ -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))
|
|
||||||
.padding(horizontal = 6.dp, vertical = 2.dp),
|
|
||||||
)
|
)
|
||||||
|
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()) {
|
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,37 +359,93 @@ 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(
|
Text(
|
||||||
"post",
|
"post",
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = if (composeText.isNotBlank()) accent else subtleColor.copy(alpha = 0.3f),
|
color = if (composeField.text.isNotBlank()) accent else subtleColor.copy(alpha = 0.3f),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.then(
|
.then(
|
||||||
if (composeText.isNotBlank()) Modifier.clickable {
|
if (composeField.text.isNotBlank()) Modifier.clickable {
|
||||||
viewModel.createMemo(composeText, composeVisibility, uploadedAttachmentNames)
|
viewModel.createMemo(composeField.text, composeVisibility, uploadedAttachmentNames)
|
||||||
composeText = ""
|
composeField = TextFieldValue("")
|
||||||
uploadedAttachmentNames = emptyList()
|
uploadedAttachmentNames = emptyList()
|
||||||
} else Modifier
|
} else Modifier
|
||||||
)
|
)
|
||||||
@@ -375,6 +453,7 @@ fun MemoListScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(
|
Spacer(
|
||||||
Modifier.fillMaxWidth().height(1.dp)
|
Modifier.fillMaxWidth().height(1.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