mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-03-28 14:27:02 +00:00
Migrate description fragment to Jetpack Compose
This commit is contained in:
parent
99996d940d
commit
de6285b1e2
@ -216,7 +216,7 @@ dependencies {
|
|||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
implementation 'androidx.fragment:fragment-compose:1.8.2'
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
||||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
||||||
|
@ -1,140 +1,36 @@
|
|||||||
package org.schabi.newpipe.fragments.detail;
|
package org.schabi.newpipe.fragments.detail
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
import android.os.Bundle
|
||||||
import static org.schabi.newpipe.util.Localization.getAppLocale;
|
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.VideoDescriptionSection
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
import org.schabi.newpipe.util.KEY_INFO
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
class DescriptionFragment : Fragment() {
|
||||||
import android.view.View;
|
override fun onCreateView(
|
||||||
import android.widget.LinearLayout;
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
import androidx.annotation.NonNull;
|
savedInstanceState: Bundle?
|
||||||
import androidx.annotation.Nullable;
|
) = content {
|
||||||
import androidx.annotation.StringRes;
|
AppTheme {
|
||||||
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
import org.schabi.newpipe.R;
|
VideoDescriptionSection(requireArguments().serializable(KEY_INFO)!!)
|
||||||
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;
|
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
|
|
||||||
public class DescriptionFragment extends BaseDescriptionFragment {
|
|
||||||
|
|
||||||
@State
|
|
||||||
StreamInfo streamInfo;
|
|
||||||
|
|
||||||
public DescriptionFragment(final StreamInfo streamInfo) {
|
|
||||||
this.streamInfo = streamInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DescriptionFragment() {
|
companion object {
|
||||||
// keep empty constructor for IcePick when resuming fragment from memory
|
@JvmStatic
|
||||||
}
|
fun getInstance(streamInfo: StreamInfo) = DescriptionFragment().apply {
|
||||||
|
arguments = bundleOf(KEY_INFO to streamInfo)
|
||||||
|
|
||||||
@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(getContext())));
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -946,7 +946,7 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showDescription) {
|
if (showDescription) {
|
||||||
pageAdapter.updateItem(DESCRIPTION_TAB_TAG, new DescriptionFragment(info));
|
pageAdapter.updateItem(DESCRIPTION_TAB_TAG, DescriptionFragment.getInstance(info));
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.viewPager.setVisibility(View.VISIBLE);
|
binding.viewPager.setVisibility(View.VISIBLE);
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.common
|
||||||
|
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextLayoutResult
|
||||||
|
import androidx.compose.ui.text.TextLinkStyles
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.fromHtml
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DescriptionText(
|
||||||
|
description: Description,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
overflow: TextOverflow = TextOverflow.Clip,
|
||||||
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
|
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||||
|
style: TextStyle = LocalTextStyle.current
|
||||||
|
) {
|
||||||
|
// TODO: Handle links and hashtags, Markdown.
|
||||||
|
val parsedDescription = remember(description) {
|
||||||
|
if (description.type == Description.HTML) {
|
||||||
|
val styles = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.Underline))
|
||||||
|
AnnotatedString.fromHtml(description.content, styles)
|
||||||
|
} else {
|
||||||
|
AnnotatedString(description.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
modifier = modifier,
|
||||||
|
text = parsedDescription,
|
||||||
|
maxLines = maxLines,
|
||||||
|
style = style,
|
||||||
|
overflow = overflow,
|
||||||
|
onTextLayout = onTextLayout
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,106 @@
|
|||||||
|
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
|
||||||
|
import org.schabi.newpipe.util.image.PreferredImageQuality
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ImageMetadataItem(
|
||||||
|
@StringRes title: Int,
|
||||||
|
images: List<Image>,
|
||||||
|
preferredUrl: String? = ImageStrategy.choosePreferredImage(images)
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val imageLinks = remember { convertImagesToLinks(context, images, preferredUrl) }
|
||||||
|
|
||||||
|
MetadataItem(title = title, value = imageLinks)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LazyListScope.imageMetadataItem(@StringRes title: Int, images: List<Image>) {
|
||||||
|
ImageStrategy.choosePreferredImage(images)?.let {
|
||||||
|
item {
|
||||||
|
ImageMetadataItem(title, images, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertImagesToLinks(
|
||||||
|
context: Context,
|
||||||
|
images: List<Image>,
|
||||||
|
preferredUrl: String?
|
||||||
|
): AnnotatedString {
|
||||||
|
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() {
|
||||||
|
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.MEDIUM)
|
||||||
|
val images = listOf(
|
||||||
|
Image("https://example.com/image_low.png", 16, 16, ResolutionLevel.LOW),
|
||||||
|
Image("https://example.com/image_mid.png", 32, 32, ResolutionLevel.MEDIUM)
|
||||||
|
)
|
||||||
|
|
||||||
|
AppTheme {
|
||||||
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
|
ImageMetadataItem(
|
||||||
|
title = R.string.metadata_uploader_avatars,
|
||||||
|
images = images
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
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.lazy.LazyListScope
|
||||||
|
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.font.FontWeight
|
||||||
|
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.weight(0.3f),
|
||||||
|
textAlign = TextAlign.End,
|
||||||
|
text = stringResource(title).uppercase(),
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.weight(0.7f),
|
||||||
|
text = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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,68 @@
|
|||||||
|
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.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SuggestionChip
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
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()
|
||||||
|
.wrapContentSize(Alignment.Center),
|
||||||
|
text = stringResource(R.string.metadata_tags),
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
for (tag in sortedTags) {
|
||||||
|
SuggestionChip(
|
||||||
|
onClick = {
|
||||||
|
NavigationHelper.openSearchFragment(
|
||||||
|
(context as FragmentActivity).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,218 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.video
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
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.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.LocalContext
|
||||||
|
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.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import my.nanihadesuka.compose.LazyColumnScrollbar
|
||||||
|
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.DescriptionText
|
||||||
|
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.components.metadata.metadataItem
|
||||||
|
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 VideoDescriptionSection(streamInfo: StreamInfo) {
|
||||||
|
var isSelectable by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val hasDescription = streamInfo.description != Description.EMPTY_DESCRIPTION
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
|
LazyColumnScrollbar(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(LocalContext.current, it.offsetDateTime())
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.upload_date_text, date),
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if (isSelectable) {
|
||||||
|
SelectionContainer {
|
||||||
|
DescriptionText(description = streamInfo.description)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DescriptionText(description = streamInfo.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streamInfo.ageLimit != StreamExtractor.NO_AGE_LIMIT) {
|
||||||
|
item {
|
||||||
|
MetadataItem(
|
||||||
|
title = R.string.metadata_age_limit,
|
||||||
|
value = streamInfo.ageLimit.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
streamInfo.languageInfo?.let {
|
||||||
|
item {
|
||||||
|
val locale = Localization.getAppLocale(LocalContext.current)
|
||||||
|
MetadataItem(
|
||||||
|
title = R.string.metadata_language,
|
||||||
|
value = it.getDisplayLanguage(locale)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun VideoDescriptionSectionPreview() {
|
||||||
|
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) {
|
||||||
|
VideoDescriptionSection(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -755,7 +755,7 @@
|
|||||||
<string name="select_night_theme_toast">You can select your favorite night theme below</string>
|
<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="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="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_enable">Enable selecting text in the description</string>
|
||||||
<string name="description_select_disable">Disable selecting text in the description</string>
|
<string name="description_select_disable">Disable selecting text in the description</string>
|
||||||
<string name="metadata_category">Category</string>
|
<string name="metadata_category">Category</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user