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:
parent
507b3b4108
commit
45aa445b50
@ -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;
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
)
|
||||
}
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user