This commit is contained in:
faisalcodes 2023-01-16 21:36:23 +05:30
parent 620e985dd2
commit 0ee5753e91
3 changed files with 267 additions and 23 deletions

View File

@ -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
*/ */

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

View 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)
}
}