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.androidx.navigation.compose)
|
||||||
|
|
||||||
implementation(libs.jsoup)
|
implementation(libs.jsoup)
|
||||||
|
implementation(libs.markdown)
|
||||||
|
|
||||||
// Legacy dependencies
|
// Legacy dependencies
|
||||||
implementation(libs.androidx.transition)
|
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.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.mm20.launcher2.ui.component.markdown.MarkdownEditor
|
||||||
import de.mm20.launcher2.widgets.NotesWidget
|
import de.mm20.launcher2.widgets.NotesWidget
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -25,13 +26,9 @@ fun NotesWidget(widget: NotesWidget) {
|
|||||||
|
|
||||||
val text by viewModel.noteText
|
val text by viewModel.noteText
|
||||||
|
|
||||||
BasicTextField(
|
MarkdownEditor(
|
||||||
value = text,
|
value = text,
|
||||||
onValueChange = { viewModel.setText(it) },
|
onValueChange = { viewModel.setText(it) },
|
||||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
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
|
package de.mm20.launcher2.ui.launcher.widgets.notes
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@ -11,6 +12,10 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
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.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,13 @@ val OpenSourceLicenses = arrayOf(
|
|||||||
licenseText = R.raw.license_apache_2,
|
licenseText = R.raw.license_apache_2,
|
||||||
url = "https://github.com/Kotlin/kotlinx.collections.immutable"
|
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(
|
OpenSourceLibrary(
|
||||||
name = "KotlinX Serialization",
|
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.",
|
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")
|
library("kotlinx.serialization.json", "org.jetbrains.kotlinx", "kotlinx-serialization-json")
|
||||||
.versionRef("kotlinx.serialization")
|
.versionRef("kotlinx.serialization")
|
||||||
|
|
||||||
|
library("markdown", "org.jetbrains", "markdown")
|
||||||
|
.version("0.4.1")
|
||||||
|
|
||||||
version("androidx.compose.compiler", "1.4.5")
|
version("androidx.compose.compiler", "1.4.5")
|
||||||
library("androidx.compose.runtime", "androidx.compose.runtime", "runtime")
|
library("androidx.compose.runtime", "androidx.compose.runtime", "runtime")
|
||||||
.version("1.5.0-alpha03")
|
.version("1.5.0-alpha03")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user