From de6285b1e2d3cd5ca4078c6736b181d2dc0ec243 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Tue, 27 Aug 2024 08:18:53 +0530 Subject: [PATCH] Migrate description fragment to Jetpack Compose --- app/build.gradle | 2 +- .../fragments/detail/DescriptionFragment.kt | 164 +++---------- .../fragments/detail/VideoDetailFragment.java | 2 +- .../ui/components/common/DescriptionText.kt | 45 ++++ .../components/metadata/ImageMetadataItem.kt | 106 +++++++++ .../ui/components/metadata/MetadataItem.kt | 71 ++++++ .../ui/components/metadata/TagsSection.kt | 68 ++++++ .../video/VideoDescriptionSection.kt | 218 ++++++++++++++++++ app/src/main/res/values/strings.xml | 2 +- 9 files changed, 541 insertions(+), 137 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/common/DescriptionText.kt create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/metadata/ImageMetadataItem.kt create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/metadata/MetadataItem.kt create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/metadata/TagsSection.kt create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/video/VideoDescriptionSection.kt diff --git a/app/build.gradle b/app/build.gradle index 92fd2a7e960..76251f63eac 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' 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 index 581e5415656..a00059f6508 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.kt @@ -1,140 +1,36 @@ -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 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 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()))); +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.VideoDescriptionSection +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) { + VideoDescriptionSection(requireArguments().serializable(KEY_INFO)!!) + } } - - 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)); - } + companion object { + @JvmStatic + fun getInstance(streamInfo: StreamInfo) = DescriptionFragment().apply { + arguments = bundleOf(KEY_INFO to streamInfo) } } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 11a315d691f..abcbc302301 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -946,7 +946,7 @@ private void updateTabs(@NonNull final StreamInfo info) { } if (showDescription) { - pageAdapter.updateItem(DESCRIPTION_TAB_TAG, new DescriptionFragment(info)); + pageAdapter.updateItem(DESCRIPTION_TAB_TAG, DescriptionFragment.getInstance(info)); } binding.viewPager.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/common/DescriptionText.kt b/app/src/main/java/org/schabi/newpipe/ui/components/common/DescriptionText.kt new file mode 100644 index 00000000000..9c79f1a9574 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/common/DescriptionText.kt @@ -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 + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/metadata/ImageMetadataItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/metadata/ImageMetadataItem.kt new file mode 100644 index 00000000000..dce287f55ed --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/metadata/ImageMetadataItem.kt @@ -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, + 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) { + ImageStrategy.choosePreferredImage(images)?.let { + item { + ImageMetadataItem(title, images, it) + } + } +} + +private fun convertImagesToLinks( + context: Context, + images: List, + 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 + ) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/metadata/MetadataItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/metadata/MetadataItem.kt new file mode 100644 index 00000000000..29ead79156a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/metadata/MetadataItem.kt @@ -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") + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/metadata/TagsSection.kt b/app/src/main/java/org/schabi/newpipe/ui/components/metadata/TagsSection.kt new file mode 100644 index 00000000000..cb355eebdae --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/metadata/TagsSection.kt @@ -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) { + 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")) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/video/VideoDescriptionSection.kt b/app/src/main/java/org/schabi/newpipe/ui/components/video/VideoDescriptionSection.kt new file mode 100644 index 00000000000..122e09017db --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/video/VideoDescriptionSection.kt @@ -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 example description", Description.HTML) + + AppTheme { + Surface(color = MaterialTheme.colorScheme.background) { + VideoDescriptionSection(info) + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 938a2497d00..6a7fe277964 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -755,7 +755,7 @@ You can select your favorite night theme below This option is only available if %s is selected for Theme Download has started - You can now select text inside the description. Note that the page may flicker and links may not be clickable while in selection mode. + You can now select text inside the description. Enable selecting text in the description Disable selecting text in the description Category