Markdown editor: improve list handling

This commit is contained in:
MM20 2023-04-27 20:54:26 +02:00
parent 328a69e75b
commit e726ff1ffd
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
3 changed files with 57 additions and 14 deletions

View File

@ -23,8 +23,10 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
@ -32,8 +34,8 @@ import org.intellij.markdown.parser.MarkdownParser
@Composable
fun MarkdownEditor(
value: String,
onValueChange: (String) -> Unit,
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier,
placeholder: (@Composable () -> Unit)? = null
) {
@ -58,7 +60,46 @@ fun MarkdownEditor(
BasicTextField(
value = value,
onValueChange = onValueChange,
onValueChange = {
val cursorPosition = if (it.selection.collapsed) it.selection.start else null
// If:
// - multiple chars are selected
// - the last action was not an insert
// - the cursor char before the selection is not a newline
// do nothing.
if (cursorPosition == null || it.text.length <= value.text.length || it.text.getOrNull(
cursorPosition - 1
) != '\n'
) {
onValueChange(it)
} else {
// else check if the previous line was a list, if yes, add a list item
val prevLine = it.text.substring(0, cursorPosition - 1).substringAfterLast('\n')
val leadingSpaces = prevLine.takeWhile { it == ' ' }
val prevLineWithoutLeadingSpaces = prevLine.trimStart()
val listMarker = leadingSpaces + when {
prevLineWithoutLeadingSpaces.startsWith("- [ ] ") -> "- [ ] "
prevLineWithoutLeadingSpaces.startsWith("- [x] ") -> "- [ ] "
prevLineWithoutLeadingSpaces.startsWith("- ") -> "- "
prevLineWithoutLeadingSpaces.startsWith("* ") -> "* "
prevLineWithoutLeadingSpaces.startsWith("+ ") -> "+ "
prevLineWithoutLeadingSpaces.startsWith("1. ") -> "1. "
else -> {
onValueChange(it)
return@BasicTextField
}
}
onValueChange(
it.copy(
text = it.text.substring(
0,
cursorPosition
) + listMarker + it.text.substring(cursorPosition),
selection = TextRange(cursorPosition + listMarker.length)
)
)
}
},
modifier = modifier.focusRequester(focusRequester),
textStyle = MaterialTheme.typography.bodyMedium.copy(
color = LocalContentColor.current,
@ -72,7 +113,7 @@ fun MarkdownEditor(
)
} else {
if (placeholder != null && value.isBlank()) {
if (placeholder != null && value.text.isBlank()) {
Box(
modifier = modifier.clickable(
indication = null,
@ -91,14 +132,14 @@ fun MarkdownEditor(
}
} else {
MarkdownText(
value,
value.text,
modifier = modifier.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
) {
focused = true
},
onTextChange = onValueChange,
onTextChange = { onValueChange(TextFieldValue(it)) },
)
}
}

View File

@ -34,6 +34,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
@ -93,7 +94,7 @@ fun NotesWidget(
}
)
AnimatedVisibility(isLastWidget == false && text.isBlank()) {
AnimatedVisibility(isLastWidget == false && text.text.isBlank()) {
IconButton(
onClick = {
viewModel.dismissNote()
@ -105,7 +106,7 @@ fun NotesWidget(
}
}
AnimatedVisibility(text.isNotBlank()) {
AnimatedVisibility(text.text.isNotBlank()) {
var showMenu by remember { mutableStateOf(false) }
Box(
modifier = Modifier
@ -168,7 +169,7 @@ fun NotesWidget(
}
} else {
val content = text
viewModel.setText("")
viewModel.setText(TextFieldValue(""))
lifecycleOwner.lifecycleScope.launch {
val result = snackbarHostState.showSnackbar(
message = context.getString(R.string.notes_widget_dismissed),

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
@ -26,7 +27,7 @@ class NotesWidgetVM(
) : ViewModel() {
private val widget = MutableStateFlow<NotesWidget?>(null)
val noteText = mutableStateOf(widget.value?.config?.storedText ?: "")
val noteText = mutableStateOf(TextFieldValue(widget.value?.config?.storedText ?: ""))
val isLastNoteWidget = widgetsService.countWidgets(NotesWidget.Type).map {
it == 1
@ -35,23 +36,23 @@ class NotesWidgetVM(
fun updateWidget(widget: NotesWidget) {
val oldId = this.widget.value?.id
this.widget.value = widget
if (widget.id != oldId) noteText.value = widget.config.storedText
if (widget.id != oldId) noteText.value = TextFieldValue(widget.config.storedText)
}
private var updateJob: Job? = null
fun setText(text: String) {
fun setText(text: TextFieldValue) {
noteText.value = text
updateJob?.cancel()
val widget = widget.value ?: return
updateJob = viewModelScope.launch {
delay(1000)
widgetsService.updateWidget(widget.copy(config = widget.config.copy(storedText = text)))
widgetsService.updateWidget(widget.copy(config = widget.config.copy(storedText = text.text)))
}
}
fun exportNote(context: Context, uri: Uri) {
viewModelScope.launch(Dispatchers.IO) {
val text = noteText.value
val text = noteText.value.text
Log.d("MM20", text)
val outputStream = context.contentResolver.openOutputStream(uri)
outputStream?.use {