Markdown editor: improve list handling
This commit is contained in:
parent
328a69e75b
commit
e726ff1ffd
@ -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)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user