diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java deleted file mode 100644 index 4789b02e65b..00000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java +++ /dev/null @@ -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 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 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 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; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.kt new file mode 100644 index 00000000000..7e93959c83d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.kt @@ -0,0 +1,325 @@ +package org.schabi.newpipe.fragments.detail + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.StringRes +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +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.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withLink +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.reactivex.rxjava3.disposables.CompositeDisposable +import java.lang.String.CASE_INSENSITIVE_ORDER +import org.schabi.newpipe.BaseFragment +import org.schabi.newpipe.R +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.extractor.Image.ResolutionLevel +import org.schabi.newpipe.extractor.stream.Description +import org.schabi.newpipe.ui.components.common.DescriptionText +import org.schabi.newpipe.ui.theme.AppTheme +import org.schabi.newpipe.util.NavigationHelper +import org.schabi.newpipe.util.external_communication.ShareUtils +import org.schabi.newpipe.util.image.ImageStrategy.choosePreferredImage + +abstract class BaseDescriptionFragment : BaseFragment() { + private val descriptionDisposables = CompositeDisposable() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return ComposeView(requireContext(), null).apply { + setContent { + val nestedScrollInterop = rememberNestedScrollInteropConnection() + AppTheme { + Surface( + modifier = Modifier + .padding(start = 8.dp) + .nestedScroll(nestedScrollInterop) + ) { + Column { + Description(description, uploadDate ?: "") + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(0.dp, 8.dp) + ) { + Metadata() + } + + Tags(tags, ::onTagClick, ::onTagLongClick) + } + } + } + } + } + } + + override fun onDestroy() { + descriptionDisposables.clear() + super.onDestroy() + } + + /** + * Description to display. + */ + protected abstract val description: Description + + /** + * Streaming service ID. Used for tag links. + */ + protected abstract val serviceId: Int + + /** + * List of tags to display below the description. + */ + protected abstract val tags: List + + /** + * Upload date of described piece of data. + */ + protected abstract val uploadDate: String? + + @Composable + protected abstract fun Metadata() + + @Composable + private fun Description(description: Description, uploadDate: String) { + if (description.content.isNullOrEmpty() || description === Description.EMPTY_DESCRIPTION) { + return + } + + var selectable by remember { mutableStateOf(false) } + + Column( + modifier = Modifier.padding(bottom = 8.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = uploadDate, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.weight(1f) + ) + IconButton( + onClick = { selectable = !selectable } + ) { + if (!selectable) { + Icon( + painter = painterResource(R.drawable.ic_select_all), + contentDescription = getString(R.string.description_select_enable) + ) + } else { + Icon( + painter = painterResource(R.drawable.ic_close), + contentDescription = getString(R.string.description_select_disable) + ) + } + } + } + if (selectable) { + Text( + text = stringResource(R.string.description_select_note), + style = MaterialTheme.typography.labelSmall, + modifier = Modifier + .align(Alignment.CenterHorizontally) + ) + } + } + + // TODO: DescriptionText is incomplete (TextLinkifier needs port to AnnotatedString) + if (!selectable) { + DescriptionText( + description = description, + style = MaterialTheme.typography.bodyLarge + ) + } else { + SelectionContainer { + DescriptionText( + description = description, + style = MaterialTheme.typography.bodyLarge + ) + } + } + } + + @Composable + protected fun MetadataItem( + @StringRes type: Int, + content: String, + // TODO: linkify text when TextLinkifier ported to AnnotationString + linkify: Boolean + ) { + if (content.isBlank()) { + return + } + + MetadataItem(type, AnnotatedString(content)) + } + + private fun imageSizeToText(extent: Int): String { + return if (extent < 0) getString(R.string.question_mark) else extent.toString() + } + + @Composable + protected fun MetadataItem(@StringRes type: Int, images: List) { + val preferredImageUrl = choosePreferredImage(images) ?: return + val itemString = buildAnnotatedString { + for (image in images) { + if (this.length > 0) { + append(", ") + } + + val link = LinkAnnotation.Url( + image.url, + TextLinkStyles( + SpanStyle( + textDecoration = TextDecoration.Underline, + fontWeight = FontWeight.Bold.takeIf { preferredImageUrl == image.url } + ) + ) + ) + + withLink(link) { + if (image.height != Image.HEIGHT_UNKNOWN || image.width != Image.WIDTH_UNKNOWN || // if even the resolution level is unknown, ?x? will be shown + image.estimatedResolutionLevel == ResolutionLevel.UNKNOWN + ) { + append(imageSizeToText(image.height)) + append('x') + append(imageSizeToText(image.width)) + } else { + when (image.estimatedResolutionLevel) { + ResolutionLevel.LOW -> append(getString(R.string.image_quality_low)) + ResolutionLevel.MEDIUM -> append(getString(R.string.image_quality_medium)) + ResolutionLevel.HIGH -> append(getString(R.string.image_quality_high)) + else -> {} + } + } + } + } + } + MetadataItem(type, itemString) + } + + private fun onTagClick(tag: String) { + if (parentFragment != null) { + NavigationHelper.openSearchFragment( + parentFragment?.getParentFragmentManager(), + this.serviceId, + tag + ) + } + } + + private fun onTagLongClick(tag: String): Boolean { + ShareUtils.copyToClipboard(requireContext(), tag) + return true + } +} + +@Composable +internal fun MetadataItem(@StringRes type: Int, content: AnnotatedString) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(type).uppercase(), + style = MaterialTheme.typography.bodyMedium, + fontSize = 15.sp, + textAlign = TextAlign.Right, + fontWeight = FontWeight.Bold, + modifier = Modifier.requiredWidth(96.dp) + ) + Text( + text = content, + style = MaterialTheme.typography.bodyMedium, + fontSize = 15.sp + ) + } +} + +@Composable +internal fun Tags( + tags: List, + onClick: (String) -> Unit, + onLongClick: (String) -> Unit +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.metadata_tags).uppercase(), + style = MaterialTheme.typography.titleSmall, + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 6.dp) + ) + FlowRow( + verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + for (tag in tags.sortedWith(CASE_INSENSITIVE_ORDER)) { + Surface( + color = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.tertiary, + modifier = Modifier + .clip(CircleShape) + .combinedClickable( + enabled = true, + interactionSource = null, + indication = ripple(false), + role = Role.Button, + onLongClickLabel = "Copy tag to clipboard", + onLongClick = { onLongClick(tag) }, + onClickLabel = "Search for tag", + onClick = { onClick(tag) } + ) + ) { + Text( + text = tag, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(12.dp, 4.dp) + ) + } + } + } + } +} 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 deleted file mode 100644 index 2b0d22a32ed..00000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java +++ /dev/null @@ -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 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)); - } - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.kt new file mode 100644 index 00000000000..1819823ed2e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.kt @@ -0,0 +1,106 @@ +package org.schabi.newpipe.fragments.detail + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import com.evernote.android.state.State +import org.schabi.newpipe.R +import org.schabi.newpipe.extractor.stream.Description +import org.schabi.newpipe.extractor.stream.StreamExtractor +import org.schabi.newpipe.extractor.stream.StreamExtractor.Privacy +import org.schabi.newpipe.extractor.stream.StreamInfo +import org.schabi.newpipe.util.Localization + +class DescriptionFragment : BaseDescriptionFragment { + @JvmField + @State + var streamInfo: StreamInfo? = null + + constructor(streamInfo: StreamInfo?) { + this.streamInfo = streamInfo + } + + constructor() + + override val description: Description + get() = streamInfo!!.description + + override val serviceId: Int + get() = streamInfo!!.serviceId + + override val tags: List + get() = streamInfo!!.tags + + override val uploadDate: String? + get() = Localization.localizeUploadDate(activity, streamInfo!!.uploadDate.offsetDateTime()) + + @Composable + override fun Metadata() { + if (streamInfo == null) { + return + } + + MetadataItem( + R.string.metadata_category, + streamInfo!!.category, + false + ) + + MetadataItem( + R.string.metadata_licence, + streamInfo!!.licence, + false + ) + + @StringRes val contentRes = when (streamInfo?.privacy) { + Privacy.PUBLIC -> R.string.metadata_privacy_public + Privacy.UNLISTED -> R.string.metadata_privacy_unlisted + Privacy.PRIVATE -> R.string.metadata_privacy_private + Privacy.INTERNAL -> R.string.metadata_privacy_internal + else -> -1 + } + + if (contentRes != -1) { + MetadataItem(R.string.metadata_privacy, getString(contentRes), false) + } + + if (streamInfo!!.ageLimit != StreamExtractor.NO_AGE_LIMIT) { + MetadataItem( + R.string.metadata_age_limit, + streamInfo!!.ageLimit.toString(), + false + ) + } + + if (streamInfo!!.languageInfo != null) { + MetadataItem( + R.string.metadata_language, + streamInfo!!.languageInfo.getDisplayLanguage(Localization.getAppLocale()), + false + ) + } + + MetadataItem( + R.string.metadata_support, + streamInfo!!.supportInfo, + true + ) + MetadataItem( + R.string.metadata_host, + streamInfo!!.host, + true + ) + + MetadataItem( + R.string.metadata_thumbnails, + streamInfo!!.thumbnails + ) + MetadataItem( + R.string.metadata_uploader_avatars, + streamInfo!!.uploaderAvatars + ) + MetadataItem( + R.string.metadata_subchannel_avatars, + streamInfo!!.subChannelAvatars + ) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java deleted file mode 100644 index e3a39813904..00000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java +++ /dev/null @@ -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 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()); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.kt new file mode 100644 index 00000000000..f7b3dcf7031 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.kt @@ -0,0 +1,51 @@ +package org.schabi.newpipe.fragments.list.channel + +import androidx.compose.runtime.Composable +import com.evernote.android.state.State +import org.schabi.newpipe.R +import org.schabi.newpipe.extractor.channel.ChannelInfo +import org.schabi.newpipe.extractor.stream.Description +import org.schabi.newpipe.extractor.stream.StreamExtractor +import org.schabi.newpipe.fragments.detail.BaseDescriptionFragment +import org.schabi.newpipe.util.Localization + +class ChannelAboutFragment : BaseDescriptionFragment { + @State + var channelInfo: ChannelInfo? = null + + constructor(channelInfo: ChannelInfo) { + this.channelInfo = channelInfo + } + + constructor() + + override val description: Description + get() = Description(channelInfo!!.description, Description.PLAIN_TEXT) + + override val serviceId: Int + get() = channelInfo!!.serviceId + + override val tags: List + get() = channelInfo!!.tags + + override val uploadDate: String? + get() = null + + @Composable + override fun Metadata() { + if (channelInfo == null) { + return + } + + if (channelInfo!!.subscriberCount != StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT) { + MetadataItem( + R.string.metadata_subscribers, + Localization.localizeNumber(channelInfo!!.subscriberCount), + false + ) + } + + MetadataItem(R.string.metadata_avatars, channelInfo!!.avatars) + MetadataItem(R.string.metadata_banners, channelInfo!!.banners) + } +} diff --git a/app/src/main/res/layout/chip.xml b/app/src/main/res/layout/chip.xml deleted file mode 100644 index 41e5223a982..00000000000 --- a/app/src/main/res/layout/chip.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - \ 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 deleted file mode 100644 index b20905d4ad2..00000000000 --- a/app/src/main/res/layout/fragment_description.xml +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_metadata.xml b/app/src/main/res/layout/item_metadata.xml deleted file mode 100644 index 251b9e83236..00000000000 --- a/app/src/main/res/layout/item_metadata.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - \ 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 deleted file mode 100644 index febe3ff4a7c..00000000000 --- a/app/src/main/res/layout/item_metadata_tags.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - \ No newline at end of file