Merge pull request #4979 from Bnyro/master

refactor: move description layout into its own view
This commit is contained in:
Bnyro 2023-10-14 11:29:51 +02:00 committed by GitHub
commit a59633a726
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 282 additions and 235 deletions

View File

@ -14,11 +14,9 @@ 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.util.Linkify
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.constraintlayout.motion.widget.MotionLayout import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.motion.widget.TransitionAdapter import androidx.constraintlayout.motion.widget.TransitionAdapter
@ -27,8 +25,6 @@ import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import androidx.core.text.method.LinkMovementMethodCompat
import androidx.core.text.parseAsHtml
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
@ -76,7 +72,6 @@ import com.github.libretube.extensions.seekBy
import com.github.libretube.extensions.serializableExtra import com.github.libretube.extensions.serializableExtra
import com.github.libretube.extensions.setMetadata import com.github.libretube.extensions.setMetadata
import com.github.libretube.extensions.toID import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toLocalDateSafe
import com.github.libretube.extensions.toastFromMainDispatcher import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.extensions.togglePlayPauseState import com.github.libretube.extensions.togglePlayPauseState
import com.github.libretube.extensions.updateParameters import com.github.libretube.extensions.updateParameters
@ -95,7 +90,6 @@ import com.github.libretube.obj.ShareData
import com.github.libretube.obj.VideoResolution import com.github.libretube.obj.VideoResolution
import com.github.libretube.parcelable.PlayerData import com.github.libretube.parcelable.PlayerData
import com.github.libretube.ui.activities.MainActivity import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.activities.VideoTagsAdapter
import com.github.libretube.ui.adapters.VideosAdapter import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.dialogs.AddToPlaylistDialog import com.github.libretube.ui.dialogs.AddToPlaylistDialog
import com.github.libretube.ui.dialogs.DownloadDialog import com.github.libretube.ui.dialogs.DownloadDialog
@ -110,8 +104,6 @@ import com.github.libretube.ui.sheets.ChaptersBottomSheet
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.ui.sheets.StatsSheet import com.github.libretube.ui.sheets.StatsSheet
import com.github.libretube.util.HtmlParser
import com.github.libretube.util.LinkHandler
import com.github.libretube.util.NowPlayingNotification import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.OnlineTimeFrameReceiver import com.github.libretube.util.OnlineTimeFrameReceiver
import com.github.libretube.util.PlayingQueue import com.github.libretube.util.PlayingQueue
@ -391,11 +383,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
exoPlayer.togglePlayPauseState() exoPlayer.togglePlayPauseState()
} }
// video description and chapters toggle
binding.playerTitleLayout.setOnClickListener {
if (this::streams.isInitialized) toggleDescription()
}
binding.commentsToggle.setOnClickListener { binding.commentsToggle.setOnClickListener {
// set the max height to not cover the currently playing video // set the max height to not cover the currently playing video
commentsViewModel.handleLink = this::handleLink commentsViewModel.handleLink = this::handleLink
@ -489,6 +476,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
LinearLayoutManager.HORIZONTAL, LinearLayoutManager.HORIZONTAL,
false false
) )
binding.descriptionLayout.handleLink = this::handleLink
} }
private fun updateMaxSheetHeight() { private fun updateMaxSheetHeight() {
@ -577,37 +566,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
updateResolutionOnFullscreenChange(false) updateResolutionOnFullscreenChange(false)
} }
private fun toggleDescription() {
val views = if (binding.descLinLayout.isVisible) {
// show formatted short view count
streams.views.formatShort()
} else {
// show exact view count
"%,d".format(streams.views)
}
val viewInfo = getString(R.string.normal_views, views, localizeDate(streams))
if (binding.descLinLayout.isVisible) {
// hide the description and chapters
binding.playerDescriptionArrow.animate().rotation(0F).setDuration(250).start()
binding.descLinLayout.isGone = true
// limit the title height to two lines
binding.playerTitle.maxLines = 2
} else {
// show the description and chapters
binding.playerDescriptionArrow.animate().rotation(180F).setDuration(250).start()
binding.descLinLayout.isVisible = true
// show the whole title
binding.playerTitle.maxLines = Int.MAX_VALUE
}
binding.playerViewsInfo.text = viewInfo
if (viewModel.chapters.isNotEmpty()) {
setCurrentChapterName(forceUpdate = true, enqueueNew = false)
}
}
override fun onPause() { override fun onPause() {
// check whether the screen is on // check whether the screen is on
val isInteractive = requireContext().getSystemService<PowerManager>()!!.isInteractive val isInteractive = requireContext().getSystemService<PowerManager>()!!.isInteractive
@ -766,7 +724,14 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
// set media sources for the player // set media sources for the player
initStreamSources() initStreamSources()
prepareExoPlayerView()
binding.player.apply {
useController = false
player = exoPlayer
}
playerBinding.exoProgress.setPlayer(exoPlayer)
initializePlayerView() initializePlayerView()
setupSeekbarPreview() setupSeekbarPreview()
@ -862,59 +827,21 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
chaptersBottomSheet = null chaptersBottomSheet = null
} }
private fun prepareExoPlayerView() {
binding.player.apply {
useController = false
player = exoPlayer
}
playerBinding.exoProgress.setPlayer(exoPlayer)
}
private fun localizeDate(streams: Streams): String {
if (streams.livestream) return ""
return TextUtils.SEPARATOR + TextUtils.localizeDate(streams.uploadDate.toLocalDateSafe())
}
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun initializePlayerView() { private fun initializePlayerView() {
// initialize the player view actions // initialize the player view actions
binding.player.initialize(doubleTapOverlayBinding, playerGestureControlsViewBinding) binding.player.initialize(doubleTapOverlayBinding, playerGestureControlsViewBinding)
binding.player.initPlayerOptions(viewModel, viewLifecycleOwner, trackSelector, this) binding.player.initPlayerOptions(viewModel, viewLifecycleOwner, trackSelector, this)
binding.descriptionLayout.setStreams(streams)
binding.apply { binding.apply {
val views = streams.views.formatShort()
playerViewsInfo.text = getString(R.string.normal_views, views, localizeDate(streams))
textLike.text = streams.likes.formatShort()
textDislike.isVisible = streams.dislikes >= 0
textDislike.text = streams.dislikes.formatShort()
ImageHelper.loadImage(streams.uploaderAvatar, binding.playerChannelImage) ImageHelper.loadImage(streams.uploaderAvatar, binding.playerChannelImage)
playerChannelName.text = streams.uploader playerChannelName.text = streams.uploader
titleTextView.text = streams.title titleTextView.text = streams.title
playerTitle.text = streams.title
playerDescription.text = streams.description
metaInfo.isVisible = streams.metaInfo.isNotEmpty()
// generate a meta info text with clickable links using html
val metaInfoText = streams.metaInfo.joinToString("\n\n") { info ->
val text = info.description.takeIf { it.isNotBlank() } ?: info.title
val links = info.urls.mapIndexed { index, url ->
"<a href=\"$url\">${info.urlTexts.getOrNull(index).orEmpty()}</a>"
}.joinToString(", ")
"$text $links"
}
metaInfo.text = metaInfoText.parseAsHtml()
playerChannelSubCount.text = context?.getString( playerChannelSubCount.text = context?.getString(
R.string.subscribers, R.string.subscribers,
streams.uploaderSubscriberCount.formatShort() streams.uploaderSubscriberCount.formatShort()
) )
player.isLive = streams.livestream player.isLive = streams.livestream
} }
playerBinding.exoTitle.text = streams.title playerBinding.exoTitle.text = streams.title
@ -1034,29 +961,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
PictureInPictureCompat.enterPictureInPictureMode(requireActivity(), pipParams) PictureInPictureCompat.enterPictureInPictureMode(requireActivity(), pipParams)
} }
initializeRelatedVideos(streams.relatedStreams.filter { !it.title.isNullOrBlank() }) initializeRelatedVideos(streams.relatedStreams.filter { !it.title.isNullOrBlank() })
// set video description
val description = streams.description
setupDescription(binding.playerDescription, description)
val visibility = when (streams.visibility) {
"public" -> context?.getString(R.string.visibility_public)
"unlisted" -> context?.getString(R.string.visibility_unlisted)
// currently no other visibility could be returned, might change in the future however
else -> streams.visibility.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
}
}.orEmpty()
binding.additionalVideoInfo.text =
"${context?.getString(R.string.category)}: ${streams.category}\n" +
"${context?.getString(R.string.license)}: ${streams.license}\n" +
"${context?.getString(R.string.visibility)}: $visibility"
if (streams.tags.isNotEmpty()) {
binding.tagsRecycler.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
binding.tagsRecycler.adapter = VideoTagsAdapter(streams.tags)
}
binding.tagsRecycler.isVisible = streams.tags.isNotEmpty()
binding.playerChannel.setOnClickListener { binding.playerChannel.setOnClickListener {
val activity = view?.context as MainActivity val activity = view?.context as MainActivity
@ -1108,22 +1012,6 @@ class PlayerFragment : Fragment(), 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 = LinkMovementMethodCompat.getInstance()
descTextView.text = description.replace("</a>", "</a> ")
.parseAsHtml(tagHandler = HtmlParser(LinkHandler(this::handleLink)))
} else {
// Links can be present as plain text
descTextView.autoLinkMask = Linkify.WEB_URLS
descTextView.text = description
}
}
/** /**
* Handle a link clicked in the description * Handle a link clicked in the description
*/ */

View File

@ -0,0 +1,144 @@
package com.github.libretube.ui.views
import android.annotation.SuppressLint
import android.content.Context
import android.text.util.Linkify
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.core.text.method.LinkMovementMethodCompat
import androidx.core.text.parseAsHtml
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.api.obj.Streams
import com.github.libretube.databinding.DescriptionLayoutBinding
import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.toLocalDateSafe
import com.github.libretube.ui.activities.VideoTagsAdapter
import com.github.libretube.util.HtmlParser
import com.github.libretube.util.LinkHandler
import com.github.libretube.util.TextUtils
import java.util.Locale
class DescriptionLayout(
context: Context,
attributeSet: AttributeSet?
): LinearLayout(context, attributeSet) {
val binding = DescriptionLayoutBinding.inflate(LayoutInflater.from(context), this, true)
private var streams: Streams? = null
var handleLink: (link: String) -> Unit = {}
init {
binding.playerTitleLayout.setOnClickListener {
toggleDescription()
}
}
@SuppressLint("SetTextI18n")
fun setStreams(streams: Streams) {
this.streams = streams
val views = streams.views.formatShort()
binding.run {
playerViewsInfo.text = context.getString(R.string.normal_views, views, localizeDate(streams))
textLike.text = streams.likes.formatShort()
textDislike.isVisible = streams.dislikes >= 0
textDislike.text = streams.dislikes.formatShort()
playerTitle.text = streams.title
playerDescription.text = streams.description
metaInfo.isVisible = streams.metaInfo.isNotEmpty()
// generate a meta info text with clickable links using html
val metaInfoText = streams.metaInfo.joinToString("\n\n") { info ->
val text = info.description.takeIf { it.isNotBlank() } ?: info.title
val links = info.urls.mapIndexed { index, url ->
"<a href=\"$url\">${info.urlTexts.getOrNull(index).orEmpty()}</a>"
}.joinToString(", ")
"$text $links"
}
metaInfo.text = metaInfoText.parseAsHtml()
val visibility = when (streams.visibility) {
"public" -> context?.getString(R.string.visibility_public)
"unlisted" -> context?.getString(R.string.visibility_unlisted)
// currently no other visibility could be returned, might change in the future however
else -> streams.visibility.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
}
}.orEmpty()
additionalVideoInfo.text =
"${context?.getString(R.string.category)}: ${streams.category}\n" +
"${context?.getString(R.string.license)}: ${streams.license}\n" +
"${context?.getString(R.string.visibility)}: $visibility"
if (streams.tags.isNotEmpty()) {
binding.tagsRecycler.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
binding.tagsRecycler.adapter = VideoTagsAdapter(streams.tags)
}
binding.tagsRecycler.isVisible = streams.tags.isNotEmpty()
setupDescription(streams.description)
}
}
/**
* Set up the description text with video links and timestamps
*/
private fun setupDescription(description: String) {
val descTextView = binding.playerDescription
// detect whether the description is html formatted
if (description.contains("<") && description.contains(">")) {
descTextView.movementMethod = LinkMovementMethodCompat.getInstance()
descTextView.text = description.replace("</a>", "</a> ")
.parseAsHtml(tagHandler = HtmlParser(LinkHandler(handleLink)))
} else {
// Links can be present as plain text
descTextView.autoLinkMask = Linkify.WEB_URLS
descTextView.text = description
}
}
private fun toggleDescription() {
val streams = streams ?: return
val views = if (binding.descLinLayout.isVisible) {
// show formatted short view count
streams.views.formatShort()
} else {
// show exact view count
"%,d".format(streams.views)
}
val viewInfo = context.getString(R.string.normal_views, views, localizeDate(streams))
if (binding.descLinLayout.isVisible) {
// hide the description and chapters
binding.playerDescriptionArrow.animate().rotation(0F).setDuration(ANIMATION_DURATION).start()
binding.descLinLayout.isGone = true
// limit the title height to two lines
binding.playerTitle.maxLines = 2
} else {
// show the description and chapters
binding.playerDescriptionArrow.animate().rotation(180F).setDuration(ANIMATION_DURATION).start()
binding.descLinLayout.isVisible = true
// show the whole title
binding.playerTitle.maxLines = Int.MAX_VALUE
}
binding.playerViewsInfo.text = viewInfo
}
private fun localizeDate(streams: Streams): String {
if (streams.livestream) return ""
return TextUtils.SEPARATOR + TextUtils.localizeDate(streams.uploadDate.toLocalDateSafe())
}
companion object {
private const val ANIMATION_DURATION = 250L
}
}

View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="vertical">
<LinearLayout
android:id="@+id/player_title_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginHorizontal="4dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:orientation="horizontal">
<TextView
android:id="@+id/player_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:layout_marginEnd="5dp"
android:layout_weight="1"
android:maxLines="2"
android:textSize="18sp"
tools:text="Video Title" />
<ImageView
android:id="@+id/player_description_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="10dp"
android:src="@drawable/ic_arrow_down" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:orientation="horizontal">
<TextView
android:id="@+id/player_views_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:layout_weight="1"
tools:text="10M views 2 days ago " />
<com.github.libretube.ui.views.DrawableTextView
android:id="@+id/textLike"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="5dp"
android:drawablePadding="5dp"
app:drawableStartCompat="@drawable/ic_like"
app:drawableStartDimen="12dp"
tools:text="4.2K" />
<com.github.libretube.ui.views.DrawableTextView
android:id="@+id/textDislike"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="5dp"
android:drawablePadding="5dp"
android:visibility="gone"
app:drawableStartCompat="@drawable/ic_dislike"
app:drawableStartDimen="12dp"
tools:text="1.3K" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/desc_linLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="5dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/meta_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:textSize="14sp"
android:visibility="gone" />
<TextView
android:id="@+id/player_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:textIsSelectable="true"
android:textSize="14sp" />
<TextView
android:id="@+id/additional_video_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:textIsSelectable="true"
android:textSize="14sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tags_recycler"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>

View File

@ -22,122 +22,13 @@
android:id="@+id/linLayout" android:id="@+id/linLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <com.github.libretube.ui.views.DescriptionLayout
android:id="@+id/player_title_layout" android:id="@+id/descriptionLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:animateLayoutChanges="true"/>
android:layout_marginHorizontal="4dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:orientation="horizontal">
<TextView
android:id="@+id/player_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:layout_marginEnd="5dp"
android:layout_weight="1"
android:maxLines="2"
android:textSize="18sp"
tools:text="Video Title" />
<ImageView
android:id="@+id/player_description_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="10dp"
android:src="@drawable/ic_arrow_down" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:orientation="horizontal">
<TextView
android:id="@+id/player_views_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:layout_weight="1"
tools:text="10M views 2 days ago " />
<com.github.libretube.ui.views.DrawableTextView
android:id="@+id/textLike"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="5dp"
android:drawablePadding="5dp"
app:drawableStartCompat="@drawable/ic_like"
app:drawableStartDimen="12dp"
tools:text="4.2K" />
<com.github.libretube.ui.views.DrawableTextView
android:id="@+id/textDislike"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="5dp"
android:drawablePadding="5dp"
app:drawableStartCompat="@drawable/ic_dislike"
app:drawableStartDimen="12dp"
android:visibility="gone"
tools:text="1.3K" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/desc_linLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="5dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/meta_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:textSize="14sp"
android:visibility="gone" />
<TextView
android:id="@+id/player_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:textIsSelectable="true"
android:textSize="14sp" />
<TextView
android:id="@+id/additional_video_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:textIsSelectable="true"
android:textSize="14sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tags_recycler"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
style="@style/Widget.Material3.CardView.Elevated" style="@style/Widget.Material3.CardView.Elevated"