-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
31d164d
commit edf3782
Showing
9 changed files
with
279 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 0 additions & 45 deletions
45
app/src/main/java/org/schabi/newpipe/ui/components/common/DescriptionText.kt
This file was deleted.
Oops, something went wrong.
165 changes: 165 additions & 0 deletions
165
app/src/main/java/org/schabi/newpipe/ui/components/common/Markdown.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) } | ||
} |
72 changes: 72 additions & 0 deletions
72
app/src/main/java/org/schabi/newpipe/ui/components/common/ParseDescription.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) | ||
} | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
app/src/main/java/org/schabi/newpipe/ui/components/common/link/YouTubeLinkHandler.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: Redirect other links to 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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.