mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-14 22:30:30 +05:30
Fixed #2670
This commit is contained in:
parent
620e985dd2
commit
0ee5753e91
@ -17,12 +17,14 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
import android.text.util.Linkify
|
import android.text.util.Linkify
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
@ -82,17 +84,7 @@ import com.github.libretube.ui.models.PlayerViewModel
|
|||||||
import com.github.libretube.ui.sheets.BaseBottomSheet
|
import com.github.libretube.ui.sheets.BaseBottomSheet
|
||||||
import com.github.libretube.ui.sheets.CommentsSheet
|
import com.github.libretube.ui.sheets.CommentsSheet
|
||||||
import com.github.libretube.ui.sheets.PlayingQueueSheet
|
import com.github.libretube.ui.sheets.PlayingQueueSheet
|
||||||
import com.github.libretube.util.BackgroundHelper
|
import com.github.libretube.util.*
|
||||||
import com.github.libretube.util.DashHelper
|
|
||||||
import com.github.libretube.util.DataSaverMode
|
|
||||||
import com.github.libretube.util.ImageHelper
|
|
||||||
import com.github.libretube.util.NavigationHelper
|
|
||||||
import com.github.libretube.util.NowPlayingNotification
|
|
||||||
import com.github.libretube.util.PlayerHelper
|
|
||||||
import com.github.libretube.util.PlayingQueue
|
|
||||||
import com.github.libretube.util.PreferenceHelper
|
|
||||||
import com.github.libretube.util.SeekbarPreviewListener
|
|
||||||
import com.github.libretube.util.TextUtils
|
|
||||||
import com.google.android.exoplayer2.C
|
import com.google.android.exoplayer2.C
|
||||||
import com.google.android.exoplayer2.DefaultLoadControl
|
import com.google.android.exoplayer2.DefaultLoadControl
|
||||||
import com.google.android.exoplayer2.ExoPlayer
|
import com.google.android.exoplayer2.ExoPlayer
|
||||||
@ -859,7 +851,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
|||||||
binding.apply {
|
binding.apply {
|
||||||
playerViewsInfo.text =
|
playerViewsInfo.text =
|
||||||
context?.getString(R.string.views, streams.views.formatShort()) +
|
context?.getString(R.string.views, streams.views.formatShort()) +
|
||||||
if (!isLive) TextUtils.SEPARATOR + localizedDate(streams.uploadDate) else ""
|
if (!isLive) TextUtils.SEPARATOR + localizedDate(streams.uploadDate) else ""
|
||||||
|
|
||||||
textLike.text = streams.likes.formatShort()
|
textLike.text = streams.likes.formatShort()
|
||||||
textDislike.text = streams.dislikes.formatShort()
|
textDislike.text = streams.dislikes.formatShort()
|
||||||
@ -926,9 +918,9 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
|||||||
|
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
exoPlayerView.keepScreenOn = !(
|
exoPlayerView.keepScreenOn = !(
|
||||||
playbackState == Player.STATE_IDLE ||
|
playbackState == Player.STATE_IDLE ||
|
||||||
playbackState == Player.STATE_ENDED
|
playbackState == Player.STATE_ENDED
|
||||||
)
|
)
|
||||||
|
|
||||||
// save the watch position to the database
|
// save the watch position to the database
|
||||||
// only called when the position is unequal to 0, otherwise it would become reset
|
// only called when the position is unequal to 0, otherwise it would become reset
|
||||||
@ -1000,14 +992,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
|||||||
// set video description
|
// set video description
|
||||||
val description = streams.description!!
|
val description = streams.description!!
|
||||||
|
|
||||||
// detect whether the description is html formatted
|
setupDescription(binding.playerDescription, description, exoPlayer)
|
||||||
if (description.contains("<") && description.contains(">")) {
|
|
||||||
binding.playerDescription.setFormattedHtml(description)
|
|
||||||
} else {
|
|
||||||
// Links can be present as plain text
|
|
||||||
binding.playerDescription.autoLinkMask = Linkify.WEB_URLS
|
|
||||||
binding.playerDescription.text = description
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.playerChannel.setOnClickListener {
|
binding.playerChannel.setOnClickListener {
|
||||||
val activity = view?.context as MainActivity
|
val activity = view?.context as MainActivity
|
||||||
@ -1041,6 +1026,116 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupDescription(descTextView: TextView, description: String, exoPlayer: ExoPlayer) {
|
||||||
|
// detect whether the description is html formatted
|
||||||
|
if (description.contains("<") && description.contains(">")) {
|
||||||
|
descTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
descTextView.text = HtmlParser.createSpannedText(
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Links can be present as plain text
|
||||||
|
descTextView.autoLinkMask = Linkify.WEB_URLS
|
||||||
|
descTextView.text = description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getVideoIdIfVideoLink(link: String): 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
|
||||||
|
}
|
||||||
|
} else if (link.contains("youtu.be")) {
|
||||||
|
val videoId = link.substringAfter("be/").substringBefore("&")
|
||||||
|
if (videoId.isNotEmpty() && videoId != link) {
|
||||||
|
return videoId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTimeInMillis(time: String): Long {
|
||||||
|
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
|
||||||
|
if (time.contains("h")) {
|
||||||
|
val longOrNull = time.substringBefore("h").substringAfter("m").toLongOrNull()
|
||||||
|
if (longOrNull != null) {
|
||||||
|
timeInSeconds += longOrNull * 60 * 60
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
val longOrNull = time.toLongOrNull()
|
||||||
|
if (longOrNull != null) {
|
||||||
|
timeInSeconds += longOrNull
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (found) timeInSeconds * 1000 else -1
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the displayed duration of the video
|
* Update the displayed duration of the video
|
||||||
*/
|
*/
|
||||||
|
104
app/src/main/java/com/github/libretube/util/HtmlParser.kt
Normal file
104
app/src/main/java/com/github/libretube/util/HtmlParser.kt
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
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.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class HtmlParser private constructor(private val handler: TagHandler) : Html.TagHandler, ContentHandler {
|
||||||
|
private val tagStatus = ArrayDeque<Boolean>()
|
||||||
|
private var wrapped: ContentHandler? = null
|
||||||
|
private var text: Editable? = null
|
||||||
|
override fun handleTag(opening: Boolean, tag: String, output: Editable, xmlReader: XMLReader) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(SAXException::class) override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) {
|
||||||
|
val isHandled = handler.handleTag(true, localName, text, attributes)
|
||||||
|
tagStatus.addLast(isHandled)
|
||||||
|
if (!isHandled) {
|
||||||
|
wrapped?.startElement(uri, localName, qName, attributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(SAXException::class) override fun endElement(uri: String, localName: String, qName: String) {
|
||||||
|
if (!tagStatus.removeLast()) {
|
||||||
|
wrapped?.endElement(uri, localName, qName)
|
||||||
|
}
|
||||||
|
handler.handleTag(false, localName, text, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setDocumentLocator(locator: Locator) {
|
||||||
|
wrapped?.setDocumentLocator(locator)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(SAXException::class) override fun startDocument() {
|
||||||
|
wrapped?.startDocument()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(SAXException::class) override fun endDocument() {
|
||||||
|
wrapped?.endDocument()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(SAXException::class) override fun startPrefixMapping(prefix: String, uri: String) {
|
||||||
|
wrapped?.startPrefixMapping(prefix, uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(SAXException::class) override fun endPrefixMapping(prefix: String) {
|
||||||
|
wrapped?.endPrefixMapping(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(SAXException::class) override fun characters(ch: CharArray, start: Int, length: Int) {
|
||||||
|
wrapped?.characters(ch, start, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(SAXException::class) override fun ignorableWhitespace(ch: CharArray, start: Int, length: Int) {
|
||||||
|
wrapped?.ignorableWhitespace(ch, start, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(SAXException::class) override fun processingInstruction(target: String, data: String) {
|
||||||
|
wrapped?.processingInstruction(target, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(SAXException::class) 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
app/src/main/java/com/github/libretube/util/LinkHandler.kt
Normal file
45
app/src/main/java/com/github/libretube/util/LinkHandler.kt
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package com.github.libretube.util
|
||||||
|
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.TextPaint
|
||||||
|
import android.text.style.ClickableSpan
|
||||||
|
import android.util.Log
|
||||||
|
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 {
|
||||||
|
private var linkTagStartIndex = -1
|
||||||
|
private var link: String? = null
|
||||||
|
override 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLinkSpans(output: Editable, start: Int, end: Int, link: String?) {
|
||||||
|
output.setSpan(object : ClickableSpan() {
|
||||||
|
override fun onClick(widget: View) {
|
||||||
|
if (clickCallback != null && link != null) {
|
||||||
|
clickCallback.invoke(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateDrawState(ds: TextPaint) {
|
||||||
|
super.updateDrawState(ds)
|
||||||
|
ds.isUnderlineText = false
|
||||||
|
}
|
||||||
|
}, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user