Merge pull request #9182 from Theta-Dev/channel-tabs
Add support for channel tabs
This commit is contained in:
commit
0eae9e7cdc
42 changed files with 1796 additions and 907 deletions
|
@ -197,7 +197,7 @@ dependencies {
|
||||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||||
// This works thanks to JitPack: https://jitpack.io/
|
// This works thanks to JitPack: https://jitpack.io/
|
||||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:340095515d45ecbee576872c7198992ebd8e4f08'
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:95a3cc0a173bba28c179f9f9503b1010ec6bff21'
|
||||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
|
|
|
@ -10,19 +10,13 @@ import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.schabi.newpipe.database.AppDatabase;
|
import org.schabi.newpipe.database.AppDatabase;
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
|
||||||
import org.schabi.newpipe.testUtil.TestDatabase;
|
import org.schabi.newpipe.testUtil.TestDatabase;
|
||||||
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule;
|
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class SubscriptionManagerTest {
|
public class SubscriptionManagerTest {
|
||||||
|
@ -58,7 +52,7 @@ public class SubscriptionManagerTest {
|
||||||
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/3blue1brown");
|
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/3blue1brown");
|
||||||
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
||||||
|
|
||||||
manager.insertSubscription(subscription, info);
|
manager.insertSubscription(subscription);
|
||||||
final SubscriptionEntity readSubscription = getAssertOneSubscriptionEntity();
|
final SubscriptionEntity readSubscription = getAssertOneSubscriptionEntity();
|
||||||
|
|
||||||
// the uid has changed, since the uid is chosen upon inserting, but the rest should match
|
// the uid has changed, since the uid is chosen upon inserting, but the rest should match
|
||||||
|
@ -76,7 +70,7 @@ public class SubscriptionManagerTest {
|
||||||
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
||||||
subscription.setNotificationMode(0);
|
subscription.setNotificationMode(0);
|
||||||
|
|
||||||
manager.insertSubscription(subscription, info);
|
manager.insertSubscription(subscription);
|
||||||
manager.updateNotificationMode(subscription.getServiceId(), subscription.getUrl(), 1)
|
manager.updateNotificationMode(subscription.getServiceId(), subscription.getUrl(), 1)
|
||||||
.blockingAwait();
|
.blockingAwait();
|
||||||
final SubscriptionEntity anotherSubscription = getAssertOneSubscriptionEntity();
|
final SubscriptionEntity anotherSubscription = getAssertOneSubscriptionEntity();
|
||||||
|
@ -85,35 +79,4 @@ public class SubscriptionManagerTest {
|
||||||
assertEquals(subscription.getUrl(), anotherSubscription.getUrl());
|
assertEquals(subscription.getUrl(), anotherSubscription.getUrl());
|
||||||
assertEquals(1, anotherSubscription.getNotificationMode());
|
assertEquals(1, anotherSubscription.getNotificationMode());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRememberRecentStreams() throws ExtractionException, IOException {
|
|
||||||
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/Polyphia");
|
|
||||||
final List<StreamInfoItem> relatedItems = List.of(
|
|
||||||
new StreamInfoItem(0, "a", "b", StreamType.VIDEO_STREAM),
|
|
||||||
new StreamInfoItem(1, "c", "d", StreamType.AUDIO_STREAM),
|
|
||||||
new StreamInfoItem(2, "e", "f", StreamType.AUDIO_LIVE_STREAM),
|
|
||||||
new StreamInfoItem(3, "g", "h", StreamType.LIVE_STREAM));
|
|
||||||
relatedItems.forEach(item -> {
|
|
||||||
// these two fields must be non-null for the insert to succeed
|
|
||||||
item.setUploaderUrl(info.getUrl());
|
|
||||||
item.setUploaderName(info.getName());
|
|
||||||
// the upload date must not be too much in the past for the item to actually be inserted
|
|
||||||
item.setUploadDate(new DateWrapper(OffsetDateTime.now()));
|
|
||||||
});
|
|
||||||
info.setRelatedItems(relatedItems);
|
|
||||||
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
|
||||||
|
|
||||||
manager.insertSubscription(subscription, info);
|
|
||||||
final List<StreamEntity> streams = database.streamDAO().getAll().blockingFirst();
|
|
||||||
|
|
||||||
assertEquals(4, streams.size());
|
|
||||||
streams.sort(Comparator.comparing(StreamEntity::getServiceId));
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
assertEquals(relatedItems.get(0).getServiceId(), streams.get(0).getServiceId());
|
|
||||||
assertEquals(relatedItems.get(0).getUrl(), streams.get(0).getUrl());
|
|
||||||
assertEquals(relatedItems.get(0).getName(), streams.get(0).getTitle());
|
|
||||||
assertEquals(relatedItems.get(0).getStreamType(), streams.get(0).getStreamType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,7 @@ import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
|
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
|
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
|
@ -72,10 +73,11 @@ import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.player.PlayerType;
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
|
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
@ -1022,7 +1024,16 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
playQueue = new SinglePlayQueue((StreamInfo) info);
|
playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||||
} else if (info instanceof ChannelInfo) {
|
} else if (info instanceof ChannelInfo) {
|
||||||
playQueue = new ChannelPlayQueue((ChannelInfo) info);
|
final Optional<ListLinkHandler> playableTab = ((ChannelInfo) info).getTabs()
|
||||||
|
.stream()
|
||||||
|
.filter(ChannelTabHelper::isStreamsTab)
|
||||||
|
.findFirst();
|
||||||
|
|
||||||
|
if (playableTab.isPresent()) {
|
||||||
|
playQueue = new ChannelTabPlayQueue(info.getServiceId(), playableTab.get());
|
||||||
|
} else {
|
||||||
|
return; // there is no playable tab
|
||||||
|
}
|
||||||
} else if (info instanceof PlaylistInfo) {
|
} else if (info instanceof PlaylistInfo) {
|
||||||
playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
|
playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,209 @@
|
||||||
|
package org.schabi.newpipe.fragments.detail;
|
||||||
|
|
||||||
|
import static android.text.TextUtils.isEmpty;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||||
|
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.appcompat.widget.TooltipCompat;
|
||||||
|
import androidx.core.text.HtmlCompat;
|
||||||
|
|
||||||
|
import com.google.android.material.chip.Chip;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.BaseFragment;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
|
||||||
|
import org.schabi.newpipe.databinding.ItemMetadataBinding;
|
||||||
|
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.text.TextLinkifier;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
|
|
||||||
|
public abstract class BaseDescriptionFragment extends BaseFragment {
|
||||||
|
private final CompositeDisposable descriptionDisposables = new CompositeDisposable();
|
||||||
|
protected FragmentDescriptionBinding binding;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup container,
|
||||||
|
@Nullable final Bundle savedInstanceState) {
|
||||||
|
binding = FragmentDescriptionBinding.inflate(inflater, container, false);
|
||||||
|
setupDescription();
|
||||||
|
setupMetadata(inflater, binding.detailMetadataLayout);
|
||||||
|
addTagsMetadataItem(inflater, binding.detailMetadataLayout);
|
||||||
|
return binding.getRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
descriptionDisposables.clear();
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the description to display.
|
||||||
|
* @return description object
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected abstract Description getDescription();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the streaming service. Used for generating description links.
|
||||||
|
* @return streaming service
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected abstract StreamingService getService();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the streaming service ID. Used for tag links.
|
||||||
|
* @return service ID
|
||||||
|
*/
|
||||||
|
protected abstract int getServiceId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the URL of the described video or audio, used to generate description links.
|
||||||
|
* @return stream URL
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected abstract String getStreamUrl();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of tags to display below the description.
|
||||||
|
* @return tag list
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public abstract List<String> getTags();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add additional metadata to display.
|
||||||
|
* @param inflater LayoutInflater
|
||||||
|
* @param layout detailMetadataLayout
|
||||||
|
*/
|
||||||
|
protected abstract void setupMetadata(LayoutInflater inflater, LinearLayout layout);
|
||||||
|
|
||||||
|
private void setupDescription() {
|
||||||
|
final Description description = getDescription();
|
||||||
|
if (description == null || isEmpty(description.getContent())
|
||||||
|
|| description == Description.EMPTY_DESCRIPTION) {
|
||||||
|
binding.detailDescriptionView.setVisibility(View.GONE);
|
||||||
|
binding.detailSelectDescriptionButton.setVisibility(View.GONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// start with disabled state. This also loads description content (!)
|
||||||
|
disableDescriptionSelection();
|
||||||
|
|
||||||
|
binding.detailSelectDescriptionButton.setOnClickListener(v -> {
|
||||||
|
if (binding.detailDescriptionNoteView.getVisibility() == View.VISIBLE) {
|
||||||
|
disableDescriptionSelection();
|
||||||
|
} else {
|
||||||
|
// enable selection only when button is clicked to prevent flickering
|
||||||
|
enableDescriptionSelection();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enableDescriptionSelection() {
|
||||||
|
binding.detailDescriptionNoteView.setVisibility(View.VISIBLE);
|
||||||
|
binding.detailDescriptionView.setTextIsSelectable(true);
|
||||||
|
|
||||||
|
final String buttonLabel = getString(R.string.description_select_disable);
|
||||||
|
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
|
||||||
|
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
|
||||||
|
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disableDescriptionSelection() {
|
||||||
|
// show description content again, otherwise some links are not clickable
|
||||||
|
final Description description = getDescription();
|
||||||
|
if (description != null) {
|
||||||
|
TextLinkifier.fromDescription(binding.detailDescriptionView,
|
||||||
|
description, HtmlCompat.FROM_HTML_MODE_LEGACY,
|
||||||
|
getService(), getStreamUrl(),
|
||||||
|
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.detailDescriptionNoteView.setVisibility(View.GONE);
|
||||||
|
binding.detailDescriptionView.setTextIsSelectable(false);
|
||||||
|
|
||||||
|
final String buttonLabel = getString(R.string.description_select_enable);
|
||||||
|
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
|
||||||
|
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
|
||||||
|
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addMetadataItem(final LayoutInflater inflater,
|
||||||
|
final LinearLayout layout,
|
||||||
|
final boolean linkifyContent,
|
||||||
|
@StringRes final int type,
|
||||||
|
@Nullable final String content) {
|
||||||
|
if (isBlank(content)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ItemMetadataBinding itemBinding =
|
||||||
|
ItemMetadataBinding.inflate(inflater, layout, false);
|
||||||
|
|
||||||
|
itemBinding.metadataTypeView.setText(type);
|
||||||
|
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
|
||||||
|
ShareUtils.copyToClipboard(requireContext(), content);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (linkifyContent) {
|
||||||
|
TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null,
|
||||||
|
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
|
||||||
|
} else {
|
||||||
|
itemBinding.metadataContentView.setText(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
itemBinding.metadataContentView.setClickable(true);
|
||||||
|
|
||||||
|
layout.addView(itemBinding.getRoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
||||||
|
final List<String> tags = getTags();
|
||||||
|
|
||||||
|
if (tags != null && !tags.isEmpty()) {
|
||||||
|
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
|
||||||
|
|
||||||
|
tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
|
||||||
|
final Chip chip = (Chip) inflater.inflate(R.layout.chip,
|
||||||
|
itemBinding.metadataTagsChips, false);
|
||||||
|
chip.setText(tag);
|
||||||
|
chip.setOnClickListener(this::onTagClick);
|
||||||
|
chip.setOnLongClickListener(this::onTagLongClick);
|
||||||
|
itemBinding.metadataTagsChips.addView(chip);
|
||||||
|
});
|
||||||
|
|
||||||
|
layout.addView(itemBinding.getRoot());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onTagClick(final View chip) {
|
||||||
|
if (getParentFragment() != null) {
|
||||||
|
NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(),
|
||||||
|
getServiceId(), ((Chip) chip).getText().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean onTagLongClick(final View chip) {
|
||||||
|
ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,46 +1,29 @@
|
||||||
package org.schabi.newpipe.fragments.detail;
|
package org.schabi.newpipe.fragments.detail;
|
||||||
|
|
||||||
import static android.text.TextUtils.isEmpty;
|
|
||||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
|
||||||
import static org.schabi.newpipe.util.Localization.getAppLocale;
|
import static org.schabi.newpipe.util.Localization.getAppLocale;
|
||||||
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.appcompat.widget.TooltipCompat;
|
|
||||||
import androidx.core.text.HtmlCompat;
|
|
||||||
|
|
||||||
import com.google.android.material.chip.Chip;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.BaseFragment;
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.databinding.ItemMetadataBinding;
|
|
||||||
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
|
|
||||||
import org.schabi.newpipe.extractor.stream.Description;
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import java.util.List;
|
||||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
|
|
||||||
public class DescriptionFragment extends BaseFragment {
|
public class DescriptionFragment extends BaseDescriptionFragment {
|
||||||
|
|
||||||
@State
|
@State
|
||||||
StreamInfo streamInfo = null;
|
StreamInfo streamInfo = null;
|
||||||
final CompositeDisposable descriptionDisposables = new CompositeDisposable();
|
|
||||||
FragmentDescriptionBinding binding;
|
|
||||||
|
|
||||||
public DescriptionFragment() {
|
public DescriptionFragment() {
|
||||||
}
|
}
|
||||||
|
@ -49,86 +32,64 @@ public class DescriptionFragment extends BaseFragment {
|
||||||
this.streamInfo = streamInfo;
|
this.streamInfo = streamInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
protected Description getDescription() {
|
||||||
@Nullable final ViewGroup container,
|
if (streamInfo == null) {
|
||||||
@Nullable final Bundle savedInstanceState) {
|
return null;
|
||||||
binding = FragmentDescriptionBinding.inflate(inflater, container, false);
|
|
||||||
if (streamInfo != null) {
|
|
||||||
setupUploadDate();
|
|
||||||
setupDescription();
|
|
||||||
setupMetadata(inflater, binding.detailMetadataLayout);
|
|
||||||
}
|
}
|
||||||
return binding.getRoot();
|
return streamInfo.getDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected StreamingService getService() {
|
||||||
|
if (streamInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return streamInfo.getService();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
protected int getServiceId() {
|
||||||
descriptionDisposables.clear();
|
if (streamInfo == null) {
|
||||||
super.onDestroy();
|
return -1;
|
||||||
|
}
|
||||||
|
return streamInfo.getServiceId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected String getStreamUrl() {
|
||||||
|
if (streamInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return streamInfo.getUrl();
|
||||||
|
}
|
||||||
|
|
||||||
private void setupUploadDate() {
|
@Nullable
|
||||||
if (streamInfo.getUploadDate() != null) {
|
@Override
|
||||||
|
public List<String> getTags() {
|
||||||
|
if (streamInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return streamInfo.getTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setupMetadata(final LayoutInflater inflater,
|
||||||
|
final LinearLayout layout) {
|
||||||
|
if (streamInfo != null && streamInfo.getUploadDate() != null) {
|
||||||
binding.detailUploadDateView.setText(Localization
|
binding.detailUploadDateView.setText(Localization
|
||||||
.localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime()));
|
.localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime()));
|
||||||
} else {
|
} else {
|
||||||
binding.detailUploadDateView.setVisibility(View.GONE);
|
binding.detailUploadDateView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
if (streamInfo == null) {
|
||||||
private void setupDescription() {
|
|
||||||
final Description description = streamInfo.getDescription();
|
|
||||||
if (description == null || isEmpty(description.getContent())
|
|
||||||
|| description == Description.EMPTY_DESCRIPTION) {
|
|
||||||
binding.detailDescriptionView.setVisibility(View.GONE);
|
|
||||||
binding.detailSelectDescriptionButton.setVisibility(View.GONE);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// start with disabled state. This also loads description content (!)
|
|
||||||
disableDescriptionSelection();
|
|
||||||
|
|
||||||
binding.detailSelectDescriptionButton.setOnClickListener(v -> {
|
|
||||||
if (binding.detailDescriptionNoteView.getVisibility() == View.VISIBLE) {
|
|
||||||
disableDescriptionSelection();
|
|
||||||
} else {
|
|
||||||
// enable selection only when button is clicked to prevent flickering
|
|
||||||
enableDescriptionSelection();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void enableDescriptionSelection() {
|
|
||||||
binding.detailDescriptionNoteView.setVisibility(View.VISIBLE);
|
|
||||||
binding.detailDescriptionView.setTextIsSelectable(true);
|
|
||||||
|
|
||||||
final String buttonLabel = getString(R.string.description_select_disable);
|
|
||||||
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
|
|
||||||
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
|
|
||||||
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void disableDescriptionSelection() {
|
|
||||||
// show description content again, otherwise some links are not clickable
|
|
||||||
TextLinkifier.fromDescription(binding.detailDescriptionView,
|
|
||||||
streamInfo.getDescription(), HtmlCompat.FROM_HTML_MODE_LEGACY,
|
|
||||||
streamInfo.getService(), streamInfo.getUrl(),
|
|
||||||
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
|
|
||||||
|
|
||||||
binding.detailDescriptionNoteView.setVisibility(View.GONE);
|
|
||||||
binding.detailDescriptionView.setTextIsSelectable(false);
|
|
||||||
|
|
||||||
final String buttonLabel = getString(R.string.description_select_enable);
|
|
||||||
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
|
|
||||||
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
|
|
||||||
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupMetadata(final LayoutInflater inflater,
|
|
||||||
final LinearLayout layout) {
|
|
||||||
addMetadataItem(inflater, layout, false, R.string.metadata_category,
|
addMetadataItem(inflater, layout, false, R.string.metadata_category,
|
||||||
streamInfo.getCategory());
|
streamInfo.getCategory());
|
||||||
|
|
||||||
|
@ -153,67 +114,6 @@ public class DescriptionFragment extends BaseFragment {
|
||||||
streamInfo.getHost());
|
streamInfo.getHost());
|
||||||
addMetadataItem(inflater, layout, true, R.string.metadata_thumbnail_url,
|
addMetadataItem(inflater, layout, true, R.string.metadata_thumbnail_url,
|
||||||
streamInfo.getThumbnailUrl());
|
streamInfo.getThumbnailUrl());
|
||||||
|
|
||||||
addTagsMetadataItem(inflater, layout);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addMetadataItem(final LayoutInflater inflater,
|
|
||||||
final LinearLayout layout,
|
|
||||||
final boolean linkifyContent,
|
|
||||||
@StringRes final int type,
|
|
||||||
@Nullable final String content) {
|
|
||||||
if (isBlank(content)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final ItemMetadataBinding itemBinding =
|
|
||||||
ItemMetadataBinding.inflate(inflater, layout, false);
|
|
||||||
|
|
||||||
itemBinding.metadataTypeView.setText(type);
|
|
||||||
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
|
|
||||||
ShareUtils.copyToClipboard(requireContext(), content);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (linkifyContent) {
|
|
||||||
TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null,
|
|
||||||
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
|
|
||||||
} else {
|
|
||||||
itemBinding.metadataContentView.setText(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
itemBinding.metadataContentView.setClickable(true);
|
|
||||||
|
|
||||||
layout.addView(itemBinding.getRoot());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
|
||||||
if (streamInfo.getTags() != null && !streamInfo.getTags().isEmpty()) {
|
|
||||||
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
|
|
||||||
|
|
||||||
streamInfo.getTags().stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
|
|
||||||
final Chip chip = (Chip) inflater.inflate(R.layout.chip,
|
|
||||||
itemBinding.metadataTagsChips, false);
|
|
||||||
chip.setText(tag);
|
|
||||||
chip.setOnClickListener(this::onTagClick);
|
|
||||||
chip.setOnLongClickListener(this::onTagLongClick);
|
|
||||||
itemBinding.metadataTagsChips.addView(chip);
|
|
||||||
});
|
|
||||||
|
|
||||||
layout.addView(itemBinding.getRoot());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onTagClick(final View chip) {
|
|
||||||
if (getParentFragment() != null) {
|
|
||||||
NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(),
|
|
||||||
streamInfo.getServiceId(), ((Chip) chip).getText().toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean onTagLongClick(final View chip) {
|
|
||||||
ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString());
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
||||||
|
|
|
@ -112,6 +112,7 @@ import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
@ -535,9 +536,11 @@ public final class VideoDetailFragment
|
||||||
}));
|
}));
|
||||||
|
|
||||||
binding.detailControlsBackground.setOnLongClickListener(makeOnLongClickListener(info ->
|
binding.detailControlsBackground.setOnLongClickListener(makeOnLongClickListener(info ->
|
||||||
openBackgroundPlayer(true)));
|
openBackgroundPlayer(true)
|
||||||
|
));
|
||||||
binding.detailControlsPopup.setOnLongClickListener(makeOnLongClickListener(info ->
|
binding.detailControlsPopup.setOnLongClickListener(makeOnLongClickListener(info ->
|
||||||
openPopupPlayer(true)));
|
openPopupPlayer(true)
|
||||||
|
));
|
||||||
binding.detailControlsDownload.setOnLongClickListener(makeOnLongClickListener(info ->
|
binding.detailControlsDownload.setOnLongClickListener(makeOnLongClickListener(info ->
|
||||||
NavigationHelper.openDownloads(activity)));
|
NavigationHelper.openDownloads(activity)));
|
||||||
|
|
||||||
|
@ -620,8 +623,7 @@ public final class VideoDetailFragment
|
||||||
|
|
||||||
final View.OnTouchListener controlsTouchListener = (view, motionEvent) -> {
|
final View.OnTouchListener controlsTouchListener = (view, motionEvent) -> {
|
||||||
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN
|
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN
|
||||||
&& PreferenceManager.getDefaultSharedPreferences(activity)
|
&& PlayButtonHelper.shouldShowHoldToAppendTip(activity)) {
|
||||||
.getBoolean(getString(R.string.show_hold_to_append_key), true)) {
|
|
||||||
|
|
||||||
animate(binding.touchAppendDetail, true, 250, AnimationType.ALPHA, 0, () ->
|
animate(binding.touchAppendDetail, true, 250, AnimationType.ALPHA, 0, () ->
|
||||||
animate(binding.touchAppendDetail, false, 1500, AnimationType.ALPHA, 1000));
|
animate(binding.touchAppendDetail, false, 1500, AnimationType.ALPHA, 1000));
|
||||||
|
|
|
@ -16,7 +16,6 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.views.NewPipeRecyclerView;
|
import org.schabi.newpipe.views.NewPipeRecyclerView;
|
||||||
|
@ -234,11 +233,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
|
||||||
showListFooter(hasMoreItems());
|
showListFooter(hasMoreItems());
|
||||||
} else {
|
} else {
|
||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
// showEmptyState should be called only if there is no item as
|
showEmptyState();
|
||||||
// well as no header in infoListAdapter
|
|
||||||
if (!(result instanceof ChannelInfo && infoListAdapter.getItemCount() == 1)) {
|
|
||||||
showEmptyState();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
package org.schabi.newpipe.fragments.list.channel;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
import org.schabi.newpipe.fragments.detail.BaseDescriptionFragment;
|
||||||
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import icepick.State;
|
||||||
|
|
||||||
|
public class ChannelAboutFragment extends BaseDescriptionFragment {
|
||||||
|
@State
|
||||||
|
protected ChannelInfo channelInfo;
|
||||||
|
|
||||||
|
public static ChannelAboutFragment getInstance(final ChannelInfo channelInfo) {
|
||||||
|
final ChannelAboutFragment fragment = new ChannelAboutFragment();
|
||||||
|
fragment.channelInfo = channelInfo;
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelAboutFragment() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
binding.constraintLayout.setPadding(0, DeviceUtils.dpToPx(8, requireContext()), 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected Description getDescription() {
|
||||||
|
if (channelInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Description(channelInfo.getDescription(), Description.PLAIN_TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected StreamingService getService() {
|
||||||
|
if (channelInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return channelInfo.getService();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getServiceId() {
|
||||||
|
if (channelInfo == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return channelInfo.getServiceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected String getStreamUrl() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public List<String> getTags() {
|
||||||
|
if (channelInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return channelInfo.getTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setupMetadata(final LayoutInflater inflater,
|
||||||
|
final LinearLayout layout) {
|
||||||
|
// There is no upload date available for channels, so hide the relevant UI element
|
||||||
|
binding.detailUploadDateView.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
if (channelInfo == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Context context = getContext();
|
||||||
|
if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) {
|
||||||
|
addMetadataItem(inflater, layout, false, R.string.metadata_subscribers,
|
||||||
|
Localization.localizeNumber(context, channelInfo.getSubscriberCount()));
|
||||||
|
}
|
||||||
|
|
||||||
|
addMetadataItem(inflater, layout, true, R.string.metadata_avatar_url,
|
||||||
|
channelInfo.getAvatarUrl());
|
||||||
|
addMetadataItem(inflater, layout, true, R.string.metadata_banner_url,
|
||||||
|
channelInfo.getBannerUrl());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
package org.schabi.newpipe.fragments.list.channel;
|
package org.schabi.newpipe.fragments.list.channel;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||||
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
|
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
@ -16,51 +18,49 @@ import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Button;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.core.graphics.ColorUtils;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
import com.google.android.material.tabs.TabLayout;
|
||||||
import com.jakewharton.rxbinding4.view.RxView;
|
import com.jakewharton.rxbinding4.view.RxView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.subscription.NotificationMode;
|
import org.schabi.newpipe.database.subscription.NotificationMode;
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
import org.schabi.newpipe.databinding.ChannelHeaderBinding;
|
|
||||||
import org.schabi.newpipe.databinding.FragmentChannelBinding;
|
import org.schabi.newpipe.databinding.FragmentChannelBinding;
|
||||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||||
|
import org.schabi.newpipe.fragments.detail.TabAdapter;
|
||||||
import org.schabi.newpipe.ktx.AnimationType;
|
import org.schabi.newpipe.ktx.AnimationType;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
||||||
import org.schabi.newpipe.player.PlayerType;
|
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Queue;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Supplier;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
|
import icepick.State;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Observable;
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.functions.Action;
|
import io.reactivex.rxjava3.functions.Action;
|
||||||
|
@ -68,29 +68,37 @@ import io.reactivex.rxjava3.functions.Consumer;
|
||||||
import io.reactivex.rxjava3.functions.Function;
|
import io.reactivex.rxjava3.functions.Function;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, ChannelInfo>
|
public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||||
implements View.OnClickListener {
|
implements StateSaver.WriteRead {
|
||||||
|
|
||||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||||
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
|
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
|
||||||
|
|
||||||
|
@State
|
||||||
|
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||||
|
@State
|
||||||
|
protected String name;
|
||||||
|
@State
|
||||||
|
protected String url;
|
||||||
|
|
||||||
|
private ChannelInfo currentInfo;
|
||||||
|
private Disposable currentWorker;
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
private Disposable subscribeButtonMonitor;
|
private Disposable subscribeButtonMonitor;
|
||||||
|
private SubscriptionManager subscriptionManager;
|
||||||
|
private int lastTab;
|
||||||
private boolean channelContentNotSupported = false;
|
private boolean channelContentNotSupported = false;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private SubscriptionManager subscriptionManager;
|
private FragmentChannelBinding binding;
|
||||||
|
private TabAdapter tabAdapter;
|
||||||
private FragmentChannelBinding channelBinding;
|
|
||||||
private ChannelHeaderBinding headerBinding;
|
|
||||||
private PlaylistControlBinding playlistControlBinding;
|
|
||||||
|
|
||||||
private MenuItem menuRssButton;
|
private MenuItem menuRssButton;
|
||||||
private MenuItem menuNotifyButton;
|
private MenuItem menuNotifyButton;
|
||||||
|
private SubscriptionEntity channelSubscription;
|
||||||
|
|
||||||
public static ChannelFragment getInstance(final int serviceId, final String url,
|
public static ChannelFragment getInstance(final int serviceId, final String url,
|
||||||
final String name) {
|
final String name) {
|
||||||
|
@ -99,22 +107,23 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChannelFragment() {
|
private void setInitialData(final int sid, final String u, final String title) {
|
||||||
super(UserAction.REQUESTED_CHANNEL);
|
this.serviceId = sid;
|
||||||
|
this.url = u;
|
||||||
|
this.name = !TextUtils.isEmpty(title) ? title : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
if (activity != null && useAsFrontPage) {
|
|
||||||
setTitle(currentInfo != null ? currentInfo.getName() : name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// LifeCycle
|
// LifeCycle
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(@NonNull final Context context) {
|
public void onAttach(@NonNull final Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
|
@ -125,49 +134,58 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
@Nullable final ViewGroup container,
|
@Nullable final ViewGroup container,
|
||||||
@Nullable final Bundle savedInstanceState) {
|
@Nullable final Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.fragment_channel, container, false);
|
binding = FragmentChannelBinding.inflate(inflater, container, false);
|
||||||
|
return binding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override // called from onViewCreated in BaseFragment.onViewCreated
|
||||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.onViewCreated(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
channelBinding = FragmentChannelBinding.bind(rootView);
|
|
||||||
showContentNotSupportedIfNeeded();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
tabAdapter = new TabAdapter(getChildFragmentManager());
|
||||||
public void onDestroy() {
|
binding.viewPager.setAdapter(tabAdapter);
|
||||||
super.onDestroy();
|
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||||
disposables.clear();
|
|
||||||
if (subscribeButtonMonitor != null) {
|
setTitle(name);
|
||||||
subscribeButtonMonitor.dispose();
|
binding.channelTitleView.setText(name);
|
||||||
|
if (!PicassoHelper.getShouldLoadImages()) {
|
||||||
|
// do not waste space for the banner if it is not going to be loaded
|
||||||
|
binding.channelBannerImage.setImageDrawable(null);
|
||||||
}
|
}
|
||||||
channelBinding = null;
|
|
||||||
headerBinding = null;
|
|
||||||
playlistControlBinding = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Init
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Supplier<View> getListHeaderSupplier() {
|
|
||||||
headerBinding = ChannelHeaderBinding
|
|
||||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
|
||||||
playlistControlBinding = headerBinding.playlistControl;
|
|
||||||
|
|
||||||
return headerBinding::getRoot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
|
|
||||||
headerBinding.subChannelTitleView.setOnClickListener(this);
|
final View.OnClickListener openSubChannel = v -> {
|
||||||
headerBinding.subChannelAvatarView.setOnClickListener(this);
|
if (!TextUtils.isEmpty(currentInfo.getParentChannelUrl())) {
|
||||||
|
try {
|
||||||
|
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
|
||||||
|
currentInfo.getParentChannelUrl(),
|
||||||
|
currentInfo.getParentChannelName());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
|
||||||
|
}
|
||||||
|
} else if (DEBUG) {
|
||||||
|
Log.i(TAG, "Can't open parent channel because we got no channel URL");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
binding.subChannelAvatarView.setOnClickListener(openSubChannel);
|
||||||
|
binding.subChannelTitleView.setOnClickListener(openSubChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (currentWorker != null) {
|
||||||
|
currentWorker.dispose();
|
||||||
|
}
|
||||||
|
disposables.clear();
|
||||||
|
binding = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Menu
|
// Menu
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -176,32 +194,33 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||||
@NonNull final MenuInflater inflater) {
|
@NonNull final MenuInflater inflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
inflater.inflate(R.menu.menu_channel, menu);
|
||||||
if (useAsFrontPage && supportActionBar != null) {
|
|
||||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
|
||||||
} else {
|
|
||||||
inflater.inflate(R.menu.menu_channel, menu);
|
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||||
}
|
|
||||||
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
|
||||||
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
public void onPrepareOptionsMenu(@NonNull final Menu menu) {
|
||||||
|
super.onPrepareOptionsMenu(menu);
|
||||||
|
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
||||||
|
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
|
||||||
|
updateNotifyButton(channelSubscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.action_settings:
|
|
||||||
NavigationHelper.openSettings(requireContext());
|
|
||||||
break;
|
|
||||||
case R.id.menu_item_notify:
|
case R.id.menu_item_notify:
|
||||||
final boolean value = !item.isChecked();
|
final boolean value = !item.isChecked();
|
||||||
item.setEnabled(false);
|
item.setEnabled(false);
|
||||||
setNotify(value);
|
setNotify(value);
|
||||||
break;
|
break;
|
||||||
|
case R.id.action_settings:
|
||||||
|
NavigationHelper.openSettings(requireContext());
|
||||||
|
break;
|
||||||
case R.id.menu_item_rss:
|
case R.id.menu_item_rss:
|
||||||
if (currentInfo != null) {
|
if (currentInfo != null) {
|
||||||
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
||||||
|
@ -224,13 +243,14 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Channel Subscription
|
// Channel Subscription
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void monitorSubscription(final ChannelInfo info) {
|
private void monitorSubscription(final ChannelInfo info) {
|
||||||
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
||||||
animate(headerBinding.channelSubscribeButton, false, 100);
|
animate(binding.channelSubscribeButton, false, 100);
|
||||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
|
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
|
||||||
"Get subscription status", currentInfo));
|
"Get subscription status", currentInfo));
|
||||||
};
|
};
|
||||||
|
@ -263,10 +283,9 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
}, onError));
|
}, onError));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription,
|
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
|
||||||
final ChannelInfo info) {
|
|
||||||
return (@NonNull Object o) -> {
|
return (@NonNull Object o) -> {
|
||||||
subscriptionManager.insertSubscription(subscription, info);
|
subscriptionManager.insertSubscription(subscription);
|
||||||
return o;
|
return o;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -298,8 +317,7 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
.subscribe(onComplete, onError));
|
.subscribe(onComplete, onError));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Disposable monitorSubscribeButton(final Button subscribeButton,
|
private Disposable monitorSubscribeButton(final Function<Object, Object> action) {
|
||||||
final Function<Object, Object> action) {
|
|
||||||
final Consumer<Object> onNext = (@NonNull Object o) -> {
|
final Consumer<Object> onNext = (@NonNull Object o) -> {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Changed subscription status to this channel!");
|
Log.d(TAG, "Changed subscription status to this channel!");
|
||||||
|
@ -311,7 +329,7 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
"Changing subscription for " + currentInfo.getUrl(), currentInfo));
|
"Changing subscription for " + currentInfo.getUrl(), currentInfo));
|
||||||
|
|
||||||
/* Emit clicks from main thread unto io thread */
|
/* Emit clicks from main thread unto io thread */
|
||||||
return RxView.clicks(subscribeButton)
|
return RxView.clicks(binding.channelSubscribeButton)
|
||||||
.subscribeOn(AndroidSchedulers.mainThread())
|
.subscribeOn(AndroidSchedulers.mainThread())
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
.debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks
|
.debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks
|
||||||
|
@ -340,17 +358,17 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
info.getAvatarUrl(),
|
info.getAvatarUrl(),
|
||||||
info.getDescription(),
|
info.getDescription(),
|
||||||
info.getSubscriberCount());
|
info.getSubscriberCount());
|
||||||
|
channelSubscription = null;
|
||||||
updateNotifyButton(null);
|
updateNotifyButton(null);
|
||||||
subscribeButtonMonitor = monitorSubscribeButton(
|
subscribeButtonMonitor = monitorSubscribeButton(mapOnSubscribe(channel));
|
||||||
headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info));
|
|
||||||
} else {
|
} else {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Found subscription to this channel!");
|
Log.d(TAG, "Found subscription to this channel!");
|
||||||
}
|
}
|
||||||
final SubscriptionEntity subscription = subscriptionEntities.get(0);
|
channelSubscription = subscriptionEntities.get(0);
|
||||||
updateNotifyButton(subscription);
|
updateNotifyButton(channelSubscription);
|
||||||
subscribeButtonMonitor = monitorSubscribeButton(
|
subscribeButtonMonitor =
|
||||||
headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription));
|
monitorSubscribeButton(mapOnUnsubscribe(channelSubscription));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -361,34 +379,33 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
+ "isSubscribed = [" + isSubscribed + "]");
|
+ "isSubscribed = [" + isSubscribed + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean isButtonVisible = headerBinding.channelSubscribeButton.getVisibility()
|
final boolean isButtonVisible = binding.channelSubscribeButton.getVisibility()
|
||||||
== View.VISIBLE;
|
== View.VISIBLE;
|
||||||
final int backgroundDuration = isButtonVisible ? 300 : 0;
|
final int backgroundDuration = isButtonVisible ? 300 : 0;
|
||||||
final int textDuration = isButtonVisible ? 200 : 0;
|
final int textDuration = isButtonVisible ? 200 : 0;
|
||||||
|
|
||||||
final int subscribeBackground = ThemeHelper
|
|
||||||
.resolveColorFromAttr(activity, R.attr.colorPrimary);
|
|
||||||
final int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
|
|
||||||
final int subscribedBackground = ContextCompat
|
final int subscribedBackground = ContextCompat
|
||||||
.getColor(activity, R.color.subscribed_background_color);
|
.getColor(activity, R.color.subscribed_background_color);
|
||||||
final int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
|
final int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
|
||||||
|
final int subscribeBackground = ColorUtils.blendARGB(ThemeHelper
|
||||||
|
.resolveColorFromAttr(activity, R.attr.colorPrimary), subscribedBackground, 0.35f);
|
||||||
|
final int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
|
||||||
|
|
||||||
if (!isSubscribed) {
|
if (isSubscribed) {
|
||||||
headerBinding.channelSubscribeButton.setText(R.string.subscribe_button_title);
|
binding.channelSubscribeButton.setText(R.string.subscribed_button_title);
|
||||||
animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
|
animateBackgroundColor(binding.channelSubscribeButton, backgroundDuration,
|
||||||
subscribedBackground, subscribeBackground);
|
|
||||||
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribedText,
|
|
||||||
subscribeText);
|
|
||||||
} else {
|
|
||||||
headerBinding.channelSubscribeButton.setText(R.string.subscribed_button_title);
|
|
||||||
animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
|
|
||||||
subscribeBackground, subscribedBackground);
|
subscribeBackground, subscribedBackground);
|
||||||
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribeText,
|
animateTextColor(binding.channelSubscribeButton, textDuration, subscribeText,
|
||||||
subscribedText);
|
subscribedText);
|
||||||
|
} else {
|
||||||
|
binding.channelSubscribeButton.setText(R.string.subscribe_button_title);
|
||||||
|
animateBackgroundColor(binding.channelSubscribeButton, backgroundDuration,
|
||||||
|
subscribedBackground, subscribeBackground);
|
||||||
|
animateTextColor(binding.channelSubscribeButton, textDuration, subscribedText,
|
||||||
|
subscribeText);
|
||||||
}
|
}
|
||||||
|
|
||||||
animate(headerBinding.channelSubscribeButton, true, 100,
|
animate(binding.channelSubscribeButton, true, 100, AnimationType.LIGHT_SCALE_AND_ALPHA);
|
||||||
AnimationType.LIGHT_SCALE_AND_ALPHA);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
|
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
|
||||||
|
@ -424,108 +441,179 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
* Show a snackbar with the option to enable notifications on new streams for this channel.
|
* Show a snackbar with the option to enable notifications on new streams for this channel.
|
||||||
*/
|
*/
|
||||||
private void showNotifySnackbar() {
|
private void showNotifySnackbar() {
|
||||||
Snackbar.make(itemsList, R.string.you_successfully_subscribed, Snackbar.LENGTH_LONG)
|
Snackbar.make(binding.getRoot(), R.string.you_successfully_subscribed, Snackbar.LENGTH_LONG)
|
||||||
.setAction(R.string.get_notified, v -> setNotify(true))
|
.setAction(R.string.get_notified, v -> setNotify(true))
|
||||||
.setActionTextColor(Color.YELLOW)
|
.setActionTextColor(Color.YELLOW)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Load and handle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<ListExtractor.InfoItemsPage<StreamInfoItem>> loadMoreItemsLogic() {
|
|
||||||
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<ChannelInfo> loadResult(final boolean forceLoad) {
|
|
||||||
return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// OnClick
|
// Init
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
private void updateTabs() {
|
||||||
public void onClick(final View v) {
|
tabAdapter.clearAllItems();
|
||||||
if (isLoading.get() || currentInfo == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (v.getId()) {
|
if (currentInfo != null && !channelContentNotSupported) {
|
||||||
case R.id.sub_channel_avatar_view:
|
final Context context = requireContext();
|
||||||
case R.id.sub_channel_title_view:
|
final SharedPreferences preferences = PreferenceManager
|
||||||
if (!TextUtils.isEmpty(currentInfo.getParentChannelUrl())) {
|
.getDefaultSharedPreferences(context);
|
||||||
try {
|
|
||||||
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
|
for (final ListLinkHandler linkHandler : currentInfo.getTabs()) {
|
||||||
currentInfo.getParentChannelUrl(),
|
final String tab = linkHandler.getContentFilters().get(0);
|
||||||
currentInfo.getParentChannelName());
|
if (ChannelTabHelper.showChannelTab(context, preferences, tab)) {
|
||||||
} catch (final Exception e) {
|
final ChannelTabFragment channelTabFragment =
|
||||||
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
|
ChannelTabFragment.getInstance(serviceId, linkHandler, name);
|
||||||
}
|
channelTabFragment.useAsFrontPage(useAsFrontPage);
|
||||||
} else if (DEBUG) {
|
tabAdapter.addFragment(channelTabFragment,
|
||||||
Log.i(TAG, "Can't open parent channel because we got no channel URL");
|
context.getString(ChannelTabHelper.getTranslationKey(tab)));
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
|
|
||||||
|
if (ChannelTabHelper.showChannelTab(
|
||||||
|
context, preferences, R.string.show_channel_tabs_about)) {
|
||||||
|
tabAdapter.addFragment(
|
||||||
|
ChannelAboutFragment.getInstance(currentInfo),
|
||||||
|
context.getString(R.string.channel_tab_about));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tabAdapter.notifyDataSetUpdate();
|
||||||
|
|
||||||
|
for (int i = 0; i < tabAdapter.getCount(); i++) {
|
||||||
|
binding.tabLayout.getTabAt(i).setText(tabAdapter.getItemTitle(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore previously selected tab
|
||||||
|
final TabLayout.Tab ltab = binding.tabLayout.getTabAt(lastTab);
|
||||||
|
if (ltab != null) {
|
||||||
|
binding.tabLayout.selectTab(ltab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// State Saving
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateSuffix() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(final Queue<Object> objectsToSave) {
|
||||||
|
objectsToSave.add(currentInfo);
|
||||||
|
objectsToSave.add(binding == null ? 0 : binding.tabLayout.getSelectedTabPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFrom(@NonNull final Queue<Object> savedObjects) {
|
||||||
|
currentInfo = (ChannelInfo) savedObjects.poll();
|
||||||
|
lastTab = (Integer) savedObjects.poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(final @NonNull Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
if (binding != null) {
|
||||||
|
outState.putInt("LastTab", binding.tabLayout.getSelectedTabPosition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||||
|
super.onRestoreInstanceState(savedInstanceState);
|
||||||
|
lastTab = savedInstanceState.getInt("LastTab", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Contract
|
// Contract
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doInitialLoadLogic() {
|
||||||
|
if (currentInfo == null) {
|
||||||
|
startLoading(false);
|
||||||
|
} else {
|
||||||
|
handleResult(currentInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startLoading(final boolean forceLoad) {
|
||||||
|
super.startLoading(forceLoad);
|
||||||
|
|
||||||
|
currentInfo = null;
|
||||||
|
updateTabs();
|
||||||
|
if (currentWorker != null) {
|
||||||
|
currentWorker.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
runWorker(forceLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runWorker(final boolean forceLoad) {
|
||||||
|
currentWorker = ExtractorHelper.getChannelInfo(serviceId, url, forceLoad)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(result -> {
|
||||||
|
isLoading.set(false);
|
||||||
|
handleResult(result);
|
||||||
|
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL,
|
||||||
|
url == null ? "No URL" : url, serviceId)));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showLoading() {
|
public void showLoading() {
|
||||||
super.showLoading();
|
super.showLoading();
|
||||||
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
|
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
|
||||||
animate(headerBinding.channelSubscribeButton, false, 100);
|
animate(binding.channelSubscribeButton, false, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(@NonNull final ChannelInfo result) {
|
public void handleResult(@NonNull final ChannelInfo result) {
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
|
currentInfo = result;
|
||||||
|
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
|
||||||
|
|
||||||
|
if (PicassoHelper.getShouldLoadImages() && !isBlank(result.getBannerUrl())) {
|
||||||
|
PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||||
|
.into(binding.channelBannerImage);
|
||||||
|
} else {
|
||||||
|
// do not waste space for the banner, if the user disabled images or there is not one
|
||||||
|
binding.channelBannerImage.setImageDrawable(null);
|
||||||
|
}
|
||||||
|
|
||||||
headerBinding.getRoot().setVisibility(View.VISIBLE);
|
|
||||||
PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG)
|
|
||||||
.into(headerBinding.channelBannerImage);
|
|
||||||
PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||||
.into(headerBinding.channelAvatarView);
|
.into(binding.channelAvatarView);
|
||||||
PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||||
.into(headerBinding.subChannelAvatarView);
|
.into(binding.subChannelAvatarView);
|
||||||
|
|
||||||
headerBinding.channelSubscriberView.setVisibility(View.VISIBLE);
|
binding.channelTitleView.setText(result.getName());
|
||||||
|
binding.channelSubscriberView.setVisibility(View.VISIBLE);
|
||||||
if (result.getSubscriberCount() >= 0) {
|
if (result.getSubscriberCount() >= 0) {
|
||||||
headerBinding.channelSubscriberView.setText(Localization
|
binding.channelSubscriberView.setText(Localization
|
||||||
.shortSubscriberCount(activity, result.getSubscriberCount()));
|
.shortSubscriberCount(activity, result.getSubscriberCount()));
|
||||||
} else {
|
} else {
|
||||||
headerBinding.channelSubscriberView.setText(R.string.subscribers_count_not_available);
|
binding.channelSubscriberView.setText(R.string.subscribers_count_not_available);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
|
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
|
||||||
headerBinding.subChannelTitleView.setText(String.format(
|
binding.subChannelTitleView.setText(String.format(
|
||||||
getString(R.string.channel_created_by),
|
getString(R.string.channel_created_by),
|
||||||
currentInfo.getParentChannelName())
|
currentInfo.getParentChannelName())
|
||||||
);
|
);
|
||||||
headerBinding.subChannelTitleView.setVisibility(View.VISIBLE);
|
binding.subChannelTitleView.setVisibility(View.VISIBLE);
|
||||||
headerBinding.subChannelAvatarView.setVisibility(View.VISIBLE);
|
binding.subChannelAvatarView.setVisibility(View.VISIBLE);
|
||||||
} else {
|
|
||||||
headerBinding.subChannelTitleView.setVisibility(View.GONE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menuRssButton != null) {
|
if (menuRssButton != null) {
|
||||||
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlaylistControls should be visible only if there is some item in
|
|
||||||
// infoListAdapter other than header
|
|
||||||
if (infoListAdapter.getItemCount() != 1) {
|
|
||||||
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
playlistControlBinding.getRoot().setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
channelContentNotSupported = false;
|
channelContentNotSupported = false;
|
||||||
for (final Throwable throwable : result.getErrors()) {
|
for (final Throwable throwable : result.getErrors()) {
|
||||||
if (throwable instanceof ContentNotSupportedException) {
|
if (throwable instanceof ContentNotSupportedException) {
|
||||||
|
@ -539,62 +627,21 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
if (subscribeButtonMonitor != null) {
|
if (subscribeButtonMonitor != null) {
|
||||||
subscribeButtonMonitor.dispose();
|
subscribeButtonMonitor.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateTabs();
|
||||||
updateSubscription(result);
|
updateSubscription(result);
|
||||||
monitorSubscription(result);
|
monitorSubscription(result);
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayAllButton
|
|
||||||
.setOnClickListener(view -> NavigationHelper
|
|
||||||
.playOnMainPlayer(activity, getPlayQueue()));
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton
|
|
||||||
.setOnClickListener(view -> NavigationHelper
|
|
||||||
.playOnPopupPlayer(activity, getPlayQueue(), false));
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton
|
|
||||||
.setOnClickListener(view -> NavigationHelper
|
|
||||||
.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showContentNotSupportedIfNeeded() {
|
private void showContentNotSupportedIfNeeded() {
|
||||||
// channelBinding might not be initialized when handleResult() is called
|
// channelBinding might not be initialized when handleResult() is called
|
||||||
// (e.g. after rotating the screen, #6696)
|
// (e.g. after rotating the screen, #6696)
|
||||||
if (!channelContentNotSupported || channelBinding == null) {
|
if (!channelContentNotSupported || binding == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
binding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
||||||
channelBinding.channelKaomoji.setText("(︶︹︺)");
|
binding.channelKaomoji.setText("(︶︹︺)");
|
||||||
channelBinding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
binding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
||||||
channelBinding.emptyStateMessage.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private PlayQueue getPlayQueue() {
|
|
||||||
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
|
||||||
.filter(StreamInfoItem.class::isInstance)
|
|
||||||
.map(StreamInfoItem.class::cast)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
|
|
||||||
currentInfo.getNextPage(), streamItems, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Utils
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTitle(final String title) {
|
|
||||||
super.setTitle(title);
|
|
||||||
if (!useAsFrontPage) {
|
|
||||||
headerBinding.channelTitleView.setText(title);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
package org.schabi.newpipe.fragments.list.channel;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||||
|
import org.schabi.newpipe.error.UserAction;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||||
|
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import icepick.State;
|
||||||
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
|
||||||
|
public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTabInfo>
|
||||||
|
implements PlaylistControlViewHolder {
|
||||||
|
|
||||||
|
// states must be protected and not private for IcePick being able to access them
|
||||||
|
@State
|
||||||
|
protected ListLinkHandler tabHandler;
|
||||||
|
@State
|
||||||
|
protected String channelName;
|
||||||
|
|
||||||
|
private PlaylistControlBinding playlistControlBinding;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static ChannelTabFragment getInstance(final int serviceId,
|
||||||
|
final ListLinkHandler tabHandler,
|
||||||
|
final String channelName) {
|
||||||
|
final ChannelTabFragment instance = new ChannelTabFragment();
|
||||||
|
instance.serviceId = serviceId;
|
||||||
|
instance.tabHandler = tabHandler;
|
||||||
|
instance.channelName = channelName;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelTabFragment() {
|
||||||
|
super(UserAction.REQUESTED_CHANNEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// LifeCycle
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setHasOptionsMenu(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup container,
|
||||||
|
@Nullable final Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_channel_tab, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
playlistControlBinding = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Supplier<View> getListHeaderSupplier() {
|
||||||
|
if (ChannelTabHelper.isStreamsTab(tabHandler)) {
|
||||||
|
playlistControlBinding = PlaylistControlBinding
|
||||||
|
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||||
|
return playlistControlBinding::getRoot;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<ChannelTabInfo> loadResult(final boolean forceLoad) {
|
||||||
|
return ExtractorHelper.getChannelTab(serviceId, tabHandler, forceLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<ListExtractor.InfoItemsPage<InfoItem>> loadMoreItemsLogic() {
|
||||||
|
return ExtractorHelper.getMoreChannelTabItems(serviceId, tabHandler, currentNextPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTitle(final String title) {
|
||||||
|
// The channel name is displayed as title in the toolbar.
|
||||||
|
// The title is always a description of the content of the tab fragment.
|
||||||
|
// It should be unique for each channel because multiple channel tabs
|
||||||
|
// can be added to the main page. Therefore, the channel name is used.
|
||||||
|
// Using the title variable would cause the title to be the same for all channel tabs.
|
||||||
|
super.setTitle(channelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleResult(@NonNull final ChannelTabInfo result) {
|
||||||
|
super.handleResult(result);
|
||||||
|
|
||||||
|
if (playlistControlBinding != null) {
|
||||||
|
// PlaylistControls should be visible only if there is some item in
|
||||||
|
// infoListAdapter other than header
|
||||||
|
if (infoListAdapter.getItemCount() > 1) {
|
||||||
|
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
playlistControlBinding.getRoot().setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayButtonHelper.initPlaylistControlClickListener(
|
||||||
|
activity, playlistControlBinding, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayQueue getPlayQueue() {
|
||||||
|
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
||||||
|
.filter(StreamInfoItem.class::isInstance)
|
||||||
|
.map(StreamInfoItem.class::cast)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return new ChannelTabPlayQueue(currentInfo.getServiceId(), tabHandler,
|
||||||
|
currentInfo.getNextPage(), streamItems, 0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.fragments.list.playlist;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for {@code R.layout.playlist_control} view holders
|
||||||
|
* to give access to the play queue.
|
||||||
|
*/
|
||||||
|
public interface PlaylistControlViewHolder {
|
||||||
|
PlayQueue getPlayQueue();
|
||||||
|
}
|
|
@ -43,7 +43,6 @@ import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
import org.schabi.newpipe.player.PlayerType;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
@ -51,6 +50,7 @@ import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -64,7 +64,8 @@ import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo> {
|
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
|
||||||
|
implements PlaylistControlViewHolder {
|
||||||
|
|
||||||
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
|
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
|
||||||
|
|
||||||
|
@ -332,25 +333,10 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(getPlaylistBookmarkSubscriber());
|
.subscribe(getPlaylistBookmarkSubscriber());
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
|
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
|
||||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
|
|
||||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
|
|
||||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueue getPlayQueue() {
|
public PlayQueue getPlayQueue() {
|
||||||
return getPlayQueue(0);
|
return getPlayQueue(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,7 @@ import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
import org.schabi.newpipe.databinding.FragmentFeedBinding
|
import org.schabi.newpipe.databinding.FragmentFeedBinding
|
||||||
import org.schabi.newpipe.error.ErrorInfo
|
import org.schabi.newpipe.error.ErrorInfo
|
||||||
|
import org.schabi.newpipe.error.ErrorUtil
|
||||||
import org.schabi.newpipe.error.UserAction
|
import org.schabi.newpipe.error.UserAction
|
||||||
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||||
|
@ -453,24 +454,33 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
if (t is FeedLoadService.RequestException &&
|
if (t is FeedLoadService.RequestException &&
|
||||||
t.cause is ContentNotAvailableException
|
t.cause is ContentNotAvailableException
|
||||||
) {
|
) {
|
||||||
Single.fromCallable {
|
disposables.add(
|
||||||
NewPipeDatabase.getInstance(requireContext()).subscriptionDAO()
|
Single.fromCallable {
|
||||||
.getSubscription(t.subscriptionId)
|
NewPipeDatabase.getInstance(requireContext()).subscriptionDAO()
|
||||||
}.subscribeOn(Schedulers.io())
|
.getSubscription(t.subscriptionId)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
}
|
||||||
.subscribe(
|
.subscribeOn(Schedulers.io())
|
||||||
{ subscriptionEntity ->
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
handleFeedNotAvailable(
|
.subscribe(
|
||||||
subscriptionEntity,
|
{ subscriptionEntity ->
|
||||||
t.cause,
|
handleFeedNotAvailable(
|
||||||
errors.subList(i + 1, errors.size)
|
subscriptionEntity,
|
||||||
)
|
t.cause,
|
||||||
},
|
errors.subList(i + 1, errors.size)
|
||||||
{ throwable -> Log.e(TAG, "Unable to process", throwable) }
|
)
|
||||||
)
|
},
|
||||||
return // this will be called on the remaining errors by handleFeedNotAvailable()
|
{ throwable -> Log.e(TAG, "Unable to process", throwable) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// this will be called on the remaining errors by handleFeedNotAvailable()
|
||||||
|
return@handleItemsErrors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (errors.isNotEmpty()) {
|
||||||
|
// if no error was a ContentNotAvailableException, show a general error snackbar
|
||||||
|
ErrorUtil.showSnackbar(this, ErrorInfo(errors, UserAction.REQUESTED_FEED, ""))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFeedNotAvailable(
|
private fun handleFeedNotAvailable(
|
||||||
|
|
|
@ -13,9 +13,9 @@ sealed class FeedState {
|
||||||
|
|
||||||
data class LoadedState(
|
data class LoadedState(
|
||||||
val items: List<StreamItem>,
|
val items: List<StreamItem>,
|
||||||
val oldestUpdate: OffsetDateTime? = null,
|
val oldestUpdate: OffsetDateTime?,
|
||||||
val notLoadedCount: Long,
|
val notLoadedCount: Long,
|
||||||
val itemsErrors: List<Throwable> = emptyList()
|
val itemsErrors: List<Throwable>
|
||||||
) : FeedState()
|
) : FeedState()
|
||||||
|
|
||||||
data class ErrorState(
|
data class ErrorState(
|
||||||
|
|
|
@ -86,7 +86,7 @@ class FeedViewModel(
|
||||||
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
|
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
|
||||||
mutableStateLiveData.postValue(
|
mutableStateLiveData.postValue(
|
||||||
when (event) {
|
when (event) {
|
||||||
is IdleEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount)
|
is IdleEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount, listOf())
|
||||||
is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage)
|
is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage)
|
||||||
is SuccessResultEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount, event.itemsErrors)
|
is SuccessResultEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount, event.itemsErrors)
|
||||||
is ErrorResultEvent -> FeedState.ErrorState(event.error)
|
is ErrorResultEvent -> FeedState.ErrorState(event.error)
|
||||||
|
|
|
@ -58,7 +58,7 @@ class NotificationHelper(val context: Context) {
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setCategory(NotificationCompat.CATEGORY_SOCIAL)
|
.setCategory(NotificationCompat.CATEGORY_SOCIAL)
|
||||||
.setGroupSummary(true)
|
.setGroupSummary(true)
|
||||||
.setGroup(data.listInfo.url)
|
.setGroup(data.originalInfo.url)
|
||||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
|
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
|
||||||
|
|
||||||
// Build a summary notification for Android versions < 7.0
|
// Build a summary notification for Android versions < 7.0
|
||||||
|
@ -73,7 +73,7 @@ class NotificationHelper(val context: Context) {
|
||||||
context,
|
context,
|
||||||
data.pseudoId,
|
data.pseudoId,
|
||||||
NavigationHelper
|
NavigationHelper
|
||||||
.getChannelIntent(context, data.listInfo.serviceId, data.listInfo.url)
|
.getChannelIntent(context, data.originalInfo.serviceId, data.originalInfo.url)
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
0,
|
0,
|
||||||
false
|
false
|
||||||
|
@ -88,7 +88,7 @@ class NotificationHelper(val context: Context) {
|
||||||
|
|
||||||
// Show individual stream notifications, set channel icon only if there is actually
|
// Show individual stream notifications, set channel icon only if there is actually
|
||||||
// one
|
// one
|
||||||
showStreamNotifications(newStreams, data.listInfo.serviceId, bitmap)
|
showStreamNotifications(newStreams, data.originalInfo.serviceId, bitmap)
|
||||||
// Show summary notification
|
// Show summary notification
|
||||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ class NotificationHelper(val context: Context) {
|
||||||
|
|
||||||
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
||||||
// Show individual stream notifications
|
// Show individual stream notifications
|
||||||
showStreamNotifications(newStreams, data.listInfo.serviceId, null)
|
showStreamNotifications(newStreams, data.originalInfo.serviceId, null)
|
||||||
// Show summary notification
|
// Show summary notification
|
||||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.schabi.newpipe.local.feed.service
|
package org.schabi.newpipe.local.feed.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Completable
|
import io.reactivex.rxjava3.core.Completable
|
||||||
|
@ -13,11 +14,17 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
import org.schabi.newpipe.database.subscription.NotificationMode
|
import org.schabi.newpipe.database.subscription.NotificationMode
|
||||||
import org.schabi.newpipe.extractor.ListInfo
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
|
import org.schabi.newpipe.extractor.Info
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
|
import org.schabi.newpipe.extractor.feed.FeedInfo
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||||
import org.schabi.newpipe.util.ExtractorHelper
|
import org.schabi.newpipe.util.ChannelTabHelper
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper.getChannelInfo
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper.getChannelTab
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper.getMoreChannelTabItems
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
@ -75,7 +82,9 @@ class FeedLoadManager(private val context: Context) {
|
||||||
* subscriptions which have not been updated within the feed updated threshold
|
* subscriptions which have not been updated within the feed updated threshold
|
||||||
*/
|
*/
|
||||||
val outdatedSubscriptions = when (groupId) {
|
val outdatedSubscriptions = when (groupId) {
|
||||||
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(outdatedThreshold)
|
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(
|
||||||
|
outdatedThreshold
|
||||||
|
)
|
||||||
GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.outdatedSubscriptionsWithNotificationMode(
|
GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.outdatedSubscriptionsWithNotificationMode(
|
||||||
outdatedThreshold, NotificationMode.ENABLED
|
outdatedThreshold, NotificationMode.ENABLED
|
||||||
)
|
)
|
||||||
|
@ -101,52 +110,7 @@ class FeedLoadManager(private val context: Context) {
|
||||||
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
|
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
|
||||||
.filter { !cancelSignal.get() }
|
.filter { !cancelSignal.get() }
|
||||||
.map { subscriptionEntity ->
|
.map { subscriptionEntity ->
|
||||||
var error: Throwable? = null
|
loadStreams(subscriptionEntity, useFeedExtractor, defaultSharedPreferences)
|
||||||
try {
|
|
||||||
// check for and load new streams
|
|
||||||
// either by using the dedicated feed method or by getting the channel info
|
|
||||||
val listInfo = if (useFeedExtractor) {
|
|
||||||
ExtractorHelper
|
|
||||||
.getFeedInfoFallbackToChannelInfo(
|
|
||||||
subscriptionEntity.serviceId,
|
|
||||||
subscriptionEntity.url
|
|
||||||
)
|
|
||||||
.onErrorReturn {
|
|
||||||
error = it // store error, otherwise wrapped into RuntimeException
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
.blockingGet()
|
|
||||||
} else {
|
|
||||||
ExtractorHelper
|
|
||||||
.getChannelInfo(
|
|
||||||
subscriptionEntity.serviceId,
|
|
||||||
subscriptionEntity.url,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
.onErrorReturn {
|
|
||||||
error = it // store error, otherwise wrapped into RuntimeException
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
.blockingGet()
|
|
||||||
} as ListInfo<StreamInfoItem>
|
|
||||||
|
|
||||||
return@map Notification.createOnNext(
|
|
||||||
FeedUpdateInfo(
|
|
||||||
subscriptionEntity,
|
|
||||||
listInfo
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
if (error == null) {
|
|
||||||
// do this to prevent blockingGet() from wrapping into RuntimeException
|
|
||||||
error = e
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
|
|
||||||
val wrapper =
|
|
||||||
FeedLoadService.RequestException(subscriptionEntity.uid, request, error!!)
|
|
||||||
return@map Notification.createOnError<FeedUpdateInfo>(wrapper)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.sequential()
|
.sequential()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -164,7 +128,112 @@ class FeedLoadManager(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun broadcastProgress() {
|
private fun broadcastProgress() {
|
||||||
FeedEventManager.postEvent(FeedEventManager.Event.ProgressEvent(currentProgress.get(), maxProgress.get()))
|
FeedEventManager.postEvent(
|
||||||
|
FeedEventManager.Event.ProgressEvent(
|
||||||
|
currentProgress.get(),
|
||||||
|
maxProgress.get()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadStreams(
|
||||||
|
subscriptionEntity: SubscriptionEntity,
|
||||||
|
useFeedExtractor: Boolean,
|
||||||
|
defaultSharedPreferences: SharedPreferences
|
||||||
|
): Notification<FeedUpdateInfo> {
|
||||||
|
var error: Throwable? = null
|
||||||
|
val storeOriginalErrorAndRethrow = { e: Throwable ->
|
||||||
|
// keep original to prevent blockingGet() from wrapping it into RuntimeException
|
||||||
|
error = e
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// check for and load new streams
|
||||||
|
// either by using the dedicated feed method or by getting the channel info
|
||||||
|
var originalInfo: Info? = null
|
||||||
|
var streams: List<StreamInfoItem>? = null
|
||||||
|
val errors = ArrayList<Throwable>()
|
||||||
|
|
||||||
|
if (useFeedExtractor) {
|
||||||
|
NewPipe.getService(subscriptionEntity.serviceId)
|
||||||
|
.getFeedExtractor(subscriptionEntity.url)
|
||||||
|
?.also { feedExtractor ->
|
||||||
|
// the user wants to use a feed extractor and there is one, use it
|
||||||
|
val feedInfo = FeedInfo.getInfo(feedExtractor)
|
||||||
|
errors.addAll(feedInfo.errors)
|
||||||
|
originalInfo = feedInfo
|
||||||
|
streams = feedInfo.relatedItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalInfo == null) {
|
||||||
|
// use the normal channel tabs extractor if either the user wants it, or
|
||||||
|
// the current service does not have a dedicated feed extractor
|
||||||
|
|
||||||
|
val channelInfo = getChannelInfo(
|
||||||
|
subscriptionEntity.serviceId,
|
||||||
|
subscriptionEntity.url, true
|
||||||
|
)
|
||||||
|
.onErrorReturn(storeOriginalErrorAndRethrow)
|
||||||
|
.blockingGet()
|
||||||
|
errors.addAll(channelInfo.errors)
|
||||||
|
originalInfo = channelInfo
|
||||||
|
|
||||||
|
streams = channelInfo.tabs
|
||||||
|
.filter { tab ->
|
||||||
|
ChannelTabHelper.fetchFeedChannelTab(
|
||||||
|
context,
|
||||||
|
defaultSharedPreferences,
|
||||||
|
tab
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
Pair(
|
||||||
|
getChannelTab(subscriptionEntity.serviceId, it, true)
|
||||||
|
.onErrorReturn(storeOriginalErrorAndRethrow)
|
||||||
|
.blockingGet(),
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.flatMap { (channelTabInfo, linkHandler) ->
|
||||||
|
errors.addAll(channelTabInfo.errors)
|
||||||
|
if (channelTabInfo.relatedItems.isEmpty() &&
|
||||||
|
channelTabInfo.nextPage != null
|
||||||
|
) {
|
||||||
|
val infoItemsPage = getMoreChannelTabItems(
|
||||||
|
subscriptionEntity.serviceId,
|
||||||
|
linkHandler, channelTabInfo.nextPage
|
||||||
|
)
|
||||||
|
.blockingGet()
|
||||||
|
|
||||||
|
errors.addAll(infoItemsPage.errors)
|
||||||
|
return@flatMap infoItemsPage.items
|
||||||
|
} else {
|
||||||
|
return@flatMap channelTabInfo.relatedItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filterIsInstance<StreamInfoItem>()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Notification.createOnNext(
|
||||||
|
FeedUpdateInfo(
|
||||||
|
subscriptionEntity,
|
||||||
|
originalInfo!!,
|
||||||
|
streams!!,
|
||||||
|
errors,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
|
||||||
|
val wrapper = FeedLoadService.RequestException(
|
||||||
|
subscriptionEntity.uid,
|
||||||
|
request,
|
||||||
|
// do this to prevent blockingGet() from wrapping into RuntimeException
|
||||||
|
error ?: e
|
||||||
|
)
|
||||||
|
return Notification.createOnError(wrapper)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -203,24 +272,24 @@ class FeedLoadManager(private val context: Context) {
|
||||||
for (notification in list) {
|
for (notification in list) {
|
||||||
when {
|
when {
|
||||||
notification.isOnNext -> {
|
notification.isOnNext -> {
|
||||||
val subscriptionId = notification.value!!.uid
|
val info = notification.value!!
|
||||||
val info = notification.value!!.listInfo
|
|
||||||
|
|
||||||
notification.value!!.newStreams = filterNewStreams(
|
notification.value!!.newStreams = filterNewStreams(info.streams)
|
||||||
notification.value!!.listInfo.relatedItems
|
|
||||||
)
|
|
||||||
|
|
||||||
feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems)
|
feedDatabaseManager.upsertAll(info.uid, info.streams)
|
||||||
subscriptionManager.updateFromInfo(subscriptionId, info)
|
subscriptionManager.updateFromInfo(info.uid, info.originalInfo)
|
||||||
|
|
||||||
if (info.errors.isNotEmpty()) {
|
if (info.errors.isNotEmpty()) {
|
||||||
feedResultsHolder.addErrors(
|
feedResultsHolder.addErrors(
|
||||||
FeedLoadService.RequestException.wrapList(
|
info.errors.map {
|
||||||
subscriptionId,
|
FeedLoadService.RequestException(
|
||||||
info
|
info.uid,
|
||||||
)
|
"${info.originalInfo.serviceId}:${info.originalInfo.url}",
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
feedDatabaseManager.markAsOutdated(subscriptionId)
|
feedDatabaseManager.markAsOutdated(info.uid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notification.isOnError -> {
|
notification.isOnError -> {
|
||||||
|
|
|
@ -39,8 +39,6 @@ import org.schabi.newpipe.App
|
||||||
import org.schabi.newpipe.MainActivity.DEBUG
|
import org.schabi.newpipe.MainActivity.DEBUG
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
import org.schabi.newpipe.extractor.ListInfo
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
|
||||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
|
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
|
||||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
|
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -126,17 +124,7 @@ class FeedLoadService : Service() {
|
||||||
// Loading & Handling
|
// Loading & Handling
|
||||||
// /////////////////////////////////////////////////////////////////////////
|
// /////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) {
|
class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause)
|
||||||
companion object {
|
|
||||||
fun wrapList(subscriptionId: Long, info: ListInfo<StreamInfoItem>): List<Throwable> {
|
|
||||||
val toReturn = ArrayList<Throwable>(info.errors.size)
|
|
||||||
info.errors.mapTo(toReturn) {
|
|
||||||
RequestException(subscriptionId, info.serviceId.toString() + ":" + info.url, it)
|
|
||||||
}
|
|
||||||
return toReturn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// /////////////////////////////////////////////////////////////////////////
|
// /////////////////////////////////////////////////////////////////////////
|
||||||
// Notification
|
// Notification
|
||||||
|
|
|
@ -2,7 +2,7 @@ package org.schabi.newpipe.local.feed.service
|
||||||
|
|
||||||
import org.schabi.newpipe.database.subscription.NotificationMode
|
import org.schabi.newpipe.database.subscription.NotificationMode
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
import org.schabi.newpipe.extractor.ListInfo
|
import org.schabi.newpipe.extractor.Info
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
|
||||||
data class FeedUpdateInfo(
|
data class FeedUpdateInfo(
|
||||||
|
@ -11,24 +11,30 @@ data class FeedUpdateInfo(
|
||||||
val notificationMode: Int,
|
val notificationMode: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val avatarUrl: String,
|
val avatarUrl: String,
|
||||||
val listInfo: ListInfo<StreamInfoItem>,
|
val originalInfo: Info,
|
||||||
|
val streams: List<StreamInfoItem>,
|
||||||
|
val errors: List<Throwable>,
|
||||||
) {
|
) {
|
||||||
constructor(
|
constructor(
|
||||||
subscription: SubscriptionEntity,
|
subscription: SubscriptionEntity,
|
||||||
listInfo: ListInfo<StreamInfoItem>,
|
originalInfo: Info,
|
||||||
|
streams: List<StreamInfoItem>,
|
||||||
|
errors: List<Throwable>,
|
||||||
) : this(
|
) : this(
|
||||||
uid = subscription.uid,
|
uid = subscription.uid,
|
||||||
notificationMode = subscription.notificationMode,
|
notificationMode = subscription.notificationMode,
|
||||||
name = subscription.name,
|
name = subscription.name,
|
||||||
avatarUrl = subscription.avatarUrl,
|
avatarUrl = subscription.avatarUrl,
|
||||||
listInfo = listInfo,
|
originalInfo = originalInfo,
|
||||||
|
streams = streams,
|
||||||
|
errors = errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integer id, can be used as notification id, etc.
|
* Integer id, can be used as notification id, etc.
|
||||||
*/
|
*/
|
||||||
val pseudoId: Int
|
val pseudoId: Int
|
||||||
get() = listInfo.url.hashCode()
|
get() = originalInfo.url.hashCode()
|
||||||
|
|
||||||
lateinit var newStreams: List<StreamInfoItem>
|
lateinit var newStreams: List<StreamInfoItem>
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,14 +28,16 @@ import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
|
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.settings.HistorySettingsFragment;
|
import org.schabi.newpipe.settings.HistorySettingsFragment;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -49,7 +51,8 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
public class StatisticsPlaylistFragment
|
public class StatisticsPlaylistFragment
|
||||||
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
|
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void>
|
||||||
|
implements PlaylistControlViewHolder {
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
@State
|
@State
|
||||||
Parcelable itemsListState;
|
Parcelable itemsListState;
|
||||||
|
@ -195,14 +198,9 @@ public class StatisticsPlaylistFragment
|
||||||
if (itemListAdapter != null) {
|
if (itemListAdapter != null) {
|
||||||
itemListAdapter.unsetSelectedListener();
|
itemListAdapter.unsetSelectedListener();
|
||||||
}
|
}
|
||||||
if (playlistControlBinding != null) {
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null);
|
|
||||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null);
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null);
|
|
||||||
|
|
||||||
headerBinding = null;
|
headerBinding = null;
|
||||||
playlistControlBinding = null;
|
playlistControlBinding = null;
|
||||||
}
|
|
||||||
|
|
||||||
if (databaseSubscription != null) {
|
if (databaseSubscription != null) {
|
||||||
databaseSubscription.cancel();
|
databaseSubscription.cancel();
|
||||||
|
@ -276,12 +274,8 @@ public class StatisticsPlaylistFragment
|
||||||
itemsListState = null;
|
itemsListState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
|
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
|
||||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
|
|
||||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
|
|
||||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
|
||||||
headerBinding.sortButton.setOnClickListener(view -> toggleSortMode());
|
headerBinding.sortButton.setOnClickListener(view -> toggleSortMode());
|
||||||
|
|
||||||
hideLoading();
|
hideLoading();
|
||||||
|
@ -374,7 +368,7 @@ public class StatisticsPlaylistFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueue getPlayQueue() {
|
public PlayQueue getPlayQueue() {
|
||||||
return getPlayQueue(0);
|
return getPlayQueue(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@ import android.widget.Toast;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.viewbinding.ViewBinding;
|
import androidx.viewbinding.ViewBinding;
|
||||||
|
@ -42,17 +41,18 @@ import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.PlayerType;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -69,7 +69,8 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||||
|
|
||||||
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> {
|
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void>
|
||||||
|
implements PlaylistControlViewHolder {
|
||||||
// Save the list 10 seconds after the last change occurred
|
// Save the list 10 seconds after the last change occurred
|
||||||
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
|
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
|
||||||
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
||||||
|
@ -265,14 +266,10 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
if (itemListAdapter != null) {
|
if (itemListAdapter != null) {
|
||||||
itemListAdapter.unsetSelectedListener();
|
itemListAdapter.unsetSelectedListener();
|
||||||
}
|
}
|
||||||
if (playlistControlBinding != null) {
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null);
|
|
||||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null);
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null);
|
|
||||||
|
|
||||||
headerBinding = null;
|
headerBinding = null;
|
||||||
playlistControlBinding = null;
|
playlistControlBinding = null;
|
||||||
}
|
|
||||||
|
|
||||||
if (databaseSubscription != null) {
|
if (databaseSubscription != null) {
|
||||||
databaseSubscription.cancel();
|
databaseSubscription.cancel();
|
||||||
|
@ -498,38 +495,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
}
|
}
|
||||||
setVideoCount(itemListAdapter.getItemsList().size());
|
setVideoCount(itemListAdapter.getItemsList().size());
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> {
|
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
|
||||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue());
|
|
||||||
showHoldToAppendTipIfNeeded();
|
|
||||||
});
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> {
|
|
||||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false);
|
|
||||||
showHoldToAppendTipIfNeeded();
|
|
||||||
});
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> {
|
|
||||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false);
|
|
||||||
showHoldToAppendTipIfNeeded();
|
|
||||||
});
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
hideLoading();
|
hideLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showHoldToAppendTipIfNeeded() {
|
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
|
||||||
.getBoolean(getString(R.string.show_hold_to_append_key), true)) {
|
|
||||||
Toast.makeText(activity, R.string.hold_to_append, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// Fragment Error Handling
|
// Fragment Error Handling
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -853,7 +823,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueue getPlayQueue() {
|
public PlayQueue getPlayQueue() {
|
||||||
return getPlayQueue(0);
|
return getPlayQueue(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.schabi.newpipe.local.subscription
|
package org.schabi.newpipe.local.subscription
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Pair
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Completable
|
import io.reactivex.rxjava3.core.Completable
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
@ -11,8 +12,9 @@ import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
import org.schabi.newpipe.database.subscription.NotificationMode
|
import org.schabi.newpipe.database.subscription.NotificationMode
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO
|
import org.schabi.newpipe.database.subscription.SubscriptionDAO
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
import org.schabi.newpipe.extractor.ListInfo
|
import org.schabi.newpipe.extractor.Info
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo
|
||||||
import org.schabi.newpipe.extractor.feed.FeedInfo
|
import org.schabi.newpipe.extractor.feed.FeedInfo
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||||
|
@ -46,28 +48,33 @@ class SubscriptionManager(context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun upsertAll(infoList: List<ChannelInfo>): List<SubscriptionEntity> {
|
fun upsertAll(infoList: List<Pair<ChannelInfo, List<ChannelTabInfo>>>): List<SubscriptionEntity> {
|
||||||
val listEntities = subscriptionTable.upsertAll(
|
val listEntities = subscriptionTable.upsertAll(
|
||||||
infoList.map { SubscriptionEntity.from(it) }
|
infoList.map { SubscriptionEntity.from(it.first) }
|
||||||
)
|
)
|
||||||
|
|
||||||
database.runInTransaction {
|
database.runInTransaction {
|
||||||
infoList.forEachIndexed { index, info ->
|
infoList.forEachIndexed { index, info ->
|
||||||
feedDatabaseManager.upsertAll(listEntities[index].uid, info.relatedItems)
|
info.second.forEach {
|
||||||
|
feedDatabaseManager.upsertAll(
|
||||||
|
listEntities[index].uid,
|
||||||
|
it.relatedItems.filterIsInstance<StreamInfoItem>()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return listEntities
|
return listEntities
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url)
|
fun updateChannelInfo(info: ChannelInfo): Completable =
|
||||||
.flatMapCompletable {
|
subscriptionTable.getSubscription(info.serviceId, info.url)
|
||||||
Completable.fromRunnable {
|
.flatMapCompletable {
|
||||||
it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount)
|
Completable.fromRunnable {
|
||||||
subscriptionTable.update(it)
|
it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount)
|
||||||
feedDatabaseManager.upsertAll(it.uid, info.relatedItems)
|
subscriptionTable.update(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun updateNotificationMode(serviceId: Int, url: String, @NotificationMode mode: Int): Completable {
|
fun updateNotificationMode(serviceId: Int, url: String, @NotificationMode mode: Int): Completable {
|
||||||
return subscriptionTable().getSubscription(serviceId, url)
|
return subscriptionTable().getSubscription(serviceId, url)
|
||||||
|
@ -84,7 +91,7 @@ class SubscriptionManager(context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFromInfo(subscriptionId: Long, info: ListInfo<StreamInfoItem>) {
|
fun updateFromInfo(subscriptionId: Long, info: Info) {
|
||||||
val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId)
|
val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId)
|
||||||
|
|
||||||
if (info is FeedInfo) {
|
if (info is FeedInfo) {
|
||||||
|
@ -107,11 +114,8 @@ class SubscriptionManager(context: Context) {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertSubscription(subscriptionEntity: SubscriptionEntity, info: ChannelInfo) {
|
fun insertSubscription(subscriptionEntity: SubscriptionEntity) {
|
||||||
database.runInTransaction {
|
subscriptionTable.insert(subscriptionEntity)
|
||||||
val subscriptionId = subscriptionTable.insert(subscriptionEntity)
|
|
||||||
feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteSubscription(subscriptionEntity: SubscriptionEntity) {
|
fun deleteSubscription(subscriptionEntity: SubscriptionEntity) {
|
||||||
|
@ -125,7 +129,10 @@ class SubscriptionManager(context: Context) {
|
||||||
*/
|
*/
|
||||||
private fun rememberAllStreams(subscription: SubscriptionEntity): Completable {
|
private fun rememberAllStreams(subscription: SubscriptionEntity): Completable {
|
||||||
return ExtractorHelper.getChannelInfo(subscription.serviceId, subscription.url, false)
|
return ExtractorHelper.getChannelInfo(subscription.serviceId, subscription.url, false)
|
||||||
.map { channel -> channel.relatedItems.map { stream -> StreamEntity(stream) } }
|
.flatMap { info ->
|
||||||
|
ExtractorHelper.getChannelTab(subscription.serviceId, info.tabs.first(), false)
|
||||||
|
}
|
||||||
|
.map { channel -> channel.relatedItems.filterIsInstance<StreamInfoItem>().map { stream -> StreamEntity(stream) } }
|
||||||
.flatMapCompletable { entities ->
|
.flatMapCompletable { entities ->
|
||||||
Completable.fromAction {
|
Completable.fromAction {
|
||||||
database.streamDAO().upsertAll(entities)
|
database.streamDAO().upsertAll(entities)
|
||||||
|
|
|
@ -26,6 +26,7 @@ import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
@ -38,6 +39,7 @@ import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
|
||||||
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
|
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
import org.schabi.newpipe.streams.io.SharpInputStream;
|
import org.schabi.newpipe.streams.io.SharpInputStream;
|
||||||
|
@ -48,6 +50,7 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@ -199,12 +202,19 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||||
|
|
||||||
.parallel(PARALLEL_EXTRACTIONS)
|
.parallel(PARALLEL_EXTRACTIONS)
|
||||||
.runOn(Schedulers.io())
|
.runOn(Schedulers.io())
|
||||||
.map((Function<SubscriptionItem, Notification<ChannelInfo>>) subscriptionItem -> {
|
.map((Function<SubscriptionItem, Notification<Pair<ChannelInfo,
|
||||||
|
List<ChannelTabInfo>>>>) subscriptionItem -> {
|
||||||
try {
|
try {
|
||||||
return Notification.createOnNext(ExtractorHelper
|
final ChannelInfo channelInfo = ExtractorHelper
|
||||||
.getChannelInfo(subscriptionItem.getServiceId(),
|
.getChannelInfo(subscriptionItem.getServiceId(),
|
||||||
subscriptionItem.getUrl(), true)
|
subscriptionItem.getUrl(), true)
|
||||||
.blockingGet());
|
.blockingGet();
|
||||||
|
return Notification.createOnNext(new Pair<>(channelInfo,
|
||||||
|
Collections.singletonList(
|
||||||
|
ExtractorHelper.getChannelTab(
|
||||||
|
subscriptionItem.getServiceId(),
|
||||||
|
channelInfo.getTabs().get(0), true).blockingGet()
|
||||||
|
)));
|
||||||
} catch (final Throwable e) {
|
} catch (final Throwable e) {
|
||||||
return Notification.createOnError(e);
|
return Notification.createOnError(e);
|
||||||
}
|
}
|
||||||
|
@ -223,7 +233,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Subscriber<List<SubscriptionEntity>> getSubscriber() {
|
private Subscriber<List<SubscriptionEntity>> getSubscriber() {
|
||||||
return new Subscriber<List<SubscriptionEntity>>() {
|
return new Subscriber<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(final Subscription s) {
|
public void onSubscribe(final Subscription s) {
|
||||||
subscription = s;
|
subscription = s;
|
||||||
|
@ -254,10 +264,11 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Consumer<Notification<ChannelInfo>> getNotificationsConsumer() {
|
private Consumer<Notification<Pair<ChannelInfo,
|
||||||
|
List<ChannelTabInfo>>>> getNotificationsConsumer() {
|
||||||
return notification -> {
|
return notification -> {
|
||||||
if (notification.isOnNext()) {
|
if (notification.isOnNext()) {
|
||||||
final String name = notification.getValue().getName();
|
final String name = notification.getValue().first.getName();
|
||||||
eventListener.onItemCompleted(!TextUtils.isEmpty(name) ? name : "");
|
eventListener.onItemCompleted(!TextUtils.isEmpty(name) ? name : "");
|
||||||
} else if (notification.isOnError()) {
|
} else if (notification.isOnError()) {
|
||||||
final Throwable error = notification.getError();
|
final Throwable error = notification.getError();
|
||||||
|
@ -275,10 +286,12 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Function<List<Notification<ChannelInfo>>, List<SubscriptionEntity>> upsertBatch() {
|
private Function<List<Notification<Pair<ChannelInfo, List<ChannelTabInfo>>>>,
|
||||||
|
List<SubscriptionEntity>> upsertBatch() {
|
||||||
return notificationList -> {
|
return notificationList -> {
|
||||||
final List<ChannelInfo> infoList = new ArrayList<>(notificationList.size());
|
final List<Pair<ChannelInfo, List<ChannelTabInfo>>> infoList =
|
||||||
for (final Notification<ChannelInfo> n : notificationList) {
|
new ArrayList<>(notificationList.size());
|
||||||
|
for (final Notification<Pair<ChannelInfo, List<ChannelTabInfo>>> n : notificationList) {
|
||||||
if (n.isOnNext()) {
|
if (n.isOnNext()) {
|
||||||
infoList.add(n.getValue());
|
infoList.add(n.getValue());
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
|
@ -15,7 +16,7 @@ import java.util.stream.Collectors;
|
||||||
import io.reactivex.rxjava3.core.SingleObserver;
|
import io.reactivex.rxjava3.core.SingleObserver;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
abstract class AbstractInfoPlayQueue<T extends ListInfo<? extends InfoItem>>
|
||||||
extends PlayQueue {
|
extends PlayQueue {
|
||||||
boolean isInitial;
|
boolean isInitial;
|
||||||
private boolean isComplete;
|
private boolean isComplete;
|
||||||
|
@ -27,7 +28,13 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
||||||
private transient Disposable fetchReactor;
|
private transient Disposable fetchReactor;
|
||||||
|
|
||||||
protected AbstractInfoPlayQueue(final T info) {
|
protected AbstractInfoPlayQueue(final T info) {
|
||||||
this(info.getServiceId(), info.getUrl(), info.getNextPage(), info.getRelatedItems(), 0);
|
this(info.getServiceId(), info.getUrl(), info.getNextPage(),
|
||||||
|
info.getRelatedItems()
|
||||||
|
.stream()
|
||||||
|
.filter(StreamInfoItem.class::isInstance)
|
||||||
|
.map(StreamInfoItem.class::cast)
|
||||||
|
.collect(Collectors.toList()),
|
||||||
|
0);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected AbstractInfoPlayQueue(final int serviceId,
|
protected AbstractInfoPlayQueue(final int serviceId,
|
||||||
|
@ -72,7 +79,11 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
||||||
}
|
}
|
||||||
nextPage = result.getNextPage();
|
nextPage = result.getNextPage();
|
||||||
|
|
||||||
append(extractListItems(result.getRelatedItems()));
|
append(extractListItems(result.getRelatedItems()
|
||||||
|
.stream()
|
||||||
|
.filter(StreamInfoItem.class::isInstance)
|
||||||
|
.map(StreamInfoItem.class::cast)
|
||||||
|
.collect(Collectors.toList())));
|
||||||
|
|
||||||
fetchReactor.dispose();
|
fetchReactor.dispose();
|
||||||
fetchReactor = null;
|
fetchReactor = null;
|
||||||
|
@ -87,7 +98,7 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
SingleObserver<ListExtractor.InfoItemsPage<StreamInfoItem>> getNextPageObserver() {
|
SingleObserver<ListExtractor.InfoItemsPage<? extends InfoItem>> getNextPageObserver() {
|
||||||
return new SingleObserver<>() {
|
return new SingleObserver<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(@NonNull final Disposable d) {
|
public void onSubscribe(@NonNull final Disposable d) {
|
||||||
|
@ -101,13 +112,17 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(
|
public void onSuccess(
|
||||||
@NonNull final ListExtractor.InfoItemsPage<StreamInfoItem> result) {
|
@NonNull final ListExtractor.InfoItemsPage<? extends InfoItem> result) {
|
||||||
if (!result.hasNextPage()) {
|
if (!result.hasNextPage()) {
|
||||||
isComplete = true;
|
isComplete = true;
|
||||||
}
|
}
|
||||||
nextPage = result.getNextPage();
|
nextPage = result.getNextPage();
|
||||||
|
|
||||||
append(extractListItems(result.getItems()));
|
append(extractListItems(result.getItems()
|
||||||
|
.stream()
|
||||||
|
.filter(StreamInfoItem.class::isInstance)
|
||||||
|
.map(StreamInfoItem.class::cast)
|
||||||
|
.collect(Collectors.toList())));
|
||||||
|
|
||||||
fetchReactor.dispose();
|
fetchReactor.dispose();
|
||||||
fetchReactor = null;
|
fetchReactor = null;
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
package org.schabi.newpipe.player.playqueue;
|
|
||||||
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.Page;
|
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
||||||
|
|
||||||
public final class ChannelPlayQueue extends AbstractInfoPlayQueue<ChannelInfo> {
|
|
||||||
|
|
||||||
public ChannelPlayQueue(final ChannelInfo info) {
|
|
||||||
super(info);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChannelPlayQueue(final int serviceId,
|
|
||||||
final String url,
|
|
||||||
final Page nextPage,
|
|
||||||
final List<StreamInfoItem> streams,
|
|
||||||
final int index) {
|
|
||||||
super(serviceId, url, nextPage, streams, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getTag() {
|
|
||||||
return "ChannelPlayQueue@" + Integer.toHexString(hashCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void fetch() {
|
|
||||||
if (this.isInitial) {
|
|
||||||
ExtractorHelper.getChannelInfo(this.serviceId, this.baseUrl, false)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(getHeadListObserver());
|
|
||||||
} else {
|
|
||||||
ExtractorHelper.getMoreChannelItems(this.serviceId, this.baseUrl, this.nextPage)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(getNextPageObserver());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package org.schabi.newpipe.player.playqueue;
|
||||||
|
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
|
public final class ChannelTabPlayQueue extends AbstractInfoPlayQueue<ChannelTabInfo> {
|
||||||
|
|
||||||
|
final ListLinkHandler linkHandler;
|
||||||
|
|
||||||
|
public ChannelTabPlayQueue(final int serviceId,
|
||||||
|
final ListLinkHandler linkHandler,
|
||||||
|
final Page nextPage,
|
||||||
|
final List<StreamInfoItem> streams,
|
||||||
|
final int index) {
|
||||||
|
super(serviceId, linkHandler.getUrl(), nextPage, streams, index);
|
||||||
|
this.linkHandler = linkHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelTabPlayQueue(final int serviceId,
|
||||||
|
final ListLinkHandler linkHandler) {
|
||||||
|
this(serviceId, linkHandler, null, Collections.emptyList(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getTag() {
|
||||||
|
return "ChannelTabPlayQueue@" + Integer.toHexString(hashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fetch() {
|
||||||
|
if (isInitial) {
|
||||||
|
ExtractorHelper.getChannelTab(this.serviceId, this.linkHandler, false)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(getHeadListObserver());
|
||||||
|
} else {
|
||||||
|
ExtractorHelper.getMoreChannelTabItems(this.serviceId, this.linkHandler, this.nextPage)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(getNextPageObserver());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,21 +44,11 @@ public final class NewPipeSettings {
|
||||||
private NewPipeSettings() { }
|
private NewPipeSettings() { }
|
||||||
|
|
||||||
public static void initSettings(final Context context) {
|
public static void initSettings(final Context context) {
|
||||||
// check if there are entries in the prefs to determine whether this is the first app run
|
// check if the last used preference version is set
|
||||||
Boolean isFirstRun = null;
|
// to determine whether this is the first app run
|
||||||
final Set<String> prefsKeys = PreferenceManager.getDefaultSharedPreferences(context)
|
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
.getAll().keySet();
|
.getInt(context.getString(R.string.last_used_preferences_version), -1);
|
||||||
for (final String key: prefsKeys) {
|
final boolean isFirstRun = lastUsedPrefVersion == -1;
|
||||||
// ACRA stores some info in the prefs during app initialization
|
|
||||||
// which happens before this method is called. Therefore ignore ACRA-related keys.
|
|
||||||
if (!key.toLowerCase().startsWith("acra")) {
|
|
||||||
isFirstRun = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isFirstRun == null) {
|
|
||||||
isFirstRun = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// first run migrations, then setDefaultValues, since the latter requires the correct types
|
// first run migrations, then setDefaultValues, since the latter requires the correct types
|
||||||
SettingMigrations.runMigrationsIfNeeded(context, isFirstRun);
|
SettingMigrations.runMigrationsIfNeeded(context, isFirstRun);
|
||||||
|
|
151
app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java
Normal file
151
app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class ChannelTabHelper {
|
||||||
|
private ChannelTabHelper() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tab the channel tab to check
|
||||||
|
* @return whether the tab should contain (playable) streams or not
|
||||||
|
*/
|
||||||
|
public static boolean isStreamsTab(final String tab) {
|
||||||
|
switch (tab) {
|
||||||
|
case ChannelTabs.VIDEOS:
|
||||||
|
case ChannelTabs.TRACKS:
|
||||||
|
case ChannelTabs.SHORTS:
|
||||||
|
case ChannelTabs.LIVESTREAMS:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tab the channel tab link handler to check
|
||||||
|
* @return whether the tab should contain (playable) streams or not
|
||||||
|
*/
|
||||||
|
public static boolean isStreamsTab(final ListLinkHandler tab) {
|
||||||
|
final List<String> contentFilters = tab.getContentFilters();
|
||||||
|
if (contentFilters.isEmpty()) {
|
||||||
|
return false; // this should never happen, but check just to be sure
|
||||||
|
} else {
|
||||||
|
return isStreamsTab(contentFilters.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
private static int getShowTabKey(final String tab) {
|
||||||
|
switch (tab) {
|
||||||
|
case ChannelTabs.VIDEOS:
|
||||||
|
return R.string.show_channel_tabs_videos;
|
||||||
|
case ChannelTabs.TRACKS:
|
||||||
|
return R.string.show_channel_tabs_tracks;
|
||||||
|
case ChannelTabs.SHORTS:
|
||||||
|
return R.string.show_channel_tabs_shorts;
|
||||||
|
case ChannelTabs.LIVESTREAMS:
|
||||||
|
return R.string.show_channel_tabs_livestreams;
|
||||||
|
case ChannelTabs.CHANNELS:
|
||||||
|
return R.string.show_channel_tabs_channels;
|
||||||
|
case ChannelTabs.PLAYLISTS:
|
||||||
|
return R.string.show_channel_tabs_playlists;
|
||||||
|
case ChannelTabs.ALBUMS:
|
||||||
|
return R.string.show_channel_tabs_albums;
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
private static int getFetchFeedTabKey(final String tab) {
|
||||||
|
switch (tab) {
|
||||||
|
case ChannelTabs.VIDEOS:
|
||||||
|
return R.string.fetch_channel_tabs_videos;
|
||||||
|
case ChannelTabs.TRACKS:
|
||||||
|
return R.string.fetch_channel_tabs_tracks;
|
||||||
|
case ChannelTabs.SHORTS:
|
||||||
|
return R.string.fetch_channel_tabs_shorts;
|
||||||
|
case ChannelTabs.LIVESTREAMS:
|
||||||
|
return R.string.fetch_channel_tabs_livestreams;
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
public static int getTranslationKey(final String tab) {
|
||||||
|
switch (tab) {
|
||||||
|
case ChannelTabs.VIDEOS:
|
||||||
|
return R.string.channel_tab_videos;
|
||||||
|
case ChannelTabs.TRACKS:
|
||||||
|
return R.string.channel_tab_tracks;
|
||||||
|
case ChannelTabs.SHORTS:
|
||||||
|
return R.string.channel_tab_shorts;
|
||||||
|
case ChannelTabs.LIVESTREAMS:
|
||||||
|
return R.string.channel_tab_livestreams;
|
||||||
|
case ChannelTabs.CHANNELS:
|
||||||
|
return R.string.channel_tab_channels;
|
||||||
|
case ChannelTabs.PLAYLISTS:
|
||||||
|
return R.string.channel_tab_playlists;
|
||||||
|
case ChannelTabs.ALBUMS:
|
||||||
|
return R.string.channel_tab_albums;
|
||||||
|
default:
|
||||||
|
return R.string.unknown_content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean showChannelTab(final Context context,
|
||||||
|
final SharedPreferences sharedPreferences,
|
||||||
|
@StringRes final int key) {
|
||||||
|
final Set<String> enabledTabs = sharedPreferences.getStringSet(
|
||||||
|
context.getString(R.string.show_channel_tabs_key), null);
|
||||||
|
if (enabledTabs == null) {
|
||||||
|
return true; // default to true
|
||||||
|
} else {
|
||||||
|
return enabledTabs.contains(context.getString(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean showChannelTab(final Context context,
|
||||||
|
final SharedPreferences sharedPreferences,
|
||||||
|
final String tab) {
|
||||||
|
final int key = ChannelTabHelper.getShowTabKey(tab);
|
||||||
|
if (key == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return showChannelTab(context, sharedPreferences, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean fetchFeedChannelTab(final Context context,
|
||||||
|
final SharedPreferences sharedPreferences,
|
||||||
|
final ListLinkHandler tab) {
|
||||||
|
final List<String> contentFilters = tab.getContentFilters();
|
||||||
|
if (contentFilters.isEmpty()) {
|
||||||
|
return false; // this should never happen, but check just to be sure
|
||||||
|
}
|
||||||
|
|
||||||
|
final int key = ChannelTabHelper.getFetchFeedTabKey(contentFilters.get(0));
|
||||||
|
if (key == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Set<String> enabledTabs = sharedPreferences.getStringSet(
|
||||||
|
context.getString(R.string.feed_fetch_channel_tabs_key), null);
|
||||||
|
if (enabledTabs == null) {
|
||||||
|
return true; // default to true
|
||||||
|
} else {
|
||||||
|
return enabledTabs.contains(context.getString(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,17 +36,15 @@ import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.Info;
|
import org.schabi.newpipe.extractor.Info;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
|
||||||
import org.schabi.newpipe.extractor.MetaInfo;
|
import org.schabi.newpipe.extractor.MetaInfo;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.extractor.feed.FeedExtractor;
|
|
||||||
import org.schabi.newpipe.extractor.feed.FeedInfo;
|
|
||||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.extractor.search.SearchInfo;
|
import org.schabi.newpipe.extractor.search.SearchInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
@ -127,28 +125,24 @@ public final class ExtractorHelper {
|
||||||
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
|
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<InfoItemsPage<StreamInfoItem>> getMoreChannelItems(final int serviceId,
|
public static Single<ChannelTabInfo> getChannelTab(final int serviceId,
|
||||||
final String url,
|
final ListLinkHandler listLinkHandler,
|
||||||
final Page nextPage) {
|
final boolean forceLoad) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
return Single.fromCallable(() ->
|
return checkCache(forceLoad, serviceId,
|
||||||
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
|
listLinkHandler.getUrl(), InfoItem.InfoType.CHANNEL,
|
||||||
|
Single.fromCallable(() ->
|
||||||
|
ChannelTabInfo.getInfo(NewPipe.getService(serviceId), listLinkHandler)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<ListInfo<StreamInfoItem>> getFeedInfoFallbackToChannelInfo(
|
public static Single<InfoItemsPage<InfoItem>> getMoreChannelTabItems(
|
||||||
final int serviceId, final String url) {
|
final int serviceId,
|
||||||
final Maybe<ListInfo<StreamInfoItem>> maybeFeedInfo = Maybe.fromCallable(() -> {
|
final ListLinkHandler listLinkHandler,
|
||||||
final StreamingService service = NewPipe.getService(serviceId);
|
final Page nextPage) {
|
||||||
final FeedExtractor feedExtractor = service.getFeedExtractor(url);
|
checkServiceId(serviceId);
|
||||||
|
return Single.fromCallable(() ->
|
||||||
if (feedExtractor == null) {
|
ChannelTabInfo.getMoreItems(NewPipe.getService(serviceId),
|
||||||
return null;
|
listLinkHandler, nextPage));
|
||||||
}
|
|
||||||
|
|
||||||
return FeedInfo.getInfo(feedExtractor);
|
|
||||||
});
|
|
||||||
|
|
||||||
return maybeFeedInfo.switchIfEmpty(getChannelInfo(serviceId, url, true));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<CommentsInfo> getCommentsInfo(final int serviceId, final String url,
|
public static Single<CommentsInfo> getCommentsInfo(final int serviceId, final String url,
|
||||||
|
@ -229,7 +223,7 @@ public final class ExtractorHelper {
|
||||||
load = actualLoadFromNetwork;
|
load = actualLoadFromNetwork;
|
||||||
} else {
|
} else {
|
||||||
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, infoType),
|
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, infoType),
|
||||||
actualLoadFromNetwork.toMaybe())
|
actualLoadFromNetwork.toMaybe())
|
||||||
.firstElement() // Take the first valid
|
.firstElement() // Take the first valid
|
||||||
.toSingle();
|
.toSingle();
|
||||||
}
|
}
|
||||||
|
@ -240,10 +234,10 @@ public final class ExtractorHelper {
|
||||||
/**
|
/**
|
||||||
* Default implementation uses the {@link InfoCache} to get cached results.
|
* Default implementation uses the {@link InfoCache} to get cached results.
|
||||||
*
|
*
|
||||||
* @param <I> the item type's class that extends {@link Info}
|
* @param <I> the item type's class that extends {@link Info}
|
||||||
* @param serviceId the service to load from
|
* @param serviceId the service to load from
|
||||||
* @param url the URL to load
|
* @param url the URL to load
|
||||||
* @param infoType the {@link InfoItem.InfoType} of the item
|
* @param infoType the {@link InfoItem.InfoType} of the item
|
||||||
* @return a {@link Single} that loads the item
|
* @return a {@link Single} that loads the item
|
||||||
*/
|
*/
|
||||||
private static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url,
|
private static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url,
|
||||||
|
@ -274,11 +268,12 @@ public final class ExtractorHelper {
|
||||||
* Formats the text contained in the meta info list as HTML and puts it into the text view,
|
* Formats the text contained in the meta info list as HTML and puts it into the text view,
|
||||||
* while also making the separator visible. If the list is null or empty, or the user chose not
|
* while also making the separator visible. If the list is null or empty, or the user chose not
|
||||||
* to see meta information, both the text view and the separator are hidden
|
* to see meta information, both the text view and the separator are hidden
|
||||||
* @param metaInfos a list of meta information, can be null or empty
|
*
|
||||||
* @param metaInfoTextView the text view in which to show the formatted HTML
|
* @param metaInfos a list of meta information, can be null or empty
|
||||||
|
* @param metaInfoTextView the text view in which to show the formatted HTML
|
||||||
* @param metaInfoSeparator another view to be shown or hidden accordingly to the text view
|
* @param metaInfoSeparator another view to be shown or hidden accordingly to the text view
|
||||||
* @param disposables disposables created by the method are added here and their lifecycle
|
* @param disposables disposables created by the method are added here and their lifecycle
|
||||||
* should be handled by the calling class
|
* should be handled by the calling class
|
||||||
*/
|
*/
|
||||||
public static void showMetaInfoInTextView(@Nullable final List<MetaInfo> metaInfos,
|
public static void showMetaInfoInTextView(@Nullable final List<MetaInfo> metaInfos,
|
||||||
final TextView metaInfoTextView,
|
final TextView metaInfoTextView,
|
||||||
|
@ -287,7 +282,7 @@ public final class ExtractorHelper {
|
||||||
final Context context = metaInfoTextView.getContext();
|
final Context context = metaInfoTextView.getContext();
|
||||||
if (metaInfos == null || metaInfos.isEmpty()
|
if (metaInfos == null || metaInfos.isEmpty()
|
||||||
|| !PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
|
|| !PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
|
||||||
context.getString(R.string.show_meta_info_key), true)) {
|
context.getString(R.string.show_meta_info_key), true)) {
|
||||||
metaInfoTextView.setVisibility(View.GONE);
|
metaInfoTextView.setVisibility(View.GONE);
|
||||||
metaInfoSeparator.setVisibility(View.GONE);
|
metaInfoSeparator.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||||
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||||
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for play buttons and their respective click listeners.
|
||||||
|
*/
|
||||||
|
public final class PlayButtonHelper {
|
||||||
|
|
||||||
|
private PlayButtonHelper() {
|
||||||
|
// utility class
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize {@link android.view.View.OnClickListener OnClickListener}
|
||||||
|
* and {@link android.view.View.OnLongClickListener OnLongClickListener} for playlist control
|
||||||
|
* buttons defined in {@link R.layout#playlist_control}.
|
||||||
|
*
|
||||||
|
* @param activity The activity to use for the {@link android.widget.Toast Toast}.
|
||||||
|
* @param playlistControlBinding The binding of the
|
||||||
|
* {@link R.layout#playlist_control playlist control layout}.
|
||||||
|
* @param fragment The fragment to get the play queue from.
|
||||||
|
*/
|
||||||
|
public static void initPlaylistControlClickListener(
|
||||||
|
@NonNull final AppCompatActivity activity,
|
||||||
|
@NonNull final PlaylistControlBinding playlistControlBinding,
|
||||||
|
@NonNull final PlaylistControlViewHolder fragment) {
|
||||||
|
// click listener
|
||||||
|
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> {
|
||||||
|
NavigationHelper.playOnMainPlayer(activity, fragment.getPlayQueue());
|
||||||
|
showHoldToAppendToastIfNeeded(activity);
|
||||||
|
});
|
||||||
|
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> {
|
||||||
|
NavigationHelper.playOnPopupPlayer(activity, fragment.getPlayQueue(), false);
|
||||||
|
showHoldToAppendToastIfNeeded(activity);
|
||||||
|
});
|
||||||
|
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> {
|
||||||
|
NavigationHelper.playOnBackgroundPlayer(activity, fragment.getPlayQueue(), false);
|
||||||
|
showHoldToAppendToastIfNeeded(activity);
|
||||||
|
});
|
||||||
|
|
||||||
|
// long click listener
|
||||||
|
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
||||||
|
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.POPUP);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
||||||
|
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.AUDIO);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the "hold to append" toast if the corresponding preference is enabled.
|
||||||
|
*
|
||||||
|
* @param context The context to show the toast.
|
||||||
|
*/
|
||||||
|
private static void showHoldToAppendToastIfNeeded(@NonNull final Context context) {
|
||||||
|
if (shouldShowHoldToAppendTip(context)) {
|
||||||
|
Toast.makeText(context, R.string.hold_to_append, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the "hold to append" toast should be shown.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The tip is shown if the corresponding preference is enabled.
|
||||||
|
* This is the default behaviour.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param context The context to get the preference.
|
||||||
|
* @return {@code true} if the tip should be shown, {@code false} otherwise.
|
||||||
|
*/
|
||||||
|
public static boolean shouldShowHoldToAppendTip(@NonNull final Context context) {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getBoolean(context.getString(R.string.show_hold_to_append_key), true);
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 35 KiB |
|
@ -1,131 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/contrast_background_color">
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:id="@+id/channel_metadata"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/channel_banner_image"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="70dp"
|
|
||||||
android:background="@android:color/black"
|
|
||||||
android:fitsSystemWindows="true"
|
|
||||||
android:scaleType="fitCenter"
|
|
||||||
android:src="@drawable/placeholder_channel_banner"
|
|
||||||
tools:ignore="ContentDescription" />
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/avatars_layout"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
android:layout_marginTop="50dp">
|
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
|
||||||
android:id="@+id/channel_avatar_view"
|
|
||||||
android:layout_width="@dimen/channel_avatar_size"
|
|
||||||
android:layout_height="@dimen/channel_avatar_size"
|
|
||||||
android:padding="1dp"
|
|
||||||
android:src="@drawable/placeholder_person"
|
|
||||||
app:shapeAppearance="@style/CircularImageView"
|
|
||||||
app:strokeColor="#ffffff"
|
|
||||||
app:strokeWidth="2dp" />
|
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
|
||||||
android:id="@+id/sub_channel_avatar_view"
|
|
||||||
android:layout_width="@dimen/sub_channel_avatar_size"
|
|
||||||
android:layout_height="@dimen/sub_channel_avatar_size"
|
|
||||||
android:layout_gravity="bottom|right"
|
|
||||||
android:padding="1dp"
|
|
||||||
android:src="@drawable/placeholder_person"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:shapeAppearance="@style/CircularImageView"
|
|
||||||
app:strokeColor="#ffffff"
|
|
||||||
app:strokeWidth="2dp"
|
|
||||||
tools:ignore="RtlHardcoded"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/channel_title_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@id/channel_banner_image"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
android:layout_marginTop="6dp"
|
|
||||||
android:layout_marginRight="8dp"
|
|
||||||
android:layout_toLeftOf="@id/channel_subscribe_button"
|
|
||||||
android:layout_toRightOf="@id/avatars_layout"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:lines="1"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
|
||||||
android:textSize="@dimen/video_item_detail_title_text_size"
|
|
||||||
tools:ignore="RtlHardcoded"
|
|
||||||
tools:text="Lorem ipsum dolor" />
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/sub_channel_title_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@id/channel_title_view"
|
|
||||||
android:layout_alignLeft="@id/channel_title_view"
|
|
||||||
android:layout_alignRight="@id/channel_title_view"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:gravity="center|left"
|
|
||||||
android:lines="1"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
|
||||||
android:textSize="12dp"
|
|
||||||
tools:ignore="RtlHardcoded"
|
|
||||||
tools:layout_below="@id/channel_title_view"
|
|
||||||
tools:text="Lorem ipsum dolor" />
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/channel_subscriber_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@id/sub_channel_title_view"
|
|
||||||
android:layout_alignLeft="@id/channel_title_view"
|
|
||||||
android:layout_alignRight="@id/channel_title_view"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="2"
|
|
||||||
android:textSize="@dimen/channel_subscribers_text_size"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:ignore="RtlHardcoded"
|
|
||||||
tools:text="123,141,411 subscribers"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatButton
|
|
||||||
android:id="@+id/channel_subscribe_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@+id/channel_banner_image"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:layout_gravity="center_vertical|right"
|
|
||||||
android:layout_marginRight="2dp"
|
|
||||||
android:text="@string/subscribe_button_title"
|
|
||||||
android:textSize="@dimen/channel_rss_title_size"
|
|
||||||
android:theme="@style/ServiceColoredButton"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:ignore="RtlHardcoded"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@id/channel_metadata">
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/playlist_control"
|
|
||||||
layout="@layout/playlist_control" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
|
@ -1,80 +1,214 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<!-- since ToolbarTheme sets the tint to icons, it would make images all white,
|
||||||
android:id="@+id/items_list"
|
therefore app:tint="@null" is used to undo that setting -->
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/app_bar_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:theme="@style/ToolbarTheme"
|
||||||
|
app:elevation="0dp">
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.CustomCollapsingToolbarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/channel_metadata"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_collapseMode="parallax">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/channel_banner_image"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:maxHeight="70dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:src="@drawable/placeholder_channel_banner"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:tint="@null"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/channel_avatar_view"
|
||||||
|
android:layout_width="@dimen/channel_avatar_size"
|
||||||
|
android:layout_height="@dimen/channel_avatar_size"
|
||||||
|
android:layout_marginVertical="8dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:padding="1dp"
|
||||||
|
android:src="@drawable/placeholder_person"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="1.0"
|
||||||
|
app:shapeAppearance="@style/CircularImageView"
|
||||||
|
app:strokeColor="#ffffff"
|
||||||
|
app:strokeWidth="2dp"
|
||||||
|
app:tint="@null" />
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/sub_channel_avatar_view"
|
||||||
|
android:layout_width="@dimen/sub_channel_avatar_size"
|
||||||
|
android:layout_height="@dimen/sub_channel_avatar_size"
|
||||||
|
android:padding="1dp"
|
||||||
|
android:src="@drawable/placeholder_person"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/channel_avatar_view"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/channel_avatar_view"
|
||||||
|
app:shapeAppearance="@style/CircularImageView"
|
||||||
|
app:strokeColor="#ffffff"
|
||||||
|
app:strokeWidth="2dp"
|
||||||
|
app:tint="@null"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/channel_title_view"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/sub_channel_title_view"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/channel_subscribe_button"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/channel_avatar_view"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/channel_banner_image"
|
||||||
|
tools:text="@tools:sample/lorem[10]" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/sub_channel_title_view"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/channel_subscriber_view"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/channel_subscribe_button"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/channel_avatar_view"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/channel_title_view"
|
||||||
|
tools:text="@tools:sample/lorem[10]"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/channel_subscriber_view"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:textSize="12sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/channel_subscribe_button"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/channel_avatar_view"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/sub_channel_title_view"
|
||||||
|
tools:text="123,141,411 subscribers" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/channel_subscribe_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/subscribe_button_title"
|
||||||
|
android:textSize="@dimen/channel_rss_title_size"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/channel_banner_image" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</org.schabi.newpipe.views.CustomCollapsingToolbarLayout>
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:scrollbars="vertical"
|
android:layout_below="@id/app_bar_layout"
|
||||||
tools:listitem="@layout/list_stream_item" />
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/loading_progress_bar"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerInParent="true"
|
|
||||||
android:indeterminate="true"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/empty_state_view"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerInParent="true"
|
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingTop="90dp"
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible">
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
<com.google.android.material.tabs.TabLayout
|
||||||
android:id="@+id/channel_kaomoji"
|
android:id="@+id/tab_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/colorPrimary"
|
||||||
|
app:tabGravity="fill"
|
||||||
|
app:tabIndicatorColor="@color/white"
|
||||||
|
app:tabMode="scrollable"
|
||||||
|
app:tabRippleColor="@color/white"
|
||||||
|
app:tabTextColor="@color/white" />
|
||||||
|
|
||||||
|
<androidx.viewpager.widget.ViewPager
|
||||||
|
android:id="@+id/view_pager"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_below="@id/tab_layout" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/loading_progress_bar"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_centerInParent="true"
|
||||||
android:layout_marginBottom="10dp"
|
android:indeterminate="true"
|
||||||
android:fontFamily="monospace"
|
android:visibility="gone"
|
||||||
android:text="(╯°-°)╯"
|
tools:visibility="visible" />
|
||||||
android:textSize="35sp"
|
|
||||||
tools:ignore="HardcodedText,UnusedAttribute" />
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
<LinearLayout
|
||||||
android:id="@+id/empty_state_message"
|
android:id="@+id/empty_state_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_centerInParent="true"
|
||||||
android:text="@string/empty_view_no_videos"
|
android:orientation="vertical"
|
||||||
android:textSize="24sp" />
|
android:paddingTop="90dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
android:id="@+id/error_content_not_supported"
|
android:id="@+id/channel_kaomoji"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:text="(︶︹︺)"
|
||||||
|
android:textSize="35sp"
|
||||||
|
tools:ignore="HardcodedText" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/error_content_not_supported"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:text="@string/content_not_supported"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!--ERROR PANEL-->
|
||||||
|
<include
|
||||||
|
android:id="@+id/error_panel"
|
||||||
|
layout="@layout/error_panel"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="20dp"
|
android:layout_centerInParent="true"
|
||||||
android:text="@string/content_not_supported"
|
android:layout_marginTop="50dp"
|
||||||
android:textSize="15sp"
|
android:visibility="gone"
|
||||||
android:visibility="gone" />
|
tools:visibility="visible" />
|
||||||
|
</RelativeLayout>
|
||||||
</LinearLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
<!--ERROR PANEL-->
|
|
||||||
<include
|
|
||||||
android:id="@+id/error_panel"
|
|
||||||
layout="@layout/error_panel"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerInParent="true"
|
|
||||||
android:layout_marginTop="50dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="4dp"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:background="?attr/toolbar_shadow" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
50
app/src/main/res/layout/fragment_channel_tab.xml
Normal file
50
app/src/main/res/layout/fragment_channel_tab.xml
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/items_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
tools:listitem="@layout/list_stream_item" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/loading_progress_bar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:indeterminate="true"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/empty_state_view"
|
||||||
|
layout="@layout/list_empty_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:gravity="center"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<!--ERROR PANEL-->
|
||||||
|
<include
|
||||||
|
android:id="@+id/error_panel"
|
||||||
|
layout="@layout/error_panel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:layout_marginTop="50dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="4dp"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:background="?attr/toolbar_shadow" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
71
app/src/main/res/layout/fragment_channel_videos.xml
Normal file
71
app/src/main/res/layout/fragment_channel_videos.xml
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/items_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
tools:listitem="@layout/list_stream_item" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/loading_progress_bar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:indeterminate="true"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/empty_state_view"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingTop="90dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/channel_kaomoji"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:text="(╯°-°)╯"
|
||||||
|
android:textSize="35sp"
|
||||||
|
tools:ignore="HardcodedText,UnusedAttribute" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/channel_no_videos"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="@string/empty_view_no_videos"
|
||||||
|
android:textSize="24sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!--ERROR PANEL-->
|
||||||
|
<include
|
||||||
|
android:id="@+id/error_panel"
|
||||||
|
layout="@layout/error_panel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:layout_marginTop="50dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="4dp"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:background="?attr/toolbar_shadow" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
|
@ -8,6 +8,7 @@
|
||||||
android:scrollbars="vertical">
|
android:scrollbars="vertical">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/constraint_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:animateLayoutChanges="true">
|
android:animateLayoutChanges="true">
|
||||||
|
|
|
@ -620,6 +620,7 @@
|
||||||
<string name="metadata_thumbnail_url">Vorschaubild-URL</string>
|
<string name="metadata_thumbnail_url">Vorschaubild-URL</string>
|
||||||
<string name="metadata_host">Server</string>
|
<string name="metadata_host">Server</string>
|
||||||
<string name="metadata_support">Unterstützung</string>
|
<string name="metadata_support">Unterstützung</string>
|
||||||
|
<string name="metadata_subscribers">Abonnenten</string>
|
||||||
<string name="description_select_disable">Auswählen von Text in der Beschreibung deaktivieren</string>
|
<string name="description_select_disable">Auswählen von Text in der Beschreibung deaktivieren</string>
|
||||||
<string name="description_select_enable">Auswählen von Text in der Beschreibung aktivieren</string>
|
<string name="description_select_enable">Auswählen von Text in der Beschreibung aktivieren</string>
|
||||||
<string name="description_select_note">Du kannst nun Text innerhalb der Beschreibung auswählen. Beachte, dass die Seite flackern kann und Links im Auswahlmodus möglicherweise nicht anklickbar sind.</string>
|
<string name="description_select_note">Du kannst nun Text innerhalb der Beschreibung auswählen. Beachte, dass die Seite flackern kann und Links im Auswahlmodus möglicherweise nicht anklickbar sind.</string>
|
||||||
|
@ -766,4 +767,12 @@
|
||||||
<string name="disable_media_tunneling_automatic_info">Das Media-Tunneling wurde auf dem Gerät standardmäßig deaktiviert, da das Gerätemodell diese Funktion bekanntermaßen nicht unterstützt.</string>
|
<string name="disable_media_tunneling_automatic_info">Das Media-Tunneling wurde auf dem Gerät standardmäßig deaktiviert, da das Gerätemodell diese Funktion bekanntermaßen nicht unterstützt.</string>
|
||||||
<string name="no_live_streams">Keine Live-Streams</string>
|
<string name="no_live_streams">Keine Live-Streams</string>
|
||||||
<string name="no_streams">Keine Streams</string>
|
<string name="no_streams">Keine Streams</string>
|
||||||
|
<string name="channel_tab_videos">Videos</string>
|
||||||
|
<string name="channel_tab_livestreams">Live</string>
|
||||||
|
<string name="channel_tab_shorts">Shorts</string>
|
||||||
|
<string name="channel_tab_playlists">Wiedergabelisten</string>
|
||||||
|
<string name="channel_tab_channels">Kanäle</string>
|
||||||
|
<string name="channel_tab_albums">Alben</string>
|
||||||
|
<string name="show_channel_tabs">Tabs auf den Kanalseiten</string>
|
||||||
|
<string name="show_channel_tabs_summary">Welche Tabs auf den Kanalseiten angezeigt werden</string>
|
||||||
</resources>
|
</resources>
|
|
@ -32,7 +32,6 @@
|
||||||
<dimen name="video_item_detail_sub_channel_text_size">16sp</dimen>
|
<dimen name="video_item_detail_sub_channel_text_size">16sp</dimen>
|
||||||
<dimen name="video_item_detail_upload_date_text_size">14sp</dimen>
|
<dimen name="video_item_detail_upload_date_text_size">14sp</dimen>
|
||||||
<dimen name="video_item_detail_description_text_size">14sp</dimen>
|
<dimen name="video_item_detail_description_text_size">14sp</dimen>
|
||||||
<dimen name="channel_subscribers_text_size">14sp</dimen>
|
|
||||||
<dimen name="channel_rss_title_size">14sp</dimen>
|
<dimen name="channel_rss_title_size">14sp</dimen>
|
||||||
<!-- Elements Size -->
|
<!-- Elements Size -->
|
||||||
<dimen name="video_item_detail_uploader_image_size">42dp</dimen>
|
<dimen name="video_item_detail_uploader_image_size">42dp</dimen>
|
||||||
|
|
|
@ -75,7 +75,6 @@
|
||||||
<dimen name="video_item_detail_sub_channel_text_size">14sp</dimen>
|
<dimen name="video_item_detail_sub_channel_text_size">14sp</dimen>
|
||||||
<dimen name="video_item_detail_upload_date_text_size">13sp</dimen>
|
<dimen name="video_item_detail_upload_date_text_size">13sp</dimen>
|
||||||
<dimen name="video_item_detail_description_text_size">13sp</dimen>
|
<dimen name="video_item_detail_description_text_size">13sp</dimen>
|
||||||
<dimen name="channel_subscribers_text_size">12sp</dimen>
|
|
||||||
<dimen name="channel_rss_title_size">12sp</dimen>
|
<dimen name="channel_rss_title_size">12sp</dimen>
|
||||||
<!-- Elements Size -->
|
<!-- Elements Size -->
|
||||||
<dimen name="video_item_detail_uploader_image_size">32dp</dimen>
|
<dimen name="video_item_detail_uploader_image_size">32dp</dimen>
|
||||||
|
|
|
@ -274,6 +274,35 @@
|
||||||
<string name="main_tabs_position_key">main_tabs_position</string>
|
<string name="main_tabs_position_key">main_tabs_position</string>
|
||||||
|
|
||||||
<!-- Content & History -->
|
<!-- Content & History -->
|
||||||
|
<string name="show_channel_tabs_key">channel_tabs</string>
|
||||||
|
<string name="show_channel_tabs_videos">show_channel_tabs_videos</string>
|
||||||
|
<string name="show_channel_tabs_tracks">show_channel_tabs_tracks</string>
|
||||||
|
<string name="show_channel_tabs_shorts">show_channel_tabs_shorts</string>
|
||||||
|
<string name="show_channel_tabs_livestreams">show_channel_tabs_livestreams</string>
|
||||||
|
<string name="show_channel_tabs_channels">show_channel_tabs_channels</string>
|
||||||
|
<string name="show_channel_tabs_playlists">show_channel_tabs_playlists</string>
|
||||||
|
<string name="show_channel_tabs_albums">show_channel_tabs_albums</string>
|
||||||
|
<string name="show_channel_tabs_about">show_channel_tabs_about</string>
|
||||||
|
<string-array name="show_channel_tabs_value_list">
|
||||||
|
<item>@string/show_channel_tabs_videos</item>
|
||||||
|
<item>@string/show_channel_tabs_tracks</item>
|
||||||
|
<item>@string/show_channel_tabs_shorts</item>
|
||||||
|
<item>@string/show_channel_tabs_livestreams</item>
|
||||||
|
<item>@string/show_channel_tabs_channels</item>
|
||||||
|
<item>@string/show_channel_tabs_playlists</item>
|
||||||
|
<item>@string/show_channel_tabs_albums</item>
|
||||||
|
<item>@string/show_channel_tabs_about</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="show_channel_tabs_description_list">
|
||||||
|
<item>@string/channel_tab_videos</item>
|
||||||
|
<item>@string/channel_tab_tracks</item>
|
||||||
|
<item>@string/channel_tab_shorts</item>
|
||||||
|
<item>@string/channel_tab_livestreams</item>
|
||||||
|
<item>@string/channel_tab_channels</item>
|
||||||
|
<item>@string/channel_tab_playlists</item>
|
||||||
|
<item>@string/channel_tab_albums</item>
|
||||||
|
<item>@string/channel_tab_about</item>
|
||||||
|
</string-array>
|
||||||
<string name="show_search_suggestions_key">show_search_suggestions</string>
|
<string name="show_search_suggestions_key">show_search_suggestions</string>
|
||||||
<string name="show_local_search_suggestions_key">show_local_search_suggestions</string>
|
<string name="show_local_search_suggestions_key">show_local_search_suggestions</string>
|
||||||
<string name="show_remote_search_suggestions_key">show_remote_search_suggestions</string>
|
<string name="show_remote_search_suggestions_key">show_remote_search_suggestions</string>
|
||||||
|
@ -343,6 +372,24 @@
|
||||||
</string-array>
|
</string-array>
|
||||||
<string name="feed_use_dedicated_fetch_method_key">feed_use_dedicated_fetch_method</string>
|
<string name="feed_use_dedicated_fetch_method_key">feed_use_dedicated_fetch_method</string>
|
||||||
|
|
||||||
|
<string name="feed_fetch_channel_tabs_key">feed_fetch_channel_tabs</string>
|
||||||
|
<string name="fetch_channel_tabs_videos">fetch_channel_tabs_videos</string>
|
||||||
|
<string name="fetch_channel_tabs_tracks">fetch_channel_tabs_tracks</string>
|
||||||
|
<string name="fetch_channel_tabs_shorts">fetch_channel_tabs_shorts</string>
|
||||||
|
<string name="fetch_channel_tabs_livestreams">fetch_channel_tabs_livestreams</string>
|
||||||
|
<string-array name="feed_fetch_channel_tabs_value_list">
|
||||||
|
<item>@string/fetch_channel_tabs_videos</item>
|
||||||
|
<item>@string/fetch_channel_tabs_tracks</item>
|
||||||
|
<item>@string/fetch_channel_tabs_shorts</item>
|
||||||
|
<item>@string/fetch_channel_tabs_livestreams</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="feed_fetch_channel_tabs_description_list">
|
||||||
|
<item>@string/channel_tab_videos</item>
|
||||||
|
<item>@string/channel_tab_tracks</item>
|
||||||
|
<item>@string/channel_tab_shorts</item>
|
||||||
|
<item>@string/channel_tab_livestreams</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string name="import_export_data_path">import_export_data_path</string>
|
<string name="import_export_data_path">import_export_data_path</string>
|
||||||
<string name="import_data">import_data</string>
|
<string name="import_data">import_data</string>
|
||||||
<string name="export_data">export_data</string>
|
<string name="export_data">export_data</string>
|
||||||
|
|
|
@ -714,6 +714,8 @@
|
||||||
\nSo the choice boils down to what you prefer: speed or precise information.</string>
|
\nSo the choice boils down to what you prefer: speed or precise information.</string>
|
||||||
<string name="feed_hide_streams_title">Show the following streams</string>
|
<string name="feed_hide_streams_title">Show the following streams</string>
|
||||||
<string name="feed_show_hide_streams">Show/Hide streams</string>
|
<string name="feed_show_hide_streams">Show/Hide streams</string>
|
||||||
|
<string name="feed_fetch_channel_tabs">Fetch channel tabs</string>
|
||||||
|
<string name="feed_fetch_channel_tabs_summary">Tabs to fetch when updating the feed. This option has no effect if a channel is updated using fast mode.</string>
|
||||||
<string name="content_not_supported">This content is not yet supported by NewPipe.\n\nIt will hopefully be supported in a future version.</string>
|
<string name="content_not_supported">This content is not yet supported by NewPipe.\n\nIt will hopefully be supported in a future version.</string>
|
||||||
<string name="detail_sub_channel_thumbnail_view_description">Channel\'s avatar thumbnail</string>
|
<string name="detail_sub_channel_thumbnail_view_description">Channel\'s avatar thumbnail</string>
|
||||||
<string name="channel_created_by">Created by %s</string>
|
<string name="channel_created_by">Created by %s</string>
|
||||||
|
@ -754,10 +756,13 @@
|
||||||
<string name="metadata_support">Support</string>
|
<string name="metadata_support">Support</string>
|
||||||
<string name="metadata_host">Host</string>
|
<string name="metadata_host">Host</string>
|
||||||
<string name="metadata_thumbnail_url">Thumbnail URL</string>
|
<string name="metadata_thumbnail_url">Thumbnail URL</string>
|
||||||
|
<string name="metadata_avatar_url">Avatar URL</string>
|
||||||
|
<string name="metadata_banner_url">Banner URL</string>
|
||||||
<string name="metadata_privacy_public">Public</string>
|
<string name="metadata_privacy_public">Public</string>
|
||||||
<string name="metadata_privacy_unlisted">Unlisted</string>
|
<string name="metadata_privacy_unlisted">Unlisted</string>
|
||||||
<string name="metadata_privacy_private">Private</string>
|
<string name="metadata_privacy_private">Private</string>
|
||||||
<string name="metadata_privacy_internal">Internal</string>
|
<string name="metadata_privacy_internal">Internal</string>
|
||||||
|
<string name="metadata_subscribers">Subscribers</string>
|
||||||
<string name="detail_pinned_comment_view_description">Pinned comment</string>
|
<string name="detail_pinned_comment_view_description">Pinned comment</string>
|
||||||
<string name="detail_heart_img_view_description">Hearted by creator</string>
|
<string name="detail_heart_img_view_description">Hearted by creator</string>
|
||||||
<string name="open_website_license">Open website</string>
|
<string name="open_website_license">Open website</string>
|
||||||
|
@ -796,4 +801,14 @@
|
||||||
<string name="audio_track_type_original">original</string>
|
<string name="audio_track_type_original">original</string>
|
||||||
<string name="audio_track_type_dubbed">dubbed</string>
|
<string name="audio_track_type_dubbed">dubbed</string>
|
||||||
<string name="audio_track_type_descriptive">descriptive</string>
|
<string name="audio_track_type_descriptive">descriptive</string>
|
||||||
|
<string name="channel_tab_videos">Videos</string>
|
||||||
|
<string name="channel_tab_tracks">Tracks</string>
|
||||||
|
<string name="channel_tab_shorts">Shorts</string>
|
||||||
|
<string name="channel_tab_livestreams">Live</string>
|
||||||
|
<string name="channel_tab_channels">Channels</string>
|
||||||
|
<string name="channel_tab_playlists">Playlists</string>
|
||||||
|
<string name="channel_tab_albums">Albums</string>
|
||||||
|
<string name="channel_tab_about">About</string>
|
||||||
|
<string name="show_channel_tabs">Channel tabs</string>
|
||||||
|
<string name="show_channel_tabs_summary">What tabs are shown on the channel pages</string>
|
||||||
</resources>
|
</resources>
|
|
@ -41,6 +41,16 @@
|
||||||
app:singleLineTitle="false"
|
app:singleLineTitle="false"
|
||||||
app:iconSpaceReserved="false" />
|
app:iconSpaceReserved="false" />
|
||||||
|
|
||||||
|
<MultiSelectListPreference
|
||||||
|
android:key="@string/show_channel_tabs_key"
|
||||||
|
android:summary="@string/show_channel_tabs_summary"
|
||||||
|
android:title="@string/show_channel_tabs"
|
||||||
|
android:entries="@array/show_channel_tabs_description_list"
|
||||||
|
android:entryValues="@array/show_channel_tabs_value_list"
|
||||||
|
android:defaultValue="@array/show_channel_tabs_value_list"
|
||||||
|
app:iconSpaceReserved="false"
|
||||||
|
app:singleLineTitle="false" />
|
||||||
|
|
||||||
<PreferenceScreen
|
<PreferenceScreen
|
||||||
android:fragment="org.schabi.newpipe.settings.PeertubeInstanceListFragment"
|
android:fragment="org.schabi.newpipe.settings.PeertubeInstanceListFragment"
|
||||||
android:key="@string/peertube_instance_setup_key"
|
android:key="@string/peertube_instance_setup_key"
|
||||||
|
@ -152,5 +162,15 @@
|
||||||
app:singleLineTitle="false"
|
app:singleLineTitle="false"
|
||||||
app:iconSpaceReserved="false" />
|
app:iconSpaceReserved="false" />
|
||||||
|
|
||||||
|
<MultiSelectListPreference
|
||||||
|
android:key="@string/feed_fetch_channel_tabs_key"
|
||||||
|
android:summary="@string/feed_fetch_channel_tabs_summary"
|
||||||
|
android:title="@string/feed_fetch_channel_tabs"
|
||||||
|
android:entries="@array/feed_fetch_channel_tabs_description_list"
|
||||||
|
android:entryValues="@array/feed_fetch_channel_tabs_value_list"
|
||||||
|
android:defaultValue="@array/feed_fetch_channel_tabs_value_list"
|
||||||
|
app:iconSpaceReserved="false"
|
||||||
|
app:singleLineTitle="false" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
|
Loading…
Reference in a new issue