mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2024-11-22 11:02:35 +01:00
feat: add channel tabs
This commit is contained in:
parent
7e2ab0d384
commit
6d13cf5e71
@ -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.Theta-Dev:NewPipeExtractor:8446e20a71dbddbe1626a118d0adf490e5e63bbb'
|
||||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
|
@ -1,96 +1,55 @@
|
|||||||
package org.schabi.newpipe.fragments.list.channel;
|
package org.schabi.newpipe.fragments.list.channel;
|
||||||
|
|
||||||
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
|
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
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 com.google.android.material.snackbar.Snackbar;
|
|
||||||
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.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.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.linkhandler.ChannelTabHandler;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.detail.TabAdapter;
|
||||||
import org.schabi.newpipe.ktx.AnimationType;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
|
||||||
import org.schabi.newpipe.player.PlayerType;
|
|
||||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
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.ThemeHelper;
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.util.List;
|
import icepick.State;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
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.Single;
|
|
||||||
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.Consumer;
|
|
||||||
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 {
|
@State
|
||||||
|
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||||
|
@State
|
||||||
|
protected String name;
|
||||||
|
@State
|
||||||
|
protected String url;
|
||||||
|
|
||||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
private ChannelInfo currentInfo;
|
||||||
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
|
private Disposable currentWorker;
|
||||||
|
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
private MenuItem menuRssButton;
|
||||||
private Disposable subscribeButtonMonitor;
|
|
||||||
|
|
||||||
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 menuNotifyButton;
|
|
||||||
|
|
||||||
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) {
|
||||||
@ -100,15 +59,13 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ChannelFragment() {
|
public ChannelFragment() {
|
||||||
super(UserAction.REQUESTED_CHANNEL);
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
protected void setInitialData(final int sid, final String u, final String title) {
|
||||||
public void onResume() {
|
this.serviceId = sid;
|
||||||
super.onResume();
|
this.url = u;
|
||||||
if (activity != null && useAsFrontPage) {
|
this.name = !TextUtils.isEmpty(title) ? title : "";
|
||||||
setTitle(currentInfo != null ? currentInfo.getName() : name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -116,59 +73,35 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(@NonNull final Context context) {
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onAttach(context);
|
super.onCreate(savedInstanceState);
|
||||||
subscriptionManager = new SubscriptionManager(activity);
|
setHasOptionsMenu(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
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 {@link 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();
|
tabAdapter = new TabAdapter(getChildFragmentManager());
|
||||||
|
binding.viewPager.setAdapter(tabAdapter);
|
||||||
|
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
disposables.clear();
|
binding = null;
|
||||||
if (subscribeButtonMonitor != null) {
|
|
||||||
subscribeButtonMonitor.dispose();
|
|
||||||
}
|
|
||||||
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
|
|
||||||
protected void initListeners() {
|
|
||||||
super.initListeners();
|
|
||||||
|
|
||||||
headerBinding.subChannelTitleView.setOnClickListener(this);
|
|
||||||
headerBinding.subChannelAvatarView.setOnClickListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Menu
|
// Menu
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@ -176,19 +109,14 @@ 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);
|
|
||||||
}
|
}
|
||||||
|
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
||||||
|
updateRssButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -197,11 +125,6 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
|||||||
case R.id.action_settings:
|
case R.id.action_settings:
|
||||||
NavigationHelper.openSettings(requireContext());
|
NavigationHelper.openSettings(requireContext());
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_notify:
|
|
||||||
final boolean value = !item.isChecked();
|
|
||||||
item.setEnabled(false);
|
|
||||||
setNotify(value);
|
|
||||||
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,377 +147,71 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateRssButton() {
|
||||||
|
if (currentInfo != null && menuRssButton != null) {
|
||||||
|
menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Channel Subscription
|
// Init
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void monitorSubscription(final ChannelInfo info) {
|
private void updateTabs() {
|
||||||
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
tabAdapter.clearAllItems();
|
||||||
animate(headerBinding.channelSubscribeButton, false, 100);
|
|
||||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
|
|
||||||
"Get subscription status", currentInfo));
|
|
||||||
};
|
|
||||||
|
|
||||||
final Observable<List<SubscriptionEntity>> observable = subscriptionManager
|
if (currentInfo != null) {
|
||||||
.subscriptionTable()
|
tabAdapter.addFragment(ChannelVideosFragment.getInstance(currentInfo), "Videos");
|
||||||
.getSubscriptionFlowable(info.getServiceId(), info.getUrl())
|
|
||||||
.toObservable();
|
|
||||||
|
|
||||||
disposables.add(observable
|
for (final ChannelTabHandler tab : currentInfo.getTabs()) {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
tabAdapter.addFragment(
|
||||||
.subscribe(getSubscribeUpdateMonitor(info), onError));
|
ChannelTabFragment.getInstance(serviceId, tab), tab.getTab().name());
|
||||||
|
|
||||||
disposables.add(observable
|
|
||||||
.map(List::isEmpty)
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(isEmpty -> updateSubscribeButton(!isEmpty), onError));
|
|
||||||
|
|
||||||
disposables.add(observable
|
|
||||||
.map(List::isEmpty)
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.skip(1) // channel has just been opened
|
|
||||||
.filter(x -> NotificationHelper.areNewStreamsNotificationsEnabled(requireContext()))
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(isEmpty -> {
|
|
||||||
if (!isEmpty) {
|
|
||||||
showNotifySnackbar();
|
|
||||||
}
|
|
||||||
}, onError));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription,
|
|
||||||
final ChannelInfo info) {
|
|
||||||
return (@NonNull Object o) -> {
|
|
||||||
subscriptionManager.insertSubscription(subscription, info);
|
|
||||||
return o;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
|
|
||||||
return (@NonNull Object o) -> {
|
|
||||||
subscriptionManager.deleteSubscription(subscription);
|
|
||||||
return o;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateSubscription(final ChannelInfo info) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "updateSubscription() called with: info = [" + info + "]");
|
|
||||||
}
|
|
||||||
final Action onComplete = () -> {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "Updated subscription: " + info.getUrl());
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
|
final String description = currentInfo.getDescription();
|
||||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_UPDATE,
|
if (!description.isEmpty()) {
|
||||||
"Updating subscription for " + info.getUrl(), info));
|
tabAdapter.addFragment(ChannelInfoFragment.getInstance(description), "Info");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
disposables.add(subscriptionManager.updateChannelInfo(info)
|
tabAdapter.notifyDataSetUpdate();
|
||||||
|
|
||||||
|
for (int i = 0; i < tabAdapter.getCount(); i++) {
|
||||||
|
binding.tabLayout.getTabAt(i).setText(tabAdapter.getItemTitle(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(onComplete, onError));
|
.subscribe(result -> {
|
||||||
}
|
isLoading.set(false);
|
||||||
|
handleResult(result);
|
||||||
private Disposable monitorSubscribeButton(final Button subscribeButton,
|
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
|
||||||
final Function<Object, Object> action) {
|
url == null ? "no url" : url, serviceId)));
|
||||||
final Consumer<Object> onNext = (@NonNull Object o) -> {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "Changed subscription status to this channel!");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
|
|
||||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_CHANGE,
|
|
||||||
"Changing subscription for " + currentInfo.getUrl(), currentInfo));
|
|
||||||
|
|
||||||
/* Emit clicks from main thread unto io thread */
|
|
||||||
return RxView.clicks(subscribeButton)
|
|
||||||
.subscribeOn(AndroidSchedulers.mainThread())
|
|
||||||
.observeOn(Schedulers.io())
|
|
||||||
.debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks
|
|
||||||
.map(action)
|
|
||||||
.subscribe(onNext, onError);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
|
|
||||||
return (List<SubscriptionEntity> subscriptionEntities) -> {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: "
|
|
||||||
+ "subscriptionEntities = [" + subscriptionEntities + "]");
|
|
||||||
}
|
|
||||||
if (subscribeButtonMonitor != null) {
|
|
||||||
subscribeButtonMonitor.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subscriptionEntities.isEmpty()) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "No subscription to this channel!");
|
|
||||||
}
|
|
||||||
final SubscriptionEntity channel = new SubscriptionEntity();
|
|
||||||
channel.setServiceId(info.getServiceId());
|
|
||||||
channel.setUrl(info.getUrl());
|
|
||||||
channel.setData(info.getName(),
|
|
||||||
info.getAvatarUrl(),
|
|
||||||
info.getDescription(),
|
|
||||||
info.getSubscriberCount());
|
|
||||||
updateNotifyButton(null);
|
|
||||||
subscribeButtonMonitor = monitorSubscribeButton(
|
|
||||||
headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info));
|
|
||||||
} else {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "Found subscription to this channel!");
|
|
||||||
}
|
|
||||||
final SubscriptionEntity subscription = subscriptionEntities.get(0);
|
|
||||||
updateNotifyButton(subscription);
|
|
||||||
subscribeButtonMonitor = monitorSubscribeButton(
|
|
||||||
headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateSubscribeButton(final boolean isSubscribed) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "updateSubscribeButton() called with: "
|
|
||||||
+ "isSubscribed = [" + isSubscribed + "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
final boolean isButtonVisible = headerBinding.channelSubscribeButton.getVisibility()
|
|
||||||
== View.VISIBLE;
|
|
||||||
final int backgroundDuration = isButtonVisible ? 300 : 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
|
|
||||||
.getColor(activity, R.color.subscribed_background_color);
|
|
||||||
final int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
|
|
||||||
|
|
||||||
if (!isSubscribed) {
|
|
||||||
headerBinding.channelSubscribeButton.setText(R.string.subscribe_button_title);
|
|
||||||
animateBackgroundColor(headerBinding.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);
|
|
||||||
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribeText,
|
|
||||||
subscribedText);
|
|
||||||
}
|
|
||||||
|
|
||||||
animate(headerBinding.channelSubscribeButton, true, 100,
|
|
||||||
AnimationType.LIGHT_SCALE_AND_ALPHA);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
|
|
||||||
if (menuNotifyButton == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (subscription != null) {
|
|
||||||
menuNotifyButton.setEnabled(
|
|
||||||
NotificationHelper.areNewStreamsNotificationsEnabled(requireContext())
|
|
||||||
);
|
|
||||||
menuNotifyButton.setChecked(
|
|
||||||
subscription.getNotificationMode() == NotificationMode.ENABLED
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
menuNotifyButton.setVisible(subscription != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setNotify(final boolean isEnabled) {
|
|
||||||
disposables.add(
|
|
||||||
subscriptionManager
|
|
||||||
.updateNotificationMode(
|
|
||||||
currentInfo.getServiceId(),
|
|
||||||
currentInfo.getUrl(),
|
|
||||||
isEnabled ? NotificationMode.ENABLED : NotificationMode.DISABLED)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show a snackbar with the option to enable notifications on new streams for this channel.
|
|
||||||
*/
|
|
||||||
private void showNotifySnackbar() {
|
|
||||||
Snackbar.make(itemsList, R.string.you_successfully_subscribed, Snackbar.LENGTH_LONG)
|
|
||||||
.setAction(R.string.get_notified, v -> setNotify(true))
|
|
||||||
.setActionTextColor(Color.YELLOW)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Load and handle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<ListExtractor.InfoItemsPage<StreamInfoItem>> loadMoreItemsLogic() {
|
|
||||||
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Single<ChannelInfo> loadResult(final boolean forceLoad) {
|
public void handleResult(@NonNull final ChannelInfo info) {
|
||||||
return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad);
|
super.handleResult(info);
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
currentInfo = info;
|
||||||
// OnClick
|
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
updateTabs();
|
||||||
|
updateRssButton();
|
||||||
@Override
|
|
||||||
public void onClick(final View v) {
|
|
||||||
if (isLoading.get() || currentInfo == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (v.getId()) {
|
|
||||||
case R.id.sub_channel_avatar_view:
|
|
||||||
case R.id.sub_channel_title_view:
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Contract
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showLoading() {
|
|
||||||
super.showLoading();
|
|
||||||
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
|
|
||||||
animate(headerBinding.channelSubscribeButton, false, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleResult(@NonNull final ChannelInfo result) {
|
|
||||||
super.handleResult(result);
|
|
||||||
|
|
||||||
headerBinding.getRoot().setVisibility(View.VISIBLE);
|
|
||||||
PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG)
|
|
||||||
.into(headerBinding.channelBannerImage);
|
|
||||||
PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
|
||||||
.into(headerBinding.channelAvatarView);
|
|
||||||
PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
|
||||||
.into(headerBinding.subChannelAvatarView);
|
|
||||||
|
|
||||||
headerBinding.channelSubscriberView.setVisibility(View.VISIBLE);
|
|
||||||
if (result.getSubscriberCount() >= 0) {
|
|
||||||
headerBinding.channelSubscriberView.setText(Localization
|
|
||||||
.shortSubscriberCount(activity, result.getSubscriberCount()));
|
|
||||||
} else {
|
|
||||||
headerBinding.channelSubscriberView.setText(R.string.subscribers_count_not_available);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
|
|
||||||
headerBinding.subChannelTitleView.setText(String.format(
|
|
||||||
getString(R.string.channel_created_by),
|
|
||||||
currentInfo.getParentChannelName())
|
|
||||||
);
|
|
||||||
headerBinding.subChannelTitleView.setVisibility(View.VISIBLE);
|
|
||||||
headerBinding.subChannelAvatarView.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
headerBinding.subChannelTitleView.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (menuRssButton != null) {
|
|
||||||
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;
|
|
||||||
for (final Throwable throwable : result.getErrors()) {
|
|
||||||
if (throwable instanceof ContentNotSupportedException) {
|
|
||||||
channelContentNotSupported = true;
|
|
||||||
showContentNotSupportedIfNeeded();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
disposables.clear();
|
|
||||||
if (subscribeButtonMonitor != null) {
|
|
||||||
subscribeButtonMonitor.dispose();
|
|
||||||
}
|
|
||||||
updateSubscription(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() {
|
|
||||||
// channelBinding might not be initialized when handleResult() is called
|
|
||||||
// (e.g. after rotating the screen, #6696)
|
|
||||||
if (!channelContentNotSupported || channelBinding == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
|
||||||
channelBinding.channelKaomoji.setText("(︶︹︺)");
|
|
||||||
channelBinding.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,38 @@
|
|||||||
|
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.BaseFragment;
|
||||||
|
import org.schabi.newpipe.databinding.FragmentChannelInfoBinding;
|
||||||
|
|
||||||
|
public class ChannelInfoFragment extends BaseFragment {
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
public static ChannelInfoFragment getInstance(final String description) {
|
||||||
|
final ChannelInfoFragment fragment = new ChannelInfoFragment();
|
||||||
|
fragment.description = description;
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelInfoFragment() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup container,
|
||||||
|
final Bundle savedInstanceState) {
|
||||||
|
final FragmentChannelInfoBinding binding =
|
||||||
|
FragmentChannelInfoBinding.inflate(inflater, container, false);
|
||||||
|
binding.descriptionText.setText(description);
|
||||||
|
|
||||||
|
return binding.getRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
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.error.UserAction;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelTabInfo;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ChannelTabHandler;
|
||||||
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
|
import org.schabi.newpipe.util.Constants;
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
|
||||||
|
import icepick.State;
|
||||||
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
|
||||||
|
public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTabInfo> {
|
||||||
|
|
||||||
|
@State
|
||||||
|
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||||
|
|
||||||
|
@State
|
||||||
|
protected ChannelTabHandler tabHandler;
|
||||||
|
|
||||||
|
public static ChannelTabFragment getInstance(final int serviceId,
|
||||||
|
final ChannelTabHandler tabHandler) {
|
||||||
|
final ChannelTabFragment instance = new ChannelTabFragment();
|
||||||
|
instance.serviceId = serviceId;
|
||||||
|
instance.tabHandler = tabHandler;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelTabFragment() {
|
||||||
|
super(UserAction.REQUESTED_CHANNEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// LifeCycle
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@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
|
||||||
|
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) {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,584 @@
|
|||||||
|
package org.schabi.newpipe.fragments.list.channel;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
|
||||||
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
|
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
import com.jakewharton.rxbinding4.view.RxView;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.subscription.NotificationMode;
|
||||||
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
|
import org.schabi.newpipe.databinding.ChannelHeaderBinding;
|
||||||
|
import org.schabi.newpipe.databinding.FragmentChannelVideosBinding;
|
||||||
|
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||||
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
|
import org.schabi.newpipe.error.UserAction;
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
|
import org.schabi.newpipe.ktx.AnimationType;
|
||||||
|
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
||||||
|
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||||
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
|
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
import io.reactivex.rxjava3.functions.Action;
|
||||||
|
import io.reactivex.rxjava3.functions.Consumer;
|
||||||
|
import io.reactivex.rxjava3.functions.Function;
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
|
public class ChannelVideosFragment extends BaseListInfoFragment<StreamInfoItem, ChannelInfo>
|
||||||
|
implements View.OnClickListener {
|
||||||
|
|
||||||
|
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||||
|
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
|
||||||
|
|
||||||
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
private Disposable subscribeButtonMonitor;
|
||||||
|
|
||||||
|
private boolean channelContentNotSupported = false;
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Views
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
private SubscriptionManager subscriptionManager;
|
||||||
|
|
||||||
|
private FragmentChannelVideosBinding channelBinding;
|
||||||
|
private ChannelHeaderBinding headerBinding;
|
||||||
|
private PlaylistControlBinding playlistControlBinding;
|
||||||
|
|
||||||
|
private MenuItem menuNotifyButton;
|
||||||
|
|
||||||
|
public static ChannelVideosFragment getInstance(@NonNull final ChannelInfo channelInfo) {
|
||||||
|
final ChannelVideosFragment instance = new ChannelVideosFragment();
|
||||||
|
instance.setInitialData(channelInfo.getServiceId(), channelInfo.getUrl(),
|
||||||
|
channelInfo.getName());
|
||||||
|
instance.currentInfo = channelInfo;
|
||||||
|
instance.currentNextPage = channelInfo.getNextPage();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ChannelVideosFragment getInstance(
|
||||||
|
final int serviceId, final String url, final String name) {
|
||||||
|
final ChannelVideosFragment instance = new ChannelVideosFragment();
|
||||||
|
instance.setInitialData(serviceId, url, name);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelVideosFragment() {
|
||||||
|
super(UserAction.REQUESTED_CHANNEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (activity != null && useAsFrontPage) {
|
||||||
|
setTitle(currentInfo != null ? currentInfo.getName() : name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// LifeCycle
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(@NonNull final Context context) {
|
||||||
|
super.onAttach(context);
|
||||||
|
subscriptionManager = new SubscriptionManager(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup container,
|
||||||
|
@Nullable final Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_channel_videos, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(rootView, savedInstanceState);
|
||||||
|
channelBinding = FragmentChannelVideosBinding.bind(rootView);
|
||||||
|
showContentNotSupportedIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
disposables.clear();
|
||||||
|
if (subscribeButtonMonitor != null) {
|
||||||
|
subscribeButtonMonitor.dispose();
|
||||||
|
}
|
||||||
|
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
|
||||||
|
protected void initListeners() {
|
||||||
|
super.initListeners();
|
||||||
|
|
||||||
|
headerBinding.subChannelTitleView.setOnClickListener(this);
|
||||||
|
headerBinding.subChannelAvatarView.setOnClickListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Menu
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||||
|
@NonNull final MenuInflater inflater) {
|
||||||
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||||
|
if (useAsFrontPage && supportActionBar != null) {
|
||||||
|
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||||
|
} else {
|
||||||
|
inflater.inflate(R.menu.menu_channel_videos, menu);
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||||
|
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||||
|
}
|
||||||
|
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case R.id.menu_item_notify:
|
||||||
|
final boolean value = !item.isChecked();
|
||||||
|
item.setEnabled(false);
|
||||||
|
setNotify(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
|
||||||
|
if (menuNotifyButton == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (subscription != null) {
|
||||||
|
menuNotifyButton.setEnabled(
|
||||||
|
NotificationHelper.areNewStreamsNotificationsEnabled(requireContext())
|
||||||
|
);
|
||||||
|
menuNotifyButton.setChecked(
|
||||||
|
subscription.getNotificationMode() == NotificationMode.ENABLED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
menuNotifyButton.setVisible(subscription != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setNotify(final boolean isEnabled) {
|
||||||
|
disposables.add(
|
||||||
|
subscriptionManager
|
||||||
|
.updateNotificationMode(
|
||||||
|
currentInfo.getServiceId(),
|
||||||
|
currentInfo.getUrl(),
|
||||||
|
isEnabled ? NotificationMode.ENABLED : NotificationMode.DISABLED)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Channel Subscription
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
private void monitorSubscription(final ChannelInfo info) {
|
||||||
|
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
||||||
|
animate(headerBinding.channelSubscribeButton, false, 100);
|
||||||
|
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
|
||||||
|
"Get subscription status", currentInfo));
|
||||||
|
};
|
||||||
|
|
||||||
|
final Observable<List<SubscriptionEntity>> observable = subscriptionManager
|
||||||
|
.subscriptionTable()
|
||||||
|
.getSubscriptionFlowable(info.getServiceId(), info.getUrl())
|
||||||
|
.toObservable();
|
||||||
|
|
||||||
|
disposables.add(observable
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(getSubscribeUpdateMonitor(info), onError));
|
||||||
|
|
||||||
|
disposables.add(observable
|
||||||
|
.map(List::isEmpty)
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(isEmpty -> updateSubscribeButton(!isEmpty), onError));
|
||||||
|
|
||||||
|
disposables.add(observable
|
||||||
|
.map(List::isEmpty)
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.skip(1) // channel has just been opened
|
||||||
|
.filter(x -> NotificationHelper.areNewStreamsNotificationsEnabled(requireContext()))
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(isEmpty -> {
|
||||||
|
if (!isEmpty) {
|
||||||
|
showNotifySnackbar();
|
||||||
|
}
|
||||||
|
}, onError));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription,
|
||||||
|
final ChannelInfo info) {
|
||||||
|
return (@NonNull Object o) -> {
|
||||||
|
subscriptionManager.insertSubscription(subscription, info);
|
||||||
|
return o;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
|
||||||
|
return (@NonNull Object o) -> {
|
||||||
|
subscriptionManager.deleteSubscription(subscription);
|
||||||
|
return o;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSubscription(final ChannelInfo info) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "updateSubscription() called with: info = [" + info + "]");
|
||||||
|
}
|
||||||
|
final Action onComplete = () -> {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Updated subscription: " + info.getUrl());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
|
||||||
|
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_UPDATE,
|
||||||
|
"Updating subscription for " + info.getUrl(), info));
|
||||||
|
|
||||||
|
disposables.add(subscriptionManager.updateChannelInfo(info)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(onComplete, onError));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Disposable monitorSubscribeButton(final Button subscribeButton,
|
||||||
|
final Function<Object, Object> action) {
|
||||||
|
final Consumer<Object> onNext = (@NonNull Object o) -> {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Changed subscription status to this channel!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
|
||||||
|
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_CHANGE,
|
||||||
|
"Changing subscription for " + currentInfo.getUrl(), currentInfo));
|
||||||
|
|
||||||
|
/* Emit clicks from main thread unto io thread */
|
||||||
|
return RxView.clicks(subscribeButton)
|
||||||
|
.subscribeOn(AndroidSchedulers.mainThread())
|
||||||
|
.observeOn(Schedulers.io())
|
||||||
|
.debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks
|
||||||
|
.map(action)
|
||||||
|
.subscribe(onNext, onError);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
|
||||||
|
return (List<SubscriptionEntity> subscriptionEntities) -> {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: "
|
||||||
|
+ "subscriptionEntities = [" + subscriptionEntities + "]");
|
||||||
|
}
|
||||||
|
if (subscribeButtonMonitor != null) {
|
||||||
|
subscribeButtonMonitor.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscriptionEntities.isEmpty()) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "No subscription to this channel!");
|
||||||
|
}
|
||||||
|
final SubscriptionEntity channel = new SubscriptionEntity();
|
||||||
|
channel.setServiceId(info.getServiceId());
|
||||||
|
channel.setUrl(info.getUrl());
|
||||||
|
channel.setData(info.getName(),
|
||||||
|
info.getAvatarUrl(),
|
||||||
|
info.getDescription(),
|
||||||
|
info.getSubscriberCount());
|
||||||
|
updateNotifyButton(null);
|
||||||
|
subscribeButtonMonitor = monitorSubscribeButton(
|
||||||
|
headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info));
|
||||||
|
} else {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Found subscription to this channel!");
|
||||||
|
}
|
||||||
|
final SubscriptionEntity subscription = subscriptionEntities.get(0);
|
||||||
|
updateNotifyButton(subscription);
|
||||||
|
subscribeButtonMonitor = monitorSubscribeButton(
|
||||||
|
headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSubscribeButton(final boolean isSubscribed) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "updateSubscribeButton() called with: "
|
||||||
|
+ "isSubscribed = [" + isSubscribed + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean isButtonVisible = headerBinding.channelSubscribeButton.getVisibility()
|
||||||
|
== View.VISIBLE;
|
||||||
|
final int backgroundDuration = isButtonVisible ? 300 : 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
|
||||||
|
.getColor(activity, R.color.subscribed_background_color);
|
||||||
|
final int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
|
||||||
|
|
||||||
|
if (!isSubscribed) {
|
||||||
|
headerBinding.channelSubscribeButton.setText(R.string.subscribe_button_title);
|
||||||
|
animateBackgroundColor(headerBinding.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);
|
||||||
|
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribeText,
|
||||||
|
subscribedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
animate(headerBinding.channelSubscribeButton, true, 100,
|
||||||
|
AnimationType.LIGHT_SCALE_AND_ALPHA);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a snackbar with the option to enable notifications on new streams for this channel.
|
||||||
|
*/
|
||||||
|
private void showNotifySnackbar() {
|
||||||
|
Snackbar.make(itemsList, R.string.you_successfully_subscribed, Snackbar.LENGTH_LONG)
|
||||||
|
.setAction(R.string.get_notified, v -> setNotify(true))
|
||||||
|
.setActionTextColor(Color.YELLOW)
|
||||||
|
.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
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(final View v) {
|
||||||
|
if (isLoading.get() || currentInfo == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (v.getId()) {
|
||||||
|
case R.id.sub_channel_avatar_view:
|
||||||
|
case R.id.sub_channel_title_view:
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Contract
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showLoading() {
|
||||||
|
super.showLoading();
|
||||||
|
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
|
||||||
|
animate(headerBinding.channelSubscribeButton, false, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleResult(@NonNull final ChannelInfo result) {
|
||||||
|
super.handleResult(result);
|
||||||
|
|
||||||
|
headerBinding.getRoot().setVisibility(View.VISIBLE);
|
||||||
|
PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||||
|
.into(headerBinding.channelBannerImage);
|
||||||
|
PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||||
|
.into(headerBinding.channelAvatarView);
|
||||||
|
PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||||
|
.into(headerBinding.subChannelAvatarView);
|
||||||
|
|
||||||
|
headerBinding.channelSubscriberView.setVisibility(View.VISIBLE);
|
||||||
|
if (result.getSubscriberCount() >= 0) {
|
||||||
|
headerBinding.channelSubscriberView.setText(Localization
|
||||||
|
.shortSubscriberCount(activity, result.getSubscriberCount()));
|
||||||
|
} else {
|
||||||
|
headerBinding.channelSubscriberView.setText(R.string.subscribers_count_not_available);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
|
||||||
|
headerBinding.subChannelTitleView.setText(String.format(
|
||||||
|
getString(R.string.channel_created_by),
|
||||||
|
currentInfo.getParentChannelName())
|
||||||
|
);
|
||||||
|
headerBinding.subChannelTitleView.setVisibility(View.VISIBLE);
|
||||||
|
headerBinding.subChannelAvatarView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
headerBinding.subChannelTitleView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateRssButton();
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
for (final Throwable throwable : result.getErrors()) {
|
||||||
|
if (throwable instanceof ContentNotSupportedException) {
|
||||||
|
channelContentNotSupported = true;
|
||||||
|
showContentNotSupportedIfNeeded();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disposables.clear();
|
||||||
|
if (subscribeButtonMonitor != null) {
|
||||||
|
subscribeButtonMonitor.dispose();
|
||||||
|
}
|
||||||
|
updateSubscription(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() {
|
||||||
|
// channelBinding might not be initialized when handleResult() is called
|
||||||
|
// (e.g. after rotating the screen, #6696)
|
||||||
|
if (!channelContentNotSupported || channelBinding == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
||||||
|
channelBinding.channelKaomoji.setText("(︶︹︺)");
|
||||||
|
channelBinding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
||||||
|
channelBinding.channelNoVideos.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);
|
||||||
|
headerBinding.channelTitleView.setText(title);
|
||||||
|
}
|
||||||
|
}
|
@ -19,7 +19,7 @@ import org.schabi.newpipe.extractor.NewPipe;
|
|||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.fragments.BlankFragment;
|
import org.schabi.newpipe.fragments.BlankFragment;
|
||||||
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
import org.schabi.newpipe.fragments.list.channel.ChannelVideosFragment;
|
||||||
import org.schabi.newpipe.fragments.list.kiosk.DefaultKioskFragment;
|
import org.schabi.newpipe.fragments.list.kiosk.DefaultKioskFragment;
|
||||||
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
||||||
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
|
||||||
@ -432,8 +432,8 @@ public abstract class Tab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ChannelFragment getFragment(final Context context) {
|
public ChannelVideosFragment getFragment(final Context context) {
|
||||||
return ChannelFragment.getInstance(channelServiceId, channelUrl, channelName);
|
return ChannelVideosFragment.getInstance(channelServiceId, channelUrl, channelName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -42,11 +42,13 @@ 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.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
|
import org.schabi.newpipe.extractor.channel.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.FeedExtractor;
|
||||||
import org.schabi.newpipe.extractor.feed.FeedInfo;
|
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.ChannelTabHandler;
|
||||||
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;
|
||||||
@ -151,6 +153,25 @@ public final class ExtractorHelper {
|
|||||||
return maybeFeedInfo.switchIfEmpty(getChannelInfo(serviceId, url, true));
|
return maybeFeedInfo.switchIfEmpty(getChannelInfo(serviceId, url, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Single<ChannelTabInfo> getChannelTab(final int serviceId,
|
||||||
|
final ChannelTabHandler tabHandler,
|
||||||
|
final boolean forceLoad) {
|
||||||
|
checkServiceId(serviceId);
|
||||||
|
return checkCache(forceLoad, serviceId,
|
||||||
|
tabHandler.getUrl() + tabHandler.getTab().name(), InfoItem.InfoType.CHANNEL,
|
||||||
|
Single.fromCallable(() ->
|
||||||
|
ChannelTabInfo.getInfo(NewPipe.getService(serviceId), tabHandler)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Single<InfoItemsPage<InfoItem>> getMoreChannelTabItems(final int serviceId,
|
||||||
|
final ChannelTabHandler
|
||||||
|
tabHandler,
|
||||||
|
final Page nextPage) {
|
||||||
|
checkServiceId(serviceId);
|
||||||
|
return Single.fromCallable(() ->
|
||||||
|
ChannelTabInfo.getMoreItems(NewPipe.getService(serviceId), tabHandler, nextPage));
|
||||||
|
}
|
||||||
|
|
||||||
public static Single<CommentsInfo> getCommentsInfo(final int serviceId, final String url,
|
public static Single<CommentsInfo> getCommentsInfo(final int serviceId, final String url,
|
||||||
final boolean forceLoad) {
|
final boolean forceLoad) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
@ -229,7 +250,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 +261,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 +295,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 +309,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);
|
||||||
|
|
||||||
|
@ -1,15 +1,25 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<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"
|
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
|
<com.google.android.material.tabs.TabLayout
|
||||||
android:id="@+id/items_list"
|
android:id="@+id/tab_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/colorPrimary"
|
||||||
|
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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:scrollbars="vertical"
|
android:layout_below="@id/tab_layout" />
|
||||||
tools:listitem="@layout/list_stream_item" />
|
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/loading_progress_bar"
|
android:id="@+id/loading_progress_bar"
|
||||||
@ -20,46 +30,6 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
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/empty_state_message"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:text="@string/empty_view_no_videos"
|
|
||||||
android:textSize="24sp" />
|
|
||||||
|
|
||||||
<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-->
|
<!--ERROR PANEL-->
|
||||||
<include
|
<include
|
||||||
android:id="@+id/error_panel"
|
android:id="@+id/error_panel"
|
||||||
@ -70,11 +40,4 @@
|
|||||||
android:layout_marginTop="50dp"
|
android:layout_marginTop="50dp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
</RelativeLayout>
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="4dp"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:background="?attr/toolbar_shadow" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
36
app/src/main/res/layout/fragment_channel_info.xml
Normal file
36
app/src/main/res/layout/fragment_channel_info.xml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent">
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/description_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="@string/description_tab_description"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:layout_editor_absoluteX="16dp"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/description_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:textIsSelectable="true"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/description_title"
|
||||||
|
tools:layout_editor_absoluteX="36dp"
|
||||||
|
tools:visibility="visible"
|
||||||
|
tools:text="Cupcake ipsum dolor sit amet I love. I love macaroon cake sweet topping jelly beans chocolate chupa chups candy canes. Marshmallow cake jelly fruitcake soufflé pie. Jelly jelly beans cupcake topping chocolate bar jelly pudding pastry sweet roll." />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
41
app/src/main/res/layout/fragment_channel_tab.xml
Normal file
41
app/src/main/res/layout/fragment_channel_tab.xml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?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" />
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
|
||||||
|
<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_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>
|
14
app/src/main/res/menu/menu_channel_videos.xml
Normal file
14
app/src/main/res/menu/menu_channel_videos.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<menu 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"
|
||||||
|
tools:context=".fragments.list.impl.ChannelFragment">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_item_notify"
|
||||||
|
android:checkable="true"
|
||||||
|
android:orderInCategory="1"
|
||||||
|
android:title="@string/get_notified"
|
||||||
|
android:visible="false"
|
||||||
|
app:showAsAction="never"
|
||||||
|
tools:visible="true" />
|
||||||
|
</menu>
|
Loading…
Reference in New Issue
Block a user