Skip to content

Commit de6285b

Browse files
Migrate description fragment to Jetpack Compose
1 parent 99996d9 commit de6285b

File tree

9 files changed

+541
-137
lines changed

9 files changed

+541
-137
lines changed

app/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ dependencies {
216216
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
217217
implementation 'androidx.core:core-ktx:1.12.0'
218218
implementation 'androidx.documentfile:documentfile:1.0.1'
219-
implementation 'androidx.fragment:fragment-ktx:1.6.2'
219+
implementation 'androidx.fragment:fragment-compose:1.8.2'
220220
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
221221
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
222222
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
Original file line numberDiff line numberDiff line change
@@ -1,140 +1,36 @@
1-
package org.schabi.newpipe.fragments.detail;
2-
3-
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
4-
import static org.schabi.newpipe.util.Localization.getAppLocale;
5-
6-
import android.view.LayoutInflater;
7-
import android.view.View;
8-
import android.widget.LinearLayout;
9-
10-
import androidx.annotation.NonNull;
11-
import androidx.annotation.Nullable;
12-
import androidx.annotation.StringRes;
13-
14-
import org.schabi.newpipe.R;
15-
import org.schabi.newpipe.extractor.StreamingService;
16-
import org.schabi.newpipe.extractor.stream.Description;
17-
import org.schabi.newpipe.extractor.stream.StreamInfo;
18-
import org.schabi.newpipe.util.Localization;
19-
20-
import java.util.List;
21-
22-
import icepick.State;
23-
24-
public class DescriptionFragment extends BaseDescriptionFragment {
25-
26-
@State
27-
StreamInfo streamInfo;
28-
29-
public DescriptionFragment(final StreamInfo streamInfo) {
30-
this.streamInfo = streamInfo;
31-
}
32-
33-
public DescriptionFragment() {
34-
// keep empty constructor for IcePick when resuming fragment from memory
35-
}
36-
37-
38-
@Nullable
39-
@Override
40-
protected Description getDescription() {
41-
return streamInfo.getDescription();
42-
}
43-
44-
@NonNull
45-
@Override
46-
protected StreamingService getService() {
47-
return streamInfo.getService();
48-
}
49-
50-
@Override
51-
protected int getServiceId() {
52-
return streamInfo.getServiceId();
53-
}
54-
55-
@NonNull
56-
@Override
57-
protected String getStreamUrl() {
58-
return streamInfo.getUrl();
59-
}
60-
61-
@NonNull
62-
@Override
63-
public List<String> getTags() {
64-
return streamInfo.getTags();
65-
}
66-
67-
@Override
68-
protected void setupMetadata(final LayoutInflater inflater,
69-
final LinearLayout layout) {
70-
if (streamInfo != null && streamInfo.getUploadDate() != null) {
71-
binding.detailUploadDateView.setText(Localization
72-
.localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime()));
73-
} else {
74-
binding.detailUploadDateView.setVisibility(View.GONE);
75-
}
76-
77-
if (streamInfo == null) {
78-
return;
79-
}
80-
81-
addMetadataItem(inflater, layout, false, R.string.metadata_category,
82-
streamInfo.getCategory());
83-
84-
addMetadataItem(inflater, layout, false, R.string.metadata_licence,
85-
streamInfo.getLicence());
86-
87-
addPrivacyMetadataItem(inflater, layout);
88-
89-
if (streamInfo.getAgeLimit() != NO_AGE_LIMIT) {
90-
addMetadataItem(inflater, layout, false, R.string.metadata_age_limit,
91-
String.valueOf(streamInfo.getAgeLimit()));
92-
}
93-
94-
if (streamInfo.getLanguageInfo() != null) {
95-
addMetadataItem(inflater, layout, false, R.string.metadata_language,
96-
streamInfo.getLanguageInfo().getDisplayLanguage(getAppLocale(getContext())));
1+
package org.schabi.newpipe.fragments.detail
2+
3+
import android.os.Bundle
4+
import android.view.LayoutInflater
5+
import android.view.ViewGroup
6+
import androidx.compose.material3.MaterialTheme
7+
import androidx.compose.material3.Surface
8+
import androidx.core.os.bundleOf
9+
import androidx.fragment.app.Fragment
10+
import androidx.fragment.compose.content
11+
import org.schabi.newpipe.extractor.stream.StreamInfo
12+
import org.schabi.newpipe.ktx.serializable
13+
import org.schabi.newpipe.ui.components.video.VideoDescriptionSection
14+
import org.schabi.newpipe.ui.theme.AppTheme
15+
import org.schabi.newpipe.util.KEY_INFO
16+
17+
class DescriptionFragment : Fragment() {
18+
override fun onCreateView(
19+
inflater: LayoutInflater,
20+
container: ViewGroup?,
21+
savedInstanceState: Bundle?
22+
) = content {
23+
AppTheme {
24+
Surface(color = MaterialTheme.colorScheme.background) {
25+
VideoDescriptionSection(requireArguments().serializable(KEY_INFO)!!)
26+
}
9727
}
98-
99-
addMetadataItem(inflater, layout, true, R.string.metadata_support,
100-
streamInfo.getSupportInfo());
101-
addMetadataItem(inflater, layout, true, R.string.metadata_host,
102-
streamInfo.getHost());
103-
104-
addImagesMetadataItem(inflater, layout, R.string.metadata_thumbnails,
105-
streamInfo.getThumbnails());
106-
addImagesMetadataItem(inflater, layout, R.string.metadata_uploader_avatars,
107-
streamInfo.getUploaderAvatars());
108-
addImagesMetadataItem(inflater, layout, R.string.metadata_subchannel_avatars,
109-
streamInfo.getSubChannelAvatars());
11028
}
11129

112-
private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
113-
if (streamInfo.getPrivacy() != null) {
114-
@StringRes final int contentRes;
115-
switch (streamInfo.getPrivacy()) {
116-
case PUBLIC:
117-
contentRes = R.string.metadata_privacy_public;
118-
break;
119-
case UNLISTED:
120-
contentRes = R.string.metadata_privacy_unlisted;
121-
break;
122-
case PRIVATE:
123-
contentRes = R.string.metadata_privacy_private;
124-
break;
125-
case INTERNAL:
126-
contentRes = R.string.metadata_privacy_internal;
127-
break;
128-
case OTHER:
129-
default:
130-
contentRes = 0;
131-
break;
132-
}
133-
134-
if (contentRes != 0) {
135-
addMetadataItem(inflater, layout, false, R.string.metadata_privacy,
136-
getString(contentRes));
137-
}
30+
companion object {
31+
@JvmStatic
32+
fun getInstance(streamInfo: StreamInfo) = DescriptionFragment().apply {
33+
arguments = bundleOf(KEY_INFO to streamInfo)
13834
}
13935
}
14036
}

app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,7 @@ private void updateTabs(@NonNull final StreamInfo info) {
946946
}
947947

948948
if (showDescription) {
949-
pageAdapter.updateItem(DESCRIPTION_TAB_TAG, new DescriptionFragment(info));
949+
pageAdapter.updateItem(DESCRIPTION_TAB_TAG, DescriptionFragment.getInstance(info));
950950
}
951951

952952
binding.viewPager.setVisibility(View.VISIBLE);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.schabi.newpipe.ui.components.common
2+
3+
import androidx.compose.material3.LocalTextStyle
4+
import androidx.compose.material3.Text
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.runtime.remember
7+
import androidx.compose.ui.Modifier
8+
import androidx.compose.ui.text.AnnotatedString
9+
import androidx.compose.ui.text.SpanStyle
10+
import androidx.compose.ui.text.TextLayoutResult
11+
import androidx.compose.ui.text.TextLinkStyles
12+
import androidx.compose.ui.text.TextStyle
13+
import androidx.compose.ui.text.fromHtml
14+
import androidx.compose.ui.text.style.TextDecoration
15+
import androidx.compose.ui.text.style.TextOverflow
16+
import org.schabi.newpipe.extractor.stream.Description
17+
18+
@Composable
19+
fun DescriptionText(
20+
description: Description,
21+
modifier: Modifier = Modifier,
22+
overflow: TextOverflow = TextOverflow.Clip,
23+
maxLines: Int = Int.MAX_VALUE,
24+
onTextLayout: (TextLayoutResult) -> Unit = {},
25+
style: TextStyle = LocalTextStyle.current
26+
) {
27+
// TODO: Handle links and hashtags, Markdown.
28+
val parsedDescription = remember(description) {
29+
if (description.type == Description.HTML) {
30+
val styles = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.Underline))
31+
AnnotatedString.fromHtml(description.content, styles)
32+
} else {
33+
AnnotatedString(description.content)
34+
}
35+
}
36+
37+
Text(
38+
modifier = modifier,
39+
text = parsedDescription,
40+
maxLines = maxLines,
41+
style = style,
42+
overflow = overflow,
43+
onTextLayout = onTextLayout
44+
)
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package org.schabi.newpipe.ui.components.metadata
2+
3+
import android.content.Context
4+
import android.content.res.Configuration
5+
import androidx.annotation.StringRes
6+
import androidx.compose.foundation.lazy.LazyListScope
7+
import androidx.compose.material3.MaterialTheme
8+
import androidx.compose.material3.Surface
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.runtime.remember
11+
import androidx.compose.ui.platform.LocalContext
12+
import androidx.compose.ui.text.AnnotatedString
13+
import androidx.compose.ui.text.LinkAnnotation
14+
import androidx.compose.ui.text.SpanStyle
15+
import androidx.compose.ui.text.TextLinkStyles
16+
import androidx.compose.ui.text.buildAnnotatedString
17+
import androidx.compose.ui.text.font.FontWeight
18+
import androidx.compose.ui.text.style.TextDecoration
19+
import androidx.compose.ui.text.withLink
20+
import androidx.compose.ui.text.withStyle
21+
import androidx.compose.ui.tooling.preview.Preview
22+
import org.schabi.newpipe.R
23+
import org.schabi.newpipe.extractor.Image
24+
import org.schabi.newpipe.extractor.Image.ResolutionLevel
25+
import org.schabi.newpipe.ui.theme.AppTheme
26+
import org.schabi.newpipe.util.image.ImageStrategy
27+
import org.schabi.newpipe.util.image.PreferredImageQuality
28+
29+
@Composable
30+
fun ImageMetadataItem(
31+
@StringRes title: Int,
32+
images: List<Image>,
33+
preferredUrl: String? = ImageStrategy.choosePreferredImage(images)
34+
) {
35+
val context = LocalContext.current
36+
val imageLinks = remember { convertImagesToLinks(context, images, preferredUrl) }
37+
38+
MetadataItem(title = title, value = imageLinks)
39+
}
40+
41+
fun LazyListScope.imageMetadataItem(@StringRes title: Int, images: List<Image>) {
42+
ImageStrategy.choosePreferredImage(images)?.let {
43+
item {
44+
ImageMetadataItem(title, images, it)
45+
}
46+
}
47+
}
48+
49+
private fun convertImagesToLinks(
50+
context: Context,
51+
images: List<Image>,
52+
preferredUrl: String?
53+
): AnnotatedString {
54+
fun imageSizeToText(size: Int): String {
55+
return if (size == Image.HEIGHT_UNKNOWN) context.getString(R.string.question_mark)
56+
else size.toString()
57+
}
58+
59+
return buildAnnotatedString {
60+
for (image in images) {
61+
if (length != 0) {
62+
append(", ")
63+
}
64+
65+
val linkStyle = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.Underline))
66+
withLink(LinkAnnotation.Url(image.url, linkStyle)) {
67+
val weight = if (image.url == preferredUrl) FontWeight.Bold else FontWeight.Normal
68+
69+
withStyle(SpanStyle(fontWeight = weight)) {
70+
// if even the resolution level is unknown, ?x? will be shown
71+
if (image.height != Image.HEIGHT_UNKNOWN || image.width != Image.WIDTH_UNKNOWN ||
72+
image.estimatedResolutionLevel == ResolutionLevel.UNKNOWN
73+
) {
74+
append("${imageSizeToText(image.width)}x${imageSizeToText(image.height)}")
75+
} else if (image.estimatedResolutionLevel == ResolutionLevel.LOW) {
76+
append(context.getString(R.string.image_quality_low))
77+
} else if (image.estimatedResolutionLevel == ResolutionLevel.MEDIUM) {
78+
append(context.getString(R.string.image_quality_medium))
79+
} else if (image.estimatedResolutionLevel == ResolutionLevel.HIGH) {
80+
append(context.getString(R.string.image_quality_high))
81+
}
82+
}
83+
}
84+
}
85+
}
86+
}
87+
88+
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
89+
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
90+
@Composable
91+
private fun ImageMetadataItemPreview() {
92+
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.MEDIUM)
93+
val images = listOf(
94+
Image("https://example.com/image_low.png", 16, 16, ResolutionLevel.LOW),
95+
Image("https://example.com/image_mid.png", 32, 32, ResolutionLevel.MEDIUM)
96+
)
97+
98+
AppTheme {
99+
Surface(color = MaterialTheme.colorScheme.background) {
100+
ImageMetadataItem(
101+
title = R.string.metadata_uploader_avatars,
102+
images = images
103+
)
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)