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