feat(labs): sandbox for new markup experiment
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
aeed919b0d
commit
a308ecf211
|
|
@ -267,6 +267,9 @@ dependencies {
|
||||||
implementation "androidx.datastore:datastore:1.1.1"
|
implementation "androidx.datastore:datastore:1.1.1"
|
||||||
implementation "androidx.datastore:datastore-preferences:1.1.1"
|
implementation "androidx.datastore:datastore-preferences:1.1.1"
|
||||||
|
|
||||||
|
// Markup
|
||||||
|
implementation "org.jetbrains:markdown:0.7.3"
|
||||||
|
|
||||||
// Livekit
|
// Livekit
|
||||||
// FIXME temporarily not included, re-add when realtime media is to be implemented
|
// FIXME temporarily not included, re-add when realtime media is to be implemented
|
||||||
// implementation "io.livekit:livekit-android:$livekit_version"
|
// implementation "io.livekit:livekit-android:$livekit_version"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package chat.revolt.components.markdown.jbm
|
||||||
|
|
||||||
|
import org.intellij.markdown.ast.ASTNode
|
||||||
|
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
|
||||||
|
import org.intellij.markdown.parser.MarkdownParser
|
||||||
|
|
||||||
|
@RequiresOptIn(message = "This API is experimental and has many TODOs.")
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||||
|
annotation class JBM
|
||||||
|
|
||||||
|
@JBM
|
||||||
|
object JBMApi {
|
||||||
|
fun parse(src: String): ASTNode {
|
||||||
|
return MarkdownParser(GFMFlavourDescriptor()).buildMarkdownTreeFromString(src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,365 @@
|
||||||
|
package chat.revolt.components.markdown.jbm
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.text.InlineTextContent
|
||||||
|
import androidx.compose.foundation.text.appendInlineContent
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.structuralEqualityPolicy
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.Placeholder
|
||||||
|
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextLayoutResult
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.text.withStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import chat.revolt.components.markdown.Annotations
|
||||||
|
import chat.revolt.components.utils.detectTapGesturesConditionalConsume
|
||||||
|
import org.intellij.markdown.MarkdownElementTypes
|
||||||
|
import org.intellij.markdown.MarkdownTokenTypes
|
||||||
|
import org.intellij.markdown.ast.ASTNode
|
||||||
|
import org.intellij.markdown.ast.getTextInNode
|
||||||
|
import org.intellij.markdown.flavours.gfm.GFMElementTypes
|
||||||
|
import org.intellij.markdown.flavours.gfm.GFMTokenTypes
|
||||||
|
|
||||||
|
data class JBMarkdownTreeState(
|
||||||
|
val sourceText: String = "",
|
||||||
|
val ignoreLineBreaks: Boolean = false,
|
||||||
|
val listDepth: Int = 0,
|
||||||
|
val fontSizeMultiplier: Float = 1f,
|
||||||
|
val linksClickable: Boolean = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val LocalJBMarkdownTreeState =
|
||||||
|
compositionLocalOf(structuralEqualityPolicy()) { JBMarkdownTreeState() }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@JBM
|
||||||
|
fun JBMRenderer(content: String, modifier: Modifier = Modifier) {
|
||||||
|
var tree by remember { mutableStateOf(JBMApi.parse(content)) }
|
||||||
|
|
||||||
|
LaunchedEffect(content) {
|
||||||
|
tree = JBMApi.parse(content)
|
||||||
|
|
||||||
|
Log.d("JBMRenderer", "Parsed tree: ${tree.children.map { it.type.name }}")
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalJBMarkdownTreeState provides JBMarkdownTreeState(content)
|
||||||
|
) {
|
||||||
|
tree.children.map {
|
||||||
|
JBMBlock(it, modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun annotateText(
|
||||||
|
state: JBMarkdownTreeState,
|
||||||
|
node: ASTNode
|
||||||
|
): AnnotatedString {
|
||||||
|
val sourceText = state.sourceText
|
||||||
|
|
||||||
|
return try {
|
||||||
|
buildAnnotatedString {
|
||||||
|
when (node.type) {
|
||||||
|
MarkdownTokenTypes.TEXT -> {
|
||||||
|
append(node.getTextInNode(sourceText))
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownElementTypes.EMPH -> {
|
||||||
|
withStyle(SpanStyle(fontStyle = FontStyle.Italic)) {
|
||||||
|
for (child in node.children) {
|
||||||
|
append(annotateText(state, child))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownElementTypes.STRONG -> {
|
||||||
|
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||||
|
for (child in node.children) {
|
||||||
|
append(annotateText(state, child))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GFMElementTypes.STRIKETHROUGH -> {
|
||||||
|
withStyle(SpanStyle(textDecoration = TextDecoration.LineThrough)) {
|
||||||
|
// Skip the first two children and the last two children
|
||||||
|
// because they are the tilde characters
|
||||||
|
for (child in node.children.subList(2, node.children.size - 2)) {
|
||||||
|
append(annotateText(state, child))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownTokenTypes.LIST_BULLET -> {
|
||||||
|
append(" ".repeat(state.listDepth) + " " + if (state.listDepth % 2 == 0) "•" else "◦" + " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownTokenTypes.LIST_NUMBER -> {
|
||||||
|
withStyle(SpanStyle(fontFeatureSettings = "'tnum'")) {
|
||||||
|
append(" ".repeat(state.listDepth) + "${node.getTextInNode(sourceText)} ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownElementTypes.UNORDERED_LIST,
|
||||||
|
MarkdownElementTypes.ORDERED_LIST,
|
||||||
|
MarkdownElementTypes.LIST_ITEM -> {
|
||||||
|
for (child in node.children) {
|
||||||
|
append(annotateText(state, child))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GFMTokenTypes.CHECK_BOX -> {
|
||||||
|
if (node.getTextInNode(sourceText).trim() == "[ ]") {
|
||||||
|
appendInlineContent("checkbox", "❌")
|
||||||
|
} else {
|
||||||
|
appendInlineContent("checkbox", "✅")
|
||||||
|
}
|
||||||
|
append(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownElementTypes.PARAGRAPH, MarkdownElementTypes.HTML_BLOCK -> {
|
||||||
|
for (child in node.children) {
|
||||||
|
append(annotateText(state, child))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-render types
|
||||||
|
// for example, various syntactic elements like exclamation marks, brackets, etc.
|
||||||
|
// we simply append the text as is
|
||||||
|
MarkdownTokenTypes.EXCLAMATION_MARK,
|
||||||
|
MarkdownTokenTypes.LBRACKET,
|
||||||
|
MarkdownTokenTypes.RBRACKET,
|
||||||
|
MarkdownTokenTypes.LPAREN,
|
||||||
|
MarkdownTokenTypes.RPAREN,
|
||||||
|
MarkdownTokenTypes.LT,
|
||||||
|
MarkdownTokenTypes.GT,
|
||||||
|
MarkdownTokenTypes.BACKTICK,
|
||||||
|
MarkdownTokenTypes.DOUBLE_QUOTE,
|
||||||
|
MarkdownTokenTypes.SINGLE_QUOTE,
|
||||||
|
MarkdownTokenTypes.EOL,
|
||||||
|
MarkdownTokenTypes.WHITE_SPACE,
|
||||||
|
MarkdownTokenTypes.COLON,
|
||||||
|
GFMTokenTypes.TILDE -> {
|
||||||
|
append(node.getTextInNode(sourceText))
|
||||||
|
}
|
||||||
|
|
||||||
|
// no-op types
|
||||||
|
// for example, the special characters that are used to denote the markup are here
|
||||||
|
MarkdownTokenTypes.EMPH -> {
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
append("[${node.type.name}]{\n")
|
||||||
|
append(node.getTextInNode(sourceText))
|
||||||
|
append("\n}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
buildAnnotatedString {
|
||||||
|
withStyle(SpanStyle(color = Color(0xFFFF0000), background = Color(0xFF000000))) {
|
||||||
|
append("[${node.type.name}] Error: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.e("JBMRenderer", "Error rendering node: ${node.type.name}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun JBMText(node: ASTNode, modifier: Modifier) {
|
||||||
|
var layoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||||
|
val mdState = LocalJBMarkdownTreeState.current
|
||||||
|
val annotatedText = remember(node) { annotateText(mdState, node) }
|
||||||
|
val colours = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
val shouldConsumeTap = handler@{ offset: Int ->
|
||||||
|
Annotations.entries.filter { it.clickable }.map { it.tag }.forEach { tag ->
|
||||||
|
if (annotatedText.getStringAnnotations(
|
||||||
|
tag = tag,
|
||||||
|
start = offset,
|
||||||
|
end = offset
|
||||||
|
).isNotEmpty()
|
||||||
|
) {
|
||||||
|
return@handler true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return@handler false
|
||||||
|
}
|
||||||
|
|
||||||
|
val onClick = handler@{ offset: Int ->
|
||||||
|
if (mdState.linksClickable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onLongClick = handler@{ offset: Int ->
|
||||||
|
if (mdState.linksClickable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = annotatedText,
|
||||||
|
onTextLayout = { layoutResult = it },
|
||||||
|
modifier = modifier.pointerInput(onClick, onLongClick) {
|
||||||
|
detectTapGesturesConditionalConsume(
|
||||||
|
onTap = { pos ->
|
||||||
|
val index =
|
||||||
|
layoutResult?.getOffsetForPosition(pos)
|
||||||
|
?: return@detectTapGesturesConditionalConsume
|
||||||
|
onClick(index)
|
||||||
|
},
|
||||||
|
onLongPress = { pos ->
|
||||||
|
val index =
|
||||||
|
layoutResult?.getOffsetForPosition(pos)
|
||||||
|
?: return@detectTapGesturesConditionalConsume
|
||||||
|
onLongClick(index)
|
||||||
|
},
|
||||||
|
shouldConsumeTap = { pos ->
|
||||||
|
val index =
|
||||||
|
layoutResult?.getOffsetForPosition(pos)
|
||||||
|
?: return@detectTapGesturesConditionalConsume false
|
||||||
|
shouldConsumeTap(index)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
inlineContent = mapOf(
|
||||||
|
"checkbox" to InlineTextContent(
|
||||||
|
placeholder = Placeholder(
|
||||||
|
width = LocalTextStyle.current.fontSize * 1.5,
|
||||||
|
height = LocalTextStyle.current.fontSize * 1.5,
|
||||||
|
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
|
||||||
|
),
|
||||||
|
children = { alternateText ->
|
||||||
|
val isCheck = alternateText == "✅"
|
||||||
|
|
||||||
|
with(LocalDensity.current) {
|
||||||
|
Canvas(modifier = Modifier.size((LocalTextStyle.current.fontSize * 1.5).toDp())) {
|
||||||
|
drawRoundRect(
|
||||||
|
color = if (isCheck) colours.primaryContainer else colours.surfaceContainer,
|
||||||
|
cornerRadius = CornerRadius(size.width * 0.1f),
|
||||||
|
topLeft = Offset(size.width * 0.1f, size.height * 0.1f),
|
||||||
|
size = size.copy(
|
||||||
|
width = size.width * 0.8f,
|
||||||
|
height = size.height * 0.8f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isCheck) {
|
||||||
|
drawPath(
|
||||||
|
path = Path().apply {
|
||||||
|
moveTo(size.width * 0.8f, size.height * 0.3f)
|
||||||
|
lineTo(size.width * 0.4f, size.height * 0.7f)
|
||||||
|
lineTo(size.width * 0.2f, size.height * 0.5f)
|
||||||
|
},
|
||||||
|
color = colours.onPrimaryContainer,
|
||||||
|
style = Stroke(width = size.width * 0.1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun JBMBlock(node: ASTNode, modifier: Modifier) {
|
||||||
|
when (node.type) {
|
||||||
|
MarkdownElementTypes.PARAGRAPH,
|
||||||
|
MarkdownElementTypes.HTML_BLOCK -> {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalTextStyle provides LocalTextStyle.current.copy(
|
||||||
|
fontSize = LocalTextStyle.current.fontSize * LocalJBMarkdownTreeState.current.fontSizeMultiplier
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
JBMText(node, modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownElementTypes.ATX_1,
|
||||||
|
MarkdownElementTypes.ATX_2,
|
||||||
|
MarkdownElementTypes.ATX_3,
|
||||||
|
MarkdownElementTypes.ATX_4,
|
||||||
|
MarkdownElementTypes.ATX_5,
|
||||||
|
MarkdownElementTypes.ATX_6 -> {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalTextStyle provides LocalTextStyle.current.copy(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = when (node.type) {
|
||||||
|
MarkdownElementTypes.ATX_1 -> 32.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier
|
||||||
|
MarkdownElementTypes.ATX_2 -> 24.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier
|
||||||
|
MarkdownElementTypes.ATX_3 -> 20.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier
|
||||||
|
MarkdownElementTypes.ATX_4 -> 16.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier
|
||||||
|
MarkdownElementTypes.ATX_5 -> 14.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier
|
||||||
|
else -> 12.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier
|
||||||
|
},
|
||||||
|
color = when (node.type) {
|
||||||
|
MarkdownElementTypes.ATX_1 -> Color(0xFFFF0000)
|
||||||
|
MarkdownElementTypes.ATX_2 -> Color(0xFF00FF00)
|
||||||
|
MarkdownElementTypes.ATX_3 -> Color(0xFF0000FF)
|
||||||
|
MarkdownElementTypes.ATX_4 -> Color(0xFFFF00FF)
|
||||||
|
MarkdownElementTypes.ATX_5 -> Color(0xFF00FFFF)
|
||||||
|
else -> Color(0xFFFFFF00)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (node.startOffset != 0) {
|
||||||
|
Box(Modifier.padding(top = 8.dp))
|
||||||
|
}
|
||||||
|
JBMText(node, modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownElementTypes.ORDERED_LIST,
|
||||||
|
MarkdownElementTypes.UNORDERED_LIST -> {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalJBMarkdownTreeState provides LocalJBMarkdownTreeState.current.copy(
|
||||||
|
listDepth = LocalJBMarkdownTreeState.current.listDepth + 1
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
JBMText(node, modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Text(
|
||||||
|
text = buildAnnotatedString {
|
||||||
|
withStyle(SpanStyle(color = Color(0xFFFF7F50))) {
|
||||||
|
append("[Unknown block type ${node.type.name}]")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -166,6 +166,15 @@ fun LabsHomeScreen(navController: NavController) {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text("JB Markdown")
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
navController.navigate("sandboxes/jbm")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import androidx.navigation.compose.rememberNavController
|
||||||
import chat.revolt.api.settings.FeatureFlags
|
import chat.revolt.api.settings.FeatureFlags
|
||||||
import chat.revolt.screens.labs.ui.mockups.CallScreenMockup
|
import chat.revolt.screens.labs.ui.mockups.CallScreenMockup
|
||||||
import chat.revolt.screens.labs.ui.sandbox.CryptographicAgeVerificationSandbox
|
import chat.revolt.screens.labs.ui.sandbox.CryptographicAgeVerificationSandbox
|
||||||
|
import chat.revolt.screens.labs.ui.sandbox.JBMSandbox
|
||||||
import chat.revolt.screens.labs.ui.sandbox.SettingsDslSandbox
|
import chat.revolt.screens.labs.ui.sandbox.SettingsDslSandbox
|
||||||
|
|
||||||
annotation class LabsFeature
|
annotation class LabsFeature
|
||||||
|
|
@ -71,6 +72,9 @@ fun LabsRootScreen(topNav: NavController) {
|
||||||
composable("sandboxes/settingsdsl") {
|
composable("sandboxes/settingsdsl") {
|
||||||
SettingsDslSandbox(labsNav)
|
SettingsDslSandbox(labsNav)
|
||||||
}
|
}
|
||||||
|
composable("sandboxes/jbm") {
|
||||||
|
JBMSandbox(labsNav)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package chat.revolt.screens.labs.ui.sandbox
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import chat.revolt.components.markdown.jbm.JBM
|
||||||
|
import chat.revolt.components.markdown.jbm.JBMRenderer
|
||||||
|
import chat.revolt.settings.dsl.SettingsPage
|
||||||
|
|
||||||
|
@OptIn(JBM::class)
|
||||||
|
@Composable
|
||||||
|
fun JBMSandbox(navController: NavController) {
|
||||||
|
var mdSource by remember { mutableStateOf("") }
|
||||||
|
var submitMdSource by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
SettingsPage(
|
||||||
|
navController = navController,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "JB Markdown Sandbox",
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Subcategory(
|
||||||
|
title = { Text("Source", maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
value = mdSource,
|
||||||
|
onValueChange = { mdSource = it },
|
||||||
|
label = { Text("Markdown source") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
TextButton(onClick = {
|
||||||
|
submitMdSource = mdSource
|
||||||
|
}) {
|
||||||
|
Text("Submit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Subcategory(
|
||||||
|
title = { Text("Output", maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
||||||
|
) {
|
||||||
|
submitMdSource?.let { JBMRenderer(it, Modifier) }
|
||||||
|
?: Text("Submit some Markdown and see the output.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue