Skip to content

Commit 63c9308

Browse files
authored
Merge pull request #5946 from Stypox/metadata
Show content metadata below the description
2 parents f739ed7 + 9e94c81 commit 63c9308

File tree

8 files changed

+388
-27
lines changed

8 files changed

+388
-27
lines changed

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

Lines changed: 189 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,46 @@
44
import android.view.LayoutInflater;
55
import android.view.View;
66
import android.view.ViewGroup;
7-
import android.widget.TextView;
7+
import android.widget.LinearLayout;
88

99
import androidx.annotation.NonNull;
1010
import androidx.annotation.Nullable;
11+
import androidx.annotation.StringRes;
12+
import androidx.appcompat.widget.TooltipCompat;
1113
import androidx.core.text.HtmlCompat;
1214

15+
import com.google.android.material.chip.Chip;
16+
1317
import org.schabi.newpipe.BaseFragment;
18+
import org.schabi.newpipe.R;
1419
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
20+
import org.schabi.newpipe.databinding.ItemMetadataBinding;
21+
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
1522
import org.schabi.newpipe.extractor.stream.Description;
1623
import org.schabi.newpipe.extractor.stream.StreamInfo;
1724
import org.schabi.newpipe.util.Localization;
25+
import org.schabi.newpipe.util.NavigationHelper;
26+
import org.schabi.newpipe.util.ShareUtils;
1827
import org.schabi.newpipe.util.TextLinkifier;
1928

29+
import java.util.ArrayList;
30+
import java.util.Collections;
31+
import java.util.List;
32+
2033
import icepick.State;
2134
import io.reactivex.rxjava3.disposables.Disposable;
2235

2336
import static android.text.TextUtils.isEmpty;
37+
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
38+
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
2439

2540
public class DescriptionFragment extends BaseFragment {
2641

2742
@State
2843
StreamInfo streamInfo = null;
2944
@Nullable
3045
Disposable descriptionDisposable = null;
46+
FragmentDescriptionBinding binding;
3147

3248
public DescriptionFragment() {
3349
}
@@ -40,11 +56,11 @@ public DescriptionFragment(final StreamInfo streamInfo) {
4056
public View onCreateView(@NonNull final LayoutInflater inflater,
4157
@Nullable final ViewGroup container,
4258
@Nullable final Bundle savedInstanceState) {
43-
final FragmentDescriptionBinding binding =
44-
FragmentDescriptionBinding.inflate(inflater, container, false);
59+
binding = FragmentDescriptionBinding.inflate(inflater, container, false);
4560
if (streamInfo != null) {
46-
setupUploadDate(binding.detailUploadDateView);
47-
setupDescription(binding.detailDescriptionView);
61+
setupUploadDate();
62+
setupDescription();
63+
setupMetadata(inflater, binding.detailMetadataLayout);
4864
}
4965
return binding.getRoot();
5066
}
@@ -57,37 +73,197 @@ public void onDestroy() {
5773
}
5874
}
5975

60-
private void setupUploadDate(final TextView uploadDateTextView) {
76+
77+
private void setupUploadDate() {
6178
if (streamInfo.getUploadDate() != null) {
62-
uploadDateTextView.setText(Localization
79+
binding.detailUploadDateView.setText(Localization
6380
.localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime()));
6481
} else {
65-
uploadDateTextView.setVisibility(View.GONE);
82+
binding.detailUploadDateView.setVisibility(View.GONE);
6683
}
6784
}
6885

69-
private void setupDescription(final TextView descriptionTextView) {
86+
87+
private void setupDescription() {
7088
final Description description = streamInfo.getDescription();
7189
if (description == null || isEmpty(description.getContent())
7290
|| description == Description.emptyDescription) {
73-
descriptionTextView.setText("");
91+
binding.detailDescriptionView.setVisibility(View.GONE);
92+
binding.detailSelectDescriptionButton.setVisibility(View.GONE);
7493
return;
7594
}
7695

96+
// start with disabled state. This also loads description content (!)
97+
disableDescriptionSelection();
98+
99+
binding.detailSelectDescriptionButton.setOnClickListener(v -> {
100+
if (binding.detailDescriptionNoteView.getVisibility() == View.VISIBLE) {
101+
disableDescriptionSelection();
102+
} else {
103+
// enable selection only when button is clicked to prevent flickering
104+
enableDescriptionSelection();
105+
}
106+
});
107+
}
108+
109+
private void enableDescriptionSelection() {
110+
binding.detailDescriptionNoteView.setVisibility(View.VISIBLE);
111+
binding.detailDescriptionView.setTextIsSelectable(true);
112+
113+
final String buttonLabel = getString(R.string.description_select_disable);
114+
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
115+
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
116+
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close);
117+
}
118+
119+
private void disableDescriptionSelection() {
120+
// show description content again, otherwise some links are not clickable
121+
loadDescriptionContent();
122+
123+
binding.detailDescriptionNoteView.setVisibility(View.GONE);
124+
binding.detailDescriptionView.setTextIsSelectable(false);
125+
126+
final String buttonLabel = getString(R.string.description_select_enable);
127+
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
128+
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
129+
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all);
130+
}
131+
132+
private void loadDescriptionContent() {
133+
final Description description = streamInfo.getDescription();
77134
switch (description.getType()) {
78135
case Description.HTML:
79136
descriptionDisposable = TextLinkifier.createLinksFromHtmlBlock(requireContext(),
80-
description.getContent(), descriptionTextView,
137+
description.getContent(), binding.detailDescriptionView,
81138
HtmlCompat.FROM_HTML_MODE_LEGACY);
82139
break;
83140
case Description.MARKDOWN:
84141
descriptionDisposable = TextLinkifier.createLinksFromMarkdownText(requireContext(),
85-
description.getContent(), descriptionTextView);
142+
description.getContent(), binding.detailDescriptionView);
86143
break;
87144
case Description.PLAIN_TEXT: default:
88145
descriptionDisposable = TextLinkifier.createLinksFromPlainText(requireContext(),
89-
description.getContent(), descriptionTextView);
146+
description.getContent(), binding.detailDescriptionView);
90147
break;
91148
}
92149
}
150+
151+
152+
private void setupMetadata(final LayoutInflater inflater,
153+
final LinearLayout layout) {
154+
addMetadataItem(inflater, layout, false,
155+
R.string.metadata_category, streamInfo.getCategory());
156+
157+
addTagsMetadataItem(inflater, layout);
158+
159+
addMetadataItem(inflater, layout, false,
160+
R.string.metadata_licence, streamInfo.getLicence());
161+
162+
addPrivacyMetadataItem(inflater, layout);
163+
164+
if (streamInfo.getAgeLimit() != NO_AGE_LIMIT) {
165+
addMetadataItem(inflater, layout, false,
166+
R.string.metadata_age_limit, String.valueOf(streamInfo.getAgeLimit()));
167+
}
168+
169+
if (streamInfo.getLanguageInfo() != null) {
170+
addMetadataItem(inflater, layout, false,
171+
R.string.metadata_language, streamInfo.getLanguageInfo().getDisplayLanguage());
172+
}
173+
174+
addMetadataItem(inflater, layout, true,
175+
R.string.metadata_support, streamInfo.getSupportInfo());
176+
addMetadataItem(inflater, layout, true,
177+
R.string.metadata_host, streamInfo.getHost());
178+
addMetadataItem(inflater, layout, true,
179+
R.string.metadata_thumbnail_url, streamInfo.getThumbnailUrl());
180+
}
181+
182+
private void addMetadataItem(final LayoutInflater inflater,
183+
final LinearLayout layout,
184+
final boolean linkifyContent,
185+
@StringRes final int type,
186+
@Nullable final String content) {
187+
if (isBlank(content)) {
188+
return;
189+
}
190+
191+
final ItemMetadataBinding itemBinding
192+
= ItemMetadataBinding.inflate(inflater, layout, false);
193+
194+
itemBinding.metadataTypeView.setText(type);
195+
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
196+
ShareUtils.copyToClipboard(requireContext(), content);
197+
return true;
198+
});
199+
200+
if (linkifyContent) {
201+
TextLinkifier.createLinksFromPlainText(requireContext(),
202+
content, itemBinding.metadataContentView);
203+
} else {
204+
itemBinding.metadataContentView.setText(content);
205+
}
206+
207+
layout.addView(itemBinding.getRoot());
208+
}
209+
210+
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
211+
if (streamInfo.getTags() != null && !streamInfo.getTags().isEmpty()) {
212+
final ItemMetadataTagsBinding itemBinding
213+
= ItemMetadataTagsBinding.inflate(inflater, layout, false);
214+
215+
final List<String> tags = new ArrayList<>(streamInfo.getTags());
216+
Collections.sort(tags);
217+
for (final String tag : tags) {
218+
final Chip chip = (Chip) inflater.inflate(R.layout.chip,
219+
itemBinding.metadataTagsChips, false);
220+
chip.setText(tag);
221+
chip.setOnClickListener(this::onTagClick);
222+
chip.setOnLongClickListener(this::onTagLongClick);
223+
itemBinding.metadataTagsChips.addView(chip);
224+
}
225+
226+
layout.addView(itemBinding.getRoot());
227+
}
228+
}
229+
230+
private void onTagClick(final View chip) {
231+
if (getParentFragment() != null) {
232+
NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(),
233+
streamInfo.getServiceId(), ((Chip) chip).getText().toString());
234+
}
235+
}
236+
237+
private boolean onTagLongClick(final View chip) {
238+
ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString());
239+
return true;
240+
}
241+
242+
private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
243+
if (streamInfo.getPrivacy() != null) {
244+
@StringRes final int contentRes;
245+
switch (streamInfo.getPrivacy()) {
246+
case PUBLIC:
247+
contentRes = R.string.metadata_privacy_public;
248+
break;
249+
case UNLISTED:
250+
contentRes = R.string.metadata_privacy_unlisted;
251+
break;
252+
case PRIVATE:
253+
contentRes = R.string.metadata_privacy_private;
254+
break;
255+
case INTERNAL:
256+
contentRes = R.string.metadata_privacy_internal;
257+
break;
258+
case OTHER: default:
259+
contentRes = 0;
260+
break;
261+
}
262+
263+
if (contentRes != 0) {
264+
addMetadataItem(inflater, layout, false,
265+
R.string.metadata_privacy, getString(contentRes));
266+
}
267+
}
268+
}
93269
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24"
6+
android:tint="#ffffff">
7+
<path
8+
android:fillColor="#ffffff"
9+
android:pathData="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z"/>
10+
</vector>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24"
6+
android:tint="#000000">
7+
<path
8+
android:fillColor="#000000"
9+
android:pathData="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z"/>
10+
</vector>

app/src/main/res/layout/chip.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- This is used to inflate a chip with a Material theme, otherwise it would crash -->
3+
<!-- Theme.MaterialComponents.DayNight is used to guarantee auto day/night switching -->
4+
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
5+
xmlns:tools="http://schemas.android.com/tools"
6+
android:layout_width="wrap_content"
7+
android:layout_height="wrap_content"
8+
android:theme="@style/Theme.MaterialComponents.DayNight.Bridge"
9+
tools:text="I'm a correctly themed chip!" />

0 commit comments

Comments
 (0)