mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-11-23 02:35:21 +01: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.core:core-ktx:1.12.0'
|
||||
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-viewmodel-ktx:${androidxLifecycleVersion}"
|
||||
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 static org.schabi.newpipe.util.Localization.getAppLocale;
|
||||
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.VideoDescriptionSection
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.KEY_INFO
|
||||
|
||||
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 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;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
public class DescriptionFragment extends BaseDescriptionFragment {
|
||||
|
||||
@State
|
||||
StreamInfo streamInfo;
|
||||
|
||||
public DescriptionFragment(final StreamInfo streamInfo) {
|
||||
this.streamInfo = streamInfo;
|
||||
}
|
||||
|
||||
public DescriptionFragment() {
|
||||
// keep empty constructor for IcePick 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(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;
|
||||
class DescriptionFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
) = content {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
VideoDescriptionSection(requireArguments().serializable(KEY_INFO)!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (contentRes != 0) {
|
||||
addMetadataItem(inflater, layout, false, R.string.metadata_privacy,
|
||||
getString(contentRes));
|
||||
}
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getInstance(streamInfo: StreamInfo) = DescriptionFragment().apply {
|
||||
arguments = bundleOf(KEY_INFO to streamInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -946,7 +946,7 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
if (showDescription) {
|
||||
pageAdapter.updateItem(DESCRIPTION_TAB_TAG, new DescriptionFragment(info));
|
||||
pageAdapter.updateItem(DESCRIPTION_TAB_TAG, DescriptionFragment.getInstance(info));
|
||||
}
|
||||
|
||||
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="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…
Reference in New Issue
Block a user