1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-02-10 16:10:11 +00:00

Fix crash when minimizing the channel about tab

This commit is contained in:
Isira Seneviratne 2024-09-01 18:30:18 +05:30
parent 507b3b4108
commit 45aa445b50
6 changed files with 53 additions and 300 deletions

View File

@ -1,281 +0,0 @@
package org.schabi.newpipe.fragments.detail;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
import android.graphics.Typeface;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.StyleSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.TooltipCompat;
import androidx.core.text.HtmlCompat;
import com.google.android.material.chip.Chip;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
import org.schabi.newpipe.databinding.ItemMetadataBinding;
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.text.TextLinkifier;
import java.util.List;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
public abstract class BaseDescriptionFragment extends BaseFragment {
private final CompositeDisposable descriptionDisposables = new CompositeDisposable();
protected FragmentDescriptionBinding binding;
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
binding = FragmentDescriptionBinding.inflate(inflater, container, false);
setupDescription();
setupMetadata(inflater, binding.detailMetadataLayout);
addTagsMetadataItem(inflater, binding.detailMetadataLayout);
return binding.getRoot();
}
@Override
public void onDestroy() {
descriptionDisposables.clear();
super.onDestroy();
}
/**
* Get the description to display.
* @return description object, if available
*/
@Nullable
protected abstract Description getDescription();
/**
* Get the streaming service. Used for generating description links.
* @return streaming service
*/
@NonNull
protected abstract StreamingService getService();
/**
* Get the streaming service ID. Used for tag links.
* @return service ID
*/
protected abstract int getServiceId();
/**
* Get the URL of the described video or audio, used to generate description links.
* @return stream URL
*/
@Nullable
protected abstract String getStreamUrl();
/**
* Get the list of tags to display below the description.
* @return tag list
*/
@NonNull
public abstract List<String> getTags();
/**
* Add additional metadata to display.
* @param inflater LayoutInflater
* @param layout detailMetadataLayout
*/
protected abstract void setupMetadata(LayoutInflater inflater, LinearLayout layout);
private void setupDescription() {
final Description description = getDescription();
if (description == null || isEmpty(description.getContent())
|| description == Description.EMPTY_DESCRIPTION) {
binding.detailDescriptionView.setVisibility(View.GONE);
binding.detailSelectDescriptionButton.setVisibility(View.GONE);
return;
}
// start with disabled state. This also loads description content (!)
disableDescriptionSelection();
binding.detailSelectDescriptionButton.setOnClickListener(v -> {
if (binding.detailDescriptionNoteView.getVisibility() == View.VISIBLE) {
disableDescriptionSelection();
} else {
// enable selection only when button is clicked to prevent flickering
enableDescriptionSelection();
}
});
}
private void enableDescriptionSelection() {
binding.detailDescriptionNoteView.setVisibility(View.VISIBLE);
binding.detailDescriptionView.setTextIsSelectable(true);
final String buttonLabel = getString(R.string.description_select_disable);
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close);
}
private void disableDescriptionSelection() {
// show description content again, otherwise some links are not clickable
final Description description = getDescription();
if (description != null) {
TextLinkifier.fromDescription(binding.detailDescriptionView,
description, HtmlCompat.FROM_HTML_MODE_LEGACY,
getService(), getStreamUrl(),
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
}
binding.detailDescriptionNoteView.setVisibility(View.GONE);
binding.detailDescriptionView.setTextIsSelectable(false);
final String buttonLabel = getString(R.string.description_select_enable);
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all);
}
protected void addMetadataItem(final LayoutInflater inflater,
final LinearLayout layout,
final boolean linkifyContent,
@StringRes final int type,
@NonNull final String content) {
if (isBlank(content)) {
return;
}
final ItemMetadataBinding itemBinding =
ItemMetadataBinding.inflate(inflater, layout, false);
itemBinding.metadataTypeView.setText(type);
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
ShareUtils.copyToClipboard(requireContext(), content);
return true;
});
if (linkifyContent) {
TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null,
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
} else {
itemBinding.metadataContentView.setText(content);
}
itemBinding.metadataContentView.setClickable(true);
layout.addView(itemBinding.getRoot());
}
private String imageSizeToText(final int heightOrWidth) {
if (heightOrWidth < 0) {
return getString(R.string.question_mark);
} else {
return String.valueOf(heightOrWidth);
}
}
protected void addImagesMetadataItem(final LayoutInflater inflater,
final LinearLayout layout,
@StringRes final int type,
final List<Image> images) {
final String preferredImageUrl = ImageStrategy.choosePreferredImage(images);
if (preferredImageUrl == null) {
return; // null will be returned in case there is no image
}
final ItemMetadataBinding itemBinding =
ItemMetadataBinding.inflate(inflater, layout, false);
itemBinding.metadataTypeView.setText(type);
final SpannableStringBuilder urls = new SpannableStringBuilder();
for (final Image image : images) {
if (urls.length() != 0) {
urls.append(", ");
}
final int entryBegin = urls.length();
if (image.getHeight() != Image.HEIGHT_UNKNOWN
|| image.getWidth() != Image.WIDTH_UNKNOWN
// if even the resolution level is unknown, ?x? will be shown
|| image.getEstimatedResolutionLevel() == Image.ResolutionLevel.UNKNOWN) {
urls.append(imageSizeToText(image.getHeight()));
urls.append('x');
urls.append(imageSizeToText(image.getWidth()));
} else {
switch (image.getEstimatedResolutionLevel()) {
case LOW -> urls.append(getString(R.string.image_quality_low));
case MEDIUM -> urls.append(getString(R.string.image_quality_medium));
case HIGH -> urls.append(getString(R.string.image_quality_high));
default -> {
// unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out
}
}
}
urls.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull final View widget) {
ShareUtils.openUrlInBrowser(requireContext(), image.getUrl());
}
}, entryBegin, urls.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (preferredImageUrl.equals(image.getUrl())) {
urls.setSpan(new StyleSpan(Typeface.BOLD), entryBegin, urls.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
itemBinding.metadataContentView.setText(urls);
itemBinding.metadataContentView.setMovementMethod(LinkMovementMethod.getInstance());
layout.addView(itemBinding.getRoot());
}
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
final List<String> tags = getTags();
if (!tags.isEmpty()) {
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
final Chip chip = (Chip) inflater.inflate(R.layout.chip,
itemBinding.metadataTagsChips, false);
chip.setText(tag);
chip.setOnClickListener(this::onTagClick);
chip.setOnLongClickListener(this::onTagLongClick);
itemBinding.metadataTagsChips.addView(chip);
});
layout.addView(itemBinding.getRoot());
}
}
private void onTagClick(final View chip) {
if (getParentFragment() != null) {
NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(),
getServiceId(), ((Chip) chip).getText().toString());
}
}
private boolean onTagLongClick(final View chip) {
ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString());
return true;
}
}

View File

@ -9,8 +9,9 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.compose.content
import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.ktx.serializable
import org.schabi.newpipe.ktx.parcelable
import org.schabi.newpipe.ui.components.channel.AboutChannelSection
import org.schabi.newpipe.ui.components.channel.ParcelableChannelInfo
import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.util.KEY_INFO
@ -22,7 +23,7 @@ class AboutChannelFragment : Fragment() {
) = content {
AppTheme {
Surface(color = MaterialTheme.colorScheme.background) {
AboutChannelSection(requireArguments().serializable(KEY_INFO)!!)
AboutChannelSection(requireArguments().parcelable(KEY_INFO)!!)
}
}
}
@ -30,7 +31,7 @@ class AboutChannelFragment : Fragment() {
companion object {
@JvmStatic
fun getInstance(channelInfo: ChannelInfo) = AboutChannelFragment().apply {
arguments = bundleOf(KEY_INFO to channelInfo)
arguments = bundleOf(KEY_INFO to ParcelableChannelInfo(channelInfo))
}
}
}

View File

@ -5,6 +5,10 @@ import android.os.Parcelable
import androidx.core.os.BundleCompat
import java.io.Serializable
inline fun <reified T : Parcelable> Bundle.parcelable(key: String?): T? {
return BundleCompat.getParcelable(this, key, T::class.java)
}
inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? {
return BundleCompat.getParcelableArrayList(this, key, T::class.java)
}

View File

@ -13,7 +13,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.Image
import org.schabi.newpipe.extractor.Image.ResolutionLevel
import org.schabi.newpipe.ui.components.metadata.ImageMetadataItem
import org.schabi.newpipe.ui.components.metadata.MetadataItem
import org.schabi.newpipe.ui.components.metadata.TagsSection
@ -22,18 +23,18 @@ import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NO_SERVICE_ID
@Composable
fun AboutChannelSection(channelInfo: ChannelInfo) {
fun AboutChannelSection(channelInfo: ParcelableChannelInfo) {
// This tab currently holds little information, so a lazy column isn't needed here.
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
val description = channelInfo.description
if (!description.isNullOrEmpty()) {
val (serviceId, description, count, avatars, banners, tags) = channelInfo
if (description.isNotEmpty()) {
Text(text = description)
}
val count = channelInfo.subscriberCount
if (count != -1L) {
MetadataItem(
title = R.string.metadata_subscribers,
@ -41,16 +42,16 @@ fun AboutChannelSection(channelInfo: ChannelInfo) {
)
}
if (channelInfo.avatars.isNotEmpty()) {
ImageMetadataItem(R.string.metadata_avatars, channelInfo.avatars)
if (avatars.isNotEmpty()) {
ImageMetadataItem(R.string.metadata_avatars, avatars)
}
if (channelInfo.banners.isNotEmpty()) {
ImageMetadataItem(R.string.metadata_banners, channelInfo.banners)
if (banners.isNotEmpty()) {
ImageMetadataItem(R.string.metadata_banners, banners)
}
if (channelInfo.tags.isNotEmpty()) {
TagsSection(channelInfo.serviceId, channelInfo.tags)
if (tags.isNotEmpty()) {
TagsSection(serviceId, tags)
}
}
}
@ -59,9 +60,18 @@ fun AboutChannelSection(channelInfo: ChannelInfo) {
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AboutChannelSectionPreview() {
val info = ChannelInfo(NO_SERVICE_ID, "", "", "", "")
info.description = "This is an example description"
info.subscriberCount = 10
val images = listOf(
Image("https://example.com/image_low.png", 16, 16, ResolutionLevel.LOW),
Image("https://example.com/image_mid.png", 32, 32, ResolutionLevel.MEDIUM)
)
val info = ParcelableChannelInfo(
serviceId = NO_SERVICE_ID,
description = "This is an example description",
subscriberCount = 10,
avatars = images,
banners = images,
tags = listOf("Tag 1", "Tag 2")
)
AppTheme {
Surface(color = MaterialTheme.colorScheme.background) {

View File

@ -0,0 +1,21 @@
package org.schabi.newpipe.ui.components.channel
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.schabi.newpipe.extractor.Image
import org.schabi.newpipe.extractor.channel.ChannelInfo
@Parcelize
data class ParcelableChannelInfo(
val serviceId: Int,
val description: String,
val subscriberCount: Long,
val avatars: List<Image>,
val banners: List<Image>,
val tags: List<String>
) : Parcelable {
constructor(channelInfo: ChannelInfo) : this(
channelInfo.serviceId, channelInfo.description, channelInfo.subscriberCount,
channelInfo.avatars, channelInfo.banners, channelInfo.tags
)
}

View File

@ -23,7 +23,6 @@ import org.schabi.newpipe.extractor.Image
import org.schabi.newpipe.extractor.Image.ResolutionLevel
import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.util.image.ImageStrategy
import org.schabi.newpipe.util.image.PreferredImageQuality
@Composable
fun ImageMetadataItem(@StringRes title: Int, images: List<Image>) {
@ -74,7 +73,6 @@ private fun convertImagesToLinks(context: Context, images: List<Image>): Annotat
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ImageMetadataItemPreview() {
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.MEDIUM)
val images = listOf(
Image("https://example.com/image_low.png", 16, 16, ResolutionLevel.LOW),
Image("https://example.com/image_mid.png", 32, 32, ResolutionLevel.MEDIUM)