1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-12-22 16:10:31 +00:00

Move channel header to collapsible app bar

This commit is contained in:
Stypox 2023-04-13 00:00:23 +02:00
parent 193c3e5b3d
commit e3614cb932
No known key found for this signature in database
GPG Key ID: 4BDF1B40A49FDD23
8 changed files with 541 additions and 673 deletions

View File

@ -1,10 +1,16 @@
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.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
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;
@ -14,43 +20,59 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayout;
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.FragmentChannelBinding; import org.schabi.newpipe.databinding.FragmentChannelBinding;
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.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.linkhandler.ListLinkHandler; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.detail.TabAdapter; import org.schabi.newpipe.fragments.detail.TabAdapter;
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.feed.notifications.NotificationHelper; import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
import org.schabi.newpipe.local.subscription.SubscriptionManager; import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.util.ChannelTabHelper; import org.schabi.newpipe.util.ChannelTabHelper;
import org.schabi.newpipe.util.Constants; 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.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
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.Queue;
import java.util.concurrent.TimeUnit;
import icepick.State; 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.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.Consumer; 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 BaseStateFragment<ChannelInfo> public class ChannelFragment extends BaseStateFragment<ChannelInfo>
implements StateSaver.WriteRead { implements StateSaver.WriteRead {
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
@State @State
protected int serviceId = Constants.NO_SERVICE_ID; protected int serviceId = Constants.NO_SERVICE_ID;
@State @State
@ -60,13 +82,11 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
private ChannelInfo currentInfo; private ChannelInfo currentInfo;
private Disposable currentWorker; private Disposable currentWorker;
private Disposable subscriptionMonitor;
private final CompositeDisposable disposables = new CompositeDisposable(); private final CompositeDisposable disposables = new CompositeDisposable();
private Disposable subscribeButtonMonitor;
private SubscriptionManager subscriptionManager; private SubscriptionManager subscriptionManager;
private int lastTab; private int lastTab;
private boolean channelContentNotSupported = false;
private MenuItem menuRssButton;
private MenuItem menuNotifyButton;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Views // Views
@ -75,6 +95,9 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
private FragmentChannelBinding binding; private FragmentChannelBinding binding;
private TabAdapter tabAdapter; private TabAdapter tabAdapter;
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) {
final ChannelFragment instance = new ChannelFragment(); final ChannelFragment instance = new ChannelFragment();
@ -82,12 +105,13 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
return instance; return instance;
} }
protected void setInitialData(final int sid, final String u, final String title) { private void setInitialData(final int sid, final String u, final String title) {
this.serviceId = sid; this.serviceId = sid;
this.url = u; this.url = u;
this.name = !TextUtils.isEmpty(title) ? title : ""; this.name = !TextUtils.isEmpty(title) ? title : "";
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// LifeCycle // LifeCycle
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -96,12 +120,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
public void onCreate(final Bundle savedInstanceState) { public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setHasOptionsMenu(true); setHasOptionsMenu(true);
if (savedInstanceState != null) {
lastTab = savedInstanceState.getInt("LastTab");
} else {
lastTab = 0;
}
} }
@Override @Override
@ -125,14 +143,29 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
tabAdapter = new TabAdapter(getChildFragmentManager()); tabAdapter = new TabAdapter(getChildFragmentManager());
binding.viewPager.setAdapter(tabAdapter); binding.viewPager.setAdapter(tabAdapter);
binding.tabLayout.setupWithViewPager(binding.viewPager); binding.tabLayout.setupWithViewPager(binding.viewPager);
binding.channelTitleView.setText(name);
} }
@Override @Override
public void onSaveInstanceState(final @NonNull Bundle outState) { protected void initListeners() {
super.onSaveInstanceState(outState); super.initListeners();
if (binding != null) {
outState.putInt("LastTab", binding.tabLayout.getSelectedTabPosition()); final View.OnClickListener openSubChannel = v -> {
} 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 @Override
@ -141,14 +174,12 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
if (currentWorker != null) { if (currentWorker != null) {
currentWorker.dispose(); currentWorker.dispose();
} }
if (subscriptionMonitor != null) {
subscriptionMonitor.dispose();
}
disposables.clear(); disposables.clear();
binding = null; binding = null;
} }
/*//////////////////////////////////////////////////////////////////////////
/*//////////////////////////////////////////////////////////////////////////
// Menu // Menu
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -164,8 +195,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
} }
menuRssButton = menu.findItem(R.id.menu_item_rss); menuRssButton = menu.findItem(R.id.menu_item_rss);
menuNotifyButton = menu.findItem(R.id.menu_item_notify); menuNotifyButton = menu.findItem(R.id.menu_item_notify);
updateRssButton();
monitorSubscription();
} }
@Override @Override
@ -201,37 +230,168 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
return true; return true;
} }
private void updateRssButton() {
if (currentInfo != null && menuRssButton != null) { /*//////////////////////////////////////////////////////////////////////////
menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl())); // Channel Subscription
} //////////////////////////////////////////////////////////////////////////*/
private void monitorSubscription(final ChannelInfo info) {
final Consumer<Throwable> onError = (Throwable throwable) -> {
animate(binding.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 void monitorSubscription() { private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription,
if (currentInfo != null) { final ChannelInfo info) {
final Observable<List<SubscriptionEntity>> observable = subscriptionManager return (@NonNull Object o) -> {
.subscriptionTable() subscriptionManager.insertSubscription(subscription, info);
.getSubscriptionFlowable(currentInfo.getServiceId(), currentInfo.getUrl()) return o;
.toObservable(); };
if (subscriptionMonitor != null) {
subscriptionMonitor.dispose();
}
subscriptionMonitor = observable
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getSubscribeUpdateMonitor());
}
} }
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor() { private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
return (List<SubscriptionEntity> subscriptionEntities) -> { return (@NonNull Object o) -> {
if (subscriptionEntities.isEmpty()) { subscriptionManager.deleteSubscription(subscription);
updateNotifyButton(null); return o;
} else { };
final SubscriptionEntity subscription = subscriptionEntities.get(0); }
updateNotifyButton(subscription);
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 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(binding.channelSubscribeButton)
.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(mapOnSubscribe(channel, info));
} else {
if (DEBUG) {
Log.d(TAG, "Found subscription to this channel!");
}
final SubscriptionEntity subscription = subscriptionEntities.get(0);
updateNotifyButton(subscription);
subscribeButtonMonitor = monitorSubscribeButton(mapOnUnsubscribe(subscription));
}
};
}
private void updateSubscribeButton(final boolean isSubscribed) {
if (DEBUG) {
Log.d(TAG, "updateSubscribeButton() called with: "
+ "isSubscribed = [" + isSubscribed + "]");
}
final boolean isButtonVisible = binding.channelSubscribeButton.getVisibility()
== View.VISIBLE;
final int backgroundDuration = isButtonVisible ? 300 : 0;
final int textDuration = isButtonVisible ? 200 : 0;
final int subscribedBackground = ContextCompat
.getColor(activity, R.color.subscribed_background_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) {
binding.channelSubscribeButton.setText(R.string.subscribed_button_title);
animateBackgroundColor(binding.channelSubscribeButton, backgroundDuration,
subscribeBackground, subscribedBackground);
animateTextColor(binding.channelSubscribeButton, textDuration, subscribeText,
subscribedText);
} else {
binding.channelSubscribeButton.setText(R.string.subscribe_button_title);
animateBackgroundColor(binding.channelSubscribeButton, backgroundDuration,
subscribedBackground, subscribeBackground);
animateTextColor(binding.channelSubscribeButton, textDuration, subscribedText,
subscribeText);
}
animate(binding.channelSubscribeButton, true, 100, AnimationType.LIGHT_SCALE_AND_ALPHA);
} }
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) { private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
@ -263,52 +423,48 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
); );
} }
/**
* Show a snackbar with the option to enable notifications on new streams for this channel.
*/
private void showNotifySnackbar() {
Snackbar.make(binding.getRoot(), R.string.you_successfully_subscribed, Snackbar.LENGTH_LONG)
.setAction(R.string.get_notified, v -> setNotify(true))
.setActionTextColor(Color.YELLOW)
.show();
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Init // Init
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private boolean isContentUnsupported() {
for (final Throwable throwable : currentInfo.getErrors()) {
if (throwable instanceof ContentNotSupportedException) {
return true;
}
}
return false;
}
private void updateTabs() { private void updateTabs() {
tabAdapter.clearAllItems(); tabAdapter.clearAllItems();
if (currentInfo != null) { if (currentInfo != null && !channelContentNotSupported) {
if (isContentUnsupported()) { tabAdapter.addFragment(new ChannelVideosFragment(currentInfo), "Videos");
showEmptyState();
binding.errorContentNotSupported.setVisibility(View.VISIBLE);
} else {
tabAdapter.addFragment(
ChannelVideosFragment.getInstance(currentInfo), "Videos");
final Context context = getContext(); final Context context = requireContext();
final SharedPreferences preferences = PreferenceManager final SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(context); .getDefaultSharedPreferences(context);
for (final ListLinkHandler linkHandler : currentInfo.getTabs()) { for (final ListLinkHandler linkHandler : currentInfo.getTabs()) {
final String tab = linkHandler.getContentFilters().get(0); final String tab = linkHandler.getContentFilters().get(0);
if (ChannelTabHelper.showChannelTab(context, preferences, tab)) { if (ChannelTabHelper.showChannelTab(context, preferences, tab)) {
tabAdapter.addFragment(
ChannelTabFragment.getInstance(serviceId, linkHandler, name),
context.getString(ChannelTabHelper.getTranslationKey(tab)));
}
}
final String description = currentInfo.getDescription();
if (description != null && !description.isEmpty()
&& ChannelTabHelper.showChannelTab(
context, preferences, R.string.show_channel_tabs_about)) {
tabAdapter.addFragment( tabAdapter.addFragment(
ChannelAboutFragment.getInstance(currentInfo), ChannelTabFragment.getInstance(serviceId, linkHandler, name),
context.getString(R.string.channel_tab_about)); context.getString(ChannelTabHelper.getTranslationKey(tab)));
} }
} }
final String description = currentInfo.getDescription();
if (description != null && !description.isEmpty()
&& ChannelTabHelper.showChannelTab(
context, preferences, R.string.show_channel_tabs_about)) {
tabAdapter.addFragment(
ChannelAboutFragment.getInstance(currentInfo),
context.getString(R.string.channel_tab_about));
}
} }
tabAdapter.notifyDataSetUpdate(); tabAdapter.notifyDataSetUpdate();
@ -324,6 +480,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
} }
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// State Saving // State Saving
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -336,11 +493,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
@Override @Override
public void writeTo(final Queue<Object> objectsToSave) { public void writeTo(final Queue<Object> objectsToSave) {
objectsToSave.add(currentInfo); objectsToSave.add(currentInfo);
if (binding != null) { objectsToSave.add(binding == null ? 0 : binding.tabLayout.getSelectedTabPosition());
objectsToSave.add(binding.tabLayout.getSelectedTabPosition());
} else {
objectsToSave.add(0);
}
} }
@Override @Override
@ -349,6 +502,25 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
lastTab = (Integer) 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
//////////////////////////////////////////////////////////////////////////*/
@Override @Override
protected void doInitialLoadLogic() { protected void doInitialLoadLogic() {
if (currentInfo == null) { if (currentInfo == null) {
@ -382,14 +554,77 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
url == null ? "no url" : url, serviceId))); url == null ? "no url" : url, serviceId)));
} }
@Override
public void showLoading() {
super.showLoading();
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
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; currentInfo = result;
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName()); setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
binding.getRoot().setVisibility(View.VISIBLE);
PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG)
.into(binding.channelBannerImage);
PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
.into(binding.channelAvatarView);
PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
.into(binding.subChannelAvatarView);
binding.channelTitleView.setText(result.getName());
binding.channelSubscriberView.setVisibility(View.VISIBLE);
if (result.getSubscriberCount() >= 0) {
binding.channelSubscriberView.setText(Localization
.shortSubscriberCount(activity, result.getSubscriberCount()));
} else {
binding.channelSubscriberView.setText(R.string.subscribers_count_not_available);
}
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
binding.subChannelTitleView.setText(String.format(
getString(R.string.channel_created_by),
currentInfo.getParentChannelName())
);
binding.subChannelTitleView.setVisibility(View.VISIBLE);
binding.subChannelAvatarView.setVisibility(View.VISIBLE);
}
if (menuRssButton != null) {
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
}
channelContentNotSupported = false;
for (final Throwable throwable : result.getErrors()) {
if (throwable instanceof ContentNotSupportedException) {
channelContentNotSupported = true;
showContentNotSupportedIfNeeded();
break;
}
}
disposables.clear();
if (subscribeButtonMonitor != null) {
subscribeButtonMonitor.dispose();
}
updateTabs(); updateTabs();
updateRssButton(); updateSubscription(result);
monitorSubscription(); monitorSubscription(result);
}
private void showContentNotSupportedIfNeeded() {
// channelBinding might not be initialized when handleResult() is called
// (e.g. after rotating the screen, #6696)
if (!channelContentNotSupported || binding == null) {
return;
}
binding.errorContentNotSupported.setVisibility(View.VISIBLE);
binding.channelKaomoji.setText("(︶︹︺)");
binding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
} }
} }

View File

@ -1,109 +1,61 @@
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.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater; import android.view.LayoutInflater;
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.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.FragmentChannelVideosBinding;
import org.schabi.newpipe.databinding.PlaylistControlBinding; 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.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor; 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.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; 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.PlayerType;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue; 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 java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; 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.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable; 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> 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 final CompositeDisposable disposables = new CompositeDisposable();
private Disposable subscribeButtonMonitor;
private boolean channelContentNotSupported = false;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private SubscriptionManager subscriptionManager;
private FragmentChannelVideosBinding channelBinding; private FragmentChannelVideosBinding channelBinding;
private ChannelHeaderBinding headerBinding;
private PlaylistControlBinding playlistControlBinding; private PlaylistControlBinding playlistControlBinding;
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) { // Constructors and lifecycle
final ChannelVideosFragment instance = new ChannelVideosFragment(); //////////////////////////////////////////////////////////////////////////*/
instance.setInitialData(serviceId, url, name);
return instance;
}
// required by the Android framework to restore fragments after saving
public ChannelVideosFragment() { public ChannelVideosFragment() {
super(UserAction.REQUESTED_CHANNEL); super(UserAction.REQUESTED_CHANNEL);
} }
public ChannelVideosFragment(final int serviceId, final String url, final String name) {
this();
setInitialData(serviceId, url, name);
}
public ChannelVideosFragment(@NonNull final ChannelInfo info) {
this(info.getServiceId(), info.getUrl(), info.getName());
this.currentInfo = info;
this.currentNextPage = info.getNextPage();
}
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
@ -112,22 +64,12 @@ public class ChannelVideosFragment extends BaseListInfoFragment<StreamInfoItem,
} }
} }
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override @Override
public void onCreate(final Bundle savedInstanceState) { public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setHasOptionsMenu(false); setHasOptionsMenu(false);
} }
@Override
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
subscriptionManager = new SubscriptionManager(activity);
}
@Override @Override
public View onCreateView(@NonNull final LayoutInflater inflater, public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container, @Nullable final ViewGroup container,
@ -136,235 +78,24 @@ public class ChannelVideosFragment extends BaseListInfoFragment<StreamInfoItem,
return channelBinding.getRoot(); return channelBinding.getRoot();
} }
@Override
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
showContentNotSupportedIfNeeded();
}
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
disposables.clear(); disposables.clear();
if (subscribeButtonMonitor != null) {
subscribeButtonMonitor.dispose();
}
channelBinding = null; channelBinding = null;
headerBinding = null;
playlistControlBinding = null; playlistControlBinding = null;
} }
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
@Override @Override
protected Supplier<View> getListHeaderSupplier() { protected Supplier<View> getListHeaderSupplier() {
headerBinding = ChannelHeaderBinding playlistControlBinding = PlaylistControlBinding
.inflate(activity.getLayoutInflater(), itemsList, false); .inflate(activity.getLayoutInflater(), itemsList, false);
playlistControlBinding = headerBinding.playlistControl; return playlistControlBinding::getRoot;
return headerBinding::getRoot;
} }
@Override
protected void initListeners() {
super.initListeners();
headerBinding.subChannelTitleView.setOnClickListener(this);
headerBinding.subChannelAvatarView.setOnClickListener(this);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Channel Subscription // Loading
//////////////////////////////////////////////////////////////////////////*/
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());
subscribeButtonMonitor = monitorSubscribeButton(
headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info));
} else {
if (DEBUG) {
Log.d(TAG, "Found subscription to this channel!");
}
final SubscriptionEntity subscription = subscriptionEntities.get(0);
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();
}
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()
);
}
/*//////////////////////////////////////////////////////////////////////////
// Load and handle
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override @Override
@ -377,76 +108,15 @@ public class ChannelVideosFragment extends BaseListInfoFragment<StreamInfoItem,
return ExtractorHelper.getChannelInfo(serviceId, url, 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 // Contract
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
animate(headerBinding.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);
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);
}
// PlaylistControls should be visible only if there is some item in // PlaylistControls should be visible only if there is some item in
// infoListAdapter other than header // infoListAdapter other than header
if (infoListAdapter.getItemCount() != 1) { if (infoListAdapter.getItemCount() != 1) {
@ -455,31 +125,14 @@ public class ChannelVideosFragment extends BaseListInfoFragment<StreamInfoItem,
playlistControlBinding.getRoot().setVisibility(View.GONE); playlistControlBinding.getRoot().setVisibility(View.GONE);
} }
channelContentNotSupported = false;
for (final Throwable throwable : result.getErrors()) {
if (throwable instanceof ContentNotSupportedException) {
channelContentNotSupported = true;
showContentNotSupportedIfNeeded();
break;
}
}
disposables.clear(); disposables.clear();
if (subscribeButtonMonitor != null) {
subscribeButtonMonitor.dispose();
}
updateSubscription(result);
monitorSubscription(result);
playlistControlBinding.playlistCtrlPlayAllButton playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(
.setOnClickListener(view -> NavigationHelper view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
.playOnMainPlayer(activity, getPlayQueue())); playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(
playlistControlBinding.playlistCtrlPlayPopupButton view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
.setOnClickListener(view -> NavigationHelper playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(
.playOnPopupPlayer(activity, getPlayQueue(), false)); view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
playlistControlBinding.playlistCtrlPlayBgButton
.setOnClickListener(view -> NavigationHelper
.playOnBackgroundPlayer(activity, getPlayQueue(), false));
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> { playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP); NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
@ -492,19 +145,6 @@ public class ChannelVideosFragment extends BaseListInfoFragment<StreamInfoItem,
}); });
} }
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() { private PlayQueue getPlayQueue() {
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream() final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
.filter(StreamInfoItem.class::isInstance) .filter(StreamInfoItem.class::isInstance)
@ -514,14 +154,4 @@ public class ChannelVideosFragment extends BaseListInfoFragment<StreamInfoItem,
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(), return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
currentInfo.getNextPage(), streamItems, 0); currentInfo.getNextPage(), streamItems, 0);
} }
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setTitle(final String title) {
super.setTitle(title);
headerBinding.channelTitleView.setText(title);
}
} }

View File

@ -433,7 +433,7 @@ public abstract class Tab {
@Override @Override
public ChannelVideosFragment getFragment(final Context context) { public ChannelVideosFragment getFragment(final Context context) {
return ChannelVideosFragment.getInstance(channelServiceId, channelUrl, channelName); return new ChannelVideosFragment(channelServiceId, channelUrl, channelName);
} }
@Override @Override

View File

@ -109,7 +109,11 @@ public final class PicassoHelper {
} }
public static RequestCreator loadBanner(final String url) { public static RequestCreator loadBanner(final String url) {
return loadImageDefault(url, R.drawable.placeholder_channel_banner); if (!shouldLoadImages || isBlank(url)) {
return picassoInstance.load((String) null);
} else {
return picassoInstance.load(url);
}
} }
public static RequestCreator loadPlaylistThumbnail(final String url) { public static RequestCreator loadPlaylistThumbnail(final String url) {

View File

@ -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>

View File

@ -1,75 +1,207 @@
<?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: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">
<com.google.android.material.tabs.TabLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/tab_layout" android:id="@+id/app_bar_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/colorPrimary" app:elevation="0dp">
app:tabIndicatorColor="@color/white"
app:tabMode="scrollable"
app:tabRippleColor="@color/white"
app:tabTextColor="@color/white" />
<androidx.viewpager.widget.ViewPager <org.schabi.newpipe.views.CustomCollapsingToolbarLayout
android:id="@+id/view_pager" 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:scaleType="centerCrop"
tools:src="@drawable/placeholder_channel_banner"
app:layout_constraintTop_toTopOf="parent"
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" />
<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"
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:layout_below="@id/tab_layout" /> android:layout_below="@id/app_bar_layout"
<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/error_content_not_supported" 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_marginTop="20dp" android:layout_centerInParent="true"
android:text="@string/content_not_supported" android:orientation="vertical"
android:textSize="15sp" android:paddingTop="90dp"
android:visibility="gone" /> android:visibility="gone"
tools:visibility="visible">
</LinearLayout> <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" />
<!--ERROR PANEL--> <org.schabi.newpipe.views.NewPipeTextView
<include android:id="@+id/error_content_not_supported"
android:id="@+id/error_panel" android:layout_width="wrap_content"
layout="@layout/error_panel" android:layout_height="wrap_content"
android:layout_width="wrap_content" android:layout_marginTop="20dp"
android:layout_height="wrap_content" android:text="@string/content_not_supported"
android:layout_centerInParent="true" android:textSize="15sp"
android:layout_marginTop="50dp" android:visibility="gone" />
android:visibility="gone"
tools:visibility="visible" /> </LinearLayout>
</RelativeLayout>
<!--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" />
</RelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -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>

View File

@ -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>