mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-02-10 16:10:11 +00:00
Address some review comments
This commit is contained in:
parent
31d164d116
commit
8f9faf3e53
@ -298,6 +298,9 @@ dependencies {
|
||||
// Coroutines interop
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.8.1'
|
||||
|
||||
// Custom browser tab
|
||||
implementation 'androidx.browser:browser:1.8.0'
|
||||
|
||||
/** Debugging **/
|
||||
// Memory leak detection
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
||||
|
@ -10,7 +10,7 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.compose.content
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import org.schabi.newpipe.ktx.serializable
|
||||
import org.schabi.newpipe.ui.components.video.VideoDescriptionSection
|
||||
import org.schabi.newpipe.ui.components.video.StreamDescriptionSection
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.KEY_INFO
|
||||
|
||||
@ -22,7 +22,7 @@ class DescriptionFragment : Fragment() {
|
||||
) = content {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
VideoDescriptionSection(requireArguments().serializable(KEY_INFO)!!)
|
||||
StreamDescriptionSection(requireArguments().serializable(KEY_INFO)!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +0,0 @@
|
||||
package org.schabi.newpipe.ui.components.common
|
||||
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.fromHtml
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
|
||||
@Composable
|
||||
fun DescriptionText(
|
||||
description: Description,
|
||||
modifier: Modifier = Modifier,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||
style: TextStyle = LocalTextStyle.current
|
||||
) {
|
||||
// TODO: Handle links and hashtags, Markdown.
|
||||
val parsedDescription = remember(description) {
|
||||
if (description.type == Description.HTML) {
|
||||
val styles = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.Underline))
|
||||
AnnotatedString.fromHtml(description.content, styles)
|
||||
} else {
|
||||
AnnotatedString(description.content)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = parsedDescription,
|
||||
maxLines = maxLines,
|
||||
style = style,
|
||||
overflow = overflow,
|
||||
onTextLayout = onTextLayout
|
||||
)
|
||||
}
|
@ -0,0 +1,165 @@
|
||||
package org.schabi.newpipe.ui.components.common
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Layout
|
||||
import android.text.Spanned
|
||||
import android.text.style.AbsoluteSizeSpan
|
||||
import android.text.style.AlignmentSpan
|
||||
import android.text.style.BackgroundColorSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
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.TypefaceSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.text.style.UnderlineSpan
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.LinkInteractionListener
|
||||
import androidx.compose.ui.text.ParagraphStyle
|
||||
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.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.BaselineShift
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.em
|
||||
import androidx.core.text.getSpans
|
||||
|
||||
// The code below is copied from Html.android.kt in the Compose Text library, with some minor
|
||||
// changes.
|
||||
|
||||
internal fun Spanned.toAnnotatedString(
|
||||
linkStyles: TextLinkStyles? = null,
|
||||
linkInteractionListener: LinkInteractionListener? = null
|
||||
): AnnotatedString {
|
||||
return AnnotatedString.Builder(capacity = length)
|
||||
.append(this)
|
||||
.also {
|
||||
it.addSpans(this, linkStyles, linkInteractionListener)
|
||||
}
|
||||
.toAnnotatedString()
|
||||
}
|
||||
|
||||
private fun AnnotatedString.Builder.addSpans(
|
||||
spanned: Spanned,
|
||||
linkStyles: TextLinkStyles?,
|
||||
linkInteractionListener: LinkInteractionListener?
|
||||
) {
|
||||
spanned.getSpans<Any>().forEach { span ->
|
||||
addSpan(
|
||||
span,
|
||||
spanned.getSpanStart(span),
|
||||
spanned.getSpanEnd(span),
|
||||
linkStyles,
|
||||
linkInteractionListener
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun AnnotatedString.Builder.addSpan(
|
||||
span: Any,
|
||||
start: Int,
|
||||
end: Int,
|
||||
linkStyles: TextLinkStyles?,
|
||||
linkInteractionListener: LinkInteractionListener?
|
||||
) {
|
||||
when (span) {
|
||||
is AbsoluteSizeSpan -> {
|
||||
// TODO: Add Compose's implementation when it is available.
|
||||
}
|
||||
|
||||
is AlignmentSpan -> {
|
||||
addStyle(span.toParagraphStyle(), start, end)
|
||||
}
|
||||
|
||||
is BackgroundColorSpan -> {
|
||||
addStyle(SpanStyle(background = Color(span.backgroundColor)), start, end)
|
||||
}
|
||||
|
||||
is ForegroundColorSpan -> {
|
||||
addStyle(SpanStyle(color = Color(span.foregroundColor)), start, end)
|
||||
}
|
||||
|
||||
is RelativeSizeSpan -> {
|
||||
addStyle(SpanStyle(fontSize = span.sizeChange.em), start, end)
|
||||
}
|
||||
|
||||
is StrikethroughSpan -> {
|
||||
addStyle(SpanStyle(textDecoration = TextDecoration.LineThrough), start, end)
|
||||
}
|
||||
|
||||
is StyleSpan -> {
|
||||
span.toSpanStyle()?.let { addStyle(it, start, end) }
|
||||
}
|
||||
|
||||
is SubscriptSpan -> {
|
||||
addStyle(SpanStyle(baselineShift = BaselineShift.Subscript), start, end)
|
||||
}
|
||||
|
||||
is SuperscriptSpan -> {
|
||||
addStyle(SpanStyle(baselineShift = BaselineShift.Superscript), start, end)
|
||||
}
|
||||
|
||||
is TypefaceSpan -> {
|
||||
addStyle(span.toSpanStyle(), start, end)
|
||||
}
|
||||
|
||||
is UnderlineSpan -> {
|
||||
addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end)
|
||||
}
|
||||
|
||||
is URLSpan -> {
|
||||
span.url?.let { url ->
|
||||
val link = LinkAnnotation.Url(url, linkStyles, linkInteractionListener)
|
||||
addLink(link, start, end)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AlignmentSpan.toParagraphStyle(): ParagraphStyle {
|
||||
val alignment = when (this.alignment) {
|
||||
Layout.Alignment.ALIGN_NORMAL -> TextAlign.Start
|
||||
Layout.Alignment.ALIGN_CENTER -> TextAlign.Center
|
||||
Layout.Alignment.ALIGN_OPPOSITE -> TextAlign.End
|
||||
else -> TextAlign.Unspecified
|
||||
}
|
||||
return ParagraphStyle(textAlign = alignment)
|
||||
}
|
||||
|
||||
private fun StyleSpan.toSpanStyle(): SpanStyle? {
|
||||
return 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
|
||||
}
|
||||
}
|
||||
|
||||
private fun TypefaceSpan.toSpanStyle(): SpanStyle {
|
||||
val fontFamily = when (family) {
|
||||
FontFamily.Cursive.name -> FontFamily.Cursive
|
||||
FontFamily.Monospace.name -> FontFamily.Monospace
|
||||
FontFamily.SansSerif.name -> FontFamily.SansSerif
|
||||
FontFamily.Serif.name -> FontFamily.Serif
|
||||
else -> {
|
||||
optionalFontFamilyFromName(family)
|
||||
}
|
||||
}
|
||||
return SpanStyle(fontFamily = fontFamily)
|
||||
}
|
||||
|
||||
private fun optionalFontFamilyFromName(familyName: String?): FontFamily? {
|
||||
if (familyName.isNullOrEmpty()) return null
|
||||
val typeface = Typeface.create(familyName, Typeface.NORMAL)
|
||||
return typeface.takeIf {
|
||||
typeface != Typeface.DEFAULT &&
|
||||
typeface != Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
|
||||
}?.let { FontFamily(it) }
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package org.schabi.newpipe.ui.components.common
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.fromHtml
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.linkify.LinkifyPlugin
|
||||
import org.schabi.newpipe.extractor.ServiceList
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
import org.schabi.newpipe.ui.components.common.link.YouTubeLinkHandler
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.NO_SERVICE_ID
|
||||
|
||||
@Composable
|
||||
fun parseDescription(description: Description, serviceId: Int): AnnotatedString {
|
||||
val context = LocalContext.current
|
||||
val linkHandler = remember(serviceId) {
|
||||
if (serviceId == ServiceList.YouTube.serviceId) {
|
||||
YouTubeLinkHandler(context)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val styles = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.Underline))
|
||||
|
||||
return remember(description) {
|
||||
when (description.type) {
|
||||
Description.HTML -> AnnotatedString.fromHtml(description.content, styles, linkHandler)
|
||||
Description.MARKDOWN -> {
|
||||
Markwon.builder(context)
|
||||
.usePlugin(LinkifyPlugin.create())
|
||||
.build()
|
||||
.toMarkdown(description.content)
|
||||
.toAnnotatedString(styles, linkHandler)
|
||||
}
|
||||
else -> AnnotatedString(description.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DescriptionPreviewProvider : PreviewParameterProvider<Description> {
|
||||
override val values = sequenceOf(
|
||||
Description("This is a description.", Description.PLAIN_TEXT),
|
||||
Description("This is a <b>bold description</b>.", Description.HTML),
|
||||
Description("This is a [link](https://example.com).", Description.MARKDOWN),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ParseDescriptionPreview(
|
||||
@PreviewParameter(DescriptionPreviewProvider::class) description: Description
|
||||
) {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Text(text = parseDescription(description, NO_SERVICE_ID))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package org.schabi.newpipe.ui.components.common.link
|
||||
|
||||
import android.content.Context
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.LinkInteractionListener
|
||||
import androidx.core.net.toUri
|
||||
import org.schabi.newpipe.extractor.ServiceList
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
|
||||
class YouTubeLinkHandler(private val context: Context) : LinkInteractionListener {
|
||||
override fun onClick(link: LinkAnnotation) {
|
||||
val uri = (link as LinkAnnotation.Url).url.toUri()
|
||||
|
||||
// TODO: Handle other links in NewPipe as well.
|
||||
if ("hashtag" in uri.pathSegments) {
|
||||
NavigationHelper.openSearch(
|
||||
context, ServiceList.YouTube.serviceId, "#${uri.lastPathSegment}"
|
||||
)
|
||||
} else {
|
||||
// Open link in custom browser tab.
|
||||
CustomTabsIntent.Builder().build().launchUrl(context, uri)
|
||||
}
|
||||
}
|
||||
}
|
@ -37,7 +37,7 @@ fun MetadataItem(@StringRes title: Int, value: AnnotatedString) {
|
||||
Text(
|
||||
modifier = Modifier.width(96.dp),
|
||||
textAlign = TextAlign.End,
|
||||
text = stringResource(title),
|
||||
text = stringResource(title).uppercase(),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
|
@ -7,8 +7,8 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ElevatedSuggestionChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SuggestionChip
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -34,14 +34,14 @@ fun TagsSection(serviceId: Int, tags: List<String>) {
|
||||
Column(modifier = Modifier.padding(4.dp)) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.metadata_tags),
|
||||
text = stringResource(R.string.metadata_tags).uppercase(),
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
for (tag in sortedTags) {
|
||||
SuggestionChip(
|
||||
ElevatedSuggestionChip(
|
||||
onClick = {
|
||||
NavigationHelper.openSearchFragment(
|
||||
(context as FragmentActivity).supportFragmentManager, serviceId, tag
|
||||
|
@ -50,7 +50,7 @@ import org.schabi.newpipe.extractor.stream.Description
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.ui.components.common.DescriptionText
|
||||
import org.schabi.newpipe.ui.components.common.parseDescription
|
||||
import org.schabi.newpipe.ui.components.metadata.MetadataItem
|
||||
import org.schabi.newpipe.ui.components.metadata.TagsSection
|
||||
import org.schabi.newpipe.ui.components.metadata.imageMetadataItem
|
||||
@ -61,7 +61,7 @@ import java.time.OffsetDateTime
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun VideoDescriptionSection(streamInfo: StreamInfo) {
|
||||
fun StreamDescriptionSection(streamInfo: StreamInfo) {
|
||||
var isSelectable by rememberSaveable { mutableStateOf(false) }
|
||||
val hasDescription = streamInfo.description != Description.EMPTY_DESCRIPTION
|
||||
val lazyListState = rememberLazyListState()
|
||||
@ -131,12 +131,14 @@ fun VideoDescriptionSection(streamInfo: StreamInfo) {
|
||||
|
||||
if (hasDescription) {
|
||||
item {
|
||||
val description = parseDescription(streamInfo.description, streamInfo.serviceId)
|
||||
|
||||
if (isSelectable) {
|
||||
SelectionContainer {
|
||||
DescriptionText(description = streamInfo.description)
|
||||
Text(text = description)
|
||||
}
|
||||
} else {
|
||||
DescriptionText(description = streamInfo.description)
|
||||
Text(text = description)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -212,14 +214,14 @@ private fun LazyListScope.metadataItem(@StringRes title: Int, value: String) {
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun VideoDescriptionSectionPreview() {
|
||||
private fun StreamDescriptionSectionPreview() {
|
||||
val info = StreamInfo(NO_SERVICE_ID, "", "", StreamType.VIDEO_STREAM, "", "", 0)
|
||||
info.uploadDate = DateWrapper(OffsetDateTime.now())
|
||||
info.description = Description("This is an <b>example</b> description", Description.HTML)
|
||||
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
VideoDescriptionSection(info)
|
||||
StreamDescriptionSection(info)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user