mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 07:13:00 +00:00 
			
		
		
		
	Move channel header to collapsible app bar
This commit is contained in:
		| @@ -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,13 +174,11 @@ 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())); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void monitorSubscription() { |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|         if (currentInfo != null) { |     // 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 |         final Observable<List<SubscriptionEntity>> observable = subscriptionManager | ||||||
|                 .subscriptionTable() |                 .subscriptionTable() | ||||||
|                     .getSubscriptionFlowable(currentInfo.getServiceId(), currentInfo.getUrl()) |                 .getSubscriptionFlowable(info.getServiceId(), info.getUrl()) | ||||||
|                 .toObservable(); |                 .toObservable(); | ||||||
|  |  | ||||||
|             if (subscriptionMonitor != null) { |         disposables.add(observable | ||||||
|                 subscriptionMonitor.dispose(); |  | ||||||
|             } |  | ||||||
|             subscriptionMonitor = observable |  | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                     .subscribe(getSubscribeUpdateMonitor()); |                 .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 Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor() { |     private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription, | ||||||
|         return (List<SubscriptionEntity> subscriptionEntities) -> { |                                                     final ChannelInfo info) { | ||||||
|             if (subscriptionEntities.isEmpty()) { |         return (@NonNull Object o) -> { | ||||||
|                 updateNotifyButton(null); |             subscriptionManager.insertSubscription(subscription, info); | ||||||
|             } else { |             return o; | ||||||
|                 final SubscriptionEntity subscription = subscriptionEntities.get(0); |         }; | ||||||
|                 updateNotifyButton(subscription); |     } | ||||||
|  |  | ||||||
|  |     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 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,31 +423,28 @@ 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); | ||||||
|  |  | ||||||
| @@ -309,7 +466,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo> | |||||||
|                         context.getString(R.string.channel_tab_about)); |                         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); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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) { | ||||||
|   | |||||||
| @@ -1,131 +0,0 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> |  | ||||||
| <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |  | ||||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" |  | ||||||
|     xmlns:tools="http://schemas.android.com/tools" |  | ||||||
|     android:layout_width="match_parent" |  | ||||||
|     android:layout_height="wrap_content" |  | ||||||
|     android:background="?attr/contrast_background_color"> |  | ||||||
|  |  | ||||||
|     <RelativeLayout |  | ||||||
|         android:id="@+id/channel_metadata" |  | ||||||
|         android:layout_width="match_parent" |  | ||||||
|         android:layout_height="wrap_content"> |  | ||||||
|  |  | ||||||
|         <ImageView |  | ||||||
|             android:id="@+id/channel_banner_image" |  | ||||||
|             android:layout_width="match_parent" |  | ||||||
|             android:layout_height="70dp" |  | ||||||
|             android:background="@android:color/black" |  | ||||||
|             android:fitsSystemWindows="true" |  | ||||||
|             android:scaleType="fitCenter" |  | ||||||
|             android:src="@drawable/placeholder_channel_banner" |  | ||||||
|             tools:ignore="ContentDescription" /> |  | ||||||
|  |  | ||||||
|         <FrameLayout |  | ||||||
|             android:id="@+id/avatars_layout" |  | ||||||
|             android:layout_width="wrap_content" |  | ||||||
|             android:layout_height="wrap_content" |  | ||||||
|             android:layout_marginLeft="8dp" |  | ||||||
|             android:layout_marginTop="50dp"> |  | ||||||
|  |  | ||||||
|             <com.google.android.material.imageview.ShapeableImageView |  | ||||||
|                 android:id="@+id/channel_avatar_view" |  | ||||||
|                 android:layout_width="@dimen/channel_avatar_size" |  | ||||||
|                 android:layout_height="@dimen/channel_avatar_size" |  | ||||||
|                 android:padding="1dp" |  | ||||||
|                 android:src="@drawable/placeholder_person" |  | ||||||
|                 app:shapeAppearance="@style/CircularImageView" |  | ||||||
|                 app:strokeColor="#ffffff" |  | ||||||
|                 app:strokeWidth="2dp" /> |  | ||||||
|  |  | ||||||
|             <com.google.android.material.imageview.ShapeableImageView |  | ||||||
|                 android:id="@+id/sub_channel_avatar_view" |  | ||||||
|                 android:layout_width="@dimen/sub_channel_avatar_size" |  | ||||||
|                 android:layout_height="@dimen/sub_channel_avatar_size" |  | ||||||
|                 android:layout_gravity="bottom|right" |  | ||||||
|                 android:padding="1dp" |  | ||||||
|                 android:src="@drawable/placeholder_person" |  | ||||||
|                 android:visibility="gone" |  | ||||||
|                 app:shapeAppearance="@style/CircularImageView" |  | ||||||
|                 app:strokeColor="#ffffff" |  | ||||||
|                 app:strokeWidth="2dp" |  | ||||||
|                 tools:ignore="RtlHardcoded" |  | ||||||
|                 tools:visibility="visible" /> |  | ||||||
|         </FrameLayout> |  | ||||||
|  |  | ||||||
|         <org.schabi.newpipe.views.NewPipeTextView |  | ||||||
|             android:id="@+id/channel_title_view" |  | ||||||
|             android:layout_width="match_parent" |  | ||||||
|             android:layout_height="wrap_content" |  | ||||||
|             android:layout_below="@id/channel_banner_image" |  | ||||||
|             android:layout_marginLeft="8dp" |  | ||||||
|             android:layout_marginTop="6dp" |  | ||||||
|             android:layout_marginRight="8dp" |  | ||||||
|             android:layout_toLeftOf="@id/channel_subscribe_button" |  | ||||||
|             android:layout_toRightOf="@id/avatars_layout" |  | ||||||
|             android:ellipsize="end" |  | ||||||
|             android:lines="1" |  | ||||||
|             android:textAppearance="?android:attr/textAppearanceLarge" |  | ||||||
|             android:textSize="@dimen/video_item_detail_title_text_size" |  | ||||||
|             tools:ignore="RtlHardcoded" |  | ||||||
|             tools:text="Lorem ipsum dolor" /> |  | ||||||
|  |  | ||||||
|         <org.schabi.newpipe.views.NewPipeTextView |  | ||||||
|             android:id="@+id/sub_channel_title_view" |  | ||||||
|             android:layout_width="match_parent" |  | ||||||
|             android:layout_height="wrap_content" |  | ||||||
|             android:layout_below="@id/channel_title_view" |  | ||||||
|             android:layout_alignLeft="@id/channel_title_view" |  | ||||||
|             android:layout_alignRight="@id/channel_title_view" |  | ||||||
|             android:ellipsize="end" |  | ||||||
|             android:gravity="center|left" |  | ||||||
|             android:lines="1" |  | ||||||
|             android:textAppearance="?android:attr/textAppearanceLarge" |  | ||||||
|             android:textSize="12dp" |  | ||||||
|             tools:ignore="RtlHardcoded" |  | ||||||
|             tools:layout_below="@id/channel_title_view" |  | ||||||
|             tools:text="Lorem ipsum dolor" /> |  | ||||||
|  |  | ||||||
|         <org.schabi.newpipe.views.NewPipeTextView |  | ||||||
|             android:id="@+id/channel_subscriber_view" |  | ||||||
|             android:layout_width="match_parent" |  | ||||||
|             android:layout_height="wrap_content" |  | ||||||
|             android:layout_below="@id/sub_channel_title_view" |  | ||||||
|             android:layout_alignLeft="@id/channel_title_view" |  | ||||||
|             android:layout_alignRight="@id/channel_title_view" |  | ||||||
|             android:ellipsize="end" |  | ||||||
|             android:maxLines="2" |  | ||||||
|             android:textSize="@dimen/channel_subscribers_text_size" |  | ||||||
|             android:visibility="gone" |  | ||||||
|             tools:ignore="RtlHardcoded" |  | ||||||
|             tools:text="123,141,411 subscribers" |  | ||||||
|             tools:visibility="visible" /> |  | ||||||
|  |  | ||||||
|         <androidx.appcompat.widget.AppCompatButton |  | ||||||
|             android:id="@+id/channel_subscribe_button" |  | ||||||
|             android:layout_width="wrap_content" |  | ||||||
|             android:layout_height="wrap_content" |  | ||||||
|             android:layout_below="@+id/channel_banner_image" |  | ||||||
|             android:layout_alignParentRight="true" |  | ||||||
|             android:layout_gravity="center_vertical|right" |  | ||||||
|             android:layout_marginRight="2dp" |  | ||||||
|             android:text="@string/subscribe_button_title" |  | ||||||
|             android:textSize="@dimen/channel_rss_title_size" |  | ||||||
|             android:theme="@style/ServiceColoredButton" |  | ||||||
|             android:visibility="gone" |  | ||||||
|             tools:ignore="RtlHardcoded" |  | ||||||
|             tools:visibility="visible" /> |  | ||||||
|     </RelativeLayout> |  | ||||||
|  |  | ||||||
|     <LinearLayout |  | ||||||
|         android:layout_width="match_parent" |  | ||||||
|         android:layout_height="wrap_content" |  | ||||||
|         android:layout_below="@id/channel_metadata"> |  | ||||||
|  |  | ||||||
|         <include |  | ||||||
|             android:id="@+id/playlist_control" |  | ||||||
|             layout="@layout/playlist_control" /> |  | ||||||
|  |  | ||||||
|     </LinearLayout> |  | ||||||
|  |  | ||||||
| </RelativeLayout> |  | ||||||
| @@ -1,15 +1,146 @@ | |||||||
| <?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.appbar.AppBarLayout | ||||||
|  |         android:id="@+id/app_bar_layout" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         app:elevation="0dp"> | ||||||
|  |  | ||||||
|  |         <org.schabi.newpipe.views.CustomCollapsingToolbarLayout | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:fitsSystemWindows="true" | ||||||
|  |             app:layout_scrollFlags="scroll|exitUntilCollapsed"> | ||||||
|  |  | ||||||
|  |             <androidx.constraintlayout.widget.ConstraintLayout | ||||||
|  |                 android:id="@+id/channel_metadata" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:fitsSystemWindows="true" | ||||||
|  |                 android:scaleType="centerCrop" | ||||||
|  |                 app:layout_collapseMode="parallax"> | ||||||
|  |  | ||||||
|  |                 <ImageView | ||||||
|  |                     android:id="@+id/channel_banner_image" | ||||||
|  |                     android:layout_width="match_parent" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:adjustViewBounds="true" | ||||||
|  |                     android: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_height="match_parent" | ||||||
|  |         android:layout_below="@id/app_bar_layout" | ||||||
|  |         android:orientation="vertical" | ||||||
|  |         app:layout_behavior="@string/appbar_scrolling_view_behavior"> | ||||||
|  |  | ||||||
|         <com.google.android.material.tabs.TabLayout |         <com.google.android.material.tabs.TabLayout | ||||||
|             android:id="@+id/tab_layout" |             android:id="@+id/tab_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" |             android:background="?attr/colorPrimary" | ||||||
|  |             app:tabGravity="fill" | ||||||
|             app:tabIndicatorColor="@color/white" |             app:tabIndicatorColor="@color/white" | ||||||
|             app:tabMode="scrollable" |             app:tabMode="scrollable" | ||||||
|             app:tabRippleColor="@color/white" |             app:tabRippleColor="@color/white" | ||||||
| @@ -49,7 +180,7 @@ | |||||||
|                 android:fontFamily="monospace" |                 android:fontFamily="monospace" | ||||||
|                 android:text="(︶︹︺)" |                 android:text="(︶︹︺)" | ||||||
|                 android:textSize="35sp" |                 android:textSize="35sp" | ||||||
|             tools:ignore="HardcodedText,UnusedAttribute" /> |                 tools:ignore="HardcodedText" /> | ||||||
|  |  | ||||||
|             <org.schabi.newpipe.views.NewPipeTextView |             <org.schabi.newpipe.views.NewPipeTextView | ||||||
|                 android:id="@+id/error_content_not_supported" |                 android:id="@+id/error_content_not_supported" | ||||||
| @@ -73,3 +204,4 @@ | |||||||
|             android:visibility="gone" |             android:visibility="gone" | ||||||
|             tools:visibility="visible" /> |             tools:visibility="visible" /> | ||||||
|     </RelativeLayout> |     </RelativeLayout> | ||||||
|  | </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||||||
| @@ -32,7 +32,6 @@ | |||||||
|     <dimen name="video_item_detail_sub_channel_text_size">16sp</dimen> |     <dimen name="video_item_detail_sub_channel_text_size">16sp</dimen> | ||||||
|     <dimen name="video_item_detail_upload_date_text_size">14sp</dimen> |     <dimen name="video_item_detail_upload_date_text_size">14sp</dimen> | ||||||
|     <dimen name="video_item_detail_description_text_size">14sp</dimen> |     <dimen name="video_item_detail_description_text_size">14sp</dimen> | ||||||
|     <dimen name="channel_subscribers_text_size">14sp</dimen> |  | ||||||
|     <dimen name="channel_rss_title_size">14sp</dimen> |     <dimen name="channel_rss_title_size">14sp</dimen> | ||||||
|     <!-- Elements Size --> |     <!-- Elements Size --> | ||||||
|     <dimen name="video_item_detail_uploader_image_size">42dp</dimen> |     <dimen name="video_item_detail_uploader_image_size">42dp</dimen> | ||||||
|   | |||||||
| @@ -75,7 +75,6 @@ | |||||||
|     <dimen name="video_item_detail_sub_channel_text_size">14sp</dimen> |     <dimen name="video_item_detail_sub_channel_text_size">14sp</dimen> | ||||||
|     <dimen name="video_item_detail_upload_date_text_size">13sp</dimen> |     <dimen name="video_item_detail_upload_date_text_size">13sp</dimen> | ||||||
|     <dimen name="video_item_detail_description_text_size">13sp</dimen> |     <dimen name="video_item_detail_description_text_size">13sp</dimen> | ||||||
|     <dimen name="channel_subscribers_text_size">12sp</dimen> |  | ||||||
|     <dimen name="channel_rss_title_size">12sp</dimen> |     <dimen name="channel_rss_title_size">12sp</dimen> | ||||||
|     <!-- Elements Size --> |     <!-- Elements Size --> | ||||||
|     <dimen name="video_item_detail_uploader_image_size">32dp</dimen> |     <dimen name="video_item_detail_uploader_image_size">32dp</dimen> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Stypox
					Stypox