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 com.github.libretube.R import com.github.libretube.api.SponsorBlockLabelHelper import com.github.libretube.api.obj.Segment import com.github.libretube.api.obj.Streams import com.github.libretube.databinding.DescriptionLayoutBinding import com.github.libretube.enums.SbSkipOptions import com.github.libretube.extensions.formatShort import com.github.libretube.helpers.ClipboardHelper import com.github.libretube.helpers.PlayerHelper 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 = {} private val videoTagsAdapter = VideoTagsAdapter() init { binding.playerTitleLayout.setOnClickListener { toggleDescription() } binding.playerTitleLayout.setOnLongClickListener { streams?.title?.let { ClipboardHelper.save(context, text = it) } true } binding.tagsRecycler.adapter = videoTagsAdapter } fun setSegments(segments: List) { if (PlayerHelper.getSponsorBlockCategories()[SB_SPONSOR_CATEGORY] == SbSkipOptions.OFF) { // only show the badge if the user requested to show sponsors return } val category = segments.filter { it.actionType == Segment.TYPE_FULL }.firstNotNullOfOrNull { it.category } binding.playerSponsorBadge.isVisible = category != null binding.playerSponsorBadge.chipIcon = SponsorBlockLabelHelper.categoryIcon(category)?.let { context.getDrawable(it) } binding.playerSponsorBadge.text = SponsorBlockLabelHelper.categoryLabel(category)?.let { context.getString(it) } } @SuppressLint("SetTextI18n") fun setStreams(streams: Streams) { this.streams = streams val views = streams.views.formatShort() val date = TextUtils.formatRelativeDate(streams.uploaded ?: -1L) binding.run { playerViewsInfo.text = context.getString(R.string.normal_views, views, TextUtils.SEPARATOR + date) 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()) { videoTagsAdapter.submitList(streams.tags) } binding.tagsRecycler.isVisible = streams.tags.isNotEmpty() setupDescription(streams.description) } } /** * Collapses the description, if it is currently expanded. */ fun collapseDescription() { val isCollapsed = binding.descLinLayout.isGone if (!isCollapsed) { toggleDescription() } } /** * 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 isNewStateExpanded = binding.descLinLayout.isGone if (!isNewStateExpanded) { // show a short version of the view count and date val formattedDate = TextUtils.formatRelativeDate(streams.uploaded ?: -1L) binding.playerViewsInfo.text = context.getString(R.string.normal_views, streams.views.formatShort(), TextUtils.SEPARATOR + formattedDate) // limit the title height to two lines binding.playerTitle.maxLines = 2 } else { // show the full view count and upload date val formattedDate = streams.uploadTimestamp?.let { TextUtils.localizeInstant(it) }.orEmpty() binding.playerViewsInfo.text = context.getString(R.string.normal_views, "%,d".format(streams.views), TextUtils.SEPARATOR + formattedDate) // show the whole title binding.playerTitle.maxLines = Int.MAX_VALUE } binding.playerDescriptionArrow.animate() .rotation(if (isNewStateExpanded) 180F else 0F) .setDuration(ANIMATION_DURATION) .start() binding.playerDescription.isVisible = isNewStateExpanded binding.descLinLayout.isVisible = isNewStateExpanded } companion object { private const val ANIMATION_DURATION = 250L private const val SB_SPONSOR_CATEGORY = "sponsor_category" } }