1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-01-09 17:00:32 +00:00

Use AnnotatedString to handle HTML parsing

This commit is contained in:
Isira Seneviratne 2024-06-19 12:40:49 +05:30
parent e75eb2d544
commit c29fa70080
5 changed files with 48 additions and 48 deletions

View File

@ -293,6 +293,7 @@ dependencies {
implementation 'androidx.compose.material3:material3' implementation 'androidx.compose.material3:material3'
implementation 'androidx.activity:activity-compose' implementation 'androidx.activity:activity-compose'
implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.ui:ui-text:1.7.0-beta03' // Needed for parsing HTML to AnnotatedString
// Paging // Paging
implementation 'androidx.paging:paging-rxjava3:3.3.0' implementation 'androidx.paging:paging-rxjava3:3.3.0'

View File

@ -18,6 +18,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -25,8 +26,13 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import coil.compose.AsyncImage import coil.compose.AsyncImage
@ -38,6 +44,18 @@ import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.image.ImageStrategy import org.schabi.newpipe.util.image.ImageStrategy
@Composable
fun rememberParsedText(commentText: Description): AnnotatedString {
// TODO: Handle links and hashtags, Markdown.
return remember(commentText) {
if (commentText.type == Description.HTML) {
AnnotatedString.fromHtml(commentText.content)
} else {
AnnotatedString(commentText.content, ParagraphStyle())
}
}
}
@Composable @Composable
fun Comment(comment: CommentsInfoItem) { fun Comment(comment: CommentsInfoItem) {
val context = LocalContext.current val context = LocalContext.current
@ -79,23 +97,22 @@ fun Comment(comment: CommentsInfoItem) {
) )
} }
val date = Localization.relativeTimeOrTextual( val nameAndDate = remember(comment) {
context, comment.uploadDate, comment.textualUploadDate val date = Localization.relativeTimeOrTextual(
) context, comment.uploadDate, comment.textualUploadDate
Text( )
text = Localization.concatenateStrings(comment.uploaderName, date), Localization.concatenateStrings(comment.uploaderName, date)
color = MaterialTheme.colorScheme.secondary }
) Text(text = nameAndDate, color = MaterialTheme.colorScheme.secondary)
} }
// TODO: Handle HTML and Markdown formats.
Text( Text(
text = comment.commentText.content, text = rememberParsedText(comment.commentText),
// If the comment is expanded, we display all its content // If the comment is expanded, we display all its content
// otherwise we only display the first two lines // otherwise we only display the first two lines
maxLines = if (isExpanded) Int.MAX_VALUE else 2, maxLines = if (isExpanded) Int.MAX_VALUE else 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
) )
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
@ -138,12 +155,21 @@ fun CommentsInfoItem(
this.isPinned = isPinned this.isPinned = isPinned
} }
class DescriptionPreviewProvider : PreviewParameterProvider<Description> {
override val values = sequenceOf(
Description("Hello world!<br><br>This line should be hidden by default.", Description.HTML),
Description("Hello world!\n\nThis line should be hidden by default.", Description.PLAIN_TEXT),
)
}
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable @Composable
private fun CommentPreview() { private fun CommentPreview(
@PreviewParameter(DescriptionPreviewProvider::class) description: Description
) {
val comment = CommentsInfoItem( val comment = CommentsInfoItem(
commentText = Description("Hello world!\n\nThis line should be hidden by default.", Description.PLAIN_TEXT), commentText = description,
uploaderName = "Test", uploaderName = "Test",
likeCount = 100, likeCount = 100,
isPinned = true, isPinned = true,

View File

@ -8,7 +8,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import org.schabi.newpipe.extractor.comments.CommentsInfoItem import org.schabi.newpipe.extractor.comments.CommentsInfoItem
@ -18,14 +17,13 @@ import org.schabi.newpipe.ui.theme.AppTheme
@Composable @Composable
fun CommentReplies( fun CommentReplies(
comment: CommentsInfoItem, comment: CommentsInfoItem,
flow: Flow<PagingData<CommentsInfoItem>>, flow: Flow<PagingData<CommentsInfoItem>>
disposables: CompositeDisposable
) { ) {
val replies = flow.collectAsLazyPagingItems() val replies = flow.collectAsLazyPagingItems()
LazyColumn { LazyColumn {
item { item {
CommentRepliesHeader(comment = comment, disposables = disposables) CommentRepliesHeader(comment = comment)
HorizontalDivider(thickness = 1.dp) HorizontalDivider(thickness = 1.dp)
} }
@ -58,6 +56,6 @@ private fun CommentRepliesPreview() {
val flow = flowOf(PagingData.from(listOf(reply1, reply2))) val flow = flowOf(PagingData.from(listOf(reply1, reply2)))
AppTheme { AppTheme {
CommentReplies(comment = comment, flow = flow, disposables = CompositeDisposable()) CommentReplies(comment = comment, flow = flow)
} }
} }

View File

@ -9,13 +9,11 @@ import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.paging.Pager import androidx.paging.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.schabi.newpipe.extractor.comments.CommentsInfoItem import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.ktx.serializable import org.schabi.newpipe.ktx.serializable
import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.ui.theme.AppTheme
class CommentRepliesFragment : Fragment() { class CommentRepliesFragment : Fragment() {
private val disposables = CompositeDisposable()
lateinit var comment: CommentsInfoItem lateinit var comment: CommentsInfoItem
override fun onCreateView( override fun onCreateView(
@ -33,17 +31,12 @@ class CommentRepliesFragment : Fragment() {
} }
AppTheme { AppTheme {
CommentReplies(comment = comment, flow = flow, disposables = disposables) CommentReplies(comment = comment, flow = flow)
} }
} }
} }
} }
override fun onDestroyView() {
super.onDestroyView()
disposables.clear()
}
companion object { companion object {
@JvmField @JvmField
val TAG = CommentRepliesFragment::class.simpleName!! val TAG = CommentRepliesFragment::class.simpleName!!

View File

@ -1,7 +1,6 @@
package org.schabi.newpipe.fragments.list.comments package org.schabi.newpipe.fragments.list.comments
import android.content.res.Configuration import android.content.res.Configuration
import android.widget.TextView
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -25,24 +24,18 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.text.HtmlCompat
import androidx.core.text.method.LinkMovementMethodCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import coil.compose.AsyncImage import coil.compose.AsyncImage
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.comments.CommentsInfoItem import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.extractor.stream.Description import org.schabi.newpipe.extractor.stream.Description
import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.ServiceHelper
import org.schabi.newpipe.util.image.ImageStrategy import org.schabi.newpipe.util.image.ImageStrategy
import org.schabi.newpipe.util.text.TextLinkifier
@Composable @Composable
fun CommentRepliesHeader(comment: CommentsInfoItem, disposables: CompositeDisposable) { fun CommentRepliesHeader(comment: CommentsInfoItem) {
val context = LocalContext.current val context = LocalContext.current
Surface(color = MaterialTheme.colorScheme.background) { Surface(color = MaterialTheme.colorScheme.background) {
@ -117,20 +110,9 @@ fun CommentRepliesHeader(comment: CommentsInfoItem, disposables: CompositeDispos
} }
} }
AndroidView( Text(
factory = { context -> text = rememberParsedText(comment.commentText),
TextView(context).apply { style = MaterialTheme.typography.bodyMedium
movementMethod = LinkMovementMethodCompat.getInstance()
}
},
update = { view ->
// setup comment content
TextLinkifier.fromDescription(
view, comment.commentText, HtmlCompat.FROM_HTML_MODE_LEGACY,
ServiceHelper.getServiceById(comment.serviceId), comment.url, disposables,
null
)
}
) )
} }
} }
@ -149,6 +131,6 @@ fun CommentRepliesHeaderPreview() {
) )
AppTheme { AppTheme {
CommentRepliesHeader(comment, CompositeDisposable()) CommentRepliesHeader(comment)
} }
} }