diff --git a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt index e9f041245..13c3ebe63 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt @@ -31,6 +31,7 @@ import androidx.constraintlayout.motion.widget.MotionLayout import androidx.core.net.toUri import androidx.core.os.ConfigurationCompat import androidx.core.os.bundleOf +import androidx.core.text.HtmlCompat import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.lifecycle.Lifecycle @@ -1002,7 +1003,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { // set video description val description = streams.description!! - setupDescription(binding.playerDescription, description, exoPlayer) + setupDescription(binding.playerDescription, description) binding.playerChannel.setOnClickListener { val activity = view?.context as MainActivity @@ -1036,56 +1037,21 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { } } + /** + * Set up the description text with video links and timestamps + */ private fun setupDescription( descTextView: TextView, - description: String, - exoPlayer: ExoPlayer + description: String ) { // detect whether the description is html formatted if (description.contains("<") && description.contains(">")) { descTextView.movementMethod = LinkMovementMethod.getInstance() - descTextView.text = HtmlParser.createSpannedText( + descTextView.text = HtmlCompat.fromHtml( description, - LinkHandler { link -> - // check if the link is a youtube link - if (link.contains("youtube.com") || link.contains("youtu.be")) { - // check if the link is a video link - val videoId = getVideoIdIfVideoLink(link) - if (!videoId.isNullOrEmpty()) { - // check if the video is the current video - if (videoId == this.videoId) { - // get the time from the link - val time = link.substringAfter("t=").substringBefore("&") - // check if the time is valid - if (time.isNotEmpty() && time != link) { - // get the time in seconds - val timeInSeconds = getTimeInMillis(time) - if (timeInSeconds != -1L) { - // seek to the time - exoPlayer.seekTo(timeInSeconds) - } - } - // youtube link without time - // open new player - playNextVideo(videoId) - } else { - // not the current video - // open new player - playNextVideo(videoId) - } - } else { - // not a video link, might be channel or playlist link - // handle normally - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link)) - startActivity(intent) - } - } else { - // not a youtube link - // handle normally - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link)) - startActivity(intent) - } - } + HtmlCompat.FROM_HTML_MODE_LEGACY, + null, + HtmlParser(LinkHandler { link -> handleLink(link) }) ) } else { // Links can be present as plain text @@ -1094,54 +1060,92 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { } } - private fun getVideoIdIfVideoLink(link: String): String? { + /** + * Handle a link clicked in the description + */ + private fun handleLink(link: String) { + val uri = Uri.parse(link) + // get video id if the link is a valid youtube video link + val videoId = getVideoIdIfVideoLink(link, uri) + if (!videoId.isNullOrEmpty()) { + // check if the video is the current video and has a valid time + if (videoId == this.videoId) { + val timeInMillis = getTimeInMillis(uri) + if (timeInMillis != -1L) { + // seek to the time + exoPlayer.seekTo(timeInMillis) + } + // else do nothing as the video is already playing + } else { + // youtube video link without time or not the current video + // open new player + playNextVideo(videoId) + } + } else { + // not a youtube video link + // handle normally + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link)) + startActivity(intent) + } + } + + /** + * Get video id if the link is a valid youtube video link + */ + private fun getVideoIdIfVideoLink(link: String, uri: Uri): String? { if (link.contains("youtube.com")) { - // check if the link is a channel link - val videoId = link.substringAfter("v=").substringBefore("&") - if (videoId.isNotEmpty() && videoId != link) { - return videoId + // the link may be in an unsupported format, so we should try/catch it + return try { + uri.getQueryParameter("v") + } catch (e: Exception) { + null } } else if (link.contains("youtu.be")) { - val videoId = link.substringAfter("be/").substringBefore("&") - if (videoId.isNotEmpty() && videoId != link) { - return videoId - } + return uri.lastPathSegment } + return null } - private fun getTimeInMillis(time: String): Long { + /** + * Get time in milliseconds from a youtube video link + */ + private fun getTimeInMillis(uri: Uri): Long { + var time = uri.getQueryParameter("t") ?: return -1L + var timeInSeconds = 0L var found = false - // check if the time is in seconds - if (time.contains("s")) { - val longOrNull = time.substringBefore("s").toLongOrNull() - if (longOrNull != null) { - timeInSeconds += longOrNull - found = true - } - } - // check if the time is in minutes - if (time.contains("m")) { - val longOrNull = time.substringBefore("m").substringAfter("s").toLongOrNull() - if (longOrNull != null) { - timeInSeconds += longOrNull * 60 - found = true - } - } - // check if the time is in hours + + // Check if the time has hours if (time.contains("h")) { - val longOrNull = time.substringBefore("h").substringAfter("m").toLongOrNull() - if (longOrNull != null) { - timeInSeconds += longOrNull * 60 * 60 + time.substringBefore("h").toLongOrNull()?.let { + timeInSeconds += it * 60 * 60 + time = time.substringAfter("h") found = true } } + // Check if the time has minutes + if (time.contains("m")) { + time.substringBefore("m").toLongOrNull()?.let { + timeInSeconds += it * 60 + time = time.substringAfter("m") + found = true + } + } + + // Check if the time has seconds + if (time.contains("s")) { + time.substringBefore("s").toLongOrNull()?.let { + timeInSeconds += it + found = true + } + } + + // Time may not contain h, m or s. In that case, it is just a number of seconds if (!found) { - val longOrNull = time.toLongOrNull() - if (longOrNull != null) { - timeInSeconds += longOrNull + time.toLongOrNull()?.let { + timeInSeconds += it found = true } } diff --git a/app/src/main/java/com/github/libretube/util/HtmlParser.kt b/app/src/main/java/com/github/libretube/util/HtmlParser.kt index d82341949..47c1db25d 100644 --- a/app/src/main/java/com/github/libretube/util/HtmlParser.kt +++ b/app/src/main/java/com/github/libretube/util/HtmlParser.kt @@ -2,17 +2,13 @@ package com.github.libretube.util import android.text.Editable import android.text.Html -import android.text.Spanned -import androidx.core.text.HtmlCompat import org.xml.sax.Attributes import org.xml.sax.ContentHandler import org.xml.sax.Locator import org.xml.sax.SAXException import org.xml.sax.XMLReader -class HtmlParser private constructor(private val handler: TagHandler) : - Html.TagHandler, - ContentHandler { +class HtmlParser constructor(private val handler: LinkHandler) : Html.TagHandler, ContentHandler { private val tagStatus = ArrayDeque() private var wrapped: ContentHandler? = null private var text: Editable? = null @@ -20,15 +16,12 @@ class HtmlParser private constructor(private val handler: TagHandler) : if (wrapped == null) { // record result object text = output - // record current content handler wrapped = xmlReader.contentHandler - // replace content handler with our own that forwards to calls to original when needed xmlReader.contentHandler = this - - // handle endElement() callback for tag - tagStatus.addLast(java.lang.Boolean.FALSE) + // add false to the stack to make sure we always have a tag to pop + tagStatus.addLast(false) } } @@ -41,6 +34,7 @@ class HtmlParser private constructor(private val handler: TagHandler) : ) { val isHandled = handler.handleTag(true, localName, text, attributes) tagStatus.addLast(isHandled) + if (!isHandled) { wrapped?.startElement(uri, localName, qName, attributes) } @@ -97,38 +91,4 @@ class HtmlParser private constructor(private val handler: TagHandler) : override fun skippedEntity(name: String) { wrapped?.skippedEntity(name) } - - interface TagHandler { - fun handleTag( - opening: Boolean, - tag: String?, - output: Editable?, - attributes: Attributes? - ): Boolean - } - - companion object { - fun createSpannedText(html: String, handler: TagHandler): Spanned { - // add a tag at the start that is not handled by default, - // allowing custom tag handler to replace xmlReader contentHandler - return HtmlCompat.fromHtml( - html, - HtmlCompat.FROM_HTML_MODE_LEGACY, - null, - HtmlParser(handler) - ) - } - - @JvmStatic fun getValue(attributes: Attributes, name: String): String? { - var i = 0 - val n = attributes.length - while (i < n) { - if (name.equals(attributes.getLocalName(i), ignoreCase = true)) { - return attributes.getValue(i) - } - i++ - } - return null - } - } } diff --git a/app/src/main/java/com/github/libretube/util/LinkHandler.kt b/app/src/main/java/com/github/libretube/util/LinkHandler.kt index 510b8c650..5d1ff6278 100644 --- a/app/src/main/java/com/github/libretube/util/LinkHandler.kt +++ b/app/src/main/java/com/github/libretube/util/LinkHandler.kt @@ -5,31 +5,32 @@ import android.text.Spanned import android.text.TextPaint import android.text.style.ClickableSpan import android.view.View -import com.github.libretube.util.HtmlParser.Companion.getValue import org.xml.sax.Attributes -class LinkHandler(private val clickCallback: ((String) -> Unit)?) : HtmlParser.TagHandler { +class LinkHandler(private val clickCallback: ((String) -> Unit)?) { private var linkTagStartIndex = -1 private var link: String? = null - override fun handleTag( + fun handleTag( opening: Boolean, tag: String?, output: Editable?, attributes: Attributes? ): Boolean { - if (output != null) { - if ("a" == tag) { - if (opening && attributes != null) { - linkTagStartIndex = output.length - link = getValue(attributes, "href") - } else { - val refTagEndIndex = output.length - setLinkSpans(output, linkTagStartIndex, refTagEndIndex, link) - } - return true - } + // if the tag is not an anchor link, ignore for the default handler + if (output == null || "a" != tag) { + return false } - return false + + if (opening) { + if (attributes != null) { + linkTagStartIndex = output.length + link = attributes.getValue("href") + } + } else { + val refTagEndIndex = output.length + setLinkSpans(output, linkTagStartIndex, refTagEndIndex, link) + } + return true } private fun setLinkSpans(output: Editable, start: Int, end: Int, link: String?) {