From 0ee5753e91d1bc819b9844945646b4e01c3eca80 Mon Sep 17 00:00:00 2001 From: faisalcodes Date: Mon, 16 Jan 2023 21:36:23 +0530 Subject: [PATCH 1/9] Fixed #2670 --- .../libretube/ui/fragments/PlayerFragment.kt | 141 +++++++++++++++--- .../com/github/libretube/util/HtmlParser.kt | 104 +++++++++++++ .../com/github/libretube/util/LinkHandler.kt | 45 ++++++ 3 files changed, 267 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/com/github/libretube/util/HtmlParser.kt create mode 100644 app/src/main/java/com/github/libretube/util/LinkHandler.kt 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 2845a5dc8..76c26a25b 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 @@ -17,12 +17,14 @@ import android.os.Handler import android.os.Looper import android.os.PowerManager import android.text.format.DateUtils +import android.text.method.LinkMovementMethod import android.text.util.Linkify import android.util.Base64 import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.TextView import android.widget.Toast import androidx.annotation.RequiresApi 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.CommentsSheet import com.github.libretube.ui.sheets.PlayingQueueSheet -import com.github.libretube.util.BackgroundHelper -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.github.libretube.util.* import com.google.android.exoplayer2.C import com.google.android.exoplayer2.DefaultLoadControl import com.google.android.exoplayer2.ExoPlayer @@ -859,7 +851,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { binding.apply { playerViewsInfo.text = 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() textDislike.text = streams.dislikes.formatShort() @@ -926,9 +918,9 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { override fun onPlaybackStateChanged(playbackState: Int) { exoPlayerView.keepScreenOn = !( - playbackState == Player.STATE_IDLE || - playbackState == Player.STATE_ENDED - ) + playbackState == Player.STATE_IDLE || + playbackState == Player.STATE_ENDED + ) // save the watch position to the database // 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 val description = streams.description!! - // detect whether the description is html formatted - 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 - } + setupDescription(binding.playerDescription, description, exoPlayer) binding.playerChannel.setOnClickListener { 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 */ diff --git a/app/src/main/java/com/github/libretube/util/HtmlParser.kt b/app/src/main/java/com/github/libretube/util/HtmlParser.kt new file mode 100644 index 000000000..2a0d0d9a9 --- /dev/null +++ b/app/src/main/java/com/github/libretube/util/HtmlParser.kt @@ -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() + 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 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/util/LinkHandler.kt b/app/src/main/java/com/github/libretube/util/LinkHandler.kt new file mode 100644 index 000000000..ec5939aea --- /dev/null +++ b/app/src/main/java/com/github/libretube/util/LinkHandler.kt @@ -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) + } +} \ No newline at end of file From 6d322bf9f77bc6e7a9ec37ad4dfa83ace9595065 Mon Sep 17 00:00:00 2001 From: faisalcodes Date: Tue, 17 Jan 2023 00:20:33 +0530 Subject: [PATCH 2/9] Fixed #2670 : Timestamp click behaviour in the description. --- .../libretube/ui/fragments/PlayerFragment.kt | 30 ++++++--- .../com/github/libretube/util/HtmlParser.kt | 62 ++++++++++++++----- .../com/github/libretube/util/LinkHandler.kt | 35 +++++++---- 3 files changed, 90 insertions(+), 37 deletions(-) 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 76c26a25b..62609636f 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 @@ -76,7 +76,6 @@ import com.github.libretube.ui.dialogs.AddToPlaylistDialog import com.github.libretube.ui.dialogs.DownloadDialog import com.github.libretube.ui.dialogs.ShareDialog 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.interfaces.OnlinePlayerOptions import com.github.libretube.ui.models.CommentsViewModel @@ -84,7 +83,19 @@ import com.github.libretube.ui.models.PlayerViewModel import com.github.libretube.ui.sheets.BaseBottomSheet import com.github.libretube.ui.sheets.CommentsSheet import com.github.libretube.ui.sheets.PlayingQueueSheet -import com.github.libretube.util.* +import com.github.libretube.util.BackgroundHelper +import com.github.libretube.util.DashHelper +import com.github.libretube.util.DataSaverMode +import com.github.libretube.util.HtmlParser +import com.github.libretube.util.ImageHelper +import com.github.libretube.util.LinkHandler +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.DefaultLoadControl import com.google.android.exoplayer2.ExoPlayer @@ -851,7 +862,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { binding.apply { playerViewsInfo.text = 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() textDislike.text = streams.dislikes.formatShort() @@ -918,9 +929,9 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { override fun onPlaybackStateChanged(playbackState: Int) { exoPlayerView.keepScreenOn = !( - playbackState == Player.STATE_IDLE || - playbackState == Player.STATE_ENDED - ) + playbackState == Player.STATE_IDLE || + playbackState == Player.STATE_ENDED + ) // save the watch position to the database // only called when the position is unequal to 0, otherwise it would become reset @@ -1026,7 +1037,11 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { } } - private fun setupDescription(descTextView: TextView, description: String, exoPlayer: ExoPlayer) { + 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() @@ -1065,7 +1080,6 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link)) startActivity(intent) } - } else { // not a youtube link // handle normally 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 2a0d0d9a9..d82341949 100644 --- a/app/src/main/java/com/github/libretube/util/HtmlParser.kt +++ b/app/src/main/java/com/github/libretube/util/HtmlParser.kt @@ -4,10 +4,15 @@ import android.text.Editable import android.text.Html import android.text.Spanned import androidx.core.text.HtmlCompat -import org.xml.sax.* -import java.util.* +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 private constructor(private val handler: TagHandler) : + Html.TagHandler, + ContentHandler { private val tagStatus = ArrayDeque() private var wrapped: ContentHandler? = null private var text: Editable? = null @@ -27,7 +32,13 @@ class HtmlParser private constructor(private val handler: TagHandler) : Html.Tag } } - @Throws(SAXException::class) override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) { + @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) { @@ -35,7 +46,8 @@ class HtmlParser private constructor(private val handler: TagHandler) : Html.Tag } } - @Throws(SAXException::class) override fun endElement(uri: String, localName: String, qName: String) { + @Throws(SAXException::class) + override fun endElement(uri: String, localName: String, qName: String) { if (!tagStatus.removeLast()) { wrapped?.endElement(uri, localName, qName) } @@ -46,47 +58,65 @@ class HtmlParser private constructor(private val handler: TagHandler) : Html.Tag wrapped?.setDocumentLocator(locator) } - @Throws(SAXException::class) override fun startDocument() { + @Throws(SAXException::class) + override fun startDocument() { wrapped?.startDocument() } - @Throws(SAXException::class) override fun endDocument() { + @Throws(SAXException::class) + override fun endDocument() { wrapped?.endDocument() } - @Throws(SAXException::class) override fun startPrefixMapping(prefix: String, uri: String) { + @Throws(SAXException::class) + override fun startPrefixMapping(prefix: String, uri: String) { wrapped?.startPrefixMapping(prefix, uri) } - @Throws(SAXException::class) override fun endPrefixMapping(prefix: String) { + @Throws(SAXException::class) + override fun endPrefixMapping(prefix: String) { wrapped?.endPrefixMapping(prefix) } - @Throws(SAXException::class) override fun characters(ch: CharArray, start: Int, length: Int) { + @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) { + @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) { + @Throws(SAXException::class) + override fun processingInstruction(target: String, data: String) { wrapped?.processingInstruction(target, data) } - @Throws(SAXException::class) override fun skippedEntity(name: String) { + @Throws(SAXException::class) + override fun skippedEntity(name: String) { wrapped?.skippedEntity(name) } interface TagHandler { - fun handleTag(opening: Boolean, tag: String?, output: Editable?, attributes: Attributes?): Boolean + 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)) + return HtmlCompat.fromHtml( + html, + HtmlCompat.FROM_HTML_MODE_LEGACY, + null, + HtmlParser(handler) + ) } @JvmStatic fun getValue(attributes: Attributes, name: String): String? { @@ -101,4 +131,4 @@ class HtmlParser private constructor(private val handler: TagHandler) : Html.Tag return null } } -} \ No newline at end of file +} 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 ec5939aea..510b8c650 100644 --- a/app/src/main/java/com/github/libretube/util/LinkHandler.kt +++ b/app/src/main/java/com/github/libretube/util/LinkHandler.kt @@ -4,7 +4,6 @@ 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 @@ -12,7 +11,12 @@ 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 { + override fun handleTag( + opening: Boolean, + tag: String?, + output: Editable?, + attributes: Attributes? + ): Boolean { if (output != null) { if ("a" == tag) { if (opening && attributes != null) { @@ -29,17 +33,22 @@ class LinkHandler(private val clickCallback: ((String) -> Unit)?) : HtmlParser.T } 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) + 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) + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + ds.isUnderlineText = false + } + }, + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } -} \ No newline at end of file +} From 700669348575829d3b91d9c34d9d5cf874d3da73 Mon Sep 17 00:00:00 2001 From: faisalcodes Date: Wed, 18 Jan 2023 21:43:22 +0530 Subject: [PATCH 3/9] Fixed #2670 : Timestamp click behaviour in the description. --- .../libretube/ui/fragments/PlayerFragment.kt | 158 +++++++++--------- .../com/github/libretube/util/HtmlParser.kt | 48 +----- .../com/github/libretube/util/LinkHandler.kt | 31 ++-- 3 files changed, 101 insertions(+), 136 deletions(-) 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?) { From c98217f963c29ad8db89d0a945fff85dc07a2a8e Mon Sep 17 00:00:00 2001 From: faisalcodes Date: Wed, 18 Jan 2023 22:02:08 +0530 Subject: [PATCH 4/9] Fixed #2670 : Timestamp click behaviour in the description. --- .../com/github/libretube/util/LinkHandler.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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 5d1ff6278..5ad5dda92 100644 --- a/app/src/main/java/com/github/libretube/util/LinkHandler.kt +++ b/app/src/main/java/com/github/libretube/util/LinkHandler.kt @@ -14,7 +14,7 @@ class LinkHandler(private val clickCallback: ((String) -> Unit)?) { opening: Boolean, tag: String?, output: Editable?, - attributes: Attributes? + attributes: Attributes?, ): Boolean { // if the tag is not an anchor link, ignore for the default handler if (output == null || "a" != tag) { @@ -27,19 +27,21 @@ class LinkHandler(private val clickCallback: ((String) -> Unit)?) { link = attributes.getValue("href") } } else { - val refTagEndIndex = output.length - setLinkSpans(output, linkTagStartIndex, refTagEndIndex, link) + if (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?) { + 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) - } + clickCallback?.invoke(link) } override fun updateDrawState(ds: TextPaint) { From 9a926238c7527ca3a8f56ffeb6d7ae4faddc38dd Mon Sep 17 00:00:00 2001 From: faisalcodes Date: Wed, 18 Jan 2023 22:26:34 +0530 Subject: [PATCH 5/9] Fixed #2670 : Timestamp click behaviour in the description. --- app/src/main/java/com/github/libretube/util/LinkHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5ad5dda92..eccf5cb2a 100644 --- a/app/src/main/java/com/github/libretube/util/LinkHandler.kt +++ b/app/src/main/java/com/github/libretube/util/LinkHandler.kt @@ -14,7 +14,7 @@ class LinkHandler(private val clickCallback: ((String) -> Unit)?) { opening: Boolean, tag: String?, output: Editable?, - attributes: Attributes?, + attributes: Attributes? ): Boolean { // if the tag is not an anchor link, ignore for the default handler if (output == null || "a" != tag) { From 015b4145969c8d322687721517fc5f0949ddde5b Mon Sep 17 00:00:00 2001 From: faisalcodes Date: Thu, 19 Jan 2023 20:57:16 +0530 Subject: [PATCH 6/9] Fixed #2723 : Activity is recreated when entered from media notification. --- app/src/main/AndroidManifest.xml | 1 + .../libretube/ui/activities/MainActivity.kt | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0843a522c..6cacd921e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -72,6 +72,7 @@ android:name=".ui.activities.MainActivity" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" android:exported="true" + android:launchMode="singleTask" android:screenOrientation="user" android:supportsPictureInPicture="true" android:windowSoftInputMode="adjustPan"> diff --git a/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt index cbd3d7239..87b79ad4c 100644 --- a/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt +++ b/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt @@ -189,16 +189,16 @@ class MainActivity : BaseActivity() { if (viewGroup == null || viewGroup.childCount == 0) return viewGroup.children.forEach { - (it as? ScrollView)?.let { - it.smoothScrollTo(0, 0) + (it as? ScrollView)?.let { scrollView -> + scrollView.smoothScrollTo(0, 0) return } - (it as? NestedScrollView)?.let { - it.smoothScrollTo(0, 0) + (it as? NestedScrollView)?.let { scrollView -> + scrollView.smoothScrollTo(0, 0) return } - (it as? RecyclerView)?.let { - it.smoothScrollToPosition(0) + (it as? RecyclerView)?.let { recyclerView -> + recyclerView.smoothScrollToPosition(0) return } tryScrollToTop(it as? ViewGroup) @@ -373,9 +373,9 @@ class MainActivity : BaseActivity() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode ) { - moveTaskToBack(true) - intent?.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) - startActivity(intent) + val nIntent = Intent(this, MainActivity::class.java) + nIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(nIntent) } if (intent?.getBooleanExtra(IntentData.openAudioPlayer, false) == true) { From a5a38a3f661a48868614b108f3bf42b4a767613b Mon Sep 17 00:00:00 2001 From: Bnyro Date: Thu, 19 Jan 2023 17:08:57 +0100 Subject: [PATCH 7/9] Cleanup the logic of handling description links --- .../libretube/ui/fragments/PlayerFragment.kt | 101 +++--------------- .../com/github/libretube/util/TextUtils.kt | 49 +++++++++ 2 files changed, 65 insertions(+), 85 deletions(-) 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 13c3ebe63..13bac02c2 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 @@ -113,15 +113,15 @@ import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.upstream.DefaultDataSource import com.google.android.exoplayer2.util.MimeTypes import com.google.android.material.dialog.MaterialAlertDialogBuilder -import java.io.IOException -import java.util.* -import java.util.concurrent.Executors -import kotlin.math.abs import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.chromium.net.CronetEngine import retrofit2.HttpException +import java.io.IOException +import java.util.* +import java.util.concurrent.Executors +import kotlin.math.abs class PlayerFragment : BaseFragment(), OnlinePlayerOptions { @@ -1066,91 +1066,22 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { 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)) + 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) } - } - - /** - * 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")) { - // the link may be in an unsupported format, so we should try/catch it - return try { - uri.getQueryParameter("v") - } catch (e: Exception) { - null + // 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) } - } else if (link.contains("youtu.be")) { - return uri.lastPathSegment + } else { + // youtube video link without time or not the current video, thus open new player + playNextVideo(videoId) } - - return null - } - - /** - * 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 has hours - if (time.contains("h")) { - 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) { - time.toLongOrNull()?.let { - timeInSeconds += it - found = true - } - } - - return if (found) timeInSeconds * 1000 else -1 } /** diff --git a/app/src/main/java/com/github/libretube/util/TextUtils.kt b/app/src/main/java/com/github/libretube/util/TextUtils.kt index 6d100523d..201a2957a 100644 --- a/app/src/main/java/com/github/libretube/util/TextUtils.kt +++ b/app/src/main/java/com/github/libretube/util/TextUtils.kt @@ -1,5 +1,6 @@ package com.github.libretube.util +import android.net.Uri import java.net.URL import java.time.LocalDate import java.time.format.DateTimeFormatter @@ -52,4 +53,52 @@ object TextUtils { val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale) 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 + } } From 8ba28247501bc09f53a01c0f4cf580d42333d19e Mon Sep 17 00:00:00 2001 From: Bnyro Date: Thu, 19 Jan 2023 17:21:06 +0100 Subject: [PATCH 8/9] Simplify `LinkHandler` and `HTMLParser` --- .../libretube/ui/fragments/PlayerFragment.kt | 2 +- .../com/github/libretube/util/HtmlParser.kt | 15 +++-------- .../com/github/libretube/util/LinkHandler.kt | 25 ++++++++----------- 3 files changed, 15 insertions(+), 27 deletions(-) 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 13bac02c2..0261c4df2 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 @@ -1076,7 +1076,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { 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) + exoPlayer.seekTo(it * 1000) } } else { // youtube video link without time or not the current video, thus open new player 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 47c1db25d..8fb69ea4b 100644 --- a/app/src/main/java/com/github/libretube/util/HtmlParser.kt +++ b/app/src/main/java/com/github/libretube/util/HtmlParser.kt @@ -5,10 +5,11 @@ import android.text.Html 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 constructor(private val handler: LinkHandler) : Html.TagHandler, ContentHandler { +class HtmlParser( + private val handler: LinkHandler +) : Html.TagHandler, ContentHandler { private val tagStatus = ArrayDeque() private var wrapped: ContentHandler? = null private var text: Editable? = null @@ -25,7 +26,6 @@ class HtmlParser constructor(private val handler: LinkHandler) : Html.TagHandler } } - @Throws(SAXException::class) override fun startElement( uri: String, localName: String, @@ -40,7 +40,6 @@ class HtmlParser constructor(private val handler: LinkHandler) : Html.TagHandler } } - @Throws(SAXException::class) override fun endElement(uri: String, localName: String, qName: String) { if (!tagStatus.removeLast()) { wrapped?.endElement(uri, localName, qName) @@ -52,42 +51,34 @@ class HtmlParser constructor(private val handler: LinkHandler) : Html.TagHandler 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) } 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 eccf5cb2a..ebe2af6d8 100644 --- a/app/src/main/java/com/github/libretube/util/LinkHandler.kt +++ b/app/src/main/java/com/github/libretube/util/LinkHandler.kt @@ -7,7 +7,9 @@ import android.text.style.ClickableSpan import android.view.View import org.xml.sax.Attributes -class LinkHandler(private val clickCallback: ((String) -> Unit)?) { +class LinkHandler( + private val clickCallback: ((String) -> Unit)? +) { private var linkTagStartIndex = -1 private var link: String? = null fun handleTag( @@ -17,22 +19,17 @@ class LinkHandler(private val clickCallback: ((String) -> Unit)?) { attributes: Attributes? ): Boolean { // if the tag is not an anchor link, ignore for the default handler - if (output == null || "a" != tag) { + if (output == null || tag != "a") { return false } - if (opening) { - if (attributes != null) { - linkTagStartIndex = output.length - link = attributes.getValue("href") - } - } else { - if (linkTagStartIndex >= 0 && link != null) { - setLinkSpans(output, linkTagStartIndex, output.length, link!!) - - linkTagStartIndex = -1 - link = null - } + 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 } From 4ba274b612cdf7d7a96d4bd14ee48eeee4f770cc Mon Sep 17 00:00:00 2001 From: Bnyro Date: Thu, 19 Jan 2023 17:21:40 +0100 Subject: [PATCH 9/9] Run `ktlint --android -F` --- .../com/github/libretube/ui/fragments/PlayerFragment.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 0261c4df2..d152f093b 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 @@ -113,15 +113,15 @@ import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.upstream.DefaultDataSource import com.google.android.exoplayer2.util.MimeTypes import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.io.IOException +import java.util.* +import java.util.concurrent.Executors +import kotlin.math.abs import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.chromium.net.CronetEngine import retrofit2.HttpException -import java.io.IOException -import java.util.* -import java.util.concurrent.Executors -import kotlin.math.abs class PlayerFragment : BaseFragment(), OnlinePlayerOptions {