Skip to content

Commit

Permalink
Migrate description fragment to Jetpack Compose
Browse files Browse the repository at this point in the history
  • Loading branch information
Isira-Seneviratne committed Aug 31, 2024
1 parent 99996d9 commit de6285b
Show file tree
Hide file tree
Showing 9 changed files with 541 additions and 137 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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<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())));
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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
}
Original file line number Diff line number Diff line change
@@ -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
)
}
}
}
Loading

0 comments on commit de6285b

Please sign in to comment.