mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-14 17:37:05 +05:30
refactor: rewrite comment text rendering
This commit is contained in:
parent
77da21d9b0
commit
be80812dc6
18 changed files with 243 additions and 177 deletions
|
@ -110,7 +110,6 @@ dependencies {
|
|||
implementation(libs.androidx.paging.compose)
|
||||
implementation(libs.androidx.profileinstaller)
|
||||
implementation(libs.androidx.work.runtime)
|
||||
implementation(libs.copydown)
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.eithernet)
|
||||
implementation(libs.haze)
|
||||
|
|
|
@ -19,7 +19,6 @@ import androidx.compose.ui.platform.LocalUriHandler
|
|||
import androidx.core.net.toUri
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import javax.inject.Inject
|
||||
|
@ -29,7 +28,6 @@ import javax.inject.Inject
|
|||
abstract class BaseActivity : ComponentActivity() {
|
||||
|
||||
@Inject lateinit var urlLauncher: UrlLauncher
|
||||
@Inject lateinit var htmlConverter: HTMLConverter
|
||||
@Inject lateinit var viewModel: ClawViewModel
|
||||
var webUri: String? = null
|
||||
|
||||
|
|
|
@ -29,18 +29,13 @@ class MainActivity : BaseActivity() {
|
|||
WindowWidthSizeClass.Compact -> {
|
||||
LobstersPostsScreen(
|
||||
urlLauncher = urlLauncher,
|
||||
htmlConverter = htmlConverter,
|
||||
windowSizeClass = windowSizeClass,
|
||||
setWebUri = { url -> webUri = url },
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
TabletScreen(
|
||||
urlLauncher = urlLauncher,
|
||||
htmlConverter = htmlConverter,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
TabletScreen(urlLauncher = urlLauncher, modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,11 +14,6 @@ import dev.msfjarvis.claw.android.ui.screens.SearchScreen
|
|||
class SearchActivity : BaseActivity() {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
SearchScreen(
|
||||
urlLauncher = urlLauncher,
|
||||
htmlConverter = htmlConverter,
|
||||
setWebUri = { webUri = it },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
SearchScreen(urlLauncher = urlLauncher, setWebUri = { webUri = it }, viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.injection
|
||||
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dev.msfjarvis.claw.android.ui.util.HTMLConverterImpl
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
|
||||
@Module
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
interface HTMLConverterModule {
|
||||
@Binds fun bindHTMLConverter(impl: HTMLConverterImpl): HTMLConverter
|
||||
}
|
|
@ -76,7 +76,6 @@ import dev.msfjarvis.claw.android.ui.navigation.any
|
|||
import dev.msfjarvis.claw.android.ui.navigation.none
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import dev.msfjarvis.claw.common.user.UserProfile
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
@ -88,7 +87,6 @@ import kotlinx.coroutines.launch
|
|||
@Composable
|
||||
fun LobstersPostsScreen(
|
||||
urlLauncher: UrlLauncher,
|
||||
htmlConverter: HTMLConverter,
|
||||
windowSizeClass: WindowSizeClass,
|
||||
setWebUri: (String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -244,7 +242,6 @@ fun LobstersPostsScreen(
|
|||
CommentsPage(
|
||||
postId = postId,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
getSeenComments = viewModel::getSeenComments,
|
||||
markSeenComments = viewModel::markSeenComments,
|
||||
contentPadding = contentPadding,
|
||||
|
|
|
@ -25,14 +25,12 @@ import dev.msfjarvis.claw.android.ui.navigation.Search
|
|||
import dev.msfjarvis.claw.android.ui.navigation.User
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import dev.msfjarvis.claw.common.user.UserProfile
|
||||
|
||||
@Composable
|
||||
fun SearchScreen(
|
||||
urlLauncher: UrlLauncher,
|
||||
htmlConverter: HTMLConverter,
|
||||
setWebUri: (String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ClawViewModel = injectedViewModel(),
|
||||
|
@ -64,11 +62,10 @@ fun SearchScreen(
|
|||
CommentsPage(
|
||||
postId = postId,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
getSeenComments = viewModel::getSeenComments,
|
||||
markSeenComments = viewModel::markSeenComments,
|
||||
openUserProfile = { navController.navigate(User(it)) },
|
||||
contentPadding = contentPadding,
|
||||
openUserProfile = { navController.navigate(User(it)) },
|
||||
)
|
||||
}
|
||||
composable<User> { backStackEntry ->
|
||||
|
|
|
@ -66,7 +66,6 @@ import dev.msfjarvis.claw.android.ui.navigation.Saved
|
|||
import dev.msfjarvis.claw.android.ui.navigation.User
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
|
@ -82,7 +81,6 @@ private fun ThreePaneScaffoldNavigator<*>.isDetailExpanded() =
|
|||
@Composable
|
||||
fun TabletScreen(
|
||||
urlLauncher: UrlLauncher,
|
||||
htmlConverter: HTMLConverter,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ClawViewModel = injectedViewModel(),
|
||||
) {
|
||||
|
@ -219,12 +217,11 @@ fun TabletScreen(
|
|||
CommentsPage(
|
||||
postId = contentKey.postId,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
getSeenComments = viewModel::getSeenComments,
|
||||
markSeenComments = viewModel::markSeenComments,
|
||||
openUserProfile = { navController.navigate(User(it)) },
|
||||
contentPadding = PaddingValues(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
openUserProfile = { navController.navigate(User(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,6 @@ import dev.msfjarvis.claw.android.ui.navigation.Settings
|
|||
import dev.msfjarvis.claw.android.ui.navigation.User
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import dev.msfjarvis.claw.common.user.UserProfile
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
|
@ -48,7 +47,6 @@ import kotlinx.collections.immutable.persistentMapOf
|
|||
@Composable
|
||||
fun TabletScreen2(
|
||||
urlLauncher: UrlLauncher,
|
||||
htmlConverter: HTMLConverter,
|
||||
setWebUri: (String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ClawViewModel = injectedViewModel(),
|
||||
|
@ -140,7 +138,6 @@ fun TabletScreen2(
|
|||
CommentsPage(
|
||||
postId = postId,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
getSeenComments = viewModel::getSeenComments,
|
||||
markSeenComments = viewModel::markSeenComments,
|
||||
contentPadding = contentPadding,
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.util
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import io.github.furstenheim.CopyDown
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
class HTMLConverterImpl @Inject constructor() : HTMLConverter {
|
||||
private val copydown = CopyDown()
|
||||
|
||||
override fun convertHTMLToMarkdown(html: String): String {
|
||||
return copydown.convert(html)
|
||||
}
|
||||
}
|
|
@ -59,6 +59,7 @@ dependencies {
|
|||
implementation(libs.compose.richtext.markdown)
|
||||
implementation(libs.compose.richtext.material3)
|
||||
implementation(libs.compose.richtext.ui)
|
||||
implementation(libs.htmlconverter)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinResult)
|
||||
|
|
|
@ -46,7 +46,6 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
internal fun CommentsHeader(
|
||||
post: UIPost,
|
||||
postActions: PostActions,
|
||||
htmlConverter: HTMLConverter,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
@ -77,7 +76,7 @@ internal fun CommentsHeader(
|
|||
}
|
||||
|
||||
if (post.description.isNotBlank()) {
|
||||
ThemedRichText(htmlConverter.convertHTMLToMarkdown(post.description))
|
||||
ThemedRichText(post.description)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
Submitter(
|
||||
|
|
|
@ -32,7 +32,6 @@ import dev.msfjarvis.claw.model.UIPost
|
|||
fun CommentsPage(
|
||||
postId: String,
|
||||
postActions: PostActions,
|
||||
htmlConverter: HTMLConverter,
|
||||
getSeenComments: suspend (String) -> PostComments?,
|
||||
markSeenComments: (String, List<Comment>) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
|
@ -57,7 +56,6 @@ fun CommentsPage(
|
|||
CommentsPageInternal(
|
||||
details = (postDetails as Success<UIPost>).data,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
commentState = commentState,
|
||||
markSeenComments = markSeenComments,
|
||||
openUserProfile = openUserProfile,
|
||||
|
|
|
@ -63,7 +63,6 @@ private const val AnimationDuration = 100
|
|||
internal fun CommentsPageInternal(
|
||||
details: UIPost,
|
||||
postActions: PostActions,
|
||||
htmlConverter: HTMLConverter,
|
||||
commentState: PostComments?,
|
||||
markSeenComments: (String, List<Comment>) -> Unit,
|
||||
openUserProfile: (String) -> Unit,
|
||||
|
@ -103,12 +102,7 @@ internal fun CommentsPageInternal(
|
|||
Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
|
||||
LazyColumn(modifier = modifier, contentPadding = contentPadding, state = commentListState) {
|
||||
item {
|
||||
CommentsHeader(
|
||||
post = details,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
openUserProfile = openUserProfile,
|
||||
)
|
||||
CommentsHeader(post = details, postActions = postActions, openUserProfile = openUserProfile)
|
||||
}
|
||||
|
||||
if (commentNodes.isNotEmpty()) {
|
||||
|
@ -121,7 +115,7 @@ internal fun CommentsPageInternal(
|
|||
}
|
||||
|
||||
items(items = commentNodes, key = { node -> node.comment.shortId }) { node ->
|
||||
Node(node, htmlConverter, openUserProfile, onToggleExpandedState)
|
||||
Node(node, openUserProfile, onToggleExpandedState)
|
||||
}
|
||||
|
||||
item(key = "bottom_spacer") {
|
||||
|
@ -149,7 +143,6 @@ internal fun CommentsPageInternal(
|
|||
@Composable
|
||||
private fun Node(
|
||||
node: CommentNode,
|
||||
htmlConverter: HTMLConverter,
|
||||
openUserProfile: (String) -> Unit,
|
||||
onToggleExpandedState: (String, Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -158,7 +151,6 @@ private fun Node(
|
|||
NodeBox(
|
||||
node = node,
|
||||
isExpanded = node.isExpanded,
|
||||
htmlConverter = htmlConverter,
|
||||
openUserProfile,
|
||||
modifier =
|
||||
modifier.clickable(
|
||||
|
@ -172,9 +164,7 @@ private fun Node(
|
|||
exit = fadeOut(tween(AnimationDuration)) + shrinkVertically(tween(AnimationDuration)),
|
||||
) {
|
||||
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
node.children.forEach { model ->
|
||||
Node(model, htmlConverter, openUserProfile, onToggleExpandedState)
|
||||
}
|
||||
node.children.forEach { model -> Node(model, openUserProfile, onToggleExpandedState) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -184,11 +174,10 @@ private fun Node(
|
|||
private fun NodeBox(
|
||||
node: CommentNode,
|
||||
isExpanded: Boolean,
|
||||
htmlConverter: HTMLConverter,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
CommentEntry(isExpanded, node, htmlConverter, openUserProfile, modifier)
|
||||
CommentEntry(isExpanded, node, openUserProfile, modifier)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
|
@ -198,7 +187,6 @@ private val CommentEntryPadding = 16f.dp
|
|||
private fun CommentEntry(
|
||||
isExpanded: Boolean,
|
||||
commentNode: CommentNode,
|
||||
htmlConverter: HTMLConverter,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
@ -234,10 +222,7 @@ private fun CommentEntry(
|
|||
modifier = Modifier.clickable { openUserProfile(comment.user) },
|
||||
)
|
||||
if (isExpanded) {
|
||||
ThemedRichText(
|
||||
text = htmlConverter.convertHTMLToMarkdown(comment.comment),
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
ThemedRichText(text = comment.comment, modifier = Modifier.padding(top = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.common.comments
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
/** Defines a contract to convert strings of HTML to Markdown. */
|
||||
@Stable
|
||||
fun interface HTMLConverter {
|
||||
fun convertHTMLToMarkdown(html: String): String
|
||||
}
|
231
common/src/main/kotlin/dev/msfjarvis/claw/common/ui/HTML.kt
Normal file
231
common/src/main/kotlin/dev/msfjarvis/claw/common/ui/HTML.kt
Normal file
|
@ -0,0 +1,231 @@
|
|||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.common.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import be.digitalia.compose.htmlconverter.HtmlStyle
|
||||
import be.digitalia.compose.htmlconverter.htmlToAnnotatedString
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
|
||||
private sealed class Segment {
|
||||
data class Text(val content: String) : Segment()
|
||||
|
||||
data class Blockquote(val content: String) : Segment()
|
||||
|
||||
data class Code(val content: String) : Segment()
|
||||
}
|
||||
|
||||
private fun parseSegments(text: String): List<Segment> {
|
||||
val segments = mutableListOf<Segment>()
|
||||
var i = 0
|
||||
val blockquoteStart = "<blockquote>"
|
||||
val blockquoteEnd = "</blockquote>"
|
||||
val preStart = "<pre>"
|
||||
val preEnd = "</pre>"
|
||||
val codeStart = "<code"
|
||||
val codeEnd = "</code>"
|
||||
|
||||
fun addSegment(content: String, isBlockquote: Boolean = false, isCode: Boolean = false) {
|
||||
if (content.isNotBlank()) {
|
||||
when {
|
||||
isBlockquote -> segments.add(Segment.Blockquote(content))
|
||||
isCode -> segments.add(Segment.Code(content))
|
||||
else -> segments.add(Segment.Text(content))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (i < text.length) {
|
||||
val nextBlockquote = text.indexOf(blockquoteStart, i, ignoreCase = true)
|
||||
val nextPre = text.indexOf(preStart, i, ignoreCase = true)
|
||||
val (nextTag, isBlockquote) =
|
||||
when {
|
||||
nextBlockquote != -1 && (nextPre == -1 || nextBlockquote < nextPre) ->
|
||||
nextBlockquote to true
|
||||
nextPre != -1 -> nextPre to false
|
||||
else -> -1 to false
|
||||
}
|
||||
if (nextTag == -1) {
|
||||
addSegment(text.substring(i))
|
||||
break
|
||||
}
|
||||
if (nextTag > i) {
|
||||
addSegment(text.substring(i, nextTag))
|
||||
}
|
||||
if (isBlockquote) {
|
||||
val endIdx = text.indexOf(blockquoteEnd, nextTag + blockquoteStart.length, ignoreCase = true)
|
||||
if (endIdx == -1) {
|
||||
addSegment(text.substring(nextTag))
|
||||
break
|
||||
}
|
||||
addSegment(text.substring(nextTag + blockquoteStart.length, endIdx), isBlockquote = true)
|
||||
i = endIdx + blockquoteEnd.length
|
||||
} else {
|
||||
val endIdx = text.indexOf(preEnd, nextTag + preStart.length, ignoreCase = true)
|
||||
if (endIdx == -1) {
|
||||
addSegment(text.substring(nextTag))
|
||||
break
|
||||
}
|
||||
var codeContent = text.substring(nextTag + preStart.length, endIdx)
|
||||
val codeTagStart = codeContent.indexOf(codeStart, ignoreCase = true)
|
||||
val codeTagEnd = codeContent.indexOf(codeEnd, ignoreCase = true)
|
||||
if (codeTagStart != -1 && codeTagEnd != -1 && codeTagStart < codeTagEnd) {
|
||||
val codeOpenEnd = codeContent.indexOf('>', codeTagStart)
|
||||
if (codeOpenEnd != -1 && codeOpenEnd < codeTagEnd) {
|
||||
codeContent = codeContent.substring(codeOpenEnd + 1, codeTagEnd)
|
||||
}
|
||||
}
|
||||
addSegment(codeContent, isCode = true)
|
||||
i = endIdx + preEnd.length
|
||||
}
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ThemedRichText(text: String, modifier: Modifier = Modifier) {
|
||||
val linkSpanStyle =
|
||||
SpanStyle(
|
||||
background = MaterialTheme.colorScheme.surfaceVariant,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textDecoration = TextDecoration.Underline,
|
||||
)
|
||||
|
||||
val segments = parseSegments(text)
|
||||
|
||||
Column(modifier = modifier) {
|
||||
for (segment in segments) {
|
||||
when (segment) {
|
||||
is Segment.Blockquote -> {
|
||||
Row(
|
||||
modifier = Modifier.padding(vertical = 4.dp).fillMaxWidth().height(IntrinsicSize.Min)
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.width(4.dp)
|
||||
.fillMaxHeight()
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.7f))
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text =
|
||||
remember(segment.content) {
|
||||
htmlToAnnotatedString(
|
||||
html = segment.content,
|
||||
compactMode = false,
|
||||
style = HtmlStyle(textLinkStyles = TextLinkStyles(linkSpanStyle)),
|
||||
)
|
||||
},
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = contentColorFor(MaterialTheme.colorScheme.background),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is Segment.Code -> {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.85f),
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = segment.content,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
is Segment.Text -> {
|
||||
Text(
|
||||
text =
|
||||
remember(segment.content) {
|
||||
htmlToAnnotatedString(
|
||||
html = segment.content,
|
||||
compactMode = false,
|
||||
style = HtmlStyle(textLinkStyles = TextLinkStyles(linkSpanStyle)),
|
||||
)
|
||||
},
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = contentColorFor(MaterialTheme.colorScheme.background),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
internal fun ThemedRichTextPreview() {
|
||||
val text =
|
||||
"""
|
||||
<h1>Hello <strong>HTML Converter</strong> for Compose</h1>
|
||||
<p>This the first paragraph of the sample app running on <strong>Nothing</strong>!</p>
|
||||
<ul>
|
||||
<li><strong>Bold</strong></li>
|
||||
<li><em>Italic</em></li>
|
||||
<li><u>Underline</u></li>
|
||||
<li><del>Strikethrough</del></li>
|
||||
<li><code>Code</code></li>
|
||||
<li><a href="https://www.wikipedia.org/">Hyperlink with custom styling</a></li>
|
||||
<li><big>Bigger</big> and <small>smaller</small> text</li>
|
||||
<li><sup>Super</sup>text and <sub>sub</sub>text</li>
|
||||
<li>A nested ordered list:
|
||||
<ol>
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ul>
|
||||
<dl>
|
||||
<dt>Term</dt>
|
||||
<dd>Description.</dd>
|
||||
</dl>
|
||||
A few HTML entities: » © « ✓
|
||||
<blockquote>A blockquote, indented relatively to the main text.</blockquote>
|
||||
<pre><code class="language-toml">Preformatted text, preserving
|
||||
line breaks... and spaces.</code></pre>
|
||||
<p><code>Inline code example.</code> You reached the end of the document.<br />Thank you for reading!</p>
|
||||
"""
|
||||
.trimIndent()
|
||||
|
||||
LobstersTheme { ThemedRichText(text = text) }
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.common.ui
|
||||
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import com.halilibo.richtext.commonmark.Markdown
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.material3.RichText
|
||||
import com.halilibo.richtext.ui.string.RichTextStringStyle
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
|
||||
@Composable
|
||||
internal fun ThemedRichText(text: String, modifier: Modifier = Modifier) {
|
||||
val linkSpanStyle =
|
||||
SpanStyle(
|
||||
background = MaterialTheme.colorScheme.surfaceVariant,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textDecoration = TextDecoration.Underline,
|
||||
)
|
||||
val stringStyle = RichTextStringStyle(linkStyle = TextLinkStyles(linkSpanStyle))
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides MaterialTheme.typography.bodyLarge,
|
||||
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
|
||||
) {
|
||||
RichText(modifier = modifier, style = RichTextStyle.Default.copy(stringStyle = stringStyle)) {
|
||||
Markdown(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
internal fun ThemedRichTextPreview() {
|
||||
val text =
|
||||
"""
|
||||
### Heading
|
||||
This is a paragraph body
|
||||
|
||||
```
|
||||
This is a code block
|
||||
```
|
||||
|
||||
This is an `inline code block`
|
||||
|
||||
[This is a link](https://github.com/msfjarvis/compose-lobsters)
|
||||
|
||||

|
||||
"""
|
||||
.trimIndent()
|
||||
|
||||
LobstersTheme { ThemedRichText(text = text) }
|
||||
}
|
|
@ -77,7 +77,6 @@ coil3-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", versio
|
|||
compose-richtext-markdown = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" }
|
||||
compose-richtext-material3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", version.ref = "richtext" }
|
||||
compose-richtext-ui = { module = "com.halilibo.compose-richtext:richtext-ui", version.ref = "richtext" }
|
||||
copydown = "io.github.furstenheim:copy_down:1.1"
|
||||
dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
|
||||
dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
|
||||
eithernet = { module = "com.slack.eithernet:eithernet", version.ref = "eithernet" }
|
||||
|
@ -85,6 +84,7 @@ eithernet-integration-retrofit = { module = "com.slack.eithernet:eithernet-integ
|
|||
eithernet-test-fixtures = { module = "com.slack.eithernet:eithernet-test-fixtures", version.ref = "eithernet" }
|
||||
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
|
||||
haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
|
||||
htmlconverter = "be.digitalia.compose.htmlconverter:htmlconverter:1.0.4"
|
||||
javax-inject = "javax.inject:javax.inject:1"
|
||||
jsoup = "org.jsoup:jsoup:1.20.1"
|
||||
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue