diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java index b4424928fa3..5f1cbc36583 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java @@ -4,23 +4,38 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; +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.stream.Description; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.TextLinkifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import icepick.State; import io.reactivex.rxjava3.disposables.Disposable; import static android.text.TextUtils.isEmpty; +import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT; +import static org.schabi.newpipe.extractor.utils.Utils.isBlank; public class DescriptionFragment extends BaseFragment { @@ -28,6 +43,7 @@ public class DescriptionFragment extends BaseFragment { StreamInfo streamInfo = null; @Nullable Disposable descriptionDisposable = null; + FragmentDescriptionBinding binding; public DescriptionFragment() { } @@ -40,11 +56,11 @@ public DescriptionFragment(final StreamInfo streamInfo) { public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { - final FragmentDescriptionBinding binding = - FragmentDescriptionBinding.inflate(inflater, container, false); + binding = FragmentDescriptionBinding.inflate(inflater, container, false); if (streamInfo != null) { - setupUploadDate(binding.detailUploadDateView); - setupDescription(binding.detailDescriptionView); + setupUploadDate(); + setupDescription(); + setupMetadata(inflater, binding.detailMetadataLayout); } return binding.getRoot(); } @@ -57,37 +73,197 @@ public void onDestroy() { } } - private void setupUploadDate(final TextView uploadDateTextView) { + + private void setupUploadDate() { if (streamInfo.getUploadDate() != null) { - uploadDateTextView.setText(Localization + binding.detailUploadDateView.setText(Localization .localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime())); } else { - uploadDateTextView.setVisibility(View.GONE); + binding.detailUploadDateView.setVisibility(View.GONE); } } - private void setupDescription(final TextView descriptionTextView) { + + private void setupDescription() { final Description description = streamInfo.getDescription(); if (description == null || isEmpty(description.getContent()) || description == Description.emptyDescription) { - descriptionTextView.setText(""); + 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 + loadDescriptionContent(); + + 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); + } + + private void loadDescriptionContent() { + final Description description = streamInfo.getDescription(); switch (description.getType()) { case Description.HTML: descriptionDisposable = TextLinkifier.createLinksFromHtmlBlock(requireContext(), - description.getContent(), descriptionTextView, + description.getContent(), binding.detailDescriptionView, HtmlCompat.FROM_HTML_MODE_LEGACY); break; case Description.MARKDOWN: descriptionDisposable = TextLinkifier.createLinksFromMarkdownText(requireContext(), - description.getContent(), descriptionTextView); + description.getContent(), binding.detailDescriptionView); break; case Description.PLAIN_TEXT: default: descriptionDisposable = TextLinkifier.createLinksFromPlainText(requireContext(), - description.getContent(), descriptionTextView); + description.getContent(), binding.detailDescriptionView); break; } } + + + private void setupMetadata(final LayoutInflater inflater, + final LinearLayout layout) { + addMetadataItem(inflater, layout, false, + R.string.metadata_category, streamInfo.getCategory()); + + addTagsMetadataItem(inflater, layout); + + addMetadataItem(inflater, layout, false, + R.string.metadata_licence, streamInfo.getLicence()); + + addPrivacyMetadataItem(inflater, layout); + + if (streamInfo.getAgeLimit() != NO_AGE_LIMIT) { + addMetadataItem(inflater, layout, false, + R.string.metadata_age_limit, String.valueOf(streamInfo.getAgeLimit())); + } + + if (streamInfo.getLanguageInfo() != null) { + addMetadataItem(inflater, layout, false, + R.string.metadata_language, streamInfo.getLanguageInfo().getDisplayLanguage()); + } + + addMetadataItem(inflater, layout, true, + R.string.metadata_support, streamInfo.getSupportInfo()); + addMetadataItem(inflater, layout, true, + R.string.metadata_host, streamInfo.getHost()); + addMetadataItem(inflater, layout, true, + R.string.metadata_thumbnail_url, streamInfo.getThumbnailUrl()); + } + + private void addMetadataItem(final LayoutInflater inflater, + final LinearLayout layout, + final boolean linkifyContent, + @StringRes final int type, + @Nullable 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.createLinksFromPlainText(requireContext(), + content, itemBinding.metadataContentView); + } else { + itemBinding.metadataContentView.setText(content); + } + + layout.addView(itemBinding.getRoot()); + } + + private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) { + if (streamInfo.getTags() != null && !streamInfo.getTags().isEmpty()) { + final ItemMetadataTagsBinding itemBinding + = ItemMetadataTagsBinding.inflate(inflater, layout, false); + + final List tags = new ArrayList<>(streamInfo.getTags()); + Collections.sort(tags); + for (final String tag : tags) { + 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(), + streamInfo.getServiceId(), ((Chip) chip).getText().toString()); + } + } + + private boolean onTagLongClick(final View chip) { + ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString()); + return true; + } + + private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearLayout layout) { + if (streamInfo.getPrivacy() != null) { + @StringRes final int contentRes; + switch (streamInfo.getPrivacy()) { + case PUBLIC: + contentRes = R.string.metadata_privacy_public; + break; + case UNLISTED: + contentRes = R.string.metadata_privacy_unlisted; + break; + case PRIVATE: + contentRes = R.string.metadata_privacy_private; + break; + case INTERNAL: + contentRes = R.string.metadata_privacy_internal; + break; + case OTHER: default: + contentRes = 0; + break; + } + + if (contentRes != 0) { + addMetadataItem(inflater, layout, false, + R.string.metadata_privacy, getString(contentRes)); + } + } + } } diff --git a/app/src/main/res/drawable-night/ic_select_all.xml b/app/src/main/res/drawable-night/ic_select_all.xml new file mode 100644 index 00000000000..15773491175 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_select_all.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_select_all.xml b/app/src/main/res/drawable/ic_select_all.xml new file mode 100644 index 00000000000..e8693d51bc4 --- /dev/null +++ b/app/src/main/res/drawable/ic_select_all.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/chip.xml b/app/src/main/res/layout/chip.xml new file mode 100644 index 00000000000..f7a55fdf34c --- /dev/null +++ b/app/src/main/res/layout/chip.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_description.xml b/app/src/main/res/layout/fragment_description.xml index e3845e89230..c44b88cb6bb 100644 --- a/app/src/main/res/layout/fragment_description.xml +++ b/app/src/main/res/layout/fragment_description.xml @@ -2,14 +2,15 @@ + android:layout_height="wrap_content" + android:orientation="vertical" + android:scrollbars="vertical"> + android:layout_height="wrap_content" + android:animateLayoutChanges="true"> + + + + + + + + diff --git a/app/src/main/res/layout/item_metadata.xml b/app/src/main/res/layout/item_metadata.xml new file mode 100644 index 00000000000..b9015e603ca --- /dev/null +++ b/app/src/main/res/layout/item_metadata.xml @@ -0,0 +1,42 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_metadata_tags.xml b/app/src/main/res/layout/item_metadata_tags.xml new file mode 100644 index 00000000000..d887c9e2836 --- /dev/null +++ b/app/src/main/res/layout/item_metadata_tags.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b8ca33c8783..c43ea85b674 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -716,4 +716,20 @@ Select your favorite night theme — %s You can select your favorite night theme below Download has started - \ No newline at end of file + You can now select text inside the description. Note that the page may flicker and links may not be clickable while in selection mode. + Enable selecting text in the description + Disable selecting text in the description + Category + Tags + Licence + Privacy + Age limit + Language + Support + Host + Thumbnail URL + Public + Unlisted + Private + Internal +