1
0
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:
Isira Seneviratne 2024-09-06 19:51:32 +05:30
parent 31d164d116
commit 8f9faf3e53
9 changed files with 279 additions and 57 deletions

View File

@ -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}"

View File

@ -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)!!)
}
}
}

View File

@ -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
)
}

View File

@ -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) }
}

View File

@ -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))
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
)

View File

@ -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

View File

@ -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)
}
}
}