Skip to content

Commit edf3782

Browse files
Address some review comments
1 parent 31d164d commit edf3782

File tree

9 files changed

+279
-57
lines changed

9 files changed

+279
-57
lines changed

app/build.gradle

+3
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,9 @@ dependencies {
298298
// Coroutines interop
299299
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.8.1'
300300

301+
// Custom browser tab
302+
implementation 'androidx.browser:browser:1.8.0'
303+
301304
/** Debugging **/
302305
// Memory leak detection
303306
debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"

app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import androidx.fragment.app.Fragment
1010
import androidx.fragment.compose.content
1111
import org.schabi.newpipe.extractor.stream.StreamInfo
1212
import org.schabi.newpipe.ktx.serializable
13-
import org.schabi.newpipe.ui.components.video.VideoDescriptionSection
13+
import org.schabi.newpipe.ui.components.video.StreamDescriptionSection
1414
import org.schabi.newpipe.ui.theme.AppTheme
1515
import org.schabi.newpipe.util.KEY_INFO
1616

@@ -22,7 +22,7 @@ class DescriptionFragment : Fragment() {
2222
) = content {
2323
AppTheme {
2424
Surface(color = MaterialTheme.colorScheme.background) {
25-
VideoDescriptionSection(requireArguments().serializable(KEY_INFO)!!)
25+
StreamDescriptionSection(requireArguments().serializable(KEY_INFO)!!)
2626
}
2727
}
2828
}

app/src/main/java/org/schabi/newpipe/ui/components/common/DescriptionText.kt

-45
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package org.schabi.newpipe.ui.components.common
2+
3+
import android.graphics.Typeface
4+
import android.text.Layout
5+
import android.text.Spanned
6+
import android.text.style.AbsoluteSizeSpan
7+
import android.text.style.AlignmentSpan
8+
import android.text.style.BackgroundColorSpan
9+
import android.text.style.ForegroundColorSpan
10+
import android.text.style.RelativeSizeSpan
11+
import android.text.style.StrikethroughSpan
12+
import android.text.style.StyleSpan
13+
import android.text.style.SubscriptSpan
14+
import android.text.style.SuperscriptSpan
15+
import android.text.style.TypefaceSpan
16+
import android.text.style.URLSpan
17+
import android.text.style.UnderlineSpan
18+
import androidx.compose.ui.graphics.Color
19+
import androidx.compose.ui.text.AnnotatedString
20+
import androidx.compose.ui.text.LinkAnnotation
21+
import androidx.compose.ui.text.LinkInteractionListener
22+
import androidx.compose.ui.text.ParagraphStyle
23+
import androidx.compose.ui.text.SpanStyle
24+
import androidx.compose.ui.text.TextLinkStyles
25+
import androidx.compose.ui.text.font.FontFamily
26+
import androidx.compose.ui.text.font.FontStyle
27+
import androidx.compose.ui.text.font.FontWeight
28+
import androidx.compose.ui.text.style.BaselineShift
29+
import androidx.compose.ui.text.style.TextAlign
30+
import androidx.compose.ui.text.style.TextDecoration
31+
import androidx.compose.ui.unit.em
32+
import androidx.core.text.getSpans
33+
34+
// The code below is copied from Html.android.kt in the Compose Text library, with some minor
35+
// changes.
36+
37+
internal fun Spanned.toAnnotatedString(
38+
linkStyles: TextLinkStyles? = null,
39+
linkInteractionListener: LinkInteractionListener? = null
40+
): AnnotatedString {
41+
return AnnotatedString.Builder(capacity = length)
42+
.append(this)
43+
.also {
44+
it.addSpans(this, linkStyles, linkInteractionListener)
45+
}
46+
.toAnnotatedString()
47+
}
48+
49+
private fun AnnotatedString.Builder.addSpans(
50+
spanned: Spanned,
51+
linkStyles: TextLinkStyles?,
52+
linkInteractionListener: LinkInteractionListener?
53+
) {
54+
spanned.getSpans<Any>().forEach { span ->
55+
addSpan(
56+
span,
57+
spanned.getSpanStart(span),
58+
spanned.getSpanEnd(span),
59+
linkStyles,
60+
linkInteractionListener
61+
)
62+
}
63+
}
64+
65+
private fun AnnotatedString.Builder.addSpan(
66+
span: Any,
67+
start: Int,
68+
end: Int,
69+
linkStyles: TextLinkStyles?,
70+
linkInteractionListener: LinkInteractionListener?
71+
) {
72+
when (span) {
73+
is AbsoluteSizeSpan -> {
74+
// TODO: Add Compose's implementation when it is available.
75+
}
76+
77+
is AlignmentSpan -> {
78+
addStyle(span.toParagraphStyle(), start, end)
79+
}
80+
81+
is BackgroundColorSpan -> {
82+
addStyle(SpanStyle(background = Color(span.backgroundColor)), start, end)
83+
}
84+
85+
is ForegroundColorSpan -> {
86+
addStyle(SpanStyle(color = Color(span.foregroundColor)), start, end)
87+
}
88+
89+
is RelativeSizeSpan -> {
90+
addStyle(SpanStyle(fontSize = span.sizeChange.em), start, end)
91+
}
92+
93+
is StrikethroughSpan -> {
94+
addStyle(SpanStyle(textDecoration = TextDecoration.LineThrough), start, end)
95+
}
96+
97+
is StyleSpan -> {
98+
span.toSpanStyle()?.let { addStyle(it, start, end) }
99+
}
100+
101+
is SubscriptSpan -> {
102+
addStyle(SpanStyle(baselineShift = BaselineShift.Subscript), start, end)
103+
}
104+
105+
is SuperscriptSpan -> {
106+
addStyle(SpanStyle(baselineShift = BaselineShift.Superscript), start, end)
107+
}
108+
109+
is TypefaceSpan -> {
110+
addStyle(span.toSpanStyle(), start, end)
111+
}
112+
113+
is UnderlineSpan -> {
114+
addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end)
115+
}
116+
117+
is URLSpan -> {
118+
span.url?.let { url ->
119+
val link = LinkAnnotation.Url(url, linkStyles, linkInteractionListener)
120+
addLink(link, start, end)
121+
}
122+
}
123+
}
124+
}
125+
126+
private fun AlignmentSpan.toParagraphStyle(): ParagraphStyle {
127+
val alignment = when (this.alignment) {
128+
Layout.Alignment.ALIGN_NORMAL -> TextAlign.Start
129+
Layout.Alignment.ALIGN_CENTER -> TextAlign.Center
130+
Layout.Alignment.ALIGN_OPPOSITE -> TextAlign.End
131+
else -> TextAlign.Unspecified
132+
}
133+
return ParagraphStyle(textAlign = alignment)
134+
}
135+
136+
private fun StyleSpan.toSpanStyle(): SpanStyle? {
137+
return when (style) {
138+
Typeface.BOLD -> SpanStyle(fontWeight = FontWeight.Bold)
139+
Typeface.ITALIC -> SpanStyle(fontStyle = FontStyle.Italic)
140+
Typeface.BOLD_ITALIC -> SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic)
141+
else -> null
142+
}
143+
}
144+
145+
private fun TypefaceSpan.toSpanStyle(): SpanStyle {
146+
val fontFamily = when (family) {
147+
FontFamily.Cursive.name -> FontFamily.Cursive
148+
FontFamily.Monospace.name -> FontFamily.Monospace
149+
FontFamily.SansSerif.name -> FontFamily.SansSerif
150+
FontFamily.Serif.name -> FontFamily.Serif
151+
else -> {
152+
optionalFontFamilyFromName(family)
153+
}
154+
}
155+
return SpanStyle(fontFamily = fontFamily)
156+
}
157+
158+
private fun optionalFontFamilyFromName(familyName: String?): FontFamily? {
159+
if (familyName.isNullOrEmpty()) return null
160+
val typeface = Typeface.create(familyName, Typeface.NORMAL)
161+
return typeface.takeIf {
162+
typeface != Typeface.DEFAULT &&
163+
typeface != Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
164+
}?.let { FontFamily(it) }
165+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package org.schabi.newpipe.ui.components.common
2+
3+
import android.content.res.Configuration
4+
import androidx.compose.material3.MaterialTheme
5+
import androidx.compose.material3.Surface
6+
import androidx.compose.material3.Text
7+
import androidx.compose.runtime.Composable
8+
import androidx.compose.runtime.remember
9+
import androidx.compose.ui.platform.LocalContext
10+
import androidx.compose.ui.text.AnnotatedString
11+
import androidx.compose.ui.text.SpanStyle
12+
import androidx.compose.ui.text.TextLinkStyles
13+
import androidx.compose.ui.text.fromHtml
14+
import androidx.compose.ui.text.style.TextDecoration
15+
import androidx.compose.ui.tooling.preview.Preview
16+
import androidx.compose.ui.tooling.preview.PreviewParameter
17+
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
18+
import io.noties.markwon.Markwon
19+
import io.noties.markwon.linkify.LinkifyPlugin
20+
import org.schabi.newpipe.extractor.ServiceList
21+
import org.schabi.newpipe.extractor.stream.Description
22+
import org.schabi.newpipe.ui.components.common.link.YouTubeLinkHandler
23+
import org.schabi.newpipe.ui.theme.AppTheme
24+
import org.schabi.newpipe.util.NO_SERVICE_ID
25+
26+
@Composable
27+
fun parseDescription(description: Description, serviceId: Int): AnnotatedString {
28+
val context = LocalContext.current
29+
val linkHandler = remember(serviceId) {
30+
if (serviceId == ServiceList.YouTube.serviceId) {
31+
YouTubeLinkHandler(context)
32+
} else {
33+
null
34+
}
35+
}
36+
val styles = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.Underline))
37+
38+
return remember(description) {
39+
when (description.type) {
40+
Description.HTML -> AnnotatedString.fromHtml(description.content, styles, linkHandler)
41+
Description.MARKDOWN -> {
42+
Markwon.builder(context)
43+
.usePlugin(LinkifyPlugin.create())
44+
.build()
45+
.toMarkdown(description.content)
46+
.toAnnotatedString(styles, linkHandler)
47+
}
48+
else -> AnnotatedString(description.content)
49+
}
50+
}
51+
}
52+
53+
private class DescriptionPreviewProvider : PreviewParameterProvider<Description> {
54+
override val values = sequenceOf(
55+
Description("This is a description.", Description.PLAIN_TEXT),
56+
Description("This is a <b>bold description</b>.", Description.HTML),
57+
Description("This is a [link](https://example.com).", Description.MARKDOWN),
58+
)
59+
}
60+
61+
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
62+
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
63+
@Composable
64+
private fun ParseDescriptionPreview(
65+
@PreviewParameter(DescriptionPreviewProvider::class) description: Description
66+
) {
67+
AppTheme {
68+
Surface(color = MaterialTheme.colorScheme.background) {
69+
Text(text = parseDescription(description, NO_SERVICE_ID))
70+
}
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.schabi.newpipe.ui.components.common.link
2+
3+
import android.content.Context
4+
import androidx.browser.customtabs.CustomTabsIntent
5+
import androidx.compose.ui.text.LinkAnnotation
6+
import androidx.compose.ui.text.LinkInteractionListener
7+
import androidx.core.net.toUri
8+
import org.schabi.newpipe.extractor.ServiceList
9+
import org.schabi.newpipe.util.NavigationHelper
10+
11+
class YouTubeLinkHandler(private val context: Context) : LinkInteractionListener {
12+
override fun onClick(link: LinkAnnotation) {
13+
val uri = (link as LinkAnnotation.Url).url.toUri()
14+
15+
// TODO: Redirect other links to NewPipe as well.
16+
if ("hashtag" in uri.pathSegments) {
17+
NavigationHelper.openSearch(
18+
context, ServiceList.YouTube.serviceId, "#${uri.lastPathSegment}"
19+
)
20+
} else {
21+
// Open link in custom browser tab.
22+
CustomTabsIntent.Builder().build().launchUrl(context, uri)
23+
}
24+
}
25+
}

app/src/main/java/org/schabi/newpipe/ui/components/metadata/MetadataItem.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ fun MetadataItem(@StringRes title: Int, value: AnnotatedString) {
3737
Text(
3838
modifier = Modifier.width(96.dp),
3939
textAlign = TextAlign.End,
40-
text = stringResource(title),
40+
text = stringResource(title).uppercase(),
4141
fontWeight = FontWeight.Bold
4242
)
4343

app/src/main/java/org/schabi/newpipe/ui/components/metadata/TagsSection.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
77
import androidx.compose.foundation.layout.FlowRow
88
import androidx.compose.foundation.layout.fillMaxWidth
99
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.material3.ElevatedSuggestionChip
1011
import androidx.compose.material3.MaterialTheme
11-
import androidx.compose.material3.SuggestionChip
1212
import androidx.compose.material3.Surface
1313
import androidx.compose.material3.Text
1414
import androidx.compose.runtime.Composable
@@ -34,14 +34,14 @@ fun TagsSection(serviceId: Int, tags: List<String>) {
3434
Column(modifier = Modifier.padding(4.dp)) {
3535
Text(
3636
modifier = Modifier.fillMaxWidth(),
37-
text = stringResource(R.string.metadata_tags),
37+
text = stringResource(R.string.metadata_tags).uppercase(),
3838
fontWeight = FontWeight.Bold,
3939
textAlign = TextAlign.Center
4040
)
4141

4242
FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
4343
for (tag in sortedTags) {
44-
SuggestionChip(
44+
ElevatedSuggestionChip(
4545
onClick = {
4646
NavigationHelper.openSearchFragment(
4747
(context as FragmentActivity).supportFragmentManager, serviceId, tag

0 commit comments

Comments
 (0)