Notes widget: add markdown support
This commit is contained in:
parent
0ea8543e33
commit
617ac0f25f
@ -81,6 +81,7 @@ dependencies {
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
implementation(libs.jsoup)
|
||||
implementation(libs.markdown)
|
||||
|
||||
// Legacy dependencies
|
||||
implementation(libs.androidx.transition)
|
||||
|
||||
@ -0,0 +1,219 @@
|
||||
package de.mm20.launcher2.ui.component.markdown
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.input.TransformedText
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.intellij.markdown.MarkdownElementTypes
|
||||
import org.intellij.markdown.MarkdownTokenTypes
|
||||
import org.intellij.markdown.ast.ASTNode
|
||||
import org.intellij.markdown.flavours.space.SFMFlavourDescriptor
|
||||
import org.intellij.markdown.parser.MarkdownParser
|
||||
import kotlin.math.min
|
||||
|
||||
@Composable
|
||||
fun MarkdownEditor(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val typography = MaterialTheme.typography
|
||||
val delimiterColor = MaterialTheme.colorScheme.secondary
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
BackHandler(
|
||||
enabled = focused
|
||||
) {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
|
||||
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = modifier,
|
||||
textStyle = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = LocalContentColor.current,
|
||||
),
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
visualTransformation = remember(
|
||||
typography,
|
||||
focused,
|
||||
delimiterColor
|
||||
) { MarkdownTransformation(typography, focused, delimiterColor) },
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
|
||||
class MarkdownTransformation(
|
||||
private val typography: Typography,
|
||||
renderDelimiters: Boolean,
|
||||
delimiterColor: Color,
|
||||
) : VisualTransformation {
|
||||
|
||||
private val parser = MarkdownParser(SFMFlavourDescriptor())
|
||||
private val delimiterStyle = SpanStyle(
|
||||
color = delimiterColor,
|
||||
fontSize = if (renderDelimiters) TextUnit.Unspecified else 0.sp
|
||||
)
|
||||
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
val tree = parser.buildMarkdownTreeFromString(text.text)
|
||||
return TransformedText(
|
||||
buildAnnotatedString {
|
||||
append(text)
|
||||
applyStyles(tree, typography, delimiterStyle)
|
||||
},
|
||||
OffsetMapping.Identity,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun AnnotatedString.Builder.applyStyles(
|
||||
node: ASTNode,
|
||||
typography: Typography,
|
||||
delimiterStyle: SpanStyle
|
||||
) {
|
||||
when (node.type) {
|
||||
MarkdownElementTypes.STRONG -> {
|
||||
addStyle(
|
||||
SpanStyle(fontWeight = FontWeight.Bold),
|
||||
node.startOffset,
|
||||
node.endOffset
|
||||
)
|
||||
}
|
||||
|
||||
MarkdownElementTypes.EMPH -> {
|
||||
addStyle(
|
||||
SpanStyle(fontStyle = FontStyle.Italic),
|
||||
node.startOffset,
|
||||
node.endOffset
|
||||
)
|
||||
}
|
||||
|
||||
MarkdownElementTypes.CODE_SPAN -> {
|
||||
addStyle(
|
||||
SpanStyle(fontFamily = FontFamily.Monospace),
|
||||
node.startOffset,
|
||||
node.endOffset
|
||||
)
|
||||
}
|
||||
|
||||
MarkdownElementTypes.ATX_1 -> {
|
||||
addStyle(
|
||||
typography.headlineLarge.toSpanStyle(),
|
||||
node.startOffset,
|
||||
node.endOffset
|
||||
)
|
||||
addStyle(
|
||||
typography.headlineLarge.toParagraphStyle(),
|
||||
node.startOffset,
|
||||
min(node.endOffset + 1, length)
|
||||
)
|
||||
}
|
||||
|
||||
MarkdownElementTypes.ATX_2 -> {
|
||||
addStyle(
|
||||
typography.headlineMedium.toSpanStyle(),
|
||||
node.startOffset,
|
||||
node.endOffset
|
||||
)
|
||||
addStyle(
|
||||
typography.headlineMedium.toParagraphStyle(),
|
||||
node.startOffset,
|
||||
min(node.endOffset + 1, length)
|
||||
)
|
||||
}
|
||||
|
||||
MarkdownElementTypes.ATX_3 -> {
|
||||
addStyle(
|
||||
typography.headlineSmall.toSpanStyle(),
|
||||
node.startOffset,
|
||||
node.endOffset
|
||||
)
|
||||
addStyle(
|
||||
typography.headlineSmall.toParagraphStyle(),
|
||||
node.startOffset,
|
||||
min(node.endOffset + 1, length)
|
||||
)
|
||||
}
|
||||
|
||||
MarkdownElementTypes.ATX_4 -> {
|
||||
addStyle(
|
||||
typography.titleLarge.toSpanStyle(),
|
||||
node.startOffset,
|
||||
node.endOffset
|
||||
)
|
||||
addStyle(
|
||||
typography.titleLarge.toParagraphStyle(),
|
||||
node.startOffset,
|
||||
min(node.endOffset + 1, length)
|
||||
)
|
||||
}
|
||||
|
||||
MarkdownElementTypes.ATX_5 -> {
|
||||
addStyle(
|
||||
typography.titleMedium.toSpanStyle(),
|
||||
node.startOffset,
|
||||
node.endOffset
|
||||
)
|
||||
addStyle(
|
||||
typography.titleMedium.toParagraphStyle(),
|
||||
node.startOffset,
|
||||
min(node.endOffset + 1, length)
|
||||
)
|
||||
}
|
||||
|
||||
MarkdownElementTypes.ATX_6 -> {
|
||||
addStyle(
|
||||
typography.titleSmall.toSpanStyle(),
|
||||
node.startOffset,
|
||||
node.endOffset
|
||||
)
|
||||
addStyle(
|
||||
typography.titleSmall.toParagraphStyle(),
|
||||
node.startOffset,
|
||||
min(node.endOffset + 1, length)
|
||||
)
|
||||
}
|
||||
}
|
||||
for (child in node.children) {
|
||||
applyStyles(child, typography, delimiterStyle)
|
||||
}
|
||||
|
||||
if (node.children.isEmpty() && node.type != MarkdownTokenTypes.TEXT
|
||||
&& node.children.isEmpty() && node.type != MarkdownTokenTypes.LIST_BULLET
|
||||
&& node.children.isEmpty() && node.type != MarkdownTokenTypes.LIST_NUMBER
|
||||
&& node.children.isEmpty() && node.type != MarkdownTokenTypes.WHITE_SPACE
|
||||
) {
|
||||
addStyle(
|
||||
delimiterStyle,
|
||||
node.startOffset,
|
||||
node.endOffset,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -13,6 +13,7 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.ui.component.markdown.MarkdownEditor
|
||||
import de.mm20.launcher2.widgets.NotesWidget
|
||||
|
||||
@Composable
|
||||
@ -25,13 +26,9 @@ fun NotesWidget(widget: NotesWidget) {
|
||||
|
||||
val text by viewModel.noteText
|
||||
|
||||
BasicTextField(
|
||||
MarkdownEditor(
|
||||
value = text,
|
||||
onValueChange = { viewModel.setText(it) },
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
textStyle = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = LocalContentColor.current,
|
||||
),
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package de.mm20.launcher2.ui.launcher.widgets.notes
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@ -11,6 +12,10 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.intellij.markdown.ast.ASTNode
|
||||
import org.intellij.markdown.ast.getTextInNode
|
||||
import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor
|
||||
import org.intellij.markdown.parser.MarkdownParser
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
|
||||
@ -24,6 +24,13 @@ val OpenSourceLicenses = arrayOf(
|
||||
licenseText = R.raw.license_apache_2,
|
||||
url = "https://github.com/Kotlin/kotlinx.collections.immutable"
|
||||
),
|
||||
OpenSourceLibrary(
|
||||
name = "IntelliJ Markdown",
|
||||
description = "Multiplatform Markdown processor written in Kotlin.",
|
||||
licenseName = R.string.apache_license_name,
|
||||
licenseText = R.raw.license_apache_2,
|
||||
url = "https://github.com/JetBrains/markdown"
|
||||
),
|
||||
OpenSourceLibrary(
|
||||
name = "KotlinX Serialization",
|
||||
description = "Kotlin serialization consists of a compiler plugin, that generates visitor code for serializable classes, runtime library with core serialization API and support libraries with various serialization formats.",
|
||||
|
||||
@ -34,6 +34,9 @@ dependencyResolutionManagement {
|
||||
library("kotlinx.serialization.json", "org.jetbrains.kotlinx", "kotlinx-serialization-json")
|
||||
.versionRef("kotlinx.serialization")
|
||||
|
||||
library("markdown", "org.jetbrains", "markdown")
|
||||
.version("0.4.1")
|
||||
|
||||
version("androidx.compose.compiler", "1.4.5")
|
||||
library("androidx.compose.runtime", "androidx.compose.runtime", "runtime")
|
||||
.version("1.5.0-alpha03")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user