Skip to content

Commit 0eae9e7

Browse files
authored
Merge pull request #9182 from Theta-Dev/channel-tabs
Add support for channel tabs
2 parents 7e2ab0d + 031b893 commit 0eae9e7

42 files changed

Lines changed: 1789 additions & 900 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ dependencies {
197197
// name and the commit hash with the commit hash of the (pushed) commit you want to test
198198
// This works thanks to JitPack: https://jitpack.io/
199199
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
200-
implementation 'com.github.TeamNewPipe:NewPipeExtractor:340095515d45ecbee576872c7198992ebd8e4f08'
200+
implementation 'com.github.TeamNewPipe:NewPipeExtractor:95a3cc0a173bba28c179f9f9503b1010ec6bff21'
201201
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
202202

203203
/** Checkstyle **/

app/src/androidTest/java/org/schabi/newpipe/local/subscription/SubscriptionManagerTest.java

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,13 @@
1010
import org.junit.Test;
1111
import org.schabi.newpipe.database.AppDatabase;
1212
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
13-
import org.schabi.newpipe.database.stream.model.StreamEntity;
1413
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
1514
import org.schabi.newpipe.extractor.channel.ChannelInfo;
1615
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
17-
import org.schabi.newpipe.extractor.localization.DateWrapper;
18-
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
19-
import org.schabi.newpipe.extractor.stream.StreamType;
2016
import org.schabi.newpipe.testUtil.TestDatabase;
2117
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule;
2218

2319
import java.io.IOException;
24-
import java.time.OffsetDateTime;
25-
import java.util.Comparator;
2620
import java.util.List;
2721

2822
public class SubscriptionManagerTest {
@@ -58,7 +52,7 @@ public void testInsert() throws ExtractionException, IOException {
5852
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/3blue1brown");
5953
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
6054

61-
manager.insertSubscription(subscription, info);
55+
manager.insertSubscription(subscription);
6256
final SubscriptionEntity readSubscription = getAssertOneSubscriptionEntity();
6357

6458
// the uid has changed, since the uid is chosen upon inserting, but the rest should match
@@ -76,7 +70,7 @@ public void testUpdateNotificationMode() throws ExtractionException, IOException
7670
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
7771
subscription.setNotificationMode(0);
7872

79-
manager.insertSubscription(subscription, info);
73+
manager.insertSubscription(subscription);
8074
manager.updateNotificationMode(subscription.getServiceId(), subscription.getUrl(), 1)
8175
.blockingAwait();
8276
final SubscriptionEntity anotherSubscription = getAssertOneSubscriptionEntity();
@@ -85,35 +79,4 @@ public void testUpdateNotificationMode() throws ExtractionException, IOException
8579
assertEquals(subscription.getUrl(), anotherSubscription.getUrl());
8680
assertEquals(1, anotherSubscription.getNotificationMode());
8781
}
88-
89-
@Test
90-
public void testRememberRecentStreams() throws ExtractionException, IOException {
91-
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/Polyphia");
92-
final List<StreamInfoItem> relatedItems = List.of(
93-
new StreamInfoItem(0, "a", "b", StreamType.VIDEO_STREAM),
94-
new StreamInfoItem(1, "c", "d", StreamType.AUDIO_STREAM),
95-
new StreamInfoItem(2, "e", "f", StreamType.AUDIO_LIVE_STREAM),
96-
new StreamInfoItem(3, "g", "h", StreamType.LIVE_STREAM));
97-
relatedItems.forEach(item -> {
98-
// these two fields must be non-null for the insert to succeed
99-
item.setUploaderUrl(info.getUrl());
100-
item.setUploaderName(info.getName());
101-
// the upload date must not be too much in the past for the item to actually be inserted
102-
item.setUploadDate(new DateWrapper(OffsetDateTime.now()));
103-
});
104-
info.setRelatedItems(relatedItems);
105-
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
106-
107-
manager.insertSubscription(subscription, info);
108-
final List<StreamEntity> streams = database.streamDAO().getAll().blockingFirst();
109-
110-
assertEquals(4, streams.size());
111-
streams.sort(Comparator.comparing(StreamEntity::getServiceId));
112-
for (int i = 0; i < 4; i++) {
113-
assertEquals(relatedItems.get(0).getServiceId(), streams.get(0).getServiceId());
114-
assertEquals(relatedItems.get(0).getUrl(), streams.get(0).getUrl());
115-
assertEquals(relatedItems.get(0).getName(), streams.get(0).getTitle());
116-
assertEquals(relatedItems.get(0).getStreamType(), streams.get(0).getStreamType());
117-
}
118-
}
11982
}

app/src/main/java/org/schabi/newpipe/RouterActivity.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,19 @@
6565
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
6666
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
6767
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
68+
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
6869
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
6970
import org.schabi.newpipe.extractor.stream.StreamInfo;
7071
import org.schabi.newpipe.ktx.ExceptionUtils;
7172
import org.schabi.newpipe.local.dialog.PlaylistDialog;
7273
import org.schabi.newpipe.player.PlayerType;
7374
import org.schabi.newpipe.player.helper.PlayerHelper;
7475
import org.schabi.newpipe.player.helper.PlayerHolder;
75-
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
76+
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
7677
import org.schabi.newpipe.player.playqueue.PlayQueue;
7778
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
7879
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
80+
import org.schabi.newpipe.util.ChannelTabHelper;
7981
import org.schabi.newpipe.util.Constants;
8082
import org.schabi.newpipe.util.DeviceUtils;
8183
import org.schabi.newpipe.util.ExtractorHelper;
@@ -1022,7 +1024,16 @@ public Consumer<Info> getResultHandler(final Choice choice) {
10221024
}
10231025
playQueue = new SinglePlayQueue((StreamInfo) info);
10241026
} else if (info instanceof ChannelInfo) {
1025-
playQueue = new ChannelPlayQueue((ChannelInfo) info);
1027+
final Optional<ListLinkHandler> playableTab = ((ChannelInfo) info).getTabs()
1028+
.stream()
1029+
.filter(ChannelTabHelper::isStreamsTab)
1030+
.findFirst();
1031+
1032+
if (playableTab.isPresent()) {
1033+
playQueue = new ChannelTabPlayQueue(info.getServiceId(), playableTab.get());
1034+
} else {
1035+
return; // there is no playable tab
1036+
}
10261037
} else if (info instanceof PlaylistInfo) {
10271038
playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
10281039
} else {
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package org.schabi.newpipe.fragments.detail;
2+
3+
import static android.text.TextUtils.isEmpty;
4+
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
5+
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
6+
7+
import android.os.Bundle;
8+
import android.view.LayoutInflater;
9+
import android.view.View;
10+
import android.view.ViewGroup;
11+
import android.widget.LinearLayout;
12+
13+
import androidx.annotation.NonNull;
14+
import androidx.annotation.Nullable;
15+
import androidx.annotation.StringRes;
16+
import androidx.appcompat.widget.TooltipCompat;
17+
import androidx.core.text.HtmlCompat;
18+
19+
import com.google.android.material.chip.Chip;
20+
21+
import org.schabi.newpipe.BaseFragment;
22+
import org.schabi.newpipe.R;
23+
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
24+
import org.schabi.newpipe.databinding.ItemMetadataBinding;
25+
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
26+
import org.schabi.newpipe.extractor.StreamingService;
27+
import org.schabi.newpipe.extractor.stream.Description;
28+
import org.schabi.newpipe.util.NavigationHelper;
29+
import org.schabi.newpipe.util.external_communication.ShareUtils;
30+
import org.schabi.newpipe.util.text.TextLinkifier;
31+
32+
import java.util.List;
33+
34+
import io.reactivex.rxjava3.disposables.CompositeDisposable;
35+
36+
public abstract class BaseDescriptionFragment extends BaseFragment {
37+
private final CompositeDisposable descriptionDisposables = new CompositeDisposable();
38+
protected FragmentDescriptionBinding binding;
39+
40+
@Override
41+
public View onCreateView(@NonNull final LayoutInflater inflater,
42+
@Nullable final ViewGroup container,
43+
@Nullable final Bundle savedInstanceState) {
44+
binding = FragmentDescriptionBinding.inflate(inflater, container, false);
45+
setupDescription();
46+
setupMetadata(inflater, binding.detailMetadataLayout);
47+
addTagsMetadataItem(inflater, binding.detailMetadataLayout);
48+
return binding.getRoot();
49+
}
50+
51+
@Override
52+
public void onDestroy() {
53+
descriptionDisposables.clear();
54+
super.onDestroy();
55+
}
56+
57+
/**
58+
* Get the description to display.
59+
* @return description object
60+
*/
61+
@Nullable
62+
protected abstract Description getDescription();
63+
64+
/**
65+
* Get the streaming service. Used for generating description links.
66+
* @return streaming service
67+
*/
68+
@Nullable
69+
protected abstract StreamingService getService();
70+
71+
/**
72+
* Get the streaming service ID. Used for tag links.
73+
* @return service ID
74+
*/
75+
protected abstract int getServiceId();
76+
77+
/**
78+
* Get the URL of the described video or audio, used to generate description links.
79+
* @return stream URL
80+
*/
81+
@Nullable
82+
protected abstract String getStreamUrl();
83+
84+
/**
85+
* Get the list of tags to display below the description.
86+
* @return tag list
87+
*/
88+
@Nullable
89+
public abstract List<String> getTags();
90+
91+
/**
92+
* Add additional metadata to display.
93+
* @param inflater LayoutInflater
94+
* @param layout detailMetadataLayout
95+
*/
96+
protected abstract void setupMetadata(LayoutInflater inflater, LinearLayout layout);
97+
98+
private void setupDescription() {
99+
final Description description = getDescription();
100+
if (description == null || isEmpty(description.getContent())
101+
|| description == Description.EMPTY_DESCRIPTION) {
102+
binding.detailDescriptionView.setVisibility(View.GONE);
103+
binding.detailSelectDescriptionButton.setVisibility(View.GONE);
104+
return;
105+
}
106+
107+
// start with disabled state. This also loads description content (!)
108+
disableDescriptionSelection();
109+
110+
binding.detailSelectDescriptionButton.setOnClickListener(v -> {
111+
if (binding.detailDescriptionNoteView.getVisibility() == View.VISIBLE) {
112+
disableDescriptionSelection();
113+
} else {
114+
// enable selection only when button is clicked to prevent flickering
115+
enableDescriptionSelection();
116+
}
117+
});
118+
}
119+
120+
private void enableDescriptionSelection() {
121+
binding.detailDescriptionNoteView.setVisibility(View.VISIBLE);
122+
binding.detailDescriptionView.setTextIsSelectable(true);
123+
124+
final String buttonLabel = getString(R.string.description_select_disable);
125+
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
126+
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
127+
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close);
128+
}
129+
130+
private void disableDescriptionSelection() {
131+
// show description content again, otherwise some links are not clickable
132+
final Description description = getDescription();
133+
if (description != null) {
134+
TextLinkifier.fromDescription(binding.detailDescriptionView,
135+
description, HtmlCompat.FROM_HTML_MODE_LEGACY,
136+
getService(), getStreamUrl(),
137+
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
138+
}
139+
140+
binding.detailDescriptionNoteView.setVisibility(View.GONE);
141+
binding.detailDescriptionView.setTextIsSelectable(false);
142+
143+
final String buttonLabel = getString(R.string.description_select_enable);
144+
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
145+
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
146+
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all);
147+
}
148+
149+
protected void addMetadataItem(final LayoutInflater inflater,
150+
final LinearLayout layout,
151+
final boolean linkifyContent,
152+
@StringRes final int type,
153+
@Nullable final String content) {
154+
if (isBlank(content)) {
155+
return;
156+
}
157+
158+
final ItemMetadataBinding itemBinding =
159+
ItemMetadataBinding.inflate(inflater, layout, false);
160+
161+
itemBinding.metadataTypeView.setText(type);
162+
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
163+
ShareUtils.copyToClipboard(requireContext(), content);
164+
return true;
165+
});
166+
167+
if (linkifyContent) {
168+
TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null,
169+
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
170+
} else {
171+
itemBinding.metadataContentView.setText(content);
172+
}
173+
174+
itemBinding.metadataContentView.setClickable(true);
175+
176+
layout.addView(itemBinding.getRoot());
177+
}
178+
179+
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
180+
final List<String> tags = getTags();
181+
182+
if (tags != null && !tags.isEmpty()) {
183+
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
184+
185+
tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
186+
final Chip chip = (Chip) inflater.inflate(R.layout.chip,
187+
itemBinding.metadataTagsChips, false);
188+
chip.setText(tag);
189+
chip.setOnClickListener(this::onTagClick);
190+
chip.setOnLongClickListener(this::onTagLongClick);
191+
itemBinding.metadataTagsChips.addView(chip);
192+
});
193+
194+
layout.addView(itemBinding.getRoot());
195+
}
196+
}
197+
198+
private void onTagClick(final View chip) {
199+
if (getParentFragment() != null) {
200+
NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(),
201+
getServiceId(), ((Chip) chip).getText().toString());
202+
}
203+
}
204+
205+
private boolean onTagLongClick(final View chip) {
206+
ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString());
207+
return true;
208+
}
209+
}

0 commit comments

Comments
 (0)