Fixed #2670 : Timestamp click behaviour in the description.

This commit is contained in:
faisalcodes 2023-01-18 21:43:22 +05:30
parent b805e0e375
commit 7006693485
3 changed files with 101 additions and 136 deletions

View File

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

View File

@ -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<Boolean>()
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 <inject/> 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
}
}
}

View File

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