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 3166432f2..c15e88441 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 @@ -14,11 +14,9 @@ import android.os.Handler import android.os.Looper import android.os.PowerManager import android.text.format.DateUtils -import android.text.util.Linkify import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView import android.widget.Toast import androidx.constraintlayout.motion.widget.MotionLayout import androidx.constraintlayout.motion.widget.TransitionAdapter @@ -27,8 +25,6 @@ import androidx.core.graphics.drawable.toDrawable import androidx.core.net.toUri import androidx.core.os.bundleOf 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.isGone 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.setMetadata import com.github.libretube.extensions.toID -import com.github.libretube.extensions.toLocalDateSafe import com.github.libretube.extensions.toastFromMainDispatcher import com.github.libretube.extensions.togglePlayPauseState 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.parcelable.PlayerData 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.dialogs.AddToPlaylistDialog 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.PlayingQueueSheet 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.OnlineTimeFrameReceiver import com.github.libretube.util.PlayingQueue @@ -391,11 +383,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { exoPlayer.togglePlayPauseState() } - // video description and chapters toggle - binding.playerTitleLayout.setOnClickListener { - if (this::streams.isInitialized) toggleDescription() - } - binding.commentsToggle.setOnClickListener { // set the max height to not cover the currently playing video commentsViewModel.handleLink = this::handleLink @@ -489,6 +476,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { LinearLayoutManager.HORIZONTAL, false ) + + binding.descriptionLayout.handleLink = this::handleLink } private fun updateMaxSheetHeight() { @@ -577,37 +566,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { 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() { // check whether the screen is on val isInteractive = requireContext().getSystemService()!!.isInteractive @@ -766,7 +724,14 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { // set media sources for the player initStreamSources() - prepareExoPlayerView() + + binding.player.apply { + useController = false + player = exoPlayer + } + + playerBinding.exoProgress.setPlayer(exoPlayer) + initializePlayerView() setupSeekbarPreview() @@ -862,59 +827,21 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { 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") private fun initializePlayerView() { // initialize the player view actions binding.player.initialize(doubleTapOverlayBinding, playerGestureControlsViewBinding) binding.player.initPlayerOptions(viewModel, viewLifecycleOwner, trackSelector, this) + binding.descriptionLayout.setStreams(streams) 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) playerChannelName.text = streams.uploader - 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 -> - "${info.urlTexts.getOrNull(index).orEmpty()}" - }.joinToString(", ") - "$text $links" - } - metaInfo.text = metaInfoText.parseAsHtml() - playerChannelSubCount.text = context?.getString( R.string.subscribers, streams.uploaderSubscriberCount.formatShort() ) - player.isLive = streams.livestream } playerBinding.exoTitle.text = streams.title @@ -1034,29 +961,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { PictureInPictureCompat.enterPictureInPictureMode(requireActivity(), pipParams) } 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 { 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("", " ") - .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 */ diff --git a/app/src/main/java/com/github/libretube/ui/views/DescriptionLayout.kt b/app/src/main/java/com/github/libretube/ui/views/DescriptionLayout.kt new file mode 100644 index 000000000..9df407cdc --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/views/DescriptionLayout.kt @@ -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 -> + "${info.urlTexts.getOrNull(index).orEmpty()}" + }.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("", " ") + .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 + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/description_layout.xml b/app/src/main/res/layout/description_layout.xml new file mode 100644 index 000000000..9a86d8c51 --- /dev/null +++ b/app/src/main/res/layout/description_layout.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_player.xml b/app/src/main/res/layout/fragment_player.xml index c8bccd206..346c770d0 100644 --- a/app/src/main/res/layout/fragment_player.xml +++ b/app/src/main/res/layout/fragment_player.xml @@ -22,122 +22,13 @@ android:id="@+id/linLayout" android:layout_width="match_parent" android:layout_height="wrap_content" - android:animateLayoutChanges="true" android:orientation="vertical"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:animateLayoutChanges="true"/>