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.graphics.SolidColor
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.OffsetMapping 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.TransformedText
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
@ -32,8 +34,8 @@ import org.intellij.markdown.parser.MarkdownParser
@Composable @Composable
fun MarkdownEditor( fun MarkdownEditor(
value: String, value: TextFieldValue,
onValueChange: (String) -> Unit, onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
placeholder: (@Composable () -> Unit)? = null placeholder: (@Composable () -> Unit)? = null
) { ) {
@ -58,7 +60,46 @@ fun MarkdownEditor(
BasicTextField( BasicTextField(
value = value, 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), modifier = modifier.focusRequester(focusRequester),
textStyle = MaterialTheme.typography.bodyMedium.copy( textStyle = MaterialTheme.typography.bodyMedium.copy(
color = LocalContentColor.current, color = LocalContentColor.current,
@ -72,7 +113,7 @@ fun MarkdownEditor(
) )
} else { } else {
if (placeholder != null && value.isBlank()) { if (placeholder != null && value.text.isBlank()) {
Box( Box(
modifier = modifier.clickable( modifier = modifier.clickable(
indication = null, indication = null,
@ -91,14 +132,14 @@ fun MarkdownEditor(
} }
} else { } else {
MarkdownText( MarkdownText(
value, value.text,
modifier = modifier.clickable( modifier = modifier.clickable(
indication = null, indication = null,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
) { ) {
focused = true 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.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@ -93,7 +94,7 @@ fun NotesWidget(
} }
) )
AnimatedVisibility(isLastWidget == false && text.isBlank()) { AnimatedVisibility(isLastWidget == false && text.text.isBlank()) {
IconButton( IconButton(
onClick = { onClick = {
viewModel.dismissNote() viewModel.dismissNote()
@ -105,7 +106,7 @@ fun NotesWidget(
} }
} }
AnimatedVisibility(text.isNotBlank()) { AnimatedVisibility(text.text.isNotBlank()) {
var showMenu by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) }
Box( Box(
modifier = Modifier modifier = Modifier
@ -168,7 +169,7 @@ fun NotesWidget(
} }
} else { } else {
val content = text val content = text
viewModel.setText("") viewModel.setText(TextFieldValue(""))
lifecycleOwner.lifecycleScope.launch { lifecycleOwner.lifecycleScope.launch {
val result = snackbarHostState.showSnackbar( val result = snackbarHostState.showSnackbar(
message = context.getString(R.string.notes_widget_dismissed), message = context.getString(R.string.notes_widget_dismissed),

View File

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