mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-15 00:37:03 +05:30
feat: revert back to old text pipeline
Things have been entirely too slow on the lobste.rs front and I don't care much to continue pushing for it. Fixes #382 Fixes #383
This commit is contained in:
parent
c089e5ae5a
commit
b5c57500b1
16 changed files with 171 additions and 229 deletions
|
@ -9,7 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Comments and user profile text rendering was overhauled, there should be no visual changes
|
|
||||||
* Added another workaround for native library loading crash
|
* Added another workaround for native library loading crash
|
||||||
|
|
||||||
## [1.29.0] - 2023-06-08
|
## [1.29.0] - 2023-06-08
|
||||||
|
|
|
@ -70,6 +70,7 @@ dependencies {
|
||||||
implementation(libs.androidx.profileinstaller)
|
implementation(libs.androidx.profileinstaller)
|
||||||
implementation(libs.androidx.work.runtime.ktx)
|
implementation(libs.androidx.work.runtime.ktx)
|
||||||
implementation(libs.coil)
|
implementation(libs.coil)
|
||||||
|
implementation(libs.copydown)
|
||||||
implementation(libs.crux)
|
implementation(libs.crux)
|
||||||
implementation(libs.dagger)
|
implementation(libs.dagger)
|
||||||
implementation(libs.jsoup)
|
implementation(libs.jsoup)
|
||||||
|
|
|
@ -18,6 +18,7 @@ import androidx.core.view.WindowCompat
|
||||||
import com.deliveryhero.whetstone.Whetstone
|
import com.deliveryhero.whetstone.Whetstone
|
||||||
import com.deliveryhero.whetstone.activity.ContributesActivityInjector
|
import com.deliveryhero.whetstone.activity.ContributesActivityInjector
|
||||||
import dev.msfjarvis.claw.android.ui.LobstersApp
|
import dev.msfjarvis.claw.android.ui.LobstersApp
|
||||||
|
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -25,6 +26,7 @@ import javax.inject.Inject
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
@Inject lateinit var urlLauncher: UrlLauncher
|
@Inject lateinit var urlLauncher: UrlLauncher
|
||||||
|
@Inject lateinit var htmlConverter: HTMLConverter
|
||||||
private var webUri: String? = null
|
private var webUri: String? = null
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||||
|
@ -38,6 +40,7 @@ class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
LobstersApp(
|
LobstersApp(
|
||||||
urlLauncher = urlLauncher,
|
urlLauncher = urlLauncher,
|
||||||
|
htmlConverter = htmlConverter,
|
||||||
windowSizeClass = windowSizeClass,
|
windowSizeClass = windowSizeClass,
|
||||||
setWebUri = { url -> webUri = url },
|
setWebUri = { url -> webUri = url },
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2021-2023 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 HTMLConverterImpl.bind(): HTMLConverter
|
||||||
|
}
|
|
@ -64,6 +64,7 @@ import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||||
import dev.msfjarvis.claw.api.LobstersApi
|
import dev.msfjarvis.claw.api.LobstersApi
|
||||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||||
|
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||||
import dev.msfjarvis.claw.common.ui.decorations.ClawAppBar
|
import dev.msfjarvis.claw.common.ui.decorations.ClawAppBar
|
||||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||||
|
@ -76,6 +77,7 @@ import kotlinx.coroutines.launch
|
||||||
@Composable
|
@Composable
|
||||||
fun LobstersApp(
|
fun LobstersApp(
|
||||||
urlLauncher: UrlLauncher,
|
urlLauncher: UrlLauncher,
|
||||||
|
htmlConverter: HTMLConverter,
|
||||||
windowSizeClass: WindowSizeClass,
|
windowSizeClass: WindowSizeClass,
|
||||||
setWebUri: (String?) -> Unit,
|
setWebUri: (String?) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -236,6 +238,7 @@ fun LobstersApp(
|
||||||
CommentsPage(
|
CommentsPage(
|
||||||
postId = postId,
|
postId = postId,
|
||||||
postActions = postActions,
|
postActions = postActions,
|
||||||
|
htmlConverter = htmlConverter,
|
||||||
getSeenComments = viewModel::getSeenComments,
|
getSeenComments = viewModel::getSeenComments,
|
||||||
markSeenComments = viewModel::markSeenComments,
|
markSeenComments = viewModel::markSeenComments,
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2023 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,6 +45,9 @@ dependencies {
|
||||||
implementation(libs.androidx.compose.runtime)
|
implementation(libs.androidx.compose.runtime)
|
||||||
implementation(libs.androidx.compose.ui.text)
|
implementation(libs.androidx.compose.ui.text)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
implementation(libs.compose.richtext.markdown)
|
||||||
|
implementation(libs.compose.richtext.material3)
|
||||||
|
implementation(libs.compose.richtext.ui)
|
||||||
implementation(libs.kotlinx.collections.immutable)
|
implementation(libs.kotlinx.collections.immutable)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.napier)
|
implementation(libs.napier)
|
||||||
|
|
|
@ -41,8 +41,8 @@ import dev.msfjarvis.claw.common.posts.PostActions
|
||||||
import dev.msfjarvis.claw.common.posts.PostTitle
|
import dev.msfjarvis.claw.common.posts.PostTitle
|
||||||
import dev.msfjarvis.claw.common.posts.Submitter
|
import dev.msfjarvis.claw.common.posts.Submitter
|
||||||
import dev.msfjarvis.claw.common.posts.TagRow
|
import dev.msfjarvis.claw.common.posts.TagRow
|
||||||
import dev.msfjarvis.claw.common.ui.HTMLText
|
|
||||||
import dev.msfjarvis.claw.common.ui.NetworkImage
|
import dev.msfjarvis.claw.common.ui.NetworkImage
|
||||||
|
import dev.msfjarvis.claw.common.ui.ThemedRichText
|
||||||
import dev.msfjarvis.claw.model.LinkMetadata
|
import dev.msfjarvis.claw.model.LinkMetadata
|
||||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
@ -53,6 +53,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
internal fun CommentsHeader(
|
internal fun CommentsHeader(
|
||||||
postDetails: LobstersPostDetails,
|
postDetails: LobstersPostDetails,
|
||||||
postActions: PostActions,
|
postActions: PostActions,
|
||||||
|
htmlConverter: HTMLConverter,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
|
@ -83,7 +84,7 @@ internal fun CommentsHeader(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postDetails.description.isNotBlank()) {
|
if (postDetails.description.isNotBlank()) {
|
||||||
HTMLText(postDetails.description)
|
ThemedRichText(htmlConverter.convertHTMLToMarkdown(postDetails.description))
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
}
|
}
|
||||||
Submitter(
|
Submitter(
|
||||||
|
@ -133,6 +134,7 @@ private val CommentEntryPadding = 16f.dp
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CommentEntry(
|
internal fun CommentEntry(
|
||||||
commentNode: CommentNode,
|
commentNode: CommentNode,
|
||||||
|
htmlConverter: HTMLConverter,
|
||||||
toggleExpanded: (CommentNode) -> Unit,
|
toggleExpanded: (CommentNode) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
@ -167,7 +169,10 @@ internal fun CommentEntry(
|
||||||
Modifier.clickable { uriHandler.openUri("https://lobste.rs/u/${comment.user.username}") },
|
Modifier.clickable { uriHandler.openUri("https://lobste.rs/u/${comment.user.username}") },
|
||||||
)
|
)
|
||||||
if (commentNode.isExpanded) {
|
if (commentNode.isExpanded) {
|
||||||
HTMLText(text = comment.comment, modifier = Modifier.padding(top = 8.dp))
|
ThemedRichText(
|
||||||
|
text = htmlConverter.convertHTMLToMarkdown(comment.comment),
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,11 +68,13 @@ internal fun findTopMostParent(node: CommentNode): CommentNode {
|
||||||
|
|
||||||
internal fun LazyListScope.nodes(
|
internal fun LazyListScope.nodes(
|
||||||
nodes: List<CommentNode>,
|
nodes: List<CommentNode>,
|
||||||
|
htmlConverter: HTMLConverter,
|
||||||
toggleExpanded: (CommentNode) -> Unit,
|
toggleExpanded: (CommentNode) -> Unit,
|
||||||
) {
|
) {
|
||||||
nodes.forEach { node ->
|
nodes.forEach { node ->
|
||||||
node(
|
node(
|
||||||
node = node,
|
node = node,
|
||||||
|
htmlConverter = htmlConverter,
|
||||||
toggleExpanded = toggleExpanded,
|
toggleExpanded = toggleExpanded,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -80,6 +82,7 @@ internal fun LazyListScope.nodes(
|
||||||
|
|
||||||
private fun LazyListScope.node(
|
private fun LazyListScope.node(
|
||||||
node: CommentNode,
|
node: CommentNode,
|
||||||
|
htmlConverter: HTMLConverter,
|
||||||
toggleExpanded: (CommentNode) -> Unit,
|
toggleExpanded: (CommentNode) -> Unit,
|
||||||
) {
|
) {
|
||||||
// Skip the node if neither the node nor its parent is expanded
|
// Skip the node if neither the node nor its parent is expanded
|
||||||
|
@ -89,6 +92,7 @@ private fun LazyListScope.node(
|
||||||
item {
|
item {
|
||||||
CommentEntry(
|
CommentEntry(
|
||||||
commentNode = node,
|
commentNode = node,
|
||||||
|
htmlConverter = htmlConverter,
|
||||||
toggleExpanded = toggleExpanded,
|
toggleExpanded = toggleExpanded,
|
||||||
)
|
)
|
||||||
Divider()
|
Divider()
|
||||||
|
@ -96,6 +100,7 @@ private fun LazyListScope.node(
|
||||||
if (node.children.isNotEmpty()) {
|
if (node.children.isNotEmpty()) {
|
||||||
nodes(
|
nodes(
|
||||||
node.children,
|
node.children,
|
||||||
|
htmlConverter = htmlConverter,
|
||||||
toggleExpanded = toggleExpanded,
|
toggleExpanded = toggleExpanded,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||||
private fun CommentsPageInternal(
|
private fun CommentsPageInternal(
|
||||||
details: LobstersPostDetails,
|
details: LobstersPostDetails,
|
||||||
postActions: PostActions,
|
postActions: PostActions,
|
||||||
|
htmlConverter: HTMLConverter,
|
||||||
commentState: PostComments?,
|
commentState: PostComments?,
|
||||||
markSeenComments: (String, List<Comment>) -> Unit,
|
markSeenComments: (String, List<Comment>) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -54,6 +55,7 @@ private fun CommentsPageInternal(
|
||||||
CommentsHeader(
|
CommentsHeader(
|
||||||
postDetails = details,
|
postDetails = details,
|
||||||
postActions = postActions,
|
postActions = postActions,
|
||||||
|
htmlConverter = htmlConverter,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,6 +70,7 @@ private fun CommentsPageInternal(
|
||||||
|
|
||||||
nodes(
|
nodes(
|
||||||
nodes = commentNodes,
|
nodes = commentNodes,
|
||||||
|
htmlConverter = htmlConverter,
|
||||||
toggleExpanded = { node ->
|
toggleExpanded = { node ->
|
||||||
val newNode = setExpanded(node, !node.isExpanded)
|
val newNode = setExpanded(node, !node.isExpanded)
|
||||||
val parent = findTopMostParent(newNode)
|
val parent = findTopMostParent(newNode)
|
||||||
|
@ -99,6 +102,7 @@ private fun CommentsPageInternal(
|
||||||
fun CommentsPage(
|
fun CommentsPage(
|
||||||
postId: String,
|
postId: String,
|
||||||
postActions: PostActions,
|
postActions: PostActions,
|
||||||
|
htmlConverter: HTMLConverter,
|
||||||
getSeenComments: suspend (String) -> PostComments?,
|
getSeenComments: suspend (String) -> PostComments?,
|
||||||
markSeenComments: (String, List<Comment>) -> Unit,
|
markSeenComments: (String, List<Comment>) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -119,6 +123,7 @@ fun CommentsPage(
|
||||||
CommentsPageInternal(
|
CommentsPageInternal(
|
||||||
details = (postDetails as Success<LobstersPostDetails>).data,
|
details = (postDetails as Success<LobstersPostDetails>).data,
|
||||||
postActions = postActions,
|
postActions = postActions,
|
||||||
|
htmlConverter = htmlConverter,
|
||||||
commentState = commentState,
|
commentState = commentState,
|
||||||
markSeenComments = markSeenComments,
|
markSeenComments = markSeenComments,
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2023 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
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.ProvidedValue
|
import androidx.compose.runtime.ProvidedValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import com.halilibo.richtext.ui.material3.SetupMaterial3RichText
|
||||||
|
|
||||||
private val LightThemeColors =
|
private val LightThemeColors =
|
||||||
lightColorScheme(
|
lightColorScheme(
|
||||||
|
@ -97,6 +98,8 @@ fun LobstersTheme(
|
||||||
else -> if (darkTheme) DarkThemeColors else LightThemeColors
|
else -> if (darkTheme) DarkThemeColors else LightThemeColors
|
||||||
}
|
}
|
||||||
CompositionLocalProvider(*providedValues) {
|
CompositionLocalProvider(*providedValues) {
|
||||||
MaterialTheme(colorScheme = colorScheme, typography = AppTypography, content = content)
|
MaterialTheme(colorScheme = colorScheme, typography = AppTypography) {
|
||||||
|
SetupMaterial3RichText { content() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,220 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright © 2023 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 android.graphics.Typeface
|
|
||||||
import android.text.Spanned
|
|
||||||
import android.text.style.BulletSpan
|
|
||||||
import android.text.style.ForegroundColorSpan
|
|
||||||
import android.text.style.QuoteSpan
|
|
||||||
import android.text.style.RelativeSizeSpan
|
|
||||||
import android.text.style.StrikethroughSpan
|
|
||||||
import android.text.style.StyleSpan
|
|
||||||
import android.text.style.SubscriptSpan
|
|
||||||
import android.text.style.SuperscriptSpan
|
|
||||||
import android.text.style.URLSpan
|
|
||||||
import android.text.style.UnderlineSpan
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.TextLayoutResult
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
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.BaselineShift
|
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.TextUnit
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.core.text.HtmlCompat
|
|
||||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
|
||||||
|
|
||||||
private const val URL_TAG = "url_tag"
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HTMLText(
|
|
||||||
text: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
style: TextStyle = MaterialTheme.typography.bodyLarge,
|
|
||||||
softWrap: Boolean = true,
|
|
||||||
overflow: TextOverflow = TextOverflow.Clip,
|
|
||||||
maxLines: Int = Int.MAX_VALUE,
|
|
||||||
onTextLayout: (TextLayoutResult) -> Unit = {},
|
|
||||||
fontSize: TextUnit = 14.sp,
|
|
||||||
flags: Int = HtmlCompat.FROM_HTML_MODE_COMPACT,
|
|
||||||
customSpannedHandler: ((Spanned) -> AnnotatedString)? = null
|
|
||||||
) {
|
|
||||||
val content = text.asHTML(fontSize, flags, customSpannedHandler)
|
|
||||||
val uriHandler = LocalUriHandler.current
|
|
||||||
ClickableText(
|
|
||||||
modifier = modifier,
|
|
||||||
text = content,
|
|
||||||
style = style,
|
|
||||||
softWrap = softWrap,
|
|
||||||
overflow = overflow,
|
|
||||||
maxLines = maxLines,
|
|
||||||
onTextLayout = onTextLayout,
|
|
||||||
onClick = { offset ->
|
|
||||||
content.getStringAnnotations(URL_TAG, offset, offset).firstOrNull()?.let { stringAnnotation ->
|
|
||||||
uriHandler.openUri(stringAnnotation.item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun String.asHTML(
|
|
||||||
fontSize: TextUnit,
|
|
||||||
flags: Int,
|
|
||||||
customSpannedHandler: ((Spanned) -> AnnotatedString)? = null
|
|
||||||
) = buildAnnotatedString {
|
|
||||||
val spanned = HtmlCompat.fromHtml(this@asHTML, flags)
|
|
||||||
val spans = spanned.getSpans(0, spanned.length, Any::class.java)
|
|
||||||
|
|
||||||
if (customSpannedHandler != null) {
|
|
||||||
append(customSpannedHandler(spanned))
|
|
||||||
} else {
|
|
||||||
append(spanned.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
spans
|
|
||||||
.filter { it !is BulletSpan }
|
|
||||||
.forEach { span ->
|
|
||||||
val start = spanned.getSpanStart(span)
|
|
||||||
val end = spanned.getSpanEnd(span)
|
|
||||||
when (span) {
|
|
||||||
is RelativeSizeSpan -> span.spanStyle(fontSize)
|
|
||||||
is StyleSpan -> span.spanStyle()
|
|
||||||
is UnderlineSpan -> span.spanStyle()
|
|
||||||
is ForegroundColorSpan -> span.spanStyle()
|
|
||||||
is StrikethroughSpan -> span.spanStyle()
|
|
||||||
is SuperscriptSpan -> span.spanStyle()
|
|
||||||
is SubscriptSpan -> span.spanStyle()
|
|
||||||
is QuoteSpan -> span.spanStyle()
|
|
||||||
is URLSpan -> {
|
|
||||||
addStringAnnotation(tag = URL_TAG, annotation = span.url, start = start, end = end)
|
|
||||||
span.spanStyle()
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}?.let { spanStyle -> addStyle(spanStyle, start, end) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UnusedReceiverParameter")
|
|
||||||
@Composable
|
|
||||||
private fun URLSpan.spanStyle(): SpanStyle =
|
|
||||||
SpanStyle(
|
|
||||||
background = MaterialTheme.colorScheme.surfaceVariant,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
textDecoration = TextDecoration.Underline,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Suppress("UnusedReceiverParameter")
|
|
||||||
private fun UnderlineSpan.spanStyle(): SpanStyle =
|
|
||||||
SpanStyle(textDecoration = TextDecoration.Underline)
|
|
||||||
|
|
||||||
private fun ForegroundColorSpan.spanStyle(): SpanStyle = SpanStyle(color = Color(foregroundColor))
|
|
||||||
|
|
||||||
@Suppress("UnusedReceiverParameter")
|
|
||||||
private fun StrikethroughSpan.spanStyle(): SpanStyle =
|
|
||||||
SpanStyle(textDecoration = TextDecoration.LineThrough)
|
|
||||||
|
|
||||||
private fun RelativeSizeSpan.spanStyle(fontSize: TextUnit): SpanStyle =
|
|
||||||
SpanStyle(fontSize = (fontSize.value * sizeChange).sp)
|
|
||||||
|
|
||||||
@Suppress("UnusedReceiverParameter")
|
|
||||||
private fun QuoteSpan.spanStyle(): SpanStyle = SpanStyle(fontStyle = FontStyle.Italic)
|
|
||||||
|
|
||||||
private fun StyleSpan.spanStyle(): SpanStyle? =
|
|
||||||
when (style) {
|
|
||||||
Typeface.BOLD -> SpanStyle(fontWeight = FontWeight.Bold)
|
|
||||||
Typeface.ITALIC -> SpanStyle(fontStyle = FontStyle.Italic)
|
|
||||||
Typeface.BOLD_ITALIC ->
|
|
||||||
SpanStyle(
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontStyle = FontStyle.Italic,
|
|
||||||
)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UnusedReceiverParameter")
|
|
||||||
private fun SubscriptSpan.spanStyle(): SpanStyle =
|
|
||||||
SpanStyle(baselineShift = BaselineShift.Subscript)
|
|
||||||
|
|
||||||
@Suppress("UnusedReceiverParameter")
|
|
||||||
private fun SuperscriptSpan.spanStyle(): SpanStyle =
|
|
||||||
SpanStyle(baselineShift = BaselineShift.Superscript)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ClickableText(
|
|
||||||
text: AnnotatedString,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
style: TextStyle = TextStyle.Default,
|
|
||||||
softWrap: Boolean = true,
|
|
||||||
overflow: TextOverflow = TextOverflow.Clip,
|
|
||||||
maxLines: Int = Int.MAX_VALUE,
|
|
||||||
onTextLayout: (TextLayoutResult) -> Unit = {},
|
|
||||||
onClick: (Int) -> Unit
|
|
||||||
) {
|
|
||||||
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
|
|
||||||
val pressIndicator =
|
|
||||||
Modifier.pointerInput(onClick) {
|
|
||||||
detectTapGestures { pos ->
|
|
||||||
layoutResult.value?.let { layoutResult -> onClick(layoutResult.getOffsetForPosition(pos)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
modifier = modifier.then(pressIndicator),
|
|
||||||
style = style,
|
|
||||||
softWrap = softWrap,
|
|
||||||
overflow = overflow,
|
|
||||||
maxLines = maxLines,
|
|
||||||
onTextLayout = {
|
|
||||||
layoutResult.value = it
|
|
||||||
onTextLayout(it)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun HTMLTextPreview() {
|
|
||||||
LobstersTheme {
|
|
||||||
Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
|
|
||||||
HTMLText(
|
|
||||||
text =
|
|
||||||
"""
|
|
||||||
<h3 id="heading">Heading</h3>
|
|
||||||
<p>This is a paragraph body</p>
|
|
||||||
<pre><code>This is <span class="hljs-selector-tag">a</span> <span class="hljs-selector-tag">code</span> block
|
|
||||||
</code></pre><p>This is an <code>inline code block</code></p>
|
|
||||||
<blockquote><p>This is a blockquote</p></blockquote>
|
|
||||||
<p><a href="https://github.com/msfjarvis/compose-lobsters">This is a link</a></p>
|
|
||||||
<p><img src="https://avatars.githubusercontent.com/u/13348378?v=4" alt="Image"></p>
|
|
||||||
"""
|
|
||||||
.trimIndent()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2023 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.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import com.halilibo.richtext.markdown.Markdown
|
||||||
|
import com.halilibo.richtext.ui.RichText
|
||||||
|
import com.halilibo.richtext.ui.RichTextStyle
|
||||||
|
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 linkStyle =
|
||||||
|
SpanStyle(
|
||||||
|
background = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textDecoration = TextDecoration.Underline,
|
||||||
|
)
|
||||||
|
val stringStyle = RichTextStringStyle.Default.copy(linkStyle = linkStyle)
|
||||||
|
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) }
|
||||||
|
}
|
|
@ -29,10 +29,10 @@ import dev.msfjarvis.claw.common.NetworkState
|
||||||
import dev.msfjarvis.claw.common.NetworkState.Error
|
import dev.msfjarvis.claw.common.NetworkState.Error
|
||||||
import dev.msfjarvis.claw.common.NetworkState.Loading
|
import dev.msfjarvis.claw.common.NetworkState.Loading
|
||||||
import dev.msfjarvis.claw.common.NetworkState.Success
|
import dev.msfjarvis.claw.common.NetworkState.Success
|
||||||
import dev.msfjarvis.claw.common.ui.HTMLText
|
|
||||||
import dev.msfjarvis.claw.common.ui.NetworkError
|
import dev.msfjarvis.claw.common.ui.NetworkError
|
||||||
import dev.msfjarvis.claw.common.ui.NetworkImage
|
import dev.msfjarvis.claw.common.ui.NetworkImage
|
||||||
import dev.msfjarvis.claw.common.ui.ProgressBar
|
import dev.msfjarvis.claw.common.ui.ProgressBar
|
||||||
|
import dev.msfjarvis.claw.common.ui.ThemedRichText
|
||||||
import dev.msfjarvis.claw.model.User
|
import dev.msfjarvis.claw.model.User
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
@ -99,10 +99,12 @@ private fun UserProfileInternal(
|
||||||
text = user.username,
|
text = user.username,
|
||||||
style = MaterialTheme.typography.displaySmall,
|
style = MaterialTheme.typography.displaySmall,
|
||||||
)
|
)
|
||||||
HTMLText(text = user.about)
|
ThemedRichText(
|
||||||
|
text = user.about,
|
||||||
|
)
|
||||||
user.invitedBy?.let { invitedBy ->
|
user.invitedBy?.let { invitedBy ->
|
||||||
HTMLText(
|
ThemedRichText(
|
||||||
text = """Invited by <a href="https://lobste.rs/u/${invitedBy}">${invitedBy}</a>""",
|
text = "Invited by [${invitedBy}](https://lobste.rs/u/${user.invitedBy})",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ dagger = "2.46.1"
|
||||||
junit = "5.9.3"
|
junit = "5.9.3"
|
||||||
kotlin = "1.8.10"
|
kotlin = "1.8.10"
|
||||||
retrofit = "2.9.0"
|
retrofit = "2.9.0"
|
||||||
|
richtext = "0.16.0"
|
||||||
sentry-sdk = "6.24.0"
|
sentry-sdk = "6.24.0"
|
||||||
serialization = "1.5.1"
|
serialization = "1.5.1"
|
||||||
sqldelight = "2.0.0-rc02"
|
sqldelight = "2.0.0-rc02"
|
||||||
|
@ -56,6 +57,10 @@ build-vcu = "nl.littlerobots.version-catalog-update:nl.littlerobots.version-cata
|
||||||
build-versions = "com.github.ben-manes:gradle-versions-plugin:0.47.0"
|
build-versions = "com.github.ben-manes:gradle-versions-plugin:0.47.0"
|
||||||
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
|
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
|
||||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
||||||
|
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"
|
||||||
crux = "com.chimbori.crux:crux:3.12.0"
|
crux = "com.chimbori.crux:crux:3.12.0"
|
||||||
dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
|
dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
|
||||||
dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
|
dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue