mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-28 07:50:31 +05:30
Merge pull request #2717 from faisalcodes/master
Fixed #2670 : Timestamp click behaviour in the description.
This commit is contained in:
commit
fe9bb97c96
@ -72,6 +72,7 @@
|
|||||||
android:name=".ui.activities.MainActivity"
|
android:name=".ui.activities.MainActivity"
|
||||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:launchMode="singleTask"
|
||||||
android:screenOrientation="user"
|
android:screenOrientation="user"
|
||||||
android:supportsPictureInPicture="true"
|
android:supportsPictureInPicture="true"
|
||||||
android:windowSoftInputMode="adjustPan">
|
android:windowSoftInputMode="adjustPan">
|
||||||
|
@ -189,16 +189,16 @@ class MainActivity : BaseActivity() {
|
|||||||
if (viewGroup == null || viewGroup.childCount == 0) return
|
if (viewGroup == null || viewGroup.childCount == 0) return
|
||||||
|
|
||||||
viewGroup.children.forEach {
|
viewGroup.children.forEach {
|
||||||
(it as? ScrollView)?.let {
|
(it as? ScrollView)?.let { scrollView ->
|
||||||
it.smoothScrollTo(0, 0)
|
scrollView.smoothScrollTo(0, 0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
(it as? NestedScrollView)?.let {
|
(it as? NestedScrollView)?.let { scrollView ->
|
||||||
it.smoothScrollTo(0, 0)
|
scrollView.smoothScrollTo(0, 0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
(it as? RecyclerView)?.let {
|
(it as? RecyclerView)?.let { recyclerView ->
|
||||||
it.smoothScrollToPosition(0)
|
recyclerView.smoothScrollToPosition(0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tryScrollToTop(it as? ViewGroup)
|
tryScrollToTop(it as? ViewGroup)
|
||||||
@ -373,9 +373,9 @@ class MainActivity : BaseActivity() {
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
|
||||||
isInPictureInPictureMode
|
isInPictureInPictureMode
|
||||||
) {
|
) {
|
||||||
moveTaskToBack(true)
|
val nIntent = Intent(this, MainActivity::class.java)
|
||||||
intent?.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
nIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
startActivity(intent)
|
startActivity(nIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intent?.getBooleanExtra(IntentData.openAudioPlayer, false) == true) {
|
if (intent?.getBooleanExtra(IntentData.openAudioPlayer, false) == true) {
|
||||||
|
@ -17,18 +17,21 @@ 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
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.os.ConfigurationCompat
|
import androidx.core.os.ConfigurationCompat
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
@ -75,7 +78,6 @@ import com.github.libretube.ui.dialogs.AddToPlaylistDialog
|
|||||||
import com.github.libretube.ui.dialogs.DownloadDialog
|
import com.github.libretube.ui.dialogs.DownloadDialog
|
||||||
import com.github.libretube.ui.dialogs.ShareDialog
|
import com.github.libretube.ui.dialogs.ShareDialog
|
||||||
import com.github.libretube.ui.extensions.setAspectRatio
|
import com.github.libretube.ui.extensions.setAspectRatio
|
||||||
import com.github.libretube.ui.extensions.setFormattedHtml
|
|
||||||
import com.github.libretube.ui.extensions.setupSubscriptionButton
|
import com.github.libretube.ui.extensions.setupSubscriptionButton
|
||||||
import com.github.libretube.ui.interfaces.OnlinePlayerOptions
|
import com.github.libretube.ui.interfaces.OnlinePlayerOptions
|
||||||
import com.github.libretube.ui.models.CommentsViewModel
|
import com.github.libretube.ui.models.CommentsViewModel
|
||||||
@ -86,7 +88,9 @@ import com.github.libretube.ui.sheets.PlayingQueueSheet
|
|||||||
import com.github.libretube.util.BackgroundHelper
|
import com.github.libretube.util.BackgroundHelper
|
||||||
import com.github.libretube.util.DashHelper
|
import com.github.libretube.util.DashHelper
|
||||||
import com.github.libretube.util.DataSaverMode
|
import com.github.libretube.util.DataSaverMode
|
||||||
|
import com.github.libretube.util.HtmlParser
|
||||||
import com.github.libretube.util.ImageHelper
|
import com.github.libretube.util.ImageHelper
|
||||||
|
import com.github.libretube.util.LinkHandler
|
||||||
import com.github.libretube.util.NavigationHelper
|
import com.github.libretube.util.NavigationHelper
|
||||||
import com.github.libretube.util.NowPlayingNotification
|
import com.github.libretube.util.NowPlayingNotification
|
||||||
import com.github.libretube.util.PlayerHelper
|
import com.github.libretube.util.PlayerHelper
|
||||||
@ -997,14 +1001,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)
|
||||||
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
|
||||||
@ -1038,6 +1035,53 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up the description text with video links and timestamps
|
||||||
|
*/
|
||||||
|
private fun setupDescription(
|
||||||
|
descTextView: TextView,
|
||||||
|
description: String
|
||||||
|
) {
|
||||||
|
// detect whether the description is html formatted
|
||||||
|
if (description.contains("<") && description.contains(">")) {
|
||||||
|
descTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
descTextView.text = HtmlCompat.fromHtml(
|
||||||
|
description,
|
||||||
|
HtmlCompat.FROM_HTML_MODE_LEGACY,
|
||||||
|
null,
|
||||||
|
HtmlParser(LinkHandler { link -> handleLink(link) })
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Links can be present as plain text
|
||||||
|
descTextView.autoLinkMask = Linkify.WEB_URLS
|
||||||
|
descTextView.text = description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = TextUtils.getVideoIdFromUri(link)
|
||||||
|
if (videoId.isNullOrEmpty()) {
|
||||||
|
// not a youtube video link, thus handle normally
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
// check if the video is the current video and has a valid time
|
||||||
|
if (videoId == this.videoId) {
|
||||||
|
// try finding the time stamp of the url and seek to it if found
|
||||||
|
TextUtils.getTimeInSeconds(uri)?.let {
|
||||||
|
exoPlayer.seekTo(it * 1000)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// youtube video link without time or not the current video, thus open new player
|
||||||
|
playNextVideo(videoId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the displayed duration of the video
|
* Update the displayed duration of the video
|
||||||
*/
|
*/
|
||||||
|
85
app/src/main/java/com/github/libretube/util/HtmlParser.kt
Normal file
85
app/src/main/java/com/github/libretube/util/HtmlParser.kt
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package com.github.libretube.util
|
||||||
|
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.Html
|
||||||
|
import org.xml.sax.Attributes
|
||||||
|
import org.xml.sax.ContentHandler
|
||||||
|
import org.xml.sax.Locator
|
||||||
|
import org.xml.sax.XMLReader
|
||||||
|
|
||||||
|
class HtmlParser(
|
||||||
|
private val handler: LinkHandler
|
||||||
|
) : 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
|
||||||
|
// add false to the stack to make sure we always have a tag to pop
|
||||||
|
tagStatus.addLast(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startDocument() {
|
||||||
|
wrapped?.startDocument()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun endDocument() {
|
||||||
|
wrapped?.endDocument()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startPrefixMapping(prefix: String, uri: String) {
|
||||||
|
wrapped?.startPrefixMapping(prefix, uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun endPrefixMapping(prefix: String) {
|
||||||
|
wrapped?.endPrefixMapping(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun characters(ch: CharArray, start: Int, length: Int) {
|
||||||
|
wrapped?.characters(ch, start, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ignorableWhitespace(ch: CharArray, start: Int, length: Int) {
|
||||||
|
wrapped?.ignorableWhitespace(ch, start, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun processingInstruction(target: String, data: String) {
|
||||||
|
wrapped?.processingInstruction(target, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun skippedEntity(name: String) {
|
||||||
|
wrapped?.skippedEntity(name)
|
||||||
|
}
|
||||||
|
}
|
54
app/src/main/java/com/github/libretube/util/LinkHandler.kt
Normal file
54
app/src/main/java/com/github/libretube/util/LinkHandler.kt
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package com.github.libretube.util
|
||||||
|
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.TextPaint
|
||||||
|
import android.text.style.ClickableSpan
|
||||||
|
import android.view.View
|
||||||
|
import org.xml.sax.Attributes
|
||||||
|
|
||||||
|
class LinkHandler(
|
||||||
|
private val clickCallback: ((String) -> Unit)?
|
||||||
|
) {
|
||||||
|
private var linkTagStartIndex = -1
|
||||||
|
private var link: String? = null
|
||||||
|
fun handleTag(
|
||||||
|
opening: Boolean,
|
||||||
|
tag: String?,
|
||||||
|
output: Editable?,
|
||||||
|
attributes: Attributes?
|
||||||
|
): Boolean {
|
||||||
|
// if the tag is not an anchor link, ignore for the default handler
|
||||||
|
if (output == null || tag != "a") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opening && attributes != null) {
|
||||||
|
linkTagStartIndex = output.length
|
||||||
|
link = attributes.getValue("href")
|
||||||
|
} else if (!opening && linkTagStartIndex >= 0 && link != null) {
|
||||||
|
setLinkSpans(output, linkTagStartIndex, output.length, link!!)
|
||||||
|
linkTagStartIndex = -1
|
||||||
|
link = null
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLinkSpans(output: Editable, start: Int, end: Int, link: String) {
|
||||||
|
output.setSpan(
|
||||||
|
object : ClickableSpan() {
|
||||||
|
override fun onClick(widget: View) {
|
||||||
|
clickCallback?.invoke(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateDrawState(ds: TextPaint) {
|
||||||
|
super.updateDrawState(ds)
|
||||||
|
ds.isUnderlineText = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package com.github.libretube.util
|
package com.github.libretube.util
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
@ -52,4 +53,52 @@ object TextUtils {
|
|||||||
val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale)
|
val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale)
|
||||||
return dateObj.format(formatter)
|
return dateObj.format(formatter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get time in seconds from a youtube video link
|
||||||
|
*/
|
||||||
|
fun getTimeInSeconds(uri: Uri): Long? {
|
||||||
|
var time = uri.getQueryParameter("t") ?: return -1L
|
||||||
|
|
||||||
|
var timeInSeconds: Long? = null
|
||||||
|
|
||||||
|
// Find all spans containing hours, minutes or seconds
|
||||||
|
listOf(Pair("h", 60 * 60), Pair("m", 60), Pair("s", 1)).forEach { (separator, timeFactor) ->
|
||||||
|
if (time.contains(separator)) {
|
||||||
|
time.substringBefore(separator).toLongOrNull()?.let {
|
||||||
|
timeInSeconds = (timeInSeconds ?: 0L) + it * timeFactor
|
||||||
|
time = time.substringAfter(separator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time may not contain h, m or s. In that case, it is just a number of seconds
|
||||||
|
if (timeInSeconds == null) {
|
||||||
|
time.toLongOrNull()?.let {
|
||||||
|
timeInSeconds = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeInSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get video id if the link is a valid youtube video link
|
||||||
|
*/
|
||||||
|
fun getVideoIdFromUri(link: String): String? {
|
||||||
|
val uri = Uri.parse(link)
|
||||||
|
|
||||||
|
if (link.contains("youtube.com")) {
|
||||||
|
// 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")) {
|
||||||
|
return uri.lastPathSegment
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user