Merge 6872e7c6f6520d264779161ddaa2182efd259de8 into 61c25d458901e90bd02d35ec060f9217a4cdd251
This commit is contained in:
commit
02a0f04b24
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,140 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||
import static org.schabi.newpipe.util.Localization.getAppLocale;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DescriptionFragment extends BaseDescriptionFragment {
|
||||
|
||||
@State
|
||||
StreamInfo streamInfo;
|
||||
|
||||
public DescriptionFragment(final StreamInfo streamInfo) {
|
||||
this.streamInfo = streamInfo;
|
||||
}
|
||||
|
||||
public DescriptionFragment() {
|
||||
// keep empty constructor for State when resuming fragment from memory
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Description getDescription() {
|
||||
return streamInfo.getDescription();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected StreamingService getService() {
|
||||
return streamInfo.getService();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getServiceId() {
|
||||
return streamInfo.getServiceId();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected String getStreamUrl() {
|
||||
return streamInfo.getUrl();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<String> getTags() {
|
||||
return streamInfo.getTags();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupMetadata(final LayoutInflater inflater,
|
||||
final LinearLayout layout) {
|
||||
if (streamInfo != null && streamInfo.getUploadDate() != null) {
|
||||
binding.detailUploadDateView.setText(Localization
|
||||
.localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime()));
|
||||
} else {
|
||||
binding.detailUploadDateView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (streamInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
addMetadataItem(inflater, layout, false, R.string.metadata_category,
|
||||
streamInfo.getCategory());
|
||||
|
||||
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(getAppLocale()));
|
||||
}
|
||||
|
||||
addMetadataItem(inflater, layout, true, R.string.metadata_support,
|
||||
streamInfo.getSupportInfo());
|
||||
addMetadataItem(inflater, layout, true, R.string.metadata_host,
|
||||
streamInfo.getHost());
|
||||
|
||||
addImagesMetadataItem(inflater, layout, R.string.metadata_thumbnails,
|
||||
streamInfo.getThumbnails());
|
||||
addImagesMetadataItem(inflater, layout, R.string.metadata_uploader_avatars,
|
||||
streamInfo.getUploaderAvatars());
|
||||
addImagesMetadataItem(inflater, layout, R.string.metadata_subchannel_avatars,
|
||||
streamInfo.getSubChannelAvatars());
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package org.schabi.newpipe.fragments.detail
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.compose.content
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import org.schabi.newpipe.ktx.serializable
|
||||
import org.schabi.newpipe.ui.components.video.StreamDescriptionSection
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.KEY_INFO
|
||||
|
||||
class DescriptionFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
) = content {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
StreamDescriptionSection(requireArguments().serializable(KEY_INFO)!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getInstance(streamInfo: StreamInfo) = DescriptionFragment().apply {
|
||||
arguments = bundleOf(KEY_INFO to streamInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -41,9 +41,12 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.os.postDelayed
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.fragment.app.replace
|
||||
import androidx.preference.PreferenceManager
|
||||
import coil3.util.CoilUtils
|
||||
import com.evernote.android.state.State
|
||||
@ -79,8 +82,8 @@ import org.schabi.newpipe.fragments.BackPressable
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment
|
||||
import org.schabi.newpipe.fragments.EmptyFragment
|
||||
import org.schabi.newpipe.fragments.MainFragment
|
||||
import org.schabi.newpipe.fragments.list.comments.CommentsFragment.Companion.getInstance
|
||||
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment.Companion.getInstance
|
||||
import org.schabi.newpipe.fragments.list.comments.CommentsFragment
|
||||
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment
|
||||
import org.schabi.newpipe.ktx.AnimationType
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.ktx.animateRotation
|
||||
@ -103,6 +106,7 @@ import org.schabi.newpipe.util.DependentPreferenceHelper
|
||||
import org.schabi.newpipe.util.DeviceUtils
|
||||
import org.schabi.newpipe.util.ExtractorHelper
|
||||
import org.schabi.newpipe.util.InfoCache
|
||||
import org.schabi.newpipe.util.KEY_INFO
|
||||
import org.schabi.newpipe.util.ListHelper
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.NO_SERVICE_ID
|
||||
@ -791,7 +795,7 @@ class VideoDetailFragment :
|
||||
tabContentDescriptions.clear()
|
||||
|
||||
if (shouldShowComments()) {
|
||||
pageAdapter.addFragment(getInstance(serviceId, url), COMMENTS_TAB_TAG)
|
||||
pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url), COMMENTS_TAB_TAG)
|
||||
tabIcons.add(R.drawable.ic_comment)
|
||||
tabContentDescriptions.add(R.string.comments_tab_description)
|
||||
}
|
||||
@ -845,18 +849,19 @@ class VideoDetailFragment :
|
||||
private fun updateTabs(info: StreamInfo) {
|
||||
if (showRelatedItems) {
|
||||
when (val relatedItemsLayout = binding.relatedItemsLayout) {
|
||||
null -> pageAdapter.updateItem(RELATED_TAB_TAG, getInstance(info)) // phone
|
||||
null -> pageAdapter.updateItem(RELATED_TAB_TAG, RelatedItemsFragment.getInstance(info)) // phone
|
||||
else -> { // tablet + TV
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.replace(R.id.relatedItemsLayout, getInstance(info))
|
||||
.commitAllowingStateLoss()
|
||||
childFragmentManager.commit(allowStateLoss = true) {
|
||||
val args = bundleOf(KEY_INFO to info)
|
||||
replace<RelatedItemsFragment>(R.id.relatedItemsLayout, args = args)
|
||||
}
|
||||
relatedItemsLayout.isVisible = !this.isFullscreen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showDescription) {
|
||||
pageAdapter.updateItem(DESCRIPTION_TAB_TAG, DescriptionFragment(info))
|
||||
pageAdapter.updateItem(DESCRIPTION_TAB_TAG, DescriptionFragment.getInstance(info))
|
||||
}
|
||||
|
||||
binding.viewPager.visibility = View.VISIBLE
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
package org.schabi.newpipe.fragments.list.channel
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
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.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
|
||||
|
||||
class AboutChannelFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
) = content {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
AboutChannelSection(requireArguments().parcelable(KEY_INFO)!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getInstance(channelInfo: ChannelInfo) = AboutChannelFragment().apply {
|
||||
arguments = bundleOf(KEY_INFO to ParcelableChannelInfo(channelInfo))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,92 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.list.channel;
|
||||
|
||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
import org.schabi.newpipe.fragments.detail.BaseDescriptionFragment;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ChannelAboutFragment extends BaseDescriptionFragment {
|
||||
@State
|
||||
protected ChannelInfo channelInfo;
|
||||
|
||||
ChannelAboutFragment(@NonNull final ChannelInfo channelInfo) {
|
||||
this.channelInfo = channelInfo;
|
||||
}
|
||||
|
||||
public ChannelAboutFragment() {
|
||||
// keep empty constructor for State when resuming fragment from memory
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
binding.constraintLayout.setPadding(0, DeviceUtils.dpToPx(8, requireContext()), 0, 0);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Description getDescription() {
|
||||
return new Description(channelInfo.getDescription(), Description.PLAIN_TEXT);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected StreamingService getService() {
|
||||
return channelInfo.getService();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getServiceId() {
|
||||
return channelInfo.getServiceId();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected String getStreamUrl() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<String> getTags() {
|
||||
return channelInfo.getTags();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupMetadata(final LayoutInflater inflater,
|
||||
final LinearLayout layout) {
|
||||
// There is no upload date available for channels, so hide the relevant UI element
|
||||
binding.detailUploadDateView.setVisibility(View.GONE);
|
||||
|
||||
if (channelInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) {
|
||||
addMetadataItem(inflater, layout, false, R.string.metadata_subscribers,
|
||||
Localization.localizeNumber(channelInfo.getSubscriberCount()));
|
||||
}
|
||||
|
||||
addImagesMetadataItem(inflater, layout, R.string.metadata_avatars,
|
||||
channelInfo.getAvatars());
|
||||
addImagesMetadataItem(inflater, layout, R.string.metadata_banners,
|
||||
channelInfo.getBanners());
|
||||
}
|
||||
}
|
||||
@ -491,7 +491,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
if (ChannelTabHelper.showChannelTab(
|
||||
context, preferences, R.string.show_channel_tabs_about)) {
|
||||
tabAdapter.addFragment(
|
||||
new ChannelAboutFragment(currentInfo),
|
||||
AboutChannelFragment.getInstance(currentInfo),
|
||||
context.getString(R.string.channel_tab_about));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
package org.schabi.newpipe.ktx
|
||||
|
||||
import android.os.Bundle
|
||||
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 : Serializable> Bundle.serializable(key: String?): T? {
|
||||
return BundleCompat.getSerializable(this, key, T::class.java)
|
||||
}
|
||||
|
||||
@ -0,0 +1,93 @@
|
||||
package org.schabi.newpipe.ui.components.channel
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.Image
|
||||
import org.schabi.newpipe.extractor.Image.ResolutionLevel
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor
|
||||
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
|
||||
import org.schabi.newpipe.ui.components.metadata.MetadataItem
|
||||
import org.schabi.newpipe.ui.components.metadata.TagsSection
|
||||
import org.schabi.newpipe.ui.components.metadata.imageMetadataItem
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.NO_SERVICE_ID
|
||||
|
||||
@Composable
|
||||
fun AboutChannelSection(channelInfo: ParcelableChannelInfo) {
|
||||
val (serviceId, description, count, avatars, banners, tags) = channelInfo
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LazyColumnThemedScrollbar(state = lazyListState) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.nestedScroll(rememberNestedScrollInteropConnection()),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (description.isNotEmpty()) {
|
||||
item {
|
||||
Text(text = description, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
|
||||
if (count != StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT) {
|
||||
item {
|
||||
MetadataItem(
|
||||
title = R.string.metadata_subscribers,
|
||||
value = Localization.shortCount(LocalContext.current, count)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
imageMetadataItem(R.string.metadata_avatars, avatars)
|
||||
|
||||
imageMetadataItem(R.string.metadata_banners, banners)
|
||||
|
||||
if (tags.isNotEmpty()) {
|
||||
item {
|
||||
TagsSection(serviceId, tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun AboutChannelSectionPreview() {
|
||||
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) {
|
||||
AboutChannelSection(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,165 @@
|
||||
package org.schabi.newpipe.ui.components.common
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Layout
|
||||
import android.text.Spanned
|
||||
import android.text.style.AbsoluteSizeSpan
|
||||
import android.text.style.AlignmentSpan
|
||||
import android.text.style.BackgroundColorSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.SubscriptSpan
|
||||
import android.text.style.SuperscriptSpan
|
||||
import android.text.style.TypefaceSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.text.style.UnderlineSpan
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.LinkInteractionListener
|
||||
import androidx.compose.ui.text.ParagraphStyle
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.BaselineShift
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.em
|
||||
import androidx.core.text.getSpans
|
||||
|
||||
// The code below is copied from Html.android.kt in the Compose Text library, with some minor
|
||||
// changes.
|
||||
|
||||
internal fun Spanned.toAnnotatedString(
|
||||
linkStyles: TextLinkStyles? = null,
|
||||
linkInteractionListener: LinkInteractionListener? = null
|
||||
): AnnotatedString {
|
||||
return AnnotatedString.Builder(capacity = length)
|
||||
.append(this)
|
||||
.also {
|
||||
it.addSpans(this, linkStyles, linkInteractionListener)
|
||||
}
|
||||
.toAnnotatedString()
|
||||
}
|
||||
|
||||
private fun AnnotatedString.Builder.addSpans(
|
||||
spanned: Spanned,
|
||||
linkStyles: TextLinkStyles?,
|
||||
linkInteractionListener: LinkInteractionListener?
|
||||
) {
|
||||
spanned.getSpans<Any>().forEach { span ->
|
||||
addSpan(
|
||||
span,
|
||||
spanned.getSpanStart(span),
|
||||
spanned.getSpanEnd(span),
|
||||
linkStyles,
|
||||
linkInteractionListener
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun AnnotatedString.Builder.addSpan(
|
||||
span: Any,
|
||||
start: Int,
|
||||
end: Int,
|
||||
linkStyles: TextLinkStyles?,
|
||||
linkInteractionListener: LinkInteractionListener?
|
||||
) {
|
||||
when (span) {
|
||||
is AbsoluteSizeSpan -> {
|
||||
// TODO: Add Compose's implementation when it is available.
|
||||
}
|
||||
|
||||
is AlignmentSpan -> {
|
||||
addStyle(span.toParagraphStyle(), start, end)
|
||||
}
|
||||
|
||||
is BackgroundColorSpan -> {
|
||||
addStyle(SpanStyle(background = Color(span.backgroundColor)), start, end)
|
||||
}
|
||||
|
||||
is ForegroundColorSpan -> {
|
||||
addStyle(SpanStyle(color = Color(span.foregroundColor)), start, end)
|
||||
}
|
||||
|
||||
is RelativeSizeSpan -> {
|
||||
addStyle(SpanStyle(fontSize = span.sizeChange.em), start, end)
|
||||
}
|
||||
|
||||
is StrikethroughSpan -> {
|
||||
addStyle(SpanStyle(textDecoration = TextDecoration.LineThrough), start, end)
|
||||
}
|
||||
|
||||
is StyleSpan -> {
|
||||
span.toSpanStyle()?.let { addStyle(it, start, end) }
|
||||
}
|
||||
|
||||
is SubscriptSpan -> {
|
||||
addStyle(SpanStyle(baselineShift = BaselineShift.Subscript), start, end)
|
||||
}
|
||||
|
||||
is SuperscriptSpan -> {
|
||||
addStyle(SpanStyle(baselineShift = BaselineShift.Superscript), start, end)
|
||||
}
|
||||
|
||||
is TypefaceSpan -> {
|
||||
addStyle(span.toSpanStyle(), start, end)
|
||||
}
|
||||
|
||||
is UnderlineSpan -> {
|
||||
addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end)
|
||||
}
|
||||
|
||||
is URLSpan -> {
|
||||
span.url?.let { url ->
|
||||
val link = LinkAnnotation.Url(url, linkStyles, linkInteractionListener)
|
||||
addLink(link, start, end)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AlignmentSpan.toParagraphStyle(): ParagraphStyle {
|
||||
val alignment = when (this.alignment) {
|
||||
Layout.Alignment.ALIGN_NORMAL -> TextAlign.Start
|
||||
Layout.Alignment.ALIGN_CENTER -> TextAlign.Center
|
||||
Layout.Alignment.ALIGN_OPPOSITE -> TextAlign.End
|
||||
else -> TextAlign.Unspecified
|
||||
}
|
||||
return ParagraphStyle(textAlign = alignment)
|
||||
}
|
||||
|
||||
private fun StyleSpan.toSpanStyle(): SpanStyle? {
|
||||
return when (style) {
|
||||
Typeface.BOLD -> SpanStyle(fontWeight = FontWeight.Bold)
|
||||
Typeface.ITALIC -> SpanStyle(fontStyle = FontStyle.Italic)
|
||||
Typeface.BOLD_ITALIC -> SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun TypefaceSpan.toSpanStyle(): SpanStyle {
|
||||
val fontFamily = when (family) {
|
||||
FontFamily.Cursive.name -> FontFamily.Cursive
|
||||
FontFamily.Monospace.name -> FontFamily.Monospace
|
||||
FontFamily.SansSerif.name -> FontFamily.SansSerif
|
||||
FontFamily.Serif.name -> FontFamily.Serif
|
||||
else -> {
|
||||
optionalFontFamilyFromName(family)
|
||||
}
|
||||
}
|
||||
return SpanStyle(fontFamily = fontFamily)
|
||||
}
|
||||
|
||||
private fun optionalFontFamilyFromName(familyName: String?): FontFamily? {
|
||||
if (familyName.isNullOrEmpty()) return null
|
||||
val typeface = Typeface.create(familyName, Typeface.NORMAL)
|
||||
return typeface.takeIf {
|
||||
typeface != Typeface.DEFAULT &&
|
||||
typeface != Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
|
||||
}?.let { FontFamily(it) }
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package org.schabi.newpipe.ui.components.common
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.fromHtml
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.linkify.LinkifyPlugin
|
||||
import org.schabi.newpipe.extractor.ServiceList
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
import org.schabi.newpipe.ui.components.common.link.YouTubeLinkHandler
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.NO_SERVICE_ID
|
||||
|
||||
@Composable
|
||||
fun parseDescription(description: Description, serviceId: Int): AnnotatedString {
|
||||
val context = LocalContext.current
|
||||
val linkHandler = remember(serviceId) {
|
||||
if (serviceId == ServiceList.YouTube.serviceId) {
|
||||
YouTubeLinkHandler(context)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val styles = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.Underline))
|
||||
|
||||
return remember(description) {
|
||||
when (description.type) {
|
||||
Description.HTML -> AnnotatedString.fromHtml(description.content, styles, linkHandler)
|
||||
Description.MARKDOWN -> {
|
||||
Markwon.builder(context)
|
||||
.usePlugin(LinkifyPlugin.create())
|
||||
.build()
|
||||
.toMarkdown(description.content)
|
||||
.toAnnotatedString(styles, linkHandler)
|
||||
}
|
||||
else -> AnnotatedString(description.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DescriptionPreviewProvider : PreviewParameterProvider<Description> {
|
||||
override val values = sequenceOf(
|
||||
Description("This is a description.", Description.PLAIN_TEXT),
|
||||
Description("This is a <b>bold description</b>.", Description.HTML),
|
||||
Description("This is a [link](https://example.com).", Description.MARKDOWN),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ParseDescriptionPreview(
|
||||
@PreviewParameter(DescriptionPreviewProvider::class) description: Description
|
||||
) {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Text(text = parseDescription(description, NO_SERVICE_ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package org.schabi.newpipe.ui.components.common.link
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.platform.AndroidUriHandler
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.LinkInteractionListener
|
||||
import androidx.core.net.toUri
|
||||
import org.schabi.newpipe.extractor.ServiceList
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
|
||||
class YouTubeLinkHandler(private val context: Context) : LinkInteractionListener {
|
||||
private val uriHandler = AndroidUriHandler(context)
|
||||
|
||||
override fun onClick(link: LinkAnnotation) {
|
||||
val url = (link as LinkAnnotation.Url).url
|
||||
val uri = url.toUri()
|
||||
|
||||
// TODO: Handle other links in NewPipe as well.
|
||||
if ("hashtag" in uri.pathSegments) {
|
||||
NavigationHelper.openSearch(
|
||||
context, ServiceList.YouTube.serviceId, "#${uri.lastPathSegment}"
|
||||
)
|
||||
} else {
|
||||
uriHandler.openUri(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
package org.schabi.newpipe.ui.components.metadata
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import org.schabi.newpipe.R
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun ImageMetadataItem(@StringRes title: Int, images: List<Image>) {
|
||||
val context = LocalContext.current
|
||||
val imageLinks = remember(images) { convertImagesToLinks(context, images) }
|
||||
|
||||
MetadataItem(title = title, value = imageLinks)
|
||||
}
|
||||
|
||||
fun LazyListScope.imageMetadataItem(@StringRes title: Int, images: List<Image>) {
|
||||
if (images.isNotEmpty()) {
|
||||
item {
|
||||
ImageMetadataItem(title, images)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertImagesToLinks(context: Context, images: List<Image>): AnnotatedString {
|
||||
val preferredUrl = ImageStrategy.choosePreferredImage(images)
|
||||
|
||||
fun imageSizeToText(size: Int): String {
|
||||
return if (size == Image.HEIGHT_UNKNOWN) context.getString(R.string.question_mark)
|
||||
else size.toString()
|
||||
}
|
||||
|
||||
return buildAnnotatedString {
|
||||
for (image in images) {
|
||||
if (length != 0) {
|
||||
append(", ")
|
||||
}
|
||||
|
||||
val linkStyle = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.Underline))
|
||||
withLink(LinkAnnotation.Url(image.url, linkStyle)) {
|
||||
val weight = if (image.url == preferredUrl) FontWeight.Bold else FontWeight.Normal
|
||||
|
||||
withStyle(SpanStyle(fontWeight = weight)) {
|
||||
// if even the resolution level is unknown, ?x? will be shown
|
||||
if (image.height != Image.HEIGHT_UNKNOWN || image.width != Image.WIDTH_UNKNOWN ||
|
||||
image.estimatedResolutionLevel == ResolutionLevel.UNKNOWN
|
||||
) {
|
||||
append("${imageSizeToText(image.width)}x${imageSizeToText(image.height)}")
|
||||
} else if (image.estimatedResolutionLevel == ResolutionLevel.LOW) {
|
||||
append(context.getString(R.string.image_quality_low))
|
||||
} else if (image.estimatedResolutionLevel == ResolutionLevel.MEDIUM) {
|
||||
append(context.getString(R.string.image_quality_medium))
|
||||
} else if (image.estimatedResolutionLevel == ResolutionLevel.HIGH) {
|
||||
append(context.getString(R.string.image_quality_high))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ImageMetadataItemPreview() {
|
||||
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)
|
||||
)
|
||||
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
ImageMetadataItem(
|
||||
title = R.string.metadata_uploader_avatars,
|
||||
images = images
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
package org.schabi.newpipe.ui.components.metadata
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
fun MetadataItem(@StringRes title: Int, value: String) {
|
||||
MetadataItem(title = title, value = AnnotatedString(value))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MetadataItem(@StringRes title: Int, value: AnnotatedString) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.width(96.dp),
|
||||
textAlign = TextAlign.End,
|
||||
text = stringResource(title).uppercase(),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
|
||||
Text(text = value, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun MetadataItemPreview() {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Column {
|
||||
MetadataItem(title = R.string.metadata_category, value = "Entertainment")
|
||||
MetadataItem(title = R.string.metadata_age_limit, value = "18")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
package org.schabi.newpipe.ui.components.metadata
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ElevatedSuggestionChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.ktx.findFragmentActivity
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun TagsSection(serviceId: Int, tags: List<String>) {
|
||||
val context = LocalContext.current
|
||||
val sortedTags = remember(tags) { tags.sortedWith(String.CASE_INSENSITIVE_ORDER) }
|
||||
|
||||
Column(modifier = Modifier.padding(4.dp)) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.metadata_tags).uppercase(),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
for (tag in sortedTags) {
|
||||
ElevatedSuggestionChip(
|
||||
onClick = {
|
||||
NavigationHelper.openSearchFragment(
|
||||
context.findFragmentActivity().supportFragmentManager, serviceId, tag
|
||||
)
|
||||
},
|
||||
label = { Text(text = tag) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TagsSectionPreview() {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
TagsSection(serviceId = 1, tags = listOf("Tag 1", "Tag 2"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,224 @@
|
||||
package org.schabi.newpipe.ui.components.video
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.rememberTooltipState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.localization.DateWrapper
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
|
||||
import org.schabi.newpipe.ui.components.common.parseDescription
|
||||
import org.schabi.newpipe.ui.components.metadata.MetadataItem
|
||||
import org.schabi.newpipe.ui.components.metadata.TagsSection
|
||||
import org.schabi.newpipe.ui.components.metadata.imageMetadataItem
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.NO_SERVICE_ID
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun StreamDescriptionSection(streamInfo: StreamInfo) {
|
||||
var isSelectable by rememberSaveable { mutableStateOf(false) }
|
||||
val hasDescription = streamInfo.description != Description.EMPTY_DESCRIPTION
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LazyColumnThemedScrollbar(state = lazyListState) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 12.dp)
|
||||
.nestedScroll(rememberNestedScrollInteropConnection()),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = if (streamInfo.uploadDate != null) Arrangement.SpaceBetween else Arrangement.End,
|
||||
) {
|
||||
streamInfo.uploadDate?.let {
|
||||
val date = Localization.formatDate(it.offsetDateTime())
|
||||
Text(
|
||||
text = stringResource(R.string.upload_date_text, date),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
|
||||
if (hasDescription) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
tooltip = {
|
||||
val tooltip = stringResource(
|
||||
if (isSelectable) R.string.description_select_disable
|
||||
else R.string.description_select_enable
|
||||
)
|
||||
PlainTooltip { Text(text = tooltip) }
|
||||
},
|
||||
state = rememberTooltipState()
|
||||
) {
|
||||
val res = if (isSelectable) R.drawable.ic_close else R.drawable.ic_select_all
|
||||
Image(
|
||||
modifier = Modifier.clickable { isSelectable = !isSelectable },
|
||||
painter = painterResource(res),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val density = LocalDensity.current
|
||||
AnimatedVisibility(
|
||||
visible = isSelectable,
|
||||
enter = slideInVertically {
|
||||
with(density) { -40.dp.roundToPx() }
|
||||
} + expandVertically(
|
||||
expandFrom = Alignment.Top
|
||||
) + fadeIn(
|
||||
initialAlpha = 0.3f
|
||||
),
|
||||
exit = slideOutVertically() + shrinkVertically() + fadeOut()
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.description_select_note),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDescription) {
|
||||
item {
|
||||
val description = parseDescription(streamInfo.description, streamInfo.serviceId)
|
||||
|
||||
if (isSelectable) {
|
||||
SelectionContainer {
|
||||
Text(text = description, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
} else {
|
||||
Text(text = description, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metadataItem(title = R.string.metadata_category, value = streamInfo.category)
|
||||
|
||||
metadataItem(title = R.string.metadata_licence, value = streamInfo.licence)
|
||||
|
||||
val privacy = streamInfo.privacy ?: StreamExtractor.Privacy.OTHER
|
||||
if (privacy != StreamExtractor.Privacy.OTHER) {
|
||||
item {
|
||||
val message = when (privacy) {
|
||||
StreamExtractor.Privacy.PUBLIC -> R.string.metadata_privacy_public
|
||||
StreamExtractor.Privacy.UNLISTED -> R.string.metadata_privacy_unlisted
|
||||
StreamExtractor.Privacy.PRIVATE -> R.string.metadata_privacy_private
|
||||
StreamExtractor.Privacy.INTERNAL -> R.string.metadata_privacy_internal
|
||||
else -> 0 // Never reached
|
||||
}
|
||||
MetadataItem(title = R.string.metadata_privacy, value = stringResource(message))
|
||||
}
|
||||
}
|
||||
|
||||
val ageLimit = streamInfo.ageLimit
|
||||
if (ageLimit != StreamExtractor.NO_AGE_LIMIT) {
|
||||
item {
|
||||
MetadataItem(title = R.string.metadata_age_limit, value = ageLimit.toString())
|
||||
}
|
||||
}
|
||||
|
||||
streamInfo.languageInfo?.let {
|
||||
item {
|
||||
MetadataItem(
|
||||
title = R.string.metadata_language,
|
||||
value = it.getDisplayLanguage(Localization.getAppLocale())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
metadataItem(title = R.string.metadata_support, value = streamInfo.supportInfo)
|
||||
|
||||
metadataItem(title = R.string.metadata_host, value = streamInfo.host)
|
||||
|
||||
imageMetadataItem(title = R.string.metadata_thumbnails, images = streamInfo.thumbnails)
|
||||
|
||||
imageMetadataItem(
|
||||
title = R.string.metadata_uploader_avatars,
|
||||
images = streamInfo.uploaderAvatars
|
||||
)
|
||||
|
||||
imageMetadataItem(
|
||||
title = R.string.metadata_subchannel_avatars,
|
||||
images = streamInfo.subChannelAvatars
|
||||
)
|
||||
|
||||
if (streamInfo.tags.isNotEmpty()) {
|
||||
item {
|
||||
TagsSection(serviceId = streamInfo.serviceId, tags = streamInfo.tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.metadataItem(@StringRes title: Int, value: String) {
|
||||
if (value.isNotEmpty()) {
|
||||
item {
|
||||
MetadataItem(title, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun StreamDescriptionSectionPreview() {
|
||||
val info = StreamInfo(NO_SERVICE_ID, "", "", StreamType.VIDEO_STREAM, "", "", 0)
|
||||
info.uploadDate = DateWrapper(OffsetDateTime.now())
|
||||
info.description = Description("This is an <b>example</b> description", Description.HTML)
|
||||
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
StreamDescriptionSection(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This is used to inflate a chip with a Material theme, otherwise it would crash -->
|
||||
<!-- Theme.MaterialComponents.DayNight is used to guarantee auto day/night switching -->
|
||||
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/Theme.MaterialComponents.DayNight.Bridge"
|
||||
app:chipMinTouchTargetSize="40dp"
|
||||
tools:text="I'm a correctly themed chip!" />
|
||||
@ -1,101 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/constraint_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/detail_upload_date_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textSize="@dimen/video_item_detail_upload_date_text_size"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@+id/detail_select_description_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Published on Oct 2, 2009" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/detail_select_description_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/description_select_enable"
|
||||
android:focusable="true"
|
||||
android:padding="5dp"
|
||||
android:src="@drawable/ic_select_all"
|
||||
app:layout_constraintBottom_toTopOf="@+id/barrier"
|
||||
app:layout_constraintDimensionRatio="1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierAllowsGoneWidgets="false"
|
||||
app:barrierDirection="top"
|
||||
app:constraint_referenced_ids="detail_description_note_view,detail_description_view" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/detail_description_note_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/description_select_note"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="italic"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/detail_upload_date_view"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/detail_description_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textSize="@dimen/video_item_detail_description_text_size"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/detail_description_note_view"
|
||||
tools:text="Description Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed a ultricies ex. Integer sit amet sodales risus. Duis non mi et urna pretium bibendum." />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/detail_metadata_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/detail_description_view" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="6dp">
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/metadata_type_view"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical|end"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Licence" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/metadata_content_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/metadata_type_view"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Description Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed a ultricies ex. Integer sit amet sodales risus. Duis non mi et urna pretium bibendum." />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fadeScrollbars="false">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/metadata_type_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/metadata_tags"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/metadata_tags_chips"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/metadata_type_view" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@ -765,7 +765,7 @@
|
||||
<string name="select_night_theme_toast">You can select your favorite night theme below</string>
|
||||
<string name="night_theme_available">This option is only available if %s is selected for Theme</string>
|
||||
<string name="download_has_started">Download has started</string>
|
||||
<string name="description_select_note">You can now select text inside the description. Note that the page may flicker and links may not be clickable while in selection mode.</string>
|
||||
<string name="description_select_note">You can now select text inside the description.</string>
|
||||
<string name="description_select_enable">Enable selecting text in the description</string>
|
||||
<string name="description_select_disable">Disable selecting text in the description</string>
|
||||
<string name="metadata_category">Category</string>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user