From 6d13cf5e71bcc429c2b80f4a1af0ec3b65b97f2f Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 23 Oct 2022 10:27:35 +0200 Subject: [PATCH 01/50] feat: add channel tabs --- app/build.gradle | 2 +- .../list/channel/ChannelFragment.java | 553 +++-------------- .../list/channel/ChannelInfoFragment.java | 38 ++ .../list/channel/ChannelTabFragment.java | 68 ++ .../list/channel/ChannelVideosFragment.java | 584 ++++++++++++++++++ .../org/schabi/newpipe/settings/tabs/Tab.java | 6 +- .../schabi/newpipe/util/ExtractorHelper.java | 42 +- app/src/main/res/layout/fragment_channel.xml | 67 +- .../main/res/layout/fragment_channel_info.xml | 36 ++ .../main/res/layout/fragment_channel_tab.xml | 41 ++ .../res/layout/fragment_channel_videos.xml | 71 +++ app/src/main/res/menu/menu_channel_videos.xml | 14 + 12 files changed, 988 insertions(+), 534 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java create mode 100644 app/src/main/res/layout/fragment_channel_info.xml create mode 100644 app/src/main/res/layout/fragment_channel_tab.xml create mode 100644 app/src/main/res/layout/fragment_channel_videos.xml create mode 100644 app/src/main/res/menu/menu_channel_videos.xml diff --git a/app/build.gradle b/app/build.gradle index 396f119b24f..22ac7d67d1c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 **/ diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 8a0b49249ae..6989552f2df 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -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 - implements View.OnClickListener { - - private static final int BUTTON_DEBOUNCE_INTERVAL = 100; - private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG"; +public class ChannelFragment extends BaseStateFragment { + @State + protected int serviceId = Constants.NO_SERVICE_ID; + @State + protected String name; + @State + protected String url; - private final CompositeDisposable disposables = new CompositeDisposable(); - private Disposable subscribeButtonMonitor; + private ChannelInfo currentInfo; + private Disposable currentWorker; - 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 static ChannelFragment getInstance(final int serviceId, final String url, } 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,59 +73,35 @@ public void onResume() { //////////////////////////////////////////////////////////////////////////*/ @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 getListHeaderSupplier() { - headerBinding = ChannelHeaderBinding - .inflate(activity.getLayoutInflater(), itemsList, false); - playlistControlBinding = headerBinding.playlistControl; - - return headerBinding::getRoot; + binding = null; } - @Override - protected void initListeners() { - super.initListeners(); - - headerBinding.subChannelTitleView.setOnClickListener(this); - headerBinding.subChannelAvatarView.setOnClickListener(this); - } - - /*////////////////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////////////////// // Menu //////////////////////////////////////////////////////////////////////////*/ @@ -176,19 +109,14 @@ protected void initListeners() { 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) { - Log.d(TAG, "onCreateOptionsMenu() called with: " - + "menu = [" + menu + "], inflater = [" + inflater + "]"); - } - menuRssButton = menu.findItem(R.id.menu_item_rss); - menuNotifyButton = menu.findItem(R.id.menu_item_notify); + inflater.inflate(R.menu.menu_channel, menu); + + if (DEBUG) { + Log.d(TAG, "onCreateOptionsMenu() called with: " + + "menu = [" + menu + "], inflater = [" + inflater + "]"); } + menuRssButton = menu.findItem(R.id.menu_item_rss); + updateRssButton(); } @Override @@ -197,11 +125,6 @@ public boolean onOptionsItemSelected(final MenuItem item) { 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 boolean onOptionsItemSelected(final MenuItem item) { return true; } - /*////////////////////////////////////////////////////////////////////////// - // Channel Subscription - //////////////////////////////////////////////////////////////////////////*/ - - private void monitorSubscription(final ChannelInfo info) { - final Consumer onError = (Throwable throwable) -> { - animate(headerBinding.channelSubscribeButton, false, 100); - showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET, - "Get subscription status", currentInfo)); - }; - - final Observable> 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 mapOnSubscribe(final SubscriptionEntity subscription, - final ChannelInfo info) { - return (@NonNull Object o) -> { - subscriptionManager.insertSubscription(subscription, info); - return o; - }; - } - - private Function 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 + "]"); + private void updateRssButton() { + if (currentInfo != null && menuRssButton != null) { + menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl())); } - final Action onComplete = () -> { - if (DEBUG) { - Log.d(TAG, "Updated subscription: " + info.getUrl()); - } - }; + } - final Consumer onError = (@NonNull Throwable throwable) -> - showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_UPDATE, - "Updating subscription for " + info.getUrl(), info)); + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ - disposables.add(subscriptionManager.updateChannelInfo(info) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(onComplete, onError)); - } + private void updateTabs() { + tabAdapter.clearAllItems(); - private Disposable monitorSubscribeButton(final Button subscribeButton, - final Function action) { - final Consumer onNext = (@NonNull Object o) -> { - if (DEBUG) { - Log.d(TAG, "Changed subscription status to this channel!"); - } - }; - - final Consumer 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); - } + if (currentInfo != null) { + tabAdapter.addFragment(ChannelVideosFragment.getInstance(currentInfo), "Videos"); - private Consumer> getSubscribeUpdateMonitor(final ChannelInfo info) { - return (List subscriptionEntities) -> { - if (DEBUG) { - Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: " - + "subscriptionEntities = [" + subscriptionEntities + "]"); - } - if (subscribeButtonMonitor != null) { - subscribeButtonMonitor.dispose(); + for (final ChannelTabHandler tab : currentInfo.getTabs()) { + tabAdapter.addFragment( + ChannelTabFragment.getInstance(serviceId, tab), tab.getTab().name()); } - 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)); + final String description = currentInfo.getDescription(); + if (!description.isEmpty()) { + tabAdapter.addFragment(ChannelInfoFragment.getInstance(description), "Info"); } - }; - } - - 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); - } + tabAdapter.notifyDataSetUpdate(); - 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 - ); + for (int i = 0; i < tabAdapter.getCount(); i++) { + binding.tabLayout.getTabAt(i).setText(tabAdapter.getItemTitle(i)); } - - 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> loadMoreItemsLogic() { - return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage); } @Override - protected Single loadResult(final boolean forceLoad) { - return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad); - } - - /*////////////////////////////////////////////////////////////////////////// - // OnClick - //////////////////////////////////////////////////////////////////////////*/ + public void startLoading(final boolean forceLoad) { + super.startLoading(forceLoad); - @Override - public void onClick(final View v) { - if (isLoading.get() || currentInfo == null) { - return; + currentInfo = null; + updateTabs(); + if (currentWorker != null) { + currentWorker.dispose(); } - 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; - } + runWorker(forceLoad); } - /*////////////////////////////////////////////////////////////////////////// - // Contract - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void showLoading() { - super.showLoading(); - PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG); - animate(headerBinding.channelSubscribeButton, false, 100); + private void runWorker(final boolean forceLoad) { + currentWorker = ExtractorHelper.getChannelInfo(serviceId, url, forceLoad) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + isLoading.set(false); + handleResult(result); + }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM, + url == null ? "no url" : url, serviceId))); } @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())); - } + public void handleResult(@NonNull final ChannelInfo info) { + super.handleResult(info); - // 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 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(); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java new file mode 100644 index 00000000000..c9273f52845 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java @@ -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(); + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java new file mode 100644 index 00000000000..12514a55cc3 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java @@ -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 { + + @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 loadResult(final boolean forceLoad) { + return ExtractorHelper.getChannelTab(serviceId, tabHandler, forceLoad); + } + + @Override + protected Single> loadMoreItemsLogic() { + return ExtractorHelper.getMoreChannelTabItems(serviceId, tabHandler, currentNextPage); + } + + @Override + public void setTitle(final String title) { + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java new file mode 100644 index 00000000000..9f8c83ce7b2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java @@ -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 + 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 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 onError = (Throwable throwable) -> { + animate(headerBinding.channelSubscribeButton, false, 100); + showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET, + "Get subscription status", currentInfo)); + }; + + final Observable> 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 mapOnSubscribe(final SubscriptionEntity subscription, + final ChannelInfo info) { + return (@NonNull Object o) -> { + subscriptionManager.insertSubscription(subscription, info); + return o; + }; + } + + private Function 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 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 action) { + final Consumer onNext = (@NonNull Object o) -> { + if (DEBUG) { + Log.d(TAG, "Changed subscription status to this channel!"); + } + }; + + final Consumer 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> getSubscribeUpdateMonitor(final ChannelInfo info) { + return (List 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> loadMoreItemsLogic() { + return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage); + } + + @Override + protected Single 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 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); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java index 7e3f5d0c825..a06bf32d4cc 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java @@ -19,7 +19,7 @@ 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 int getTabIconRes(final Context context) { } @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 diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index d5d472d6f28..b4648c79b7f 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -42,11 +42,13 @@ 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 static Single> getFeedInfoFallbackToChannelInfo( return maybeFeedInfo.switchIfEmpty(getChannelInfo(serviceId, url, true)); } + public static Single 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> 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 getCommentsInfo(final int serviceId, final String url, final boolean forceLoad) { checkServiceId(serviceId); @@ -229,7 +250,7 @@ private static Single checkCache(final boolean forceLoad, load = actualLoadFromNetwork; } else { load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, infoType), - actualLoadFromNetwork.toMaybe()) + actualLoadFromNetwork.toMaybe()) .firstElement() // Take the first valid .toSingle(); } @@ -240,10 +261,10 @@ private static Single checkCache(final boolean forceLoad, /** * Default implementation uses the {@link InfoCache} to get cached results. * - * @param the item type's class that extends {@link Info} - * @param serviceId the service to load from - * @param url the URL to load - * @param infoType the {@link InfoItem.InfoType} of the item + * @param the item type's class that extends {@link Info} + * @param serviceId the service to load from + * @param url the URL to load + * @param infoType the {@link InfoItem.InfoType} of the item * @return a {@link Single} that loads the item */ private static Maybe loadFromCache(final int serviceId, final String url, @@ -274,11 +295,12 @@ public static boolean isCached(final int serviceId, final String url, * 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 metaInfos a list of meta information, can be null or empty + * @param metaInfoTextView the text view in which to show the formatted HTML * @param metaInfoSeparator another view to be shown or hidden accordingly to the text view - * @param disposables disposables created by the method are added here and their lifecycle - * should be handled by the calling class + * @param disposables disposables created by the method are added here and their lifecycle + * should be handled by the calling class */ public static void showMetaInfoInTextView(@Nullable final List metaInfos, final TextView metaInfoTextView, @@ -287,7 +309,7 @@ public static void showMetaInfoInTextView(@Nullable final List metaInf final Context context = metaInfoTextView.getContext(); if (metaInfos == null || metaInfos.isEmpty() || !PreferenceManager.getDefaultSharedPreferences(context).getBoolean( - context.getString(R.string.show_meta_info_key), true)) { + context.getString(R.string.show_meta_info_key), true)) { metaInfoTextView.setVisibility(View.GONE); metaInfoSeparator.setVisibility(View.GONE); diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml index 714b9d4f95b..d938f71a7f2 100644 --- a/app/src/main/res/layout/fragment_channel.xml +++ b/app/src/main/res/layout/fragment_channel.xml @@ -1,15 +1,25 @@ - + + + android:layout_below="@id/tab_layout" /> - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_channel_info.xml b/app/src/main/res/layout/fragment_channel_info.xml new file mode 100644 index 00000000000..fbb8e355be0 --- /dev/null +++ b/app/src/main/res/layout/fragment_channel_info.xml @@ -0,0 +1,36 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_channel_tab.xml b/app/src/main/res/layout/fragment_channel_tab.xml new file mode 100644 index 00000000000..51915629635 --- /dev/null +++ b/app/src/main/res/layout/fragment_channel_tab.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_channel_videos.xml b/app/src/main/res/layout/fragment_channel_videos.xml new file mode 100644 index 00000000000..2dfb2fbf6fd --- /dev/null +++ b/app/src/main/res/layout/fragment_channel_videos.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_channel_videos.xml b/app/src/main/res/menu/menu_channel_videos.xml new file mode 100644 index 00000000000..a3b2e7ae0e3 --- /dev/null +++ b/app/src/main/res/menu/menu_channel_videos.xml @@ -0,0 +1,14 @@ + + + + From 8627efd0a1dd3e8f3afc4e7ade8bc7f7241f3fd5 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 23 Oct 2022 11:28:34 +0200 Subject: [PATCH 02/50] fix: get notified menu option on all tabs --- .../list/channel/ChannelFragment.java | 91 ++++++++++++++++++ .../list/channel/ChannelTabFragment.java | 8 ++ .../list/channel/ChannelVideosFragment.java | 95 ++++--------------- app/src/main/res/menu/menu_channel_videos.xml | 14 --- 4 files changed, 118 insertions(+), 90 deletions(-) delete mode 100644 app/src/main/res/menu/menu_channel_videos.xml diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 6989552f2df..4938d1c0096 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.fragments.list.channel; +import android.content.Context; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; @@ -14,6 +15,8 @@ import androidx.annotation.Nullable; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.subscription.NotificationMode; +import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.databinding.FragmentChannelBinding; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; @@ -21,14 +24,21 @@ import org.schabi.newpipe.extractor.linkhandler.ChannelTabHandler; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.detail.TabAdapter; +import org.schabi.newpipe.local.feed.notifications.NotificationHelper; +import org.schabi.newpipe.local.subscription.SubscriptionManager; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.external_communication.ShareUtils; +import java.util.List; + import icepick.State; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Consumer; import io.reactivex.rxjava3.schedulers.Schedulers; public class ChannelFragment extends BaseStateFragment { @@ -41,8 +51,12 @@ public class ChannelFragment extends BaseStateFragment { private ChannelInfo currentInfo; private Disposable currentWorker; + private Disposable subscriptionMonitor; + private final CompositeDisposable disposables = new CompositeDisposable(); + private SubscriptionManager subscriptionManager; private MenuItem menuRssButton; + private MenuItem menuNotifyButton; /*////////////////////////////////////////////////////////////////////////// // Views @@ -78,6 +92,12 @@ public void onCreate(final Bundle savedInstanceState) { setHasOptionsMenu(true); } + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + subscriptionManager = new SubscriptionManager(activity); + } + @Override public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @@ -98,6 +118,13 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) { @Override public void onDestroy() { super.onDestroy(); + if (currentWorker != null) { + currentWorker.dispose(); + } + if (subscriptionMonitor != null) { + subscriptionMonitor.dispose(); + } + disposables.clear(); binding = null; } @@ -116,12 +143,19 @@ public void onCreateOptionsMenu(@NonNull final Menu menu, + "menu = [" + menu + "], inflater = [" + inflater + "]"); } menuRssButton = menu.findItem(R.id.menu_item_rss); + menuNotifyButton = menu.findItem(R.id.menu_item_notify); updateRssButton(); + monitorSubscription(); } @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; case R.id.action_settings: NavigationHelper.openSettings(requireContext()); break; @@ -153,6 +187,62 @@ private void updateRssButton() { } } + private void monitorSubscription() { + if (currentInfo != null) { + final Observable> observable = subscriptionManager + .subscriptionTable() + .getSubscriptionFlowable(currentInfo.getServiceId(), currentInfo.getUrl()) + .toObservable(); + + if (subscriptionMonitor != null) { + subscriptionMonitor.dispose(); + } + subscriptionMonitor = observable + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getSubscribeUpdateMonitor()); + } + } + + private Consumer> getSubscribeUpdateMonitor() { + return (List subscriptionEntities) -> { + if (subscriptionEntities.isEmpty()) { + updateNotifyButton(null); + } else { + final SubscriptionEntity subscription = subscriptionEntities.get(0); + updateNotifyButton(subscription); + } + }; + } + + 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() + ); + } + /*////////////////////////////////////////////////////////////////////////// // Init //////////////////////////////////////////////////////////////////////////*/ @@ -213,5 +303,6 @@ public void handleResult(@NonNull final ChannelInfo info) { setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName()); updateTabs(); updateRssButton(); + monitorSubscription(); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java index 12514a55cc3..21613d71722 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java @@ -2,6 +2,8 @@ import android.os.Bundle; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; @@ -45,6 +47,12 @@ public ChannelTabFragment() { // LifeCycle //////////////////////////////////////////////////////////////////////////*/ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(false); + } + @Override public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java index 9f8c83ce7b2..f147e4f9d77 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java @@ -11,16 +11,12 @@ 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; @@ -51,7 +47,6 @@ 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; @@ -89,8 +84,6 @@ public class ChannelVideosFragment extends BaseListInfoFragment> getSubscribeUpdateMonitor(final Chann info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount()); - updateNotifyButton(null); subscribeButtonMonitor = monitorSubscribeButton( headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info)); } else { @@ -365,7 +298,6 @@ private Consumer> getSubscribeUpdateMonitor(final Chann Log.d(TAG, "Found subscription to this channel!"); } final SubscriptionEntity subscription = subscriptionEntities.get(0); - updateNotifyButton(subscription); subscribeButtonMonitor = monitorSubscribeButton( headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription)); } @@ -418,6 +350,19 @@ private void showNotifySnackbar() { .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 //////////////////////////////////////////////////////////////////////////*/ @@ -502,8 +447,6 @@ public void handleResult(@NonNull final ChannelInfo result) { 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) { diff --git a/app/src/main/res/menu/menu_channel_videos.xml b/app/src/main/res/menu/menu_channel_videos.xml deleted file mode 100644 index a3b2e7ae0e3..00000000000 --- a/app/src/main/res/menu/menu_channel_videos.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - From 6d84d195204873535f64912cbb78ab259f3289cc Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 23 Oct 2022 15:37:40 +0200 Subject: [PATCH 03/50] fix: handle unsupported content --- .../list/channel/ChannelFragment.java | 41 +++++++++++++------ .../list/channel/ChannelVideosFragment.java | 4 +- app/src/main/res/layout/fragment_channel.xml | 32 +++++++++++++++ 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 4938d1c0096..4cd8313fcf1 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -21,6 +21,7 @@ import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.channel.ChannelInfo; +import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.linkhandler.ChannelTabHandler; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.detail.TabAdapter; @@ -247,20 +248,36 @@ private void setNotify(final boolean isEnabled) { // Init //////////////////////////////////////////////////////////////////////////*/ + private boolean isContentUnsupported() { + for (final Throwable throwable : currentInfo.getErrors()) { + if (throwable instanceof ContentNotSupportedException) { + return true; + } + } + return false; + } + private void updateTabs() { tabAdapter.clearAllItems(); if (currentInfo != null) { - tabAdapter.addFragment(ChannelVideosFragment.getInstance(currentInfo), "Videos"); - - for (final ChannelTabHandler tab : currentInfo.getTabs()) { + if (isContentUnsupported()) { + showEmptyState(); + binding.errorContentNotSupported.setVisibility(View.VISIBLE); + } else { tabAdapter.addFragment( - ChannelTabFragment.getInstance(serviceId, tab), tab.getTab().name()); - } + ChannelVideosFragment.getInstance(currentInfo), "Videos"); - final String description = currentInfo.getDescription(); - if (!description.isEmpty()) { - tabAdapter.addFragment(ChannelInfoFragment.getInstance(description), "Info"); + for (final ChannelTabHandler tab : currentInfo.getTabs()) { + tabAdapter.addFragment( + ChannelTabFragment.getInstance(serviceId, tab), tab.getTab().name()); + } + + final String description = currentInfo.getDescription(); + if (description != null && !description.isEmpty()) { + tabAdapter.addFragment( + ChannelInfoFragment.getInstance(description), "Info"); + } } } @@ -296,11 +313,11 @@ private void runWorker(final boolean forceLoad) { } @Override - public void handleResult(@NonNull final ChannelInfo info) { - super.handleResult(info); + public void handleResult(@NonNull final ChannelInfo result) { + super.handleResult(result); + currentInfo = result; + setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName()); - currentInfo = info; - setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName()); updateTabs(); updateRssButton(); monitorSubscription(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java index f147e4f9d77..23655dee201 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java @@ -132,13 +132,13 @@ public void onAttach(@NonNull final Context context) { 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); + channelBinding = FragmentChannelVideosBinding.inflate(inflater, container, false); + return channelBinding.getRoot(); } @Override public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) { super.onViewCreated(rootView, savedInstanceState); - channelBinding = FragmentChannelVideosBinding.bind(rootView); showContentNotSupportedIfNeeded(); } diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml index d938f71a7f2..db77391bccc 100644 --- a/app/src/main/res/layout/fragment_channel.xml +++ b/app/src/main/res/layout/fragment_channel.xml @@ -30,6 +30,38 @@ android:visibility="gone" tools:visibility="visible" /> + + + + + + + + Date: Sun, 23 Oct 2022 17:01:39 +0200 Subject: [PATCH 04/50] feat: prettier channel info page --- .../list/channel/ChannelFragment.java | 2 +- .../list/channel/ChannelInfoFragment.java | 120 +++++++++++++++++- .../list/channel/ChannelTabFragment.java | 2 - .../main/res/layout/fragment_channel_info.xml | 50 +++++++- app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 160 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 4cd8313fcf1..6e7473b1e7d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -276,7 +276,7 @@ private void updateTabs() { final String description = currentInfo.getDescription(); if (description != null && !description.isEmpty()) { tabAdapter.addFragment( - ChannelInfoFragment.getInstance(description), "Info"); + ChannelInfoFragment.getInstance(currentInfo), "Info"); } } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java index c9273f52845..2ab4ce4193e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java @@ -1,22 +1,47 @@ package org.schabi.newpipe.fragments.list.channel; +import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT; +import static org.schabi.newpipe.extractor.utils.Utils.isBlank; + +import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import com.google.android.material.chip.Chip; import org.schabi.newpipe.BaseFragment; +import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.FragmentChannelInfoBinding; +import org.schabi.newpipe.databinding.ItemMetadataBinding; +import org.schabi.newpipe.databinding.ItemMetadataTagsBinding; +import org.schabi.newpipe.extractor.channel.ChannelInfo; +import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.external_communication.ShareUtils; +import org.schabi.newpipe.util.external_communication.TextLinkifier; + +import java.util.List; + +import icepick.State; +import io.reactivex.rxjava3.disposables.CompositeDisposable; public class ChannelInfoFragment extends BaseFragment { - private String description; + @State + protected ChannelInfo channelInfo; - public static ChannelInfoFragment getInstance(final String description) { + private final CompositeDisposable disposables = new CompositeDisposable(); + private FragmentChannelInfoBinding binding; + + public static ChannelInfoFragment getInstance(final ChannelInfo channelInfo) { final ChannelInfoFragment fragment = new ChannelInfoFragment(); - fragment.description = description; + fragment.channelInfo = channelInfo; return fragment; } @@ -28,11 +53,92 @@ public ChannelInfoFragment() { 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); - + binding = FragmentChannelInfoBinding.inflate(inflater, container, false); + loadDescription(); + setupMetadata(inflater, binding.detailMetadataLayout); return binding.getRoot(); } + @Override + public void onDestroy() { + super.onDestroy(); + disposables.clear(); + } + + private void loadDescription() { + final String description = channelInfo.getDescription(); + + if (description == null || description.isEmpty()) { + binding.descriptionTitle.setVisibility(View.GONE); + binding.descriptionView.setVisibility(View.GONE); + } else { + TextLinkifier.createLinksFromPlainText( + binding.descriptionView, description, null, disposables); + } + } + + private void setupMetadata(final LayoutInflater inflater, + final LinearLayout layout) { + Context context = getActivity(); + + if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) { + addMetadataItem(inflater, layout, R.string.metadata_subscribers, + Localization.localizeNumber(context, channelInfo.getSubscriberCount())); + } + + addTagsMetadataItem(inflater, layout); + } + + private void addMetadataItem(final LayoutInflater inflater, + final LinearLayout layout, + @StringRes final int type, + @Nullable final String content) { + if (isBlank(content)) { + return; + } + + final ItemMetadataBinding itemBinding = + ItemMetadataBinding.inflate(inflater, layout, false); + + itemBinding.metadataTypeView.setText(type); + itemBinding.metadataTypeView.setOnLongClickListener(v -> { + ShareUtils.copyToClipboard(requireContext(), content); + return true; + }); + + itemBinding.metadataContentView.setText(content); + + layout.addView(itemBinding.getRoot()); + } + + private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) { + final List tags = channelInfo.getTags(); + + if (!tags.isEmpty()) { + final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false); + + tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> { + final Chip chip = (Chip) inflater.inflate(R.layout.chip, + itemBinding.metadataTagsChips, false); + chip.setText(tag); + chip.setOnClickListener(this::onTagClick); + chip.setOnLongClickListener(this::onTagLongClick); + itemBinding.metadataTagsChips.addView(chip); + }); + + layout.addView(itemBinding.getRoot()); + } + } + + private void onTagClick(final View chip) { + if (getParentFragment() != null) { + NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(), + channelInfo.getServiceId(), ((Chip) chip).getText().toString()); + } + } + + private boolean onTagLongClick(final View chip) { + ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString()); + return true; + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java index 21613d71722..5a26371b704 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java @@ -2,8 +2,6 @@ import android.os.Bundle; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; diff --git a/app/src/main/res/layout/fragment_channel_info.xml b/app/src/main/res/layout/fragment_channel_info.xml index fbb8e355be0..8cbadba1fe2 100644 --- a/app/src/main/res/layout/fragment_channel_info.xml +++ b/app/src/main/res/layout/fragment_channel_info.xml @@ -5,6 +5,19 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + - --> + + + tools:layout_editor_absoluteX="0dp" + 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." + tools:visibility="visible" /> + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index dfc26a4e21d..c5260fa03df 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -620,6 +620,7 @@ Vorschaubild-URL Server Unterstützung + Abonnenten Auswählen von Text in der Beschreibung deaktivieren Auswählen von Text in der Beschreibung aktivieren Du kannst nun Text innerhalb der Beschreibung auswählen. Beachte, dass die Seite flackern kann und Links im Auswahlmodus möglicherweise nicht anklickbar sind. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a37bfeb82f4..7d65807b795 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -758,6 +758,7 @@ Unlisted Private Internal + Subscribers Pinned comment Hearted by creator Open website From 506e3724a61204bb1764e4f58c6c755b95dadd7a Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 23 Oct 2022 17:11:07 +0200 Subject: [PATCH 05/50] fix: add progress spinners --- app/src/main/res/layout/fragment_channel_tab.xml | 9 +++++++++ app/src/main/res/layout/fragment_channel_videos.xml | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/app/src/main/res/layout/fragment_channel_tab.xml b/app/src/main/res/layout/fragment_channel_tab.xml index 51915629635..dd114cb77d2 100644 --- a/app/src/main/res/layout/fragment_channel_tab.xml +++ b/app/src/main/res/layout/fragment_channel_tab.xml @@ -11,6 +11,15 @@ android:scrollbars="vertical" tools:listitem="@layout/list_stream_item" /> + + + + Date: Sun, 23 Oct 2022 17:21:07 +0200 Subject: [PATCH 06/50] fix: scrollable channel description --- .../main/res/layout/fragment_channel_info.xml | 103 +++++++----------- 1 file changed, 40 insertions(+), 63 deletions(-) diff --git a/app/src/main/res/layout/fragment_channel_info.xml b/app/src/main/res/layout/fragment_channel_info.xml index 8cbadba1fe2..c9648e01fa4 100644 --- a/app/src/main/res/layout/fragment_channel_info.xml +++ b/app/src/main/res/layout/fragment_channel_info.xml @@ -1,74 +1,51 @@ - - + android:layout_height="wrap_content"> - - - + - + - + - \ No newline at end of file + + From bb062f07f9415150f4c4b53df6d599de5fae5009 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 23 Oct 2022 21:13:43 +0200 Subject: [PATCH 07/50] feat: add option to hide channel tabs --- .../list/channel/ChannelFragment.java | 18 ++++- .../list/channel/ChannelInfoFragment.java | 2 +- .../org/schabi/newpipe/util/ChannelTabs.java | 65 +++++++++++++++++++ app/src/main/res/values-de/strings.xml | 8 +++ app/src/main/res/values/settings_keys.xml | 20 ++++++ app/src/main/res/values/strings.xml | 8 +++ app/src/main/res/xml/content_settings.xml | 10 +++ 7 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 6e7473b1e7d..f71791d8e45 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.fragments.list.channel; import android.content.Context; +import android.content.SharedPreferences; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; @@ -13,6 +14,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; import org.schabi.newpipe.database.subscription.NotificationMode; @@ -27,6 +29,7 @@ import org.schabi.newpipe.fragments.detail.TabAdapter; import org.schabi.newpipe.local.feed.notifications.NotificationHelper; import org.schabi.newpipe.local.subscription.SubscriptionManager; +import org.schabi.newpipe.util.ChannelTabs; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -268,13 +271,22 @@ private void updateTabs() { tabAdapter.addFragment( ChannelVideosFragment.getInstance(currentInfo), "Videos"); + final Context context = getContext(); + final SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(context); + for (final ChannelTabHandler tab : currentInfo.getTabs()) { - tabAdapter.addFragment( - ChannelTabFragment.getInstance(serviceId, tab), tab.getTab().name()); + if (ChannelTabs.showChannelTab(context, preferences, tab.getTab())) { + tabAdapter.addFragment( + ChannelTabFragment.getInstance(serviceId, tab), + context.getString(ChannelTabs.getTranslationKey(tab.getTab()))); + } } final String description = currentInfo.getDescription(); - if (description != null && !description.isEmpty()) { + if (description != null && !description.isEmpty() && + ChannelTabs.showChannelTab( + context, preferences, R.string.show_channel_tabs_info)) { tabAdapter.addFragment( ChannelInfoFragment.getInstance(currentInfo), "Info"); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java index 2ab4ce4193e..6e7e49876a8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java @@ -79,7 +79,7 @@ private void loadDescription() { private void setupMetadata(final LayoutInflater inflater, final LinearLayout layout) { - Context context = getActivity(); + Context context = getContext(); if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) { addMetadataItem(inflater, layout, R.string.metadata_subscribers, diff --git a/app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java b/app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java new file mode 100644 index 00000000000..983daf349f5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java @@ -0,0 +1,65 @@ +package org.schabi.newpipe.util; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.StringRes; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.linkhandler.ChannelTabHandler.Tab; + +import java.util.Set; + +public class ChannelTabs { + @StringRes + private static int getShowTabKey(final Tab tab) { + switch (tab) { + case Playlists: + return R.string.show_channel_tabs_playlists; + case Livestreams: + return R.string.show_channel_tabs_livestreams; + case Shorts: + return R.string.show_channel_tabs_shorts; + case Channels: + return R.string.show_channel_tabs_channels; + } + return -1; + } + + @StringRes + public static int getTranslationKey(final Tab tab) { + switch (tab) { + case Playlists: + return R.string.channel_tab_playlists; + case Livestreams: + return R.string.channel_tab_livestreams; + case Shorts: + return R.string.channel_tab_shorts; + case Channels: + return R.string.channel_tab_channels; + } + return R.string.unknown_content; + } + + public static boolean showChannelTab(final Context context, + final SharedPreferences sharedPreferences, + @StringRes final int key) { + final Set enabledTabs = sharedPreferences.getStringSet( + context.getString(R.string.show_channel_tabs_key), null); + if (enabledTabs == null) { + return true; // default to true + } else { + return enabledTabs.contains(context.getString(key)); + } + } + + public static boolean showChannelTab(final Context context, + final SharedPreferences sharedPreferences, + final Tab tab) { + final int key = ChannelTabs.getShowTabKey(tab); + if (key == -1) { + return false; + } + return showChannelTab(context, sharedPreferences, key); + } +} diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index c5260fa03df..76abdfbe2b5 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -767,4 +767,12 @@ Das Media-Tunneling wurde auf dem Gerät standardmäßig deaktiviert, da das Gerätemodell diese Funktion bekanntermaßen nicht unterstützt. Keine Live-Streams Keine Streams + Videos + Live + Shorts + Wiedergabelisten + Kanäle + Info + Tabs auf den Kanalseiten + Welche Tabs auf den Kanalseiten angezeigt werden \ No newline at end of file diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 56fc19eedd0..00c501643ec 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -274,6 +274,26 @@ main_tabs_position + channel_tabs + show_channel_tabs_playlists + show_channel_tabs_live + show_channel_tabs_shorts + show_channel_tabs_channels + show_channel_tabs_info + + @string/show_channel_tabs_playlists + @string/show_channel_tabs_livestreams + @string/show_channel_tabs_shorts + @string/show_channel_tabs_channels + @string/show_channel_tabs_info + + + @string/channel_tab_playlists + @string/channel_tab_livestreams + @string/channel_tab_shorts + @string/channel_tab_channels + @string/channel_tab_info + show_search_suggestions show_local_search_suggestions show_remote_search_suggestions diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d65807b795..fd0971761ec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -797,4 +797,12 @@ original dubbed descriptive + Videos + Live + Shorts + Playlists + Channels + Info + Channel tabs + What tabs are shown on the channel pages \ No newline at end of file diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml index fddb966c847..8783ff1ed2d 100644 --- a/app/src/main/res/xml/content_settings.xml +++ b/app/src/main/res/xml/content_settings.xml @@ -41,6 +41,16 @@ app:singleLineTitle="false" app:iconSpaceReserved="false" /> + + Date: Sun, 23 Oct 2022 21:28:54 +0200 Subject: [PATCH 08/50] fix: remember selected channel tab on screen rotation --- .../list/channel/ChannelFragment.java | 27 ++++++++++++++++--- .../list/channel/ChannelInfoFragment.java | 2 +- .../list/channel/ChannelTabFragment.java | 2 +- .../list/channel/ChannelVideosFragment.java | 2 +- .../org/schabi/newpipe/util/ChannelTabs.java | 5 +++- 5 files changed, 31 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index f71791d8e45..51625d202ad 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -16,6 +16,8 @@ import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; +import com.google.android.material.tabs.TabLayout; + import org.schabi.newpipe.R; import org.schabi.newpipe.database.subscription.NotificationMode; import org.schabi.newpipe.database.subscription.SubscriptionEntity; @@ -58,6 +60,7 @@ public class ChannelFragment extends BaseStateFragment { private Disposable subscriptionMonitor; private final CompositeDisposable disposables = new CompositeDisposable(); private SubscriptionManager subscriptionManager; + private int lastTab; private MenuItem menuRssButton; private MenuItem menuNotifyButton; @@ -94,10 +97,16 @@ protected void setInitialData(final int sid, final String u, final String title) public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); + + if (savedInstanceState != null) { + lastTab = savedInstanceState.getInt("LastTab"); + } else { + lastTab = 0; + } } @Override - public void onAttach(@NonNull Context context) { + public void onAttach(final @NonNull Context context) { super.onAttach(context); subscriptionManager = new SubscriptionManager(activity); } @@ -119,6 +128,12 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) { binding.tabLayout.setupWithViewPager(binding.viewPager); } + @Override + public void onSaveInstanceState(final @NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt("LastTab", binding.tabLayout.getSelectedTabPosition()); + } + @Override public void onDestroy() { super.onDestroy(); @@ -284,8 +299,8 @@ private void updateTabs() { } final String description = currentInfo.getDescription(); - if (description != null && !description.isEmpty() && - ChannelTabs.showChannelTab( + if (description != null && !description.isEmpty() + && ChannelTabs.showChannelTab( context, preferences, R.string.show_channel_tabs_info)) { tabAdapter.addFragment( ChannelInfoFragment.getInstance(currentInfo), "Info"); @@ -298,6 +313,12 @@ private void updateTabs() { for (int i = 0; i < tabAdapter.getCount(); i++) { binding.tabLayout.getTabAt(i).setText(tabAdapter.getItemTitle(i)); } + + // Restore previously selected tab + final TabLayout.Tab ltab = binding.tabLayout.getTabAt(lastTab); + if (ltab != null) { + binding.tabLayout.selectTab(ltab); + } } @Override diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java index 6e7e49876a8..ba1faab8f7f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java @@ -79,7 +79,7 @@ private void loadDescription() { private void setupMetadata(final LayoutInflater inflater, final LinearLayout layout) { - Context context = getContext(); + final Context context = getContext(); if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) { addMetadataItem(inflater, layout, R.string.metadata_subscribers, diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java index 5a26371b704..1ce55df81b3 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java @@ -46,7 +46,7 @@ public ChannelTabFragment() { //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(false); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java index 23655dee201..a38b913d6e4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java @@ -117,7 +117,7 @@ public void onResume() { //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(false); } diff --git a/app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java b/app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java index 983daf349f5..0147e9c0831 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java +++ b/app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java @@ -10,7 +10,10 @@ import java.util.Set; -public class ChannelTabs { +public final class ChannelTabs { + private ChannelTabs() { + } + @StringRes private static int getShowTabKey(final Tab tab) { switch (tab) { From 74a8bfba938651e2a0fabae6dd6f475e1246a99b Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 23 Oct 2022 21:36:55 +0200 Subject: [PATCH 09/50] feat: add album tab --- app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java | 4 ++++ app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/settings_keys.xml | 3 +++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 9 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java b/app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java index 0147e9c0831..029339cefce 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java +++ b/app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java @@ -25,6 +25,8 @@ private static int getShowTabKey(final Tab tab) { return R.string.show_channel_tabs_shorts; case Channels: return R.string.show_channel_tabs_channels; + case Albums: + break; } return -1; } @@ -40,6 +42,8 @@ public static int getTranslationKey(final Tab tab) { return R.string.channel_tab_shorts; case Channels: return R.string.channel_tab_channels; + case Albums: + return R.string.channel_tab_albums; } return R.string.unknown_content; } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 76abdfbe2b5..1720d2b0a34 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -772,6 +772,7 @@ Shorts Wiedergabelisten Kanäle + Alben Info Tabs auf den Kanalseiten Welche Tabs auf den Kanalseiten angezeigt werden diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 00c501643ec..9746d78898c 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -279,12 +279,14 @@ show_channel_tabs_live show_channel_tabs_shorts show_channel_tabs_channels + show_channel_tabs_albums show_channel_tabs_info @string/show_channel_tabs_playlists @string/show_channel_tabs_livestreams @string/show_channel_tabs_shorts @string/show_channel_tabs_channels + @string/show_channel_tabs_albums @string/show_channel_tabs_info @@ -292,6 +294,7 @@ @string/channel_tab_livestreams @string/channel_tab_shorts @string/channel_tab_channels + @string/channel_tab_albums @string/channel_tab_info show_search_suggestions diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fd0971761ec..87577a81a9c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -802,6 +802,7 @@ Shorts Playlists Channels + Albums Info Channel tabs What tabs are shown on the channel pages From 16cd47fa2e3b37913729113c35b8862a0dda0a9a Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 24 Oct 2022 00:03:19 +0200 Subject: [PATCH 10/50] fix: missing album tab key --- app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java b/app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java index 029339cefce..b861824d5dc 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java +++ b/app/src/main/java/org/schabi/newpipe/util/ChannelTabs.java @@ -26,7 +26,7 @@ private static int getShowTabKey(final Tab tab) { case Channels: return R.string.show_channel_tabs_channels; case Albums: - break; + return R.string.show_channel_tabs_albums; } return -1; } From 2c98d079dec6abf42bdf5fe7c9b4e033332e3f21 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Tue, 25 Oct 2022 09:01:11 +0200 Subject: [PATCH 11/50] fix: cache channel data --- .../list/channel/ChannelFragment.java | 53 ++++++++++++++----- .../schabi/newpipe/util/ExtractorHelper.java | 3 +- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 51625d202ad..d7955eb9ddd 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -35,9 +35,11 @@ import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.external_communication.ShareUtils; import java.util.List; +import java.util.Queue; import icepick.State; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; @@ -47,7 +49,8 @@ import io.reactivex.rxjava3.functions.Consumer; import io.reactivex.rxjava3.schedulers.Schedulers; -public class ChannelFragment extends BaseStateFragment { +public class ChannelFragment extends BaseStateFragment + implements StateSaver.WriteRead { @State protected int serviceId = Constants.NO_SERVICE_ID; @State @@ -97,12 +100,6 @@ protected void setInitialData(final int sid, final String u, final String title) public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); - - if (savedInstanceState != null) { - lastTab = savedInstanceState.getInt("LastTab"); - } else { - lastTab = 0; - } } @Override @@ -128,12 +125,6 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) { binding.tabLayout.setupWithViewPager(binding.viewPager); } - @Override - public void onSaveInstanceState(final @NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putInt("LastTab", binding.tabLayout.getSelectedTabPosition()); - } - @Override public void onDestroy() { super.onDestroy(); @@ -301,7 +292,7 @@ private void updateTabs() { final String description = currentInfo.getDescription(); if (description != null && !description.isEmpty() && ChannelTabs.showChannelTab( - context, preferences, R.string.show_channel_tabs_info)) { + context, preferences, R.string.show_channel_tabs_info)) { tabAdapter.addFragment( ChannelInfoFragment.getInstance(currentInfo), "Info"); } @@ -321,6 +312,40 @@ private void updateTabs() { } } + /*////////////////////////////////////////////////////////////////////////// + // State Saving + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public String generateSuffix() { + return null; + } + + @Override + public void writeTo(final Queue objectsToSave) { + objectsToSave.add(currentInfo); + if (binding != null) { + objectsToSave.add(binding.tabLayout.getSelectedTabPosition()); + } else { + objectsToSave.add(0); + } + } + + @Override + public void readFrom(@NonNull final Queue savedObjects) { + currentInfo = (ChannelInfo) savedObjects.poll(); + lastTab = (Integer) savedObjects.poll(); + } + + @Override + protected void doInitialLoadLogic() { + if (currentInfo == null) { + startLoading(false); + } else { + handleResult(currentInfo); + } + } + @Override public void startLoading(final boolean forceLoad) { super.startLoading(forceLoad); diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index b4648c79b7f..bf99ae3d3c3 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -158,7 +158,8 @@ public static Single getChannelTab(final int serviceId, final boolean forceLoad) { checkServiceId(serviceId); return checkCache(forceLoad, serviceId, - tabHandler.getUrl() + tabHandler.getTab().name(), InfoItem.InfoType.CHANNEL, + tabHandler.getUrl() + "/" + + tabHandler.getTab().name(), InfoItem.InfoType.CHANNEL, Single.fromCallable(() -> ChannelTabInfo.getInfo(NewPipe.getService(serviceId), tabHandler))); } From 2c03ba204eabf505c9876945dcc3c95f54d8b786 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 5 Nov 2022 00:23:03 +0100 Subject: [PATCH 12/50] refactor: adjustments to updated tab extractor API --- .../list/channel/ChannelFragment.java | 15 ++++---- .../list/channel/ChannelTabFragment.java | 6 ++-- ...ChannelTabs.java => ChannelTabHelper.java} | 34 +++++++++---------- .../schabi/newpipe/util/ExtractorHelper.java | 16 ++++----- 4 files changed, 36 insertions(+), 35 deletions(-) rename app/src/main/java/org/schabi/newpipe/util/{ChannelTabs.java => ChannelTabHelper.java} (69%) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index d7955eb9ddd..32209378128 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -26,12 +26,12 @@ import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; -import org.schabi.newpipe.extractor.linkhandler.ChannelTabHandler; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.detail.TabAdapter; import org.schabi.newpipe.local.feed.notifications.NotificationHelper; import org.schabi.newpipe.local.subscription.SubscriptionManager; -import org.schabi.newpipe.util.ChannelTabs; +import org.schabi.newpipe.util.ChannelTabHelper; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -281,17 +281,18 @@ private void updateTabs() { final SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences(context); - for (final ChannelTabHandler tab : currentInfo.getTabs()) { - if (ChannelTabs.showChannelTab(context, preferences, tab.getTab())) { + for (final ListLinkHandler linkHandler : currentInfo.getTabs()) { + final String tab = linkHandler.getContentFilters().get(0); + if (ChannelTabHelper.showChannelTab(context, preferences, tab)) { tabAdapter.addFragment( - ChannelTabFragment.getInstance(serviceId, tab), - context.getString(ChannelTabs.getTranslationKey(tab.getTab()))); + ChannelTabFragment.getInstance(serviceId, linkHandler), + context.getString(ChannelTabHelper.getTranslationKey(tab))); } } final String description = currentInfo.getDescription(); if (description != null && !description.isEmpty() - && ChannelTabs.showChannelTab( + && ChannelTabHelper.showChannelTab( context, preferences, R.string.show_channel_tabs_info)) { tabAdapter.addFragment( ChannelInfoFragment.getInstance(currentInfo), "Info"); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java index 1ce55df81b3..d00cb5cf928 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java @@ -13,7 +13,7 @@ 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.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; @@ -27,10 +27,10 @@ public class ChannelTabFragment extends BaseListInfoFragment> getFeedInfoFallbackToChannelInfo( } public static Single getChannelTab(final int serviceId, - final ChannelTabHandler tabHandler, + final ListLinkHandler listLinkHandler, final boolean forceLoad) { checkServiceId(serviceId); return checkCache(forceLoad, serviceId, - tabHandler.getUrl() + "/" - + tabHandler.getTab().name(), InfoItem.InfoType.CHANNEL, + listLinkHandler.getUrl(), InfoItem.InfoType.CHANNEL, Single.fromCallable(() -> - ChannelTabInfo.getInfo(NewPipe.getService(serviceId), tabHandler))); + ChannelTabInfo.getInfo(NewPipe.getService(serviceId), listLinkHandler))); } public static Single> getMoreChannelTabItems(final int serviceId, - final ChannelTabHandler - tabHandler, + final ListLinkHandler + listLinkHandler, final Page nextPage) { checkServiceId(serviceId); return Single.fromCallable(() -> - ChannelTabInfo.getMoreItems(NewPipe.getService(serviceId), tabHandler, nextPage)); + ChannelTabInfo.getMoreItems(NewPipe.getService(serviceId), + listLinkHandler, nextPage)); } public static Single getCommentsInfo(final int serviceId, final String url, From 4357a343394ed7317253ad5a36fad36042e24574 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Tue, 22 Nov 2022 02:52:25 +0100 Subject: [PATCH 13/50] fix: ChannelFragment: save last tab --- .../fragments/list/channel/ChannelFragment.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 32209378128..f0810a03ccc 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -100,6 +100,12 @@ protected void setInitialData(final int sid, final String u, final String title) public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); + + if (savedInstanceState != null) { + lastTab = savedInstanceState.getInt("LastTab"); + } else { + lastTab = 0; + } } @Override @@ -125,6 +131,12 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) { binding.tabLayout.setupWithViewPager(binding.viewPager); } + @Override + public void onSaveInstanceState(final @NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt("LastTab", binding.tabLayout.getSelectedTabPosition()); + } + @Override public void onDestroy() { super.onDestroy(); From be548dcb521d4604f15ce313816ab82cf73845da Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Tue, 29 Nov 2022 19:25:35 +0100 Subject: [PATCH 14/50] fix: channel tab title not being set --- .../newpipe/fragments/list/channel/ChannelFragment.java | 2 +- .../fragments/list/channel/ChannelTabFragment.java | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index f0810a03ccc..8b6c0084e13 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -297,7 +297,7 @@ private void updateTabs() { final String tab = linkHandler.getContentFilters().get(0); if (ChannelTabHelper.showChannelTab(context, preferences, tab)) { tabAdapter.addFragment( - ChannelTabFragment.getInstance(serviceId, linkHandler), + ChannelTabFragment.getInstance(serviceId, linkHandler, name), context.getString(ChannelTabHelper.getTranslationKey(tab))); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java index d00cb5cf928..3f400bdf8d4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java @@ -29,11 +29,16 @@ public class ChannelTabFragment extends BaseListInfoFragment> loadMoreItemsLogic() { @Override public void setTitle(final String title) { + super.setTitle(channelName); } } From d87aa23ae037154f11f1fa5cbc54f08257cba5d7 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 5 Apr 2023 14:55:02 +0200 Subject: [PATCH 15/50] update NewPipeExtractor --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 22ac7d67d1c..10dd4bef081 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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.Theta-Dev:NewPipeExtractor:8446e20a71dbddbe1626a118d0adf490e5e63bbb' + implementation 'com.github.Theta-Dev:NewPipeExtractor:e57d43f92d0c7132b569835a659da2d3b3017602' implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' /** Checkstyle **/ From 39b4ed082c5ade58aaf098d5a9d3b20b839f047a Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 5 Apr 2023 16:17:31 +0200 Subject: [PATCH 16/50] refactor: common code from ChannelInfo/Description -> BaseInfoFragment --- .../fragments/detail/BaseInfoFragment.java | 206 ++++++++++++++++++ .../fragments/detail/DescriptionFragment.java | 186 ++++------------ .../list/channel/ChannelInfoFragment.java | 131 +++-------- 3 files changed, 280 insertions(+), 243 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/detail/BaseInfoFragment.java diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseInfoFragment.java new file mode 100644 index 00000000000..d8aea1a03be --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseInfoFragment.java @@ -0,0 +1,206 @@ +package org.schabi.newpipe.fragments.detail; + +import static android.text.TextUtils.isEmpty; +import static org.schabi.newpipe.extractor.utils.Utils.isBlank; +import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.widget.TooltipCompat; +import androidx.core.text.HtmlCompat; + +import com.google.android.material.chip.Chip; + +import org.schabi.newpipe.BaseFragment; +import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.FragmentDescriptionBinding; +import org.schabi.newpipe.databinding.ItemMetadataBinding; +import org.schabi.newpipe.databinding.ItemMetadataTagsBinding; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.stream.Description; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.external_communication.ShareUtils; +import org.schabi.newpipe.util.text.TextLinkifier; + +import java.util.List; + +import io.reactivex.rxjava3.disposables.CompositeDisposable; + +public abstract class BaseInfoFragment extends BaseFragment { + final CompositeDisposable descriptionDisposables = new CompositeDisposable(); + FragmentDescriptionBinding binding; + + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, + @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState) { + binding = FragmentDescriptionBinding.inflate(inflater, container, false); + setupDescription(); + setupMetadata(inflater, binding.detailMetadataLayout); + addTagsMetadataItem(inflater, binding.detailMetadataLayout); + return binding.getRoot(); + } + + @Override + public void onDestroy() { + descriptionDisposables.clear(); + super.onDestroy(); + } + + /** + * Get the description to display. + * @return description object + */ + @Nullable + protected abstract Description getDescription(); + + /** + * Get the streaming service. Used for generating description links. + * @return streaming service + */ + @Nullable + protected abstract StreamingService getService(); + + /** + * Get the streaming service ID. Used for tag links. + * @return service ID + */ + protected abstract int getServiceId(); + + /** + * Get the URL of the described video. Used for generating description links. + * @return stream URL + */ + @Nullable + protected abstract String getStreamUrl(); + + /** + * Get the list of tags to display below the description. + * @return tag list + */ + @Nullable + public abstract List getTags(); + + /** + * Add additional metadata to display. + * @param inflater LayoutInflater + * @param layout detailMetadataLayout + */ + protected abstract void setupMetadata(LayoutInflater inflater, LinearLayout layout); + + private void setupDescription() { + final Description description = getDescription(); + if (description == null || isEmpty(description.getContent()) + || description == Description.EMPTY_DESCRIPTION) { + binding.detailDescriptionView.setVisibility(View.GONE); + binding.detailSelectDescriptionButton.setVisibility(View.GONE); + return; + } + + // start with disabled state. This also loads description content (!) + disableDescriptionSelection(); + + binding.detailSelectDescriptionButton.setOnClickListener(v -> { + if (binding.detailDescriptionNoteView.getVisibility() == View.VISIBLE) { + disableDescriptionSelection(); + } else { + // enable selection only when button is clicked to prevent flickering + enableDescriptionSelection(); + } + }); + } + + private void enableDescriptionSelection() { + binding.detailDescriptionNoteView.setVisibility(View.VISIBLE); + binding.detailDescriptionView.setTextIsSelectable(true); + + final String buttonLabel = getString(R.string.description_select_disable); + binding.detailSelectDescriptionButton.setContentDescription(buttonLabel); + TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel); + binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close); + } + + private void disableDescriptionSelection() { + // show description content again, otherwise some links are not clickable + TextLinkifier.fromDescription(binding.detailDescriptionView, + getDescription(), HtmlCompat.FROM_HTML_MODE_LEGACY, + getService(), getStreamUrl(), + descriptionDisposables, SET_LINK_MOVEMENT_METHOD); + + binding.detailDescriptionNoteView.setVisibility(View.GONE); + binding.detailDescriptionView.setTextIsSelectable(false); + + final String buttonLabel = getString(R.string.description_select_enable); + binding.detailSelectDescriptionButton.setContentDescription(buttonLabel); + TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel); + binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all); + } + + protected void addMetadataItem(final LayoutInflater inflater, + final LinearLayout layout, + final boolean linkifyContent, + @StringRes final int type, + @Nullable final String content) { + if (isBlank(content)) { + return; + } + + final ItemMetadataBinding itemBinding = + ItemMetadataBinding.inflate(inflater, layout, false); + + itemBinding.metadataTypeView.setText(type); + itemBinding.metadataTypeView.setOnLongClickListener(v -> { + ShareUtils.copyToClipboard(requireContext(), content); + return true; + }); + + if (linkifyContent) { + TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null, + descriptionDisposables, SET_LINK_MOVEMENT_METHOD); + } else { + itemBinding.metadataContentView.setText(content); + } + + itemBinding.metadataContentView.setClickable(true); + + layout.addView(itemBinding.getRoot()); + } + + private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) { + final List tags = getTags(); + + if (tags != null && !tags.isEmpty()) { + final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false); + + tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> { + final Chip chip = (Chip) inflater.inflate(R.layout.chip, + itemBinding.metadataTagsChips, false); + chip.setText(tag); + chip.setOnClickListener(this::onTagClick); + chip.setOnLongClickListener(this::onTagLongClick); + itemBinding.metadataTagsChips.addView(chip); + }); + + layout.addView(itemBinding.getRoot()); + } + } + + private void onTagClick(final View chip) { + if (getParentFragment() != null) { + NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(), + getServiceId(), ((Chip) chip).getText().toString()); + } + } + + private boolean onTagLongClick(final View chip) { + ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString()); + return true; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java index d364c0c0fd4..cf99365dc0b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java @@ -1,46 +1,29 @@ package org.schabi.newpipe.fragments.detail; -import static android.text.TextUtils.isEmpty; import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT; -import static org.schabi.newpipe.extractor.utils.Utils.isBlank; import static org.schabi.newpipe.util.Localization.getAppLocale; -import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD; -import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.widget.LinearLayout; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; -import androidx.appcompat.widget.TooltipCompat; -import androidx.core.text.HtmlCompat; -import com.google.android.material.chip.Chip; - -import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; -import org.schabi.newpipe.databinding.FragmentDescriptionBinding; -import org.schabi.newpipe.databinding.ItemMetadataBinding; -import org.schabi.newpipe.databinding.ItemMetadataTagsBinding; +import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.util.Localization; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.external_communication.ShareUtils; -import org.schabi.newpipe.util.text.TextLinkifier; + +import java.util.List; import icepick.State; -import io.reactivex.rxjava3.disposables.CompositeDisposable; -public class DescriptionFragment extends BaseFragment { +public class DescriptionFragment extends BaseInfoFragment { @State StreamInfo streamInfo = null; - final CompositeDisposable descriptionDisposables = new CompositeDisposable(); - FragmentDescriptionBinding binding; public DescriptionFragment() { } @@ -49,86 +32,56 @@ public DescriptionFragment(final StreamInfo streamInfo) { this.streamInfo = streamInfo; } + @Nullable @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable final Bundle savedInstanceState) { - binding = FragmentDescriptionBinding.inflate(inflater, container, false); - if (streamInfo != null) { - setupUploadDate(); - setupDescription(); - setupMetadata(inflater, binding.detailMetadataLayout); + protected Description getDescription() { + if (streamInfo == null) { + return null; } - return binding.getRoot(); + return streamInfo.getDescription(); } + @Nullable @Override - public void onDestroy() { - descriptionDisposables.clear(); - super.onDestroy(); - } - - - private void setupUploadDate() { - if (streamInfo.getUploadDate() != null) { - binding.detailUploadDateView.setText(Localization - .localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime())); - } else { - binding.detailUploadDateView.setVisibility(View.GONE); + protected StreamingService getService() { + if (streamInfo == null) { + return null; } + return streamInfo.getService(); } + @Override + protected int getServiceId() { + return streamInfo.getServiceId(); + } - private void setupDescription() { - final Description description = streamInfo.getDescription(); - if (description == null || isEmpty(description.getContent()) - || description == Description.EMPTY_DESCRIPTION) { - binding.detailDescriptionView.setVisibility(View.GONE); - binding.detailSelectDescriptionButton.setVisibility(View.GONE); - return; + @Nullable + @Override + protected String getStreamUrl() { + if (streamInfo == null) { + return null; } - - // start with disabled state. This also loads description content (!) - disableDescriptionSelection(); - - binding.detailSelectDescriptionButton.setOnClickListener(v -> { - if (binding.detailDescriptionNoteView.getVisibility() == View.VISIBLE) { - disableDescriptionSelection(); - } else { - // enable selection only when button is clicked to prevent flickering - enableDescriptionSelection(); - } - }); + return streamInfo.getUrl(); } - private void enableDescriptionSelection() { - binding.detailDescriptionNoteView.setVisibility(View.VISIBLE); - binding.detailDescriptionView.setTextIsSelectable(true); - - final String buttonLabel = getString(R.string.description_select_disable); - binding.detailSelectDescriptionButton.setContentDescription(buttonLabel); - TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel); - binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close); + @Nullable + @Override + public List getTags() { + if (streamInfo == null) { + return null; + } + return streamInfo.getTags(); } - private void disableDescriptionSelection() { - // show description content again, otherwise some links are not clickable - TextLinkifier.fromDescription(binding.detailDescriptionView, - streamInfo.getDescription(), HtmlCompat.FROM_HTML_MODE_LEGACY, - streamInfo.getService(), streamInfo.getUrl(), - descriptionDisposables, SET_LINK_MOVEMENT_METHOD); - - binding.detailDescriptionNoteView.setVisibility(View.GONE); - binding.detailDescriptionView.setTextIsSelectable(false); - - final String buttonLabel = getString(R.string.description_select_enable); - binding.detailSelectDescriptionButton.setContentDescription(buttonLabel); - TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel); - binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all); - } + protected void setupMetadata(final LayoutInflater inflater, + final LinearLayout layout) { + if (streamInfo.getUploadDate() != null) { + binding.detailUploadDateView.setText(Localization + .localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime())); + } else { + binding.detailUploadDateView.setVisibility(View.GONE); + } - private void setupMetadata(final LayoutInflater inflater, - final LinearLayout layout) { addMetadataItem(inflater, layout, false, R.string.metadata_category, streamInfo.getCategory()); @@ -153,67 +106,6 @@ private void setupMetadata(final LayoutInflater inflater, streamInfo.getHost()); addMetadataItem(inflater, layout, true, R.string.metadata_thumbnail_url, streamInfo.getThumbnailUrl()); - - addTagsMetadataItem(inflater, layout); - } - - private void addMetadataItem(final LayoutInflater inflater, - final LinearLayout layout, - final boolean linkifyContent, - @StringRes final int type, - @Nullable final String content) { - if (isBlank(content)) { - return; - } - - final ItemMetadataBinding itemBinding = - ItemMetadataBinding.inflate(inflater, layout, false); - - itemBinding.metadataTypeView.setText(type); - itemBinding.metadataTypeView.setOnLongClickListener(v -> { - ShareUtils.copyToClipboard(requireContext(), content); - return true; - }); - - if (linkifyContent) { - TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null, - descriptionDisposables, SET_LINK_MOVEMENT_METHOD); - } else { - itemBinding.metadataContentView.setText(content); - } - - itemBinding.metadataContentView.setClickable(true); - - layout.addView(itemBinding.getRoot()); - } - - private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) { - if (streamInfo.getTags() != null && !streamInfo.getTags().isEmpty()) { - final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false); - - streamInfo.getTags().stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> { - final Chip chip = (Chip) inflater.inflate(R.layout.chip, - itemBinding.metadataTagsChips, false); - chip.setText(tag); - chip.setOnClickListener(this::onTagClick); - chip.setOnLongClickListener(this::onTagLongClick); - itemBinding.metadataTagsChips.addView(chip); - }); - - layout.addView(itemBinding.getRoot()); - } - } - - private void onTagClick(final View chip) { - if (getParentFragment() != null) { - NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(), - streamInfo.getServiceId(), ((Chip) chip).getText().toString()); - } - } - - private boolean onTagLongClick(final View chip) { - ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString()); - return true; } private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearLayout layout) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java index ba1faab8f7f..70b182a7552 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java @@ -1,44 +1,28 @@ package org.schabi.newpipe.fragments.list.channel; import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT; -import static org.schabi.newpipe.extractor.utils.Utils.isBlank; import android.content.Context; -import android.os.Bundle; import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; import android.widget.LinearLayout; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import com.google.android.material.chip.Chip; - -import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; -import org.schabi.newpipe.databinding.FragmentChannelInfoBinding; -import org.schabi.newpipe.databinding.ItemMetadataBinding; -import org.schabi.newpipe.databinding.ItemMetadataTagsBinding; +import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.channel.ChannelInfo; +import org.schabi.newpipe.extractor.stream.Description; +import org.schabi.newpipe.fragments.detail.BaseInfoFragment; import org.schabi.newpipe.util.Localization; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.external_communication.ShareUtils; -import org.schabi.newpipe.util.external_communication.TextLinkifier; import java.util.List; import icepick.State; -import io.reactivex.rxjava3.disposables.CompositeDisposable; -public class ChannelInfoFragment extends BaseFragment { +public class ChannelInfoFragment extends BaseInfoFragment { @State protected ChannelInfo channelInfo; - private final CompositeDisposable disposables = new CompositeDisposable(); - private FragmentChannelInfoBinding binding; - public static ChannelInfoFragment getInstance(final ChannelInfo channelInfo) { final ChannelInfoFragment fragment = new ChannelInfoFragment(); fragment.channelInfo = channelInfo; @@ -49,96 +33,51 @@ public ChannelInfoFragment() { super(); } + @Nullable @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - final Bundle savedInstanceState) { - binding = FragmentChannelInfoBinding.inflate(inflater, container, false); - loadDescription(); - setupMetadata(inflater, binding.detailMetadataLayout); - return binding.getRoot(); + protected Description getDescription() { + if (channelInfo == null) { + return null; + } + return new Description(channelInfo.getDescription(), Description.PLAIN_TEXT); } + @Nullable @Override - public void onDestroy() { - super.onDestroy(); - disposables.clear(); - } - - private void loadDescription() { - final String description = channelInfo.getDescription(); - - if (description == null || description.isEmpty()) { - binding.descriptionTitle.setVisibility(View.GONE); - binding.descriptionView.setVisibility(View.GONE); - } else { - TextLinkifier.createLinksFromPlainText( - binding.descriptionView, description, null, disposables); + protected StreamingService getService() { + if (channelInfo == null) { + return null; } + return channelInfo.getService(); } - private void setupMetadata(final LayoutInflater inflater, - final LinearLayout layout) { - final Context context = getContext(); - - if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) { - addMetadataItem(inflater, layout, R.string.metadata_subscribers, - Localization.localizeNumber(context, channelInfo.getSubscriberCount())); - } - - addTagsMetadataItem(inflater, layout); + @Override + protected int getServiceId() { + return channelInfo.getServiceId(); } - private void addMetadataItem(final LayoutInflater inflater, - final LinearLayout layout, - @StringRes final int type, - @Nullable final String content) { - if (isBlank(content)) { - return; - } - - final ItemMetadataBinding itemBinding = - ItemMetadataBinding.inflate(inflater, layout, false); - - itemBinding.metadataTypeView.setText(type); - itemBinding.metadataTypeView.setOnLongClickListener(v -> { - ShareUtils.copyToClipboard(requireContext(), content); - return true; - }); - - itemBinding.metadataContentView.setText(content); - - layout.addView(itemBinding.getRoot()); + @Nullable + @Override + protected String getStreamUrl() { + return null; } - private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) { - final List tags = channelInfo.getTags(); - - if (!tags.isEmpty()) { - final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false); - - tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> { - final Chip chip = (Chip) inflater.inflate(R.layout.chip, - itemBinding.metadataTagsChips, false); - chip.setText(tag); - chip.setOnClickListener(this::onTagClick); - chip.setOnLongClickListener(this::onTagLongClick); - itemBinding.metadataTagsChips.addView(chip); - }); - - layout.addView(itemBinding.getRoot()); + @Nullable + @Override + public List getTags() { + if (channelInfo == null) { + return null; } + return channelInfo.getTags(); } - private void onTagClick(final View chip) { - if (getParentFragment() != null) { - NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(), - channelInfo.getServiceId(), ((Chip) chip).getText().toString()); - } - } + protected void setupMetadata(final LayoutInflater inflater, + final LinearLayout layout) { + final Context context = getContext(); - private boolean onTagLongClick(final View chip) { - ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString()); - return true; + if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) { + addMetadataItem(inflater, layout, false, R.string.metadata_subscribers, + Localization.localizeNumber(context, channelInfo.getSubscriberCount())); + } } } From 88384dc35ec9c88d3fff36c1275d6ff9498fc770 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 5 Apr 2023 21:42:21 +0200 Subject: [PATCH 17/50] update extractor --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 10dd4bef081..b7430287a66 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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.Theta-Dev:NewPipeExtractor:e57d43f92d0c7132b569835a659da2d3b3017602' + implementation 'com.github.Theta-Dev:NewPipeExtractor:e278a2d6d428dec82a304d271803d35afbd7340c' implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' /** Checkstyle **/ From b7911a8fd8a40452bfc6d7afb446ec9ecaae4978 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 5 Apr 2023 21:53:45 +0200 Subject: [PATCH 18/50] remove fragment_channel_info --- .../main/res/layout/fragment_channel_info.xml | 51 ------------------- 1 file changed, 51 deletions(-) delete mode 100644 app/src/main/res/layout/fragment_channel_info.xml diff --git a/app/src/main/res/layout/fragment_channel_info.xml b/app/src/main/res/layout/fragment_channel_info.xml deleted file mode 100644 index c9648e01fa4..00000000000 --- a/app/src/main/res/layout/fragment_channel_info.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - From 25e303183007a0f7b9310eb882daf897f2bcced3 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 5 Apr 2023 21:56:01 +0200 Subject: [PATCH 19/50] cleanup: remove empty constructor from ChannelFragment --- .../newpipe/fragments/list/channel/ChannelFragment.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 8b6c0084e13..96f2522eb42 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -82,10 +82,6 @@ public static ChannelFragment getInstance(final int serviceId, final String url, return instance; } - public ChannelFragment() { - super(); - } - protected void setInitialData(final int sid, final String u, final String title) { this.serviceId = sid; this.url = u; From c03c344f4998ca76ffef1260bbaa8c4c8cf6afc5 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 5 Apr 2023 22:56:25 +0200 Subject: [PATCH 20/50] refactor: rename ChannelInfo to ChannelAbout fix: localize about tab name --- ...eInfoFragment.java => BaseDescriptionFragment.java} | 2 +- .../newpipe/fragments/detail/DescriptionFragment.java | 2 +- ...nnelInfoFragment.java => ChannelAboutFragment.java} | 10 +++++----- .../fragments/list/channel/ChannelFragment.java | 5 +++-- app/src/main/res/values/settings_keys.xml | 6 +++--- app/src/main/res/values/strings.xml | 2 +- 6 files changed, 14 insertions(+), 13 deletions(-) rename app/src/main/java/org/schabi/newpipe/fragments/detail/{BaseInfoFragment.java => BaseDescriptionFragment.java} (99%) rename app/src/main/java/org/schabi/newpipe/fragments/list/channel/{ChannelInfoFragment.java => ChannelAboutFragment.java} (85%) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java similarity index 99% rename from app/src/main/java/org/schabi/newpipe/fragments/detail/BaseInfoFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java index d8aea1a03be..fbbfdf23f90 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java @@ -33,7 +33,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable; -public abstract class BaseInfoFragment extends BaseFragment { +public abstract class BaseDescriptionFragment extends BaseFragment { final CompositeDisposable descriptionDisposables = new CompositeDisposable(); FragmentDescriptionBinding binding; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java index cf99365dc0b..ded4e907add 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java @@ -20,7 +20,7 @@ import icepick.State; -public class DescriptionFragment extends BaseInfoFragment { +public class DescriptionFragment extends BaseDescriptionFragment { @State StreamInfo streamInfo = null; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java similarity index 85% rename from app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java index 70b182a7552..ae04e8b00fd 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java @@ -12,24 +12,24 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.stream.Description; -import org.schabi.newpipe.fragments.detail.BaseInfoFragment; +import org.schabi.newpipe.fragments.detail.BaseDescriptionFragment; import org.schabi.newpipe.util.Localization; import java.util.List; import icepick.State; -public class ChannelInfoFragment extends BaseInfoFragment { +public class ChannelAboutFragment extends BaseDescriptionFragment { @State protected ChannelInfo channelInfo; - public static ChannelInfoFragment getInstance(final ChannelInfo channelInfo) { - final ChannelInfoFragment fragment = new ChannelInfoFragment(); + public static ChannelAboutFragment getInstance(final ChannelInfo channelInfo) { + final ChannelAboutFragment fragment = new ChannelAboutFragment(); fragment.channelInfo = channelInfo; return fragment; } - public ChannelInfoFragment() { + public ChannelAboutFragment() { super(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 96f2522eb42..95aa2c45a78 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -301,9 +301,10 @@ private void updateTabs() { final String description = currentInfo.getDescription(); if (description != null && !description.isEmpty() && ChannelTabHelper.showChannelTab( - context, preferences, R.string.show_channel_tabs_info)) { + context, preferences, R.string.show_channel_tabs_about)) { tabAdapter.addFragment( - ChannelInfoFragment.getInstance(currentInfo), "Info"); + ChannelAboutFragment.getInstance(currentInfo), + context.getString(R.string.channel_tab_about)); } } } diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 9746d78898c..d32fbce0cc2 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -280,14 +280,14 @@ show_channel_tabs_shorts show_channel_tabs_channels show_channel_tabs_albums - show_channel_tabs_info + show_channel_tabs_about @string/show_channel_tabs_playlists @string/show_channel_tabs_livestreams @string/show_channel_tabs_shorts @string/show_channel_tabs_channels @string/show_channel_tabs_albums - @string/show_channel_tabs_info + @string/show_channel_tabs_about @string/channel_tab_playlists @@ -295,7 +295,7 @@ @string/channel_tab_shorts @string/channel_tab_channels @string/channel_tab_albums - @string/channel_tab_info + @string/channel_tab_about show_search_suggestions show_local_search_suggestions diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 87577a81a9c..259689231af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -803,7 +803,7 @@ Playlists Channels Albums - Info + About Channel tabs What tabs are shown on the channel pages \ No newline at end of file From 193c3e5b3dd012ef9d1a8372fc093bebddf3213c Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 6 Apr 2023 11:44:01 +0200 Subject: [PATCH 21/50] fix: NPE in ChannelFragment::onSaveInstanceState --- .../newpipe/fragments/list/channel/ChannelFragment.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 95aa2c45a78..96de433f5b3 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -130,7 +130,9 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) { @Override public void onSaveInstanceState(final @NonNull Bundle outState) { super.onSaveInstanceState(outState); - outState.putInt("LastTab", binding.tabLayout.getSelectedTabPosition()); + if (binding != null) { + outState.putInt("LastTab", binding.tabLayout.getSelectedTabPosition()); + } } @Override From e3614cb93231a7ed9a8e430ef6566004a6958ed5 Mon Sep 17 00:00:00 2001 From: Stypox Date: Thu, 13 Apr 2023 00:00:23 +0200 Subject: [PATCH 22/50] Move channel header to collapsible app bar --- .../list/channel/ChannelFragment.java | 397 +++++++++++++---- .../list/channel/ChannelVideosFragment.java | 420 ++---------------- .../org/schabi/newpipe/settings/tabs/Tab.java | 2 +- .../schabi/newpipe/util/PicassoHelper.java | 6 +- app/src/main/res/layout/channel_header.xml | 131 ------ app/src/main/res/layout/fragment_channel.xml | 250 ++++++++--- app/src/main/res/values-land/dimens.xml | 1 - app/src/main/res/values/dimens.xml | 1 - 8 files changed, 538 insertions(+), 670 deletions(-) delete mode 100644 app/src/main/res/layout/channel_header.xml diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 96de433f5b3..9de14351825 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -1,10 +1,16 @@ 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.SharedPreferences; +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; @@ -14,43 +20,59 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.ColorUtils; import androidx.preference.PreferenceManager; +import com.google.android.material.snackbar.Snackbar; import com.google.android.material.tabs.TabLayout; +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.FragmentChannelBinding; import org.schabi.newpipe.error.ErrorInfo; +import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.fragments.BaseStateFragment; 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.subscription.SubscriptionManager; import org.schabi.newpipe.util.ChannelTabHelper; 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.StateSaver; +import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.external_communication.ShareUtils; import java.util.List; import java.util.Queue; +import java.util.concurrent.TimeUnit; import icepick.State; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Observable; 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 BaseStateFragment implements StateSaver.WriteRead { + + private static final int BUTTON_DEBOUNCE_INTERVAL = 100; + private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG"; + @State protected int serviceId = Constants.NO_SERVICE_ID; @State @@ -60,13 +82,11 @@ public class ChannelFragment extends BaseStateFragment private ChannelInfo currentInfo; private Disposable currentWorker; - private Disposable subscriptionMonitor; private final CompositeDisposable disposables = new CompositeDisposable(); + private Disposable subscribeButtonMonitor; private SubscriptionManager subscriptionManager; private int lastTab; - - private MenuItem menuRssButton; - private MenuItem menuNotifyButton; + private boolean channelContentNotSupported = false; /*////////////////////////////////////////////////////////////////////////// // Views @@ -75,6 +95,9 @@ public class ChannelFragment extends BaseStateFragment private FragmentChannelBinding binding; private TabAdapter tabAdapter; + private MenuItem menuRssButton; + private MenuItem menuNotifyButton; + public static ChannelFragment getInstance(final int serviceId, final String url, final String name) { final ChannelFragment instance = new ChannelFragment(); @@ -82,12 +105,13 @@ public static ChannelFragment getInstance(final int serviceId, final String url, 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.url = u; this.name = !TextUtils.isEmpty(title) ? title : ""; } + /*////////////////////////////////////////////////////////////////////////// // LifeCycle //////////////////////////////////////////////////////////////////////////*/ @@ -96,12 +120,6 @@ protected void setInitialData(final int sid, final String u, final String title) public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); - - if (savedInstanceState != null) { - lastTab = savedInstanceState.getInt("LastTab"); - } else { - lastTab = 0; - } } @Override @@ -125,14 +143,29 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) { tabAdapter = new TabAdapter(getChildFragmentManager()); binding.viewPager.setAdapter(tabAdapter); binding.tabLayout.setupWithViewPager(binding.viewPager); + + binding.channelTitleView.setText(name); } @Override - public void onSaveInstanceState(final @NonNull Bundle outState) { - super.onSaveInstanceState(outState); - if (binding != null) { - outState.putInt("LastTab", binding.tabLayout.getSelectedTabPosition()); - } + protected void initListeners() { + super.initListeners(); + + 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 @@ -141,14 +174,12 @@ public void onDestroy() { if (currentWorker != null) { currentWorker.dispose(); } - if (subscriptionMonitor != null) { - subscriptionMonitor.dispose(); - } disposables.clear(); binding = null; } - /*////////////////////////////////////////////////////////////////////////// + + /*////////////////////////////////////////////////////////////////////////// // Menu //////////////////////////////////////////////////////////////////////////*/ @@ -164,8 +195,6 @@ public void onCreateOptionsMenu(@NonNull final Menu menu, } menuRssButton = menu.findItem(R.id.menu_item_rss); menuNotifyButton = menu.findItem(R.id.menu_item_notify); - updateRssButton(); - monitorSubscription(); } @Override @@ -201,39 +230,170 @@ public boolean onOptionsItemSelected(final MenuItem item) { return true; } - private void updateRssButton() { - if (currentInfo != null && menuRssButton != null) { - menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl())); - } + + /*////////////////////////////////////////////////////////////////////////// + // Channel Subscription + //////////////////////////////////////////////////////////////////////////*/ + + private void monitorSubscription(final ChannelInfo info) { + final Consumer onError = (Throwable throwable) -> { + animate(binding.channelSubscribeButton, false, 100); + showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET, + "Get subscription status", currentInfo)); + }; + + final Observable> 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 mapOnSubscribe(final SubscriptionEntity subscription, + final ChannelInfo info) { + return (@NonNull Object o) -> { + subscriptionManager.insertSubscription(subscription, info); + return o; + }; } - private void monitorSubscription() { - if (currentInfo != null) { - final Observable> observable = subscriptionManager - .subscriptionTable() - .getSubscriptionFlowable(currentInfo.getServiceId(), currentInfo.getUrl()) - .toObservable(); + private Function mapOnUnsubscribe(final SubscriptionEntity subscription) { + return (@NonNull Object o) -> { + subscriptionManager.deleteSubscription(subscription); + return o; + }; + } - if (subscriptionMonitor != null) { - subscriptionMonitor.dispose(); - } - subscriptionMonitor = observable - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getSubscribeUpdateMonitor()); + 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 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 action) { + final Consumer onNext = (@NonNull Object o) -> { + if (DEBUG) { + Log.d(TAG, "Changed subscription status to this channel!"); + } + }; + + final Consumer 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> getSubscribeUpdateMonitor() { + private Consumer> getSubscribeUpdateMonitor(final ChannelInfo info) { return (List 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) { if (menuNotifyButton == null) { return; @@ -263,52 +423,48 @@ private void setNotify(final boolean isEnabled) { ); } + /** + * 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 //////////////////////////////////////////////////////////////////////////*/ - private boolean isContentUnsupported() { - for (final Throwable throwable : currentInfo.getErrors()) { - if (throwable instanceof ContentNotSupportedException) { - return true; - } - } - return false; - } - private void updateTabs() { tabAdapter.clearAllItems(); - if (currentInfo != null) { - if (isContentUnsupported()) { - showEmptyState(); - binding.errorContentNotSupported.setVisibility(View.VISIBLE); - } else { - tabAdapter.addFragment( - ChannelVideosFragment.getInstance(currentInfo), "Videos"); - - final Context context = getContext(); - final SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(context); - - for (final ListLinkHandler linkHandler : currentInfo.getTabs()) { - final String tab = linkHandler.getContentFilters().get(0); - if (ChannelTabHelper.showChannelTab(context, preferences, tab)) { - tabAdapter.addFragment( - ChannelTabFragment.getInstance(serviceId, linkHandler, name), - context.getString(ChannelTabHelper.getTranslationKey(tab))); - } - } + if (currentInfo != null && !channelContentNotSupported) { + tabAdapter.addFragment(new ChannelVideosFragment(currentInfo), "Videos"); + + final Context context = requireContext(); + final SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(context); - final String description = currentInfo.getDescription(); - if (description != null && !description.isEmpty() - && ChannelTabHelper.showChannelTab( - context, preferences, R.string.show_channel_tabs_about)) { + for (final ListLinkHandler linkHandler : currentInfo.getTabs()) { + final String tab = linkHandler.getContentFilters().get(0); + if (ChannelTabHelper.showChannelTab(context, preferences, tab)) { tabAdapter.addFragment( - ChannelAboutFragment.getInstance(currentInfo), - context.getString(R.string.channel_tab_about)); + ChannelTabFragment.getInstance(serviceId, linkHandler, name), + context.getString(ChannelTabHelper.getTranslationKey(tab))); } } + + final String description = currentInfo.getDescription(); + if (description != null && !description.isEmpty() + && ChannelTabHelper.showChannelTab( + context, preferences, R.string.show_channel_tabs_about)) { + tabAdapter.addFragment( + ChannelAboutFragment.getInstance(currentInfo), + context.getString(R.string.channel_tab_about)); + } } tabAdapter.notifyDataSetUpdate(); @@ -324,6 +480,7 @@ private void updateTabs() { } } + /*////////////////////////////////////////////////////////////////////////// // State Saving //////////////////////////////////////////////////////////////////////////*/ @@ -336,11 +493,7 @@ public String generateSuffix() { @Override public void writeTo(final Queue objectsToSave) { objectsToSave.add(currentInfo); - if (binding != null) { - objectsToSave.add(binding.tabLayout.getSelectedTabPosition()); - } else { - objectsToSave.add(0); - } + objectsToSave.add(binding == null ? 0 : binding.tabLayout.getSelectedTabPosition()); } @Override @@ -349,6 +502,25 @@ public void readFrom(@NonNull final Queue savedObjects) { 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 protected void doInitialLoadLogic() { if (currentInfo == null) { @@ -382,14 +554,77 @@ private void runWorker(final boolean forceLoad) { url == null ? "no url" : url, serviceId))); } + @Override + public void showLoading() { + super.showLoading(); + PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG); + animate(binding.channelSubscribeButton, false, 100); + } + @Override public void handleResult(@NonNull final ChannelInfo result) { super.handleResult(result); currentInfo = result; 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(); - updateRssButton(); - monitorSubscription(); + updateSubscription(result); + 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); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java index a38b913d6e4..a2d50836b6f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java @@ -1,107 +1,59 @@ 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.View; import android.view.ViewGroup; -import android.widget.Button; import androidx.annotation.NonNull; 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.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 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 - implements View.OnClickListener { - private static final int BUTTON_DEBOUNCE_INTERVAL = 100; - private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG"; +public class ChannelVideosFragment extends BaseListInfoFragment { private final CompositeDisposable disposables = new CompositeDisposable(); - private Disposable subscribeButtonMonitor; - private boolean channelContentNotSupported = false; + private FragmentChannelVideosBinding channelBinding; + private PlaylistControlBinding playlistControlBinding; + /*////////////////////////////////////////////////////////////////////////// - // Views + // Constructors and lifecycle //////////////////////////////////////////////////////////////////////////*/ - private SubscriptionManager subscriptionManager; - - private FragmentChannelVideosBinding channelBinding; - private ChannelHeaderBinding headerBinding; - 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; + // required by the Android framework to restore fragments after saving + public ChannelVideosFragment() { + super(UserAction.REQUESTED_CHANNEL); } - 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(final int serviceId, final String url, final String name) { + this(); + setInitialData(serviceId, url, name); } - public ChannelVideosFragment() { - super(UserAction.REQUESTED_CHANNEL); + public ChannelVideosFragment(@NonNull final ChannelInfo info) { + this(info.getServiceId(), info.getUrl(), info.getName()); + this.currentInfo = info; + this.currentNextPage = info.getNextPage(); } @Override @@ -112,22 +64,12 @@ public void onResume() { } } - /*////////////////////////////////////////////////////////////////////////// - // LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(false); } - @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, @@ -136,235 +78,24 @@ public View onCreateView(@NonNull final LayoutInflater inflater, return channelBinding.getRoot(); } - @Override - public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) { - super.onViewCreated(rootView, savedInstanceState); - showContentNotSupportedIfNeeded(); - } - @Override public void onDestroy() { super.onDestroy(); disposables.clear(); - if (subscribeButtonMonitor != null) { - subscribeButtonMonitor.dispose(); - } channelBinding = null; - headerBinding = null; playlistControlBinding = null; } - /*////////////////////////////////////////////////////////////////////////// - // Init - //////////////////////////////////////////////////////////////////////////*/ - @Override protected Supplier getListHeaderSupplier() { - headerBinding = ChannelHeaderBinding + playlistControlBinding = PlaylistControlBinding .inflate(activity.getLayoutInflater(), itemsList, false); - playlistControlBinding = headerBinding.playlistControl; - - return headerBinding::getRoot; + return playlistControlBinding::getRoot; } - @Override - protected void initListeners() { - super.initListeners(); - - headerBinding.subChannelTitleView.setOnClickListener(this); - headerBinding.subChannelAvatarView.setOnClickListener(this); - } /*////////////////////////////////////////////////////////////////////////// - // Channel Subscription - //////////////////////////////////////////////////////////////////////////*/ - - private void monitorSubscription(final ChannelInfo info) { - final Consumer onError = (Throwable throwable) -> { - animate(headerBinding.channelSubscribeButton, false, 100); - showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET, - "Get subscription status", currentInfo)); - }; - - final Observable> 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 mapOnSubscribe(final SubscriptionEntity subscription, - final ChannelInfo info) { - return (@NonNull Object o) -> { - subscriptionManager.insertSubscription(subscription, info); - return o; - }; - } - - private Function 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 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 action) { - final Consumer onNext = (@NonNull Object o) -> { - if (DEBUG) { - Log.d(TAG, "Changed subscription status to this channel!"); - } - }; - - final Consumer 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> getSubscribeUpdateMonitor(final ChannelInfo info) { - return (List 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 + // Loading //////////////////////////////////////////////////////////////////////////*/ @Override @@ -377,76 +108,15 @@ protected Single 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); - } - // PlaylistControls should be visible only if there is some item in // infoListAdapter other than header if (infoListAdapter.getItemCount() != 1) { @@ -455,31 +125,14 @@ public void handleResult(@NonNull final ChannelInfo result) { 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.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); @@ -492,19 +145,6 @@ public void handleResult(@NonNull final ChannelInfo result) { }); } - 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 streamItems = infoListAdapter.getItemsList().stream() .filter(StreamInfoItem.class::isInstance) @@ -514,14 +154,4 @@ private PlayQueue getPlayQueue() { 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); - } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java index a06bf32d4cc..b5375075f35 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java @@ -433,7 +433,7 @@ public int getTabIconRes(final Context context) { @Override public ChannelVideosFragment getFragment(final Context context) { - return ChannelVideosFragment.getInstance(channelServiceId, channelUrl, channelName); + return new ChannelVideosFragment(channelServiceId, channelUrl, channelName); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java b/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java index ece0c7e8753..750b8e799a2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java @@ -109,7 +109,11 @@ public static RequestCreator loadDetailsThumbnail(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) { diff --git a/app/src/main/res/layout/channel_header.xml b/app/src/main/res/layout/channel_header.xml deleted file mode 100644 index 9d1304635d1..00000000000 --- a/app/src/main/res/layout/channel_header.xml +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml index db77391bccc..29d9143c53b 100644 --- a/app/src/main/res/layout/fragment_channel.xml +++ b/app/src/main/res/layout/fragment_channel.xml @@ -1,75 +1,207 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + - + + - - + + - - - - - - \ No newline at end of file + android:layout_centerInParent="true" + android:orientation="vertical" + android:paddingTop="90dp" + android:visibility="gone" + tools:visibility="visible"> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml index 77e18695d3e..46244b3c9c6 100644 --- a/app/src/main/res/values-land/dimens.xml +++ b/app/src/main/res/values-land/dimens.xml @@ -32,7 +32,6 @@ 16sp 14sp 14sp - 14sp 14sp 42dp diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index e47b72c9aa6..0e5fd126f5b 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -75,7 +75,6 @@ 14sp 13sp 13sp - 12sp 12sp 32dp From b5893f3fa3a0926edc0286e102464e1a0a5a08b0 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 13 Apr 2023 21:55:37 +0200 Subject: [PATCH 23/50] fix: notification menu option disappears when switching tabs --- .../fragments/list/channel/ChannelFragment.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 9de14351825..2947cdb16c0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -97,6 +97,7 @@ public class ChannelFragment extends BaseStateFragment private MenuItem menuRssButton; private MenuItem menuNotifyButton; + private SubscriptionEntity channelSubscription; public static ChannelFragment getInstance(final int serviceId, final String url, final String name) { @@ -193,8 +194,14 @@ public void onCreateOptionsMenu(@NonNull final Menu menu, Log.d(TAG, "onCreateOptionsMenu() called with: " + "menu = [" + menu + "], inflater = [" + inflater + "]"); } + } + + @Override + public void onPrepareOptionsMenu(final @NonNull Menu menu) { + super.onPrepareOptionsMenu(menu); menuRssButton = menu.findItem(R.id.menu_item_rss); menuNotifyButton = menu.findItem(R.id.menu_item_notify); + updateNotifyButton(channelSubscription); } @Override @@ -346,15 +353,17 @@ private Consumer> getSubscribeUpdateMonitor(final Chann info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount()); + channelSubscription = null; 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)); + channelSubscription = subscriptionEntities.get(0); + updateNotifyButton(channelSubscription); + subscribeButtonMonitor = + monitorSubscribeButton(mapOnUnsubscribe(channelSubscription)); } }; } From dfbd39e8985ad4b043bc382746bf2b1126711d3d Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 13 Apr 2023 22:39:55 +0200 Subject: [PATCH 24/50] fix: limit channel header height --- app/src/main/res/layout/fragment_channel.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml index 29d9143c53b..15995f8f368 100644 --- a/app/src/main/res/layout/fragment_channel.xml +++ b/app/src/main/res/layout/fragment_channel.xml @@ -29,6 +29,7 @@ android:id="@+id/channel_banner_image" android:layout_width="match_parent" android:layout_height="wrap_content" + android:maxHeight="70dp" android:adjustViewBounds="true" android:scaleType="centerCrop" tools:src="@drawable/placeholder_channel_banner" From c076a0f77127fe6a875461dc879605405ada5ede Mon Sep 17 00:00:00 2001 From: Stypox Date: Fri, 14 Apr 2023 10:19:58 +0200 Subject: [PATCH 25/50] Channels are now an Info The previous "main" tab is now just a normal tab returned in getTabs(). Various part of the code that used to handle channels as ListInfo now either take the first (playable, i.e. with streams) tab (e.g. the ChannelTabPlayQueue), or take all of them combined (e.g. the feed). --- app/build.gradle | 2 +- .../org/schabi/newpipe/RouterActivity.java | 15 +- .../fragments/list/BaseListInfoFragment.java | 5 +- .../list/channel/ChannelFragment.java | 9 +- .../list/channel/ChannelVideosFragment.java | 157 ------------------ .../feed/notifications/NotificationHelper.kt | 8 +- .../local/feed/service/FeedLoadManager.kt | 130 ++++++++++----- .../local/feed/service/FeedLoadService.kt | 14 +- .../local/feed/service/FeedUpdateInfo.kt | 16 +- .../local/subscription/SubscriptionManager.kt | 43 +++-- .../services/SubscriptionsImportService.java | 31 +++- .../playqueue/AbstractInfoPlayQueue.java | 22 ++- .../player/playqueue/ChannelPlayQueue.java | 47 ------ .../player/playqueue/ChannelTabPlayQueue.java | 53 ++++++ .../org/schabi/newpipe/settings/tabs/Tab.java | 6 +- .../schabi/newpipe/util/ChannelTabHelper.java | 54 +++++- .../schabi/newpipe/util/ExtractorHelper.java | 28 ---- app/src/main/res/values/settings_keys.xml | 18 +- app/src/main/res/values/strings.xml | 5 +- 19 files changed, 301 insertions(+), 362 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelTabPlayQueue.java diff --git a/app/build.gradle b/app/build.gradle index b7430287a66..1f924b12f4f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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.Theta-Dev:NewPipeExtractor:e278a2d6d428dec82a304d271803d35afbd7340c' + implementation 'com.github.Theta-Dev:NewPipeExtractor:c3651bef5c622abf0cdfc34c9985ba8c33d1491e' implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' /** Checkstyle **/ diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 70c377de973..c59dc753235 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -65,6 +65,7 @@ import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException; import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.ktx.ExceptionUtils; @@ -72,10 +73,11 @@ import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHolder; -import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; +import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; +import org.schabi.newpipe.util.ChannelTabHelper; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ExtractorHelper; @@ -1022,7 +1024,16 @@ public Consumer getResultHandler(final Choice choice) { } playQueue = new SinglePlayQueue((StreamInfo) info); } else if (info instanceof ChannelInfo) { - playQueue = new ChannelPlayQueue((ChannelInfo) info); + final Optional playableTab = ((ChannelInfo) info).getTabs() + .stream() + .filter(ChannelTabHelper::isStreamsTab) + .findFirst(); + + if (playableTab.isPresent()) { + playQueue = new ChannelTabPlayQueue(info.getServiceId(), playableTab.get()); + } else { + return; // there is no playable tab + } } else if (info instanceof PlaylistInfo) { playQueue = new PlaylistPlayQueue((PlaylistInfo) info); } else { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java index c73ae8be062..d30dadfd1eb 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java @@ -16,7 +16,6 @@ import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListInfo; import org.schabi.newpipe.extractor.Page; -import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.views.NewPipeRecyclerView; @@ -236,9 +235,7 @@ public void handleResult(@NonNull final L result) { infoListAdapter.clearStreamItemList(); // showEmptyState should be called only if there is no item as // well as no header in infoListAdapter - if (!(result instanceof ChannelInfo && infoListAdapter.getItemCount() == 1)) { - showEmptyState(); - } + showEmptyState(); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 2947cdb16c0..3a8d46a4df2 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -277,10 +277,9 @@ private void monitorSubscription(final ChannelInfo info) { }, onError)); } - private Function mapOnSubscribe(final SubscriptionEntity subscription, - final ChannelInfo info) { + private Function mapOnSubscribe(final SubscriptionEntity subscription) { return (@NonNull Object o) -> { - subscriptionManager.insertSubscription(subscription, info); + subscriptionManager.insertSubscription(subscription); return o; }; } @@ -355,7 +354,7 @@ private Consumer> getSubscribeUpdateMonitor(final Chann info.getSubscriberCount()); channelSubscription = null; updateNotifyButton(null); - subscribeButtonMonitor = monitorSubscribeButton(mapOnSubscribe(channel, info)); + subscribeButtonMonitor = monitorSubscribeButton(mapOnSubscribe(channel)); } else { if (DEBUG) { Log.d(TAG, "Found subscription to this channel!"); @@ -451,8 +450,6 @@ private void updateTabs() { tabAdapter.clearAllItems(); if (currentInfo != null && !channelContentNotSupported) { - tabAdapter.addFragment(new ChannelVideosFragment(currentInfo), "Videos"); - final Context context = requireContext(); final SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences(context); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java deleted file mode 100644 index a2d50836b6f..00000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelVideosFragment.java +++ /dev/null @@ -1,157 +0,0 @@ -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.databinding.FragmentChannelVideosBinding; -import org.schabi.newpipe.databinding.PlaylistControlBinding; -import org.schabi.newpipe.error.UserAction; -import org.schabi.newpipe.extractor.ListExtractor; -import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.fragments.list.BaseListInfoFragment; -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.NavigationHelper; - -import java.util.List; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.disposables.CompositeDisposable; - -public class ChannelVideosFragment extends BaseListInfoFragment { - - private final CompositeDisposable disposables = new CompositeDisposable(); - - private FragmentChannelVideosBinding channelBinding; - private PlaylistControlBinding playlistControlBinding; - - - /*////////////////////////////////////////////////////////////////////////// - // Constructors and lifecycle - //////////////////////////////////////////////////////////////////////////*/ - - // required by the Android framework to restore fragments after saving - public ChannelVideosFragment() { - 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 - public void onResume() { - super.onResume(); - if (activity != null && useAsFrontPage) { - setTitle(currentInfo != null ? currentInfo.getName() : name); - } - } - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(false); - } - - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable final Bundle savedInstanceState) { - channelBinding = FragmentChannelVideosBinding.inflate(inflater, container, false); - return channelBinding.getRoot(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - disposables.clear(); - channelBinding = null; - playlistControlBinding = null; - } - - @Override - protected Supplier getListHeaderSupplier() { - playlistControlBinding = PlaylistControlBinding - .inflate(activity.getLayoutInflater(), itemsList, false); - return playlistControlBinding::getRoot; - } - - - /*////////////////////////////////////////////////////////////////////////// - // Loading - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected Single> loadMoreItemsLogic() { - return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage); - } - - @Override - protected Single loadResult(final boolean forceLoad) { - return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad); - } - - - /*////////////////////////////////////////////////////////////////////////// - // Contract - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void handleResult(@NonNull final ChannelInfo result) { - super.handleResult(result); - - // 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); - } - - disposables.clear(); - - 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 PlayQueue getPlayQueue() { - final List 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); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationHelper.kt b/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationHelper.kt index 5aca3ad26a3..782f5ee47fa 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationHelper.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationHelper.kt @@ -58,7 +58,7 @@ class NotificationHelper(val context: Context) { .setAutoCancel(true) .setCategory(NotificationCompat.CATEGORY_SOCIAL) .setGroupSummary(true) - .setGroup(data.listInfo.url) + .setGroup(data.originalInfo.url) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) // Build a summary notification for Android versions < 7.0 @@ -73,7 +73,7 @@ class NotificationHelper(val context: Context) { context, data.pseudoId, NavigationHelper - .getChannelIntent(context, data.listInfo.serviceId, data.listInfo.url) + .getChannelIntent(context, data.originalInfo.serviceId, data.originalInfo.url) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0, false @@ -88,7 +88,7 @@ class NotificationHelper(val context: Context) { // Show individual stream notifications, set channel icon only if there is actually // one - showStreamNotifications(newStreams, data.listInfo.serviceId, bitmap) + showStreamNotifications(newStreams, data.originalInfo.serviceId, bitmap) // Show summary notification manager.notify(data.pseudoId, summaryBuilder.build()) @@ -97,7 +97,7 @@ class NotificationHelper(val context: Context) { override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) { // Show individual stream notifications - showStreamNotifications(newStreams, data.listInfo.serviceId, null) + showStreamNotifications(newStreams, data.originalInfo.serviceId, null) // Show summary notification manager.notify(data.pseudoId, summaryBuilder.build()) iconLoadingTargets.remove(this) // allow it to be garbage-collected diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt index fec50a579a7..be2c2490e63 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt @@ -13,11 +13,16 @@ import io.reactivex.rxjava3.schedulers.Schedulers import org.schabi.newpipe.R import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.subscription.NotificationMode -import org.schabi.newpipe.extractor.ListInfo +import org.schabi.newpipe.extractor.Info +import org.schabi.newpipe.extractor.NewPipe +import org.schabi.newpipe.extractor.feed.FeedInfo import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.local.feed.FeedDatabaseManager import org.schabi.newpipe.local.subscription.SubscriptionManager -import org.schabi.newpipe.util.ExtractorHelper +import org.schabi.newpipe.util.ChannelTabHelper +import org.schabi.newpipe.util.ExtractorHelper.getChannelInfo +import org.schabi.newpipe.util.ExtractorHelper.getChannelTab +import org.schabi.newpipe.util.ExtractorHelper.getMoreChannelTabItems import java.time.OffsetDateTime import java.time.ZoneOffset import java.util.concurrent.atomic.AtomicBoolean @@ -102,49 +107,88 @@ class FeedLoadManager(private val context: Context) { .filter { !cancelSignal.get() } .map { subscriptionEntity -> var error: Throwable? = null + val storeOriginalErrorAndRethrow = { e: Throwable -> + // keep original to prevent blockingGet() from wrapping it into RuntimeException + error = e + throw e + } + try { // check for and load new streams // either by using the dedicated feed method or by getting the channel info - val listInfo = if (useFeedExtractor) { - ExtractorHelper - .getFeedInfoFallbackToChannelInfo( - subscriptionEntity.serviceId, - subscriptionEntity.url - ) - .onErrorReturn { - error = it // store error, otherwise wrapped into RuntimeException - throw it + var originalInfo: Info? = null + var streams: List? = null + val errors = ArrayList() + + if (useFeedExtractor) { + NewPipe.getService(subscriptionEntity.serviceId) + .getFeedExtractor(subscriptionEntity.url) + ?.also { feedExtractor -> + // the user wants to use a feed extractor and there is one, use it + val feedInfo = FeedInfo.getInfo(feedExtractor) + errors.addAll(feedInfo.errors) + originalInfo = feedInfo + streams = feedInfo.relatedItems } + } + + if (originalInfo == null) { + // use the normal channel tabs extractor if either the user wants it, or + // the current service does not have a dedicated feed extractor + + val channelInfo = getChannelInfo( + subscriptionEntity.serviceId, + subscriptionEntity.url, true + ) + .onErrorReturn(storeOriginalErrorAndRethrow) .blockingGet() - } else { - ExtractorHelper - .getChannelInfo( - subscriptionEntity.serviceId, - subscriptionEntity.url, - true - ) - .onErrorReturn { - error = it // store error, otherwise wrapped into RuntimeException - throw it + errors.addAll(channelInfo.errors) + originalInfo = channelInfo + + streams = channelInfo.tabs + .filter(ChannelTabHelper::isStreamsTab) + .map { + Pair( + getChannelTab(subscriptionEntity.serviceId, it, true) + .onErrorReturn(storeOriginalErrorAndRethrow) + .blockingGet(), + it + ) } - .blockingGet() - } as ListInfo + .flatMap { (channelTabInfo, linkHandler) -> + errors.addAll(channelTabInfo.errors) + if (channelTabInfo.relatedItems.isEmpty()) { + val infoItemsPage = getMoreChannelTabItems( + subscriptionEntity.serviceId, + linkHandler, channelTabInfo.nextPage + ) + .blockingGet() + + errors.addAll(infoItemsPage.errors) + return@flatMap infoItemsPage.items + } else { + return@flatMap channelTabInfo.relatedItems + } + } + .filterIsInstance() + } return@map Notification.createOnNext( FeedUpdateInfo( subscriptionEntity, - listInfo + originalInfo!!, + streams!!, + errors, ) ) } catch (e: Throwable) { - if (error == null) { - // do this to prevent blockingGet() from wrapping into RuntimeException - error = e - } - val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}" - val wrapper = - FeedLoadService.RequestException(subscriptionEntity.uid, request, error!!) + val wrapper = FeedLoadService.RequestException( + subscriptionEntity.uid, + request, + // do this to prevent blockingGet() from wrapping into RuntimeException + error ?: e + ) return@map Notification.createOnError(wrapper) } } @@ -203,24 +247,24 @@ class FeedLoadManager(private val context: Context) { for (notification in list) { when { notification.isOnNext -> { - val subscriptionId = notification.value!!.uid - val info = notification.value!!.listInfo + val info = notification.value!! - notification.value!!.newStreams = filterNewStreams( - notification.value!!.listInfo.relatedItems - ) + notification.value!!.newStreams = filterNewStreams(info.streams) - feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems) - subscriptionManager.updateFromInfo(subscriptionId, info) + feedDatabaseManager.upsertAll(info.uid, info.streams) + subscriptionManager.updateFromInfo(info.uid, info.originalInfo) if (info.errors.isNotEmpty()) { feedResultsHolder.addErrors( - FeedLoadService.RequestException.wrapList( - subscriptionId, - info - ) + info.errors.map { + FeedLoadService.RequestException( + info.uid, + "${info.originalInfo.serviceId}:${info.originalInfo.url}", + it + ) + } ) - feedDatabaseManager.markAsOutdated(subscriptionId) + feedDatabaseManager.markAsOutdated(info.uid) } } notification.isOnError -> { diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt index bde301b9224..f960040de6b 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt @@ -39,8 +39,6 @@ import org.schabi.newpipe.App import org.schabi.newpipe.MainActivity.DEBUG import org.schabi.newpipe.R import org.schabi.newpipe.database.feed.model.FeedGroupEntity -import org.schabi.newpipe.extractor.ListInfo -import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent import java.util.concurrent.TimeUnit @@ -126,17 +124,7 @@ class FeedLoadService : Service() { // Loading & Handling // ///////////////////////////////////////////////////////////////////////// - class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) { - companion object { - fun wrapList(subscriptionId: Long, info: ListInfo): List { - val toReturn = ArrayList(info.errors.size) - info.errors.mapTo(toReturn) { - RequestException(subscriptionId, info.serviceId.toString() + ":" + info.url, it) - } - return toReturn - } - } - } + class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) // ///////////////////////////////////////////////////////////////////////// // Notification diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedUpdateInfo.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedUpdateInfo.kt index 5f72a6b842a..12fbe8d4120 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedUpdateInfo.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedUpdateInfo.kt @@ -2,7 +2,7 @@ package org.schabi.newpipe.local.feed.service import org.schabi.newpipe.database.subscription.NotificationMode import org.schabi.newpipe.database.subscription.SubscriptionEntity -import org.schabi.newpipe.extractor.ListInfo +import org.schabi.newpipe.extractor.Info import org.schabi.newpipe.extractor.stream.StreamInfoItem data class FeedUpdateInfo( @@ -11,24 +11,30 @@ data class FeedUpdateInfo( val notificationMode: Int, val name: String, val avatarUrl: String, - val listInfo: ListInfo, + val originalInfo: Info, + val streams: List, + val errors: List, ) { constructor( subscription: SubscriptionEntity, - listInfo: ListInfo, + originalInfo: Info, + streams: List, + errors: List, ) : this( uid = subscription.uid, notificationMode = subscription.notificationMode, name = subscription.name, avatarUrl = subscription.avatarUrl, - listInfo = listInfo, + originalInfo = originalInfo, + streams = streams, + errors = errors, ) /** * Integer id, can be used as notification id, etc. */ val pseudoId: Int - get() = listInfo.url.hashCode() + get() = originalInfo.url.hashCode() lateinit var newStreams: List } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt index b17f498015e..9a8b53e90e9 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt @@ -1,6 +1,7 @@ package org.schabi.newpipe.local.subscription import android.content.Context +import android.util.Pair import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Flowable @@ -11,8 +12,9 @@ import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.subscription.NotificationMode import org.schabi.newpipe.database.subscription.SubscriptionDAO import org.schabi.newpipe.database.subscription.SubscriptionEntity -import org.schabi.newpipe.extractor.ListInfo +import org.schabi.newpipe.extractor.Info import org.schabi.newpipe.extractor.channel.ChannelInfo +import org.schabi.newpipe.extractor.channel.ChannelTabInfo import org.schabi.newpipe.extractor.feed.FeedInfo import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.local.feed.FeedDatabaseManager @@ -46,28 +48,33 @@ class SubscriptionManager(context: Context) { } } - fun upsertAll(infoList: List): List { + fun upsertAll(infoList: List>>): List { val listEntities = subscriptionTable.upsertAll( - infoList.map { SubscriptionEntity.from(it) } + infoList.map { SubscriptionEntity.from(it.first) } ) database.runInTransaction { infoList.forEachIndexed { index, info -> - feedDatabaseManager.upsertAll(listEntities[index].uid, info.relatedItems) + info.second.forEach { + feedDatabaseManager.upsertAll( + listEntities[index].uid, + it.relatedItems.filterIsInstance() + ) + } } } return listEntities } - fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url) - .flatMapCompletable { - Completable.fromRunnable { - it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount) - subscriptionTable.update(it) - feedDatabaseManager.upsertAll(it.uid, info.relatedItems) + fun updateChannelInfo(info: ChannelInfo): Completable = + subscriptionTable.getSubscription(info.serviceId, info.url) + .flatMapCompletable { + Completable.fromRunnable { + it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount) + subscriptionTable.update(it) + } } - } fun updateNotificationMode(serviceId: Int, url: String, @NotificationMode mode: Int): Completable { return subscriptionTable().getSubscription(serviceId, url) @@ -84,7 +91,7 @@ class SubscriptionManager(context: Context) { } } - fun updateFromInfo(subscriptionId: Long, info: ListInfo) { + fun updateFromInfo(subscriptionId: Long, info: Info) { val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId) if (info is FeedInfo) { @@ -107,11 +114,8 @@ class SubscriptionManager(context: Context) { .observeOn(AndroidSchedulers.mainThread()) } - fun insertSubscription(subscriptionEntity: SubscriptionEntity, info: ChannelInfo) { - database.runInTransaction { - val subscriptionId = subscriptionTable.insert(subscriptionEntity) - feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems) - } + fun insertSubscription(subscriptionEntity: SubscriptionEntity) { + subscriptionTable.insert(subscriptionEntity) } fun deleteSubscription(subscriptionEntity: SubscriptionEntity) { @@ -125,7 +129,10 @@ class SubscriptionManager(context: Context) { */ private fun rememberAllStreams(subscription: SubscriptionEntity): Completable { return ExtractorHelper.getChannelInfo(subscription.serviceId, subscription.url, false) - .map { channel -> channel.relatedItems.map { stream -> StreamEntity(stream) } } + .flatMap { info -> + ExtractorHelper.getChannelTab(subscription.serviceId, info.tabs.first(), false) + } + .map { channel -> channel.relatedItems.filterIsInstance().map { stream -> StreamEntity(stream) } } .flatMapCompletable { entities -> Completable.fromAction { database.streamDAO().upsertAll(entities) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java index af598b10601..66164807dc5 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java @@ -26,6 +26,7 @@ import android.net.Uri; import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -38,6 +39,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelInfo; +import org.schabi.newpipe.extractor.channel.ChannelTabInfo; import org.schabi.newpipe.extractor.subscription.SubscriptionItem; import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.streams.io.SharpInputStream; @@ -48,6 +50,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; @@ -199,12 +202,19 @@ private void startImport() { .parallel(PARALLEL_EXTRACTIONS) .runOn(Schedulers.io()) - .map((Function>) subscriptionItem -> { + .map((Function>>>) subscriptionItem -> { try { - return Notification.createOnNext(ExtractorHelper + final ChannelInfo channelInfo = ExtractorHelper .getChannelInfo(subscriptionItem.getServiceId(), subscriptionItem.getUrl(), true) - .blockingGet()); + .blockingGet(); + return Notification.createOnNext(new Pair<>(channelInfo, + Collections.singletonList( + ExtractorHelper.getChannelTab( + subscriptionItem.getServiceId(), + channelInfo.getTabs().get(0), true).blockingGet() + ))); } catch (final Throwable e) { return Notification.createOnError(e); } @@ -223,7 +233,7 @@ private void startImport() { } private Subscriber> getSubscriber() { - return new Subscriber>() { + return new Subscriber<>() { @Override public void onSubscribe(final Subscription s) { subscription = s; @@ -254,10 +264,11 @@ public void onComplete() { }; } - private Consumer> getNotificationsConsumer() { + private Consumer>>> getNotificationsConsumer() { return notification -> { if (notification.isOnNext()) { - final String name = notification.getValue().getName(); + final String name = notification.getValue().first.getName(); eventListener.onItemCompleted(!TextUtils.isEmpty(name) ? name : ""); } else if (notification.isOnError()) { final Throwable error = notification.getError(); @@ -275,10 +286,12 @@ private Consumer> getNotificationsConsumer() { }; } - private Function>, List> upsertBatch() { + private Function>>>, + List> upsertBatch() { return notificationList -> { - final List infoList = new ArrayList<>(notificationList.size()); - for (final Notification n : notificationList) { + final List>> infoList = + new ArrayList<>(notificationList.size()); + for (final Notification>> n : notificationList) { if (n.isOnNext()) { infoList.add(n.getValue()); } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java index e51ee4720d4..a0fc88eae45 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; +import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListInfo; import org.schabi.newpipe.extractor.Page; @@ -15,7 +16,7 @@ import io.reactivex.rxjava3.core.SingleObserver; import io.reactivex.rxjava3.disposables.Disposable; -abstract class AbstractInfoPlayQueue> +abstract class AbstractInfoPlayQueue> extends PlayQueue { boolean isInitial; private boolean isComplete; @@ -27,7 +28,10 @@ abstract class AbstractInfoPlayQueue> private transient Disposable fetchReactor; protected AbstractInfoPlayQueue(final T info) { - this(info.getServiceId(), info.getUrl(), info.getNextPage(), info.getRelatedItems(), 0); + this(info.getServiceId(), info.getUrl(), info.getNextPage(), + info.getRelatedItems().stream().filter(StreamInfoItem.class::isInstance) + .map(StreamInfoItem.class::cast).collect( + Collectors.toList()), 0); } protected AbstractInfoPlayQueue(final int serviceId, @@ -72,7 +76,10 @@ public void onSuccess(@NonNull final T result) { } nextPage = result.getNextPage(); - append(extractListItems(result.getRelatedItems())); + append(extractListItems(result.getRelatedItems().stream() + .filter(StreamInfoItem.class::isInstance) + .map(StreamInfoItem.class::cast).collect( + Collectors.toList()))); fetchReactor.dispose(); fetchReactor = null; @@ -87,7 +94,7 @@ public void onError(@NonNull final Throwable e) { }; } - SingleObserver> getNextPageObserver() { + SingleObserver> getNextPageObserver() { return new SingleObserver<>() { @Override public void onSubscribe(@NonNull final Disposable d) { @@ -101,13 +108,16 @@ public void onSubscribe(@NonNull final Disposable d) { @Override public void onSuccess( - @NonNull final ListExtractor.InfoItemsPage result) { + @NonNull final ListExtractor.InfoItemsPage result) { if (!result.hasNextPage()) { isComplete = true; } nextPage = result.getNextPage(); - append(extractListItems(result.getItems())); + append(extractListItems(result.getItems().stream() + .filter(StreamInfoItem.class::isInstance) + .map(StreamInfoItem.class::cast).collect( + Collectors.toList()))); fetchReactor.dispose(); fetchReactor = null; diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java deleted file mode 100644 index 1e1fef85ea2..00000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.schabi.newpipe.player.playqueue; - - -import org.schabi.newpipe.extractor.Page; -import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.util.ExtractorHelper; - -import java.util.List; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.schedulers.Schedulers; - -public final class ChannelPlayQueue extends AbstractInfoPlayQueue { - - public ChannelPlayQueue(final ChannelInfo info) { - super(info); - } - - public ChannelPlayQueue(final int serviceId, - final String url, - final Page nextPage, - final List streams, - final int index) { - super(serviceId, url, nextPage, streams, index); - } - - @Override - protected String getTag() { - return "ChannelPlayQueue@" + Integer.toHexString(hashCode()); - } - - @Override - public void fetch() { - if (this.isInitial) { - ExtractorHelper.getChannelInfo(this.serviceId, this.baseUrl, false) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getHeadListObserver()); - } else { - ExtractorHelper.getMoreChannelItems(this.serviceId, this.baseUrl, this.nextPage) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getNextPageObserver()); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelTabPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelTabPlayQueue.java new file mode 100644 index 00000000000..e422a5c5254 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelTabPlayQueue.java @@ -0,0 +1,53 @@ +package org.schabi.newpipe.player.playqueue; + + +import org.schabi.newpipe.extractor.Page; +import org.schabi.newpipe.extractor.channel.ChannelTabInfo; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.util.ExtractorHelper; + +import java.util.Collections; +import java.util.List; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public final class ChannelTabPlayQueue extends AbstractInfoPlayQueue { + + final ListLinkHandler linkHandler; + + public ChannelTabPlayQueue(final int serviceId, + final ListLinkHandler linkHandler, + final Page nextPage, + final List streams, + final int index) { + super(serviceId, linkHandler.getUrl(), nextPage, streams, index); + this.linkHandler = linkHandler; + } + + public ChannelTabPlayQueue(final int serviceId, + final ListLinkHandler linkHandler) { + this(serviceId, linkHandler, null, Collections.emptyList(), 0); + } + + @Override + protected String getTag() { + return "ChannelTabPlayQueue@" + Integer.toHexString(hashCode()); + } + + @Override + public void fetch() { + if (isInitial) { + ExtractorHelper.getChannelTab(this.serviceId, this.linkHandler, false) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getHeadListObserver()); + } else { + ExtractorHelper.getMoreChannelTabItems(this.serviceId, this.linkHandler, this.nextPage) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getNextPageObserver()); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java index b5375075f35..7e3f5d0c825 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java @@ -19,7 +19,7 @@ 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.ChannelVideosFragment; +import org.schabi.newpipe.fragments.list.channel.ChannelFragment; 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 int getTabIconRes(final Context context) { } @Override - public ChannelVideosFragment getFragment(final Context context) { - return new ChannelVideosFragment(channelServiceId, channelUrl, channelName); + public ChannelFragment getFragment(final Context context) { + return ChannelFragment.getInstance(channelServiceId, channelUrl, channelName); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java b/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java index fb384e07650..974445a9617 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java @@ -7,24 +7,58 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.linkhandler.ChannelTabs; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; +import java.util.List; import java.util.Set; public final class ChannelTabHelper { private ChannelTabHelper() { } + /** + * @param tab the channel tab to check + * @return whether the tab should contain (playable) streams or not + */ + public static boolean isStreamsTab(final String tab) { + switch (tab) { + case ChannelTabs.VIDEOS: + case ChannelTabs.TRACKS: + case ChannelTabs.SHORTS: + case ChannelTabs.LIVESTREAMS: + return true; + } + return false; + } + + /** + * @param tab the channel tab link handler to check + * @return whether the tab should contain (playable) streams or not + */ + public static boolean isStreamsTab(final ListLinkHandler tab) { + final List contentFilters = tab.getContentFilters(); + if (contentFilters.isEmpty()) { + return false; // this should never happen, but check just to be sure + } else { + return isStreamsTab(contentFilters.get(0)); + } + } + @StringRes private static int getShowTabKey(final String tab) { switch (tab) { - case ChannelTabs.PLAYLISTS: - return R.string.show_channel_tabs_playlists; - case ChannelTabs.LIVESTREAMS: - return R.string.show_channel_tabs_livestreams; + case ChannelTabs.VIDEOS: + return R.string.show_channel_tabs_videos; + case ChannelTabs.TRACKS: + return R.string.show_channel_tabs_tracks; case ChannelTabs.SHORTS: return R.string.show_channel_tabs_shorts; + case ChannelTabs.LIVESTREAMS: + return R.string.show_channel_tabs_livestreams; case ChannelTabs.CHANNELS: return R.string.show_channel_tabs_channels; + case ChannelTabs.PLAYLISTS: + return R.string.show_channel_tabs_playlists; case ChannelTabs.ALBUMS: return R.string.show_channel_tabs_albums; } @@ -34,14 +68,18 @@ private static int getShowTabKey(final String tab) { @StringRes public static int getTranslationKey(final String tab) { switch (tab) { - case ChannelTabs.PLAYLISTS: - return R.string.channel_tab_playlists; - case ChannelTabs.LIVESTREAMS: - return R.string.channel_tab_livestreams; + case ChannelTabs.VIDEOS: + return R.string.channel_tab_videos; + case ChannelTabs.TRACKS: + return R.string.channel_tab_tracks; case ChannelTabs.SHORTS: return R.string.channel_tab_shorts; + case ChannelTabs.LIVESTREAMS: + return R.string.channel_tab_livestreams; case ChannelTabs.CHANNELS: return R.string.channel_tab_channels; + case ChannelTabs.PLAYLISTS: + return R.string.channel_tab_playlists; case ChannelTabs.ALBUMS: return R.string.channel_tab_albums; } diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index d8d68f0e4e7..59a5df2054a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -36,17 +36,13 @@ import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; -import org.schabi.newpipe.extractor.ListInfo; import org.schabi.newpipe.extractor.MetaInfo; 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.ListLinkHandler; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; @@ -129,30 +125,6 @@ public static Single getChannelInfo(final int serviceId, final Stri ChannelInfo.getInfo(NewPipe.getService(serviceId), url))); } - public static Single> getMoreChannelItems(final int serviceId, - final String url, - final Page nextPage) { - checkServiceId(serviceId); - return Single.fromCallable(() -> - ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage)); - } - - public static Single> getFeedInfoFallbackToChannelInfo( - final int serviceId, final String url) { - final Maybe> maybeFeedInfo = Maybe.fromCallable(() -> { - final StreamingService service = NewPipe.getService(serviceId); - final FeedExtractor feedExtractor = service.getFeedExtractor(url); - - if (feedExtractor == null) { - return null; - } - - return FeedInfo.getInfo(feedExtractor); - }); - - return maybeFeedInfo.switchIfEmpty(getChannelInfo(serviceId, url, true)); - } - public static Single getChannelTab(final int serviceId, final ListLinkHandler listLinkHandler, final boolean forceLoad) { diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index d32fbce0cc2..9c56107034b 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -275,25 +275,31 @@ channel_tabs - show_channel_tabs_playlists - show_channel_tabs_live + show_channel_tabs_videos + show_channel_tabs_tracks show_channel_tabs_shorts + show_channel_tabs_live show_channel_tabs_channels + show_channel_tabs_playlists show_channel_tabs_albums show_channel_tabs_about - @string/show_channel_tabs_playlists - @string/show_channel_tabs_livestreams + @string/show_channel_tabs_videos + @string/show_channel_tabs_tracks @string/show_channel_tabs_shorts + @string/show_channel_tabs_livestreams @string/show_channel_tabs_channels + @string/show_channel_tabs_playlists @string/show_channel_tabs_albums @string/show_channel_tabs_about - @string/channel_tab_playlists - @string/channel_tab_livestreams + @string/channel_tab_videos + @string/channel_tab_tracks @string/channel_tab_shorts + @string/channel_tab_livestreams @string/channel_tab_channels + @string/channel_tab_playlists @string/channel_tab_albums @string/channel_tab_about diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 259689231af..565c91b5471 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -798,10 +798,11 @@ dubbed descriptive Videos - Live + Tracks Shorts - Playlists + Live Channels + Playlists Albums About Channel tabs From a1e8b9be4e16bfc1d27baf2af05a6e5e2391407b Mon Sep 17 00:00:00 2001 From: Stypox Date: Fri, 14 Apr 2023 10:27:25 +0200 Subject: [PATCH 26/50] Fix channel tabs in main page setting title themselves --- .../newpipe/fragments/list/channel/ChannelFragment.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 3a8d46a4df2..46faaf277ab 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -457,8 +457,10 @@ private void updateTabs() { for (final ListLinkHandler linkHandler : currentInfo.getTabs()) { final String tab = linkHandler.getContentFilters().get(0); if (ChannelTabHelper.showChannelTab(context, preferences, tab)) { - tabAdapter.addFragment( - ChannelTabFragment.getInstance(serviceId, linkHandler, name), + final ChannelTabFragment channelTabFragment = + ChannelTabFragment.getInstance(serviceId, linkHandler, name); + channelTabFragment.useAsFrontPage(useAsFrontPage); + tabAdapter.addFragment(channelTabFragment, context.getString(ChannelTabHelper.getTranslationKey(tab))); } } From 371f98677328e6fbfe85a8cbd30fd9a39c22321c Mon Sep 17 00:00:00 2001 From: Stypox Date: Fri, 14 Apr 2023 11:00:01 +0200 Subject: [PATCH 27/50] Fix some code smells --- .../fragments/list/channel/ChannelTabFragment.java | 5 ----- .../java/org/schabi/newpipe/util/ChannelTabHelper.java | 9 ++++++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java index 3f400bdf8d4..df3aced50b3 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java @@ -15,7 +15,6 @@ import org.schabi.newpipe.extractor.channel.ChannelTabInfo; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; -import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; import icepick.State; @@ -23,12 +22,8 @@ public class ChannelTabFragment extends BaseListInfoFragment { - @State - protected int serviceId = Constants.NO_SERVICE_ID; - @State protected ListLinkHandler tabHandler; - @State protected String channelName; diff --git a/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java b/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java index 974445a9617..d10e0a3dde2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java @@ -27,8 +27,9 @@ public static boolean isStreamsTab(final String tab) { case ChannelTabs.SHORTS: case ChannelTabs.LIVESTREAMS: return true; + default: + return false; } - return false; } /** @@ -61,8 +62,9 @@ private static int getShowTabKey(final String tab) { return R.string.show_channel_tabs_playlists; case ChannelTabs.ALBUMS: return R.string.show_channel_tabs_albums; + default: + return -1; } - return -1; } @StringRes @@ -82,8 +84,9 @@ public static int getTranslationKey(final String tab) { return R.string.channel_tab_playlists; case ChannelTabs.ALBUMS: return R.string.channel_tab_albums; + default: + return R.string.unknown_content; } - return R.string.unknown_content; } public static boolean showChannelTab(final Context context, From 753a92055c18cbee58e6b7cf4edbdd80b63d56ef Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 16 Apr 2023 00:58:30 +0200 Subject: [PATCH 28/50] feat: add playlist controls to channel tab --- .../list/channel/ChannelTabFragment.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java index df3aced50b3..86e429beade 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java @@ -9,13 +9,24 @@ import androidx.annotation.Nullable; import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.PlaylistControlBinding; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.channel.ChannelTabInfo; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; +import org.schabi.newpipe.player.PlayerType; +import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue; +import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.util.ChannelTabHelper; import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.NavigationHelper; + +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; import icepick.State; import io.reactivex.rxjava3.core.Single; @@ -27,6 +38,8 @@ public class ChannelTabFragment extends BaseListInfoFragment getListHeaderSupplier() { + if (ChannelTabHelper.isStreamsTab(tabHandler)) { + playlistControlBinding = PlaylistControlBinding + .inflate(activity.getLayoutInflater(), itemsList, false); + return playlistControlBinding::getRoot; + } + return null; + } + @Override protected Single loadResult(final boolean forceLoad) { return ExtractorHelper.getChannelTab(serviceId, tabHandler, forceLoad); @@ -72,4 +101,47 @@ protected Single> loadMoreItemsLogic() { public void setTitle(final String title) { super.setTitle(channelName); } + + @Override + public void handleResult(final @NonNull ChannelTabInfo result) { + super.handleResult(result); + + if (playlistControlBinding != null) { + // PlaylistControls should be visible only if there is some item in + // infoListAdapter other than header + if (infoListAdapter.getItemCount() > 1) { + playlistControlBinding.getRoot().setVisibility(View.VISIBLE); + } else { + playlistControlBinding.getRoot().setVisibility(View.GONE); + } + + 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 PlayQueue getPlayQueue() { + final List streamItems = infoListAdapter.getItemsList().stream() + .filter(StreamInfoItem.class::isInstance) + .map(StreamInfoItem.class::cast) + .collect(Collectors.toList()); + + return new ChannelTabPlayQueue(currentInfo.getServiceId(), tabHandler, + currentInfo.getNextPage(), streamItems, 0); + } } From a2a717bd49faa7a9009366550a71ef874dba0c5e Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 16 Apr 2023 16:00:46 +0200 Subject: [PATCH 29/50] update NPE --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 1f924b12f4f..d73cca4248f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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.Theta-Dev:NewPipeExtractor:c3651bef5c622abf0cdfc34c9985ba8c33d1491e' + implementation 'com.github.Theta-Dev:NewPipeExtractor:2ad496fc2b932dd89009f3892462014cb231f6ca' implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' /** Checkstyle **/ From 28d952a643076811c217338736ff60ff58dc2a85 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 16 Apr 2023 16:30:40 +0200 Subject: [PATCH 30/50] feat: filter fetched channel tabs --- .../local/feed/service/FeedLoadManager.kt | 19 ++++++++-- .../schabi/newpipe/util/ChannelTabHelper.java | 38 +++++++++++++++++++ app/src/main/res/values/settings_keys.xml | 18 +++++++++ app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/content_settings.xml | 10 +++++ 5 files changed, 84 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt index be2c2490e63..b5554970403 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt @@ -80,7 +80,9 @@ class FeedLoadManager(private val context: Context) { * subscriptions which have not been updated within the feed updated threshold */ val outdatedSubscriptions = when (groupId) { - FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(outdatedThreshold) + FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions( + outdatedThreshold + ) GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.outdatedSubscriptionsWithNotificationMode( outdatedThreshold, NotificationMode.ENABLED ) @@ -146,7 +148,13 @@ class FeedLoadManager(private val context: Context) { originalInfo = channelInfo streams = channelInfo.tabs - .filter(ChannelTabHelper::isStreamsTab) + .filter { tab -> + ChannelTabHelper.fetchFeedChannelTab( + context, + defaultSharedPreferences, + tab + ) + } .map { Pair( getChannelTab(subscriptionEntity.serviceId, it, true) @@ -208,7 +216,12 @@ class FeedLoadManager(private val context: Context) { } private fun broadcastProgress() { - FeedEventManager.postEvent(FeedEventManager.Event.ProgressEvent(currentProgress.get(), maxProgress.get())) + FeedEventManager.postEvent( + FeedEventManager.Event.ProgressEvent( + currentProgress.get(), + maxProgress.get() + ) + ) } /** diff --git a/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java b/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java index d10e0a3dde2..5db43886369 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java @@ -67,6 +67,22 @@ private static int getShowTabKey(final String tab) { } } + @StringRes + private static int getFetchFeedTabKey(final String tab) { + switch (tab) { + case ChannelTabs.VIDEOS: + return R.string.fetch_channel_tabs_videos; + case ChannelTabs.TRACKS: + return R.string.fetch_channel_tabs_tracks; + case ChannelTabs.SHORTS: + return R.string.fetch_channel_tabs_shorts; + case ChannelTabs.LIVESTREAMS: + return R.string.fetch_channel_tabs_livestreams; + default: + return -1; + } + } + @StringRes public static int getTranslationKey(final String tab) { switch (tab) { @@ -110,4 +126,26 @@ public static boolean showChannelTab(final Context context, } return showChannelTab(context, sharedPreferences, key); } + + public static boolean fetchFeedChannelTab(final Context context, + final SharedPreferences sharedPreferences, + final ListLinkHandler tab) { + final List contentFilters = tab.getContentFilters(); + if (contentFilters.isEmpty()) { + return false; // this should never happen, but check just to be sure + } + + final int key = ChannelTabHelper.getFetchFeedTabKey(contentFilters.get(0)); + if (key == -1) { + return false; + } + + final Set enabledTabs = sharedPreferences.getStringSet( + context.getString(R.string.feed_fetch_channel_tabs_key), null); + if (enabledTabs == null) { + return true; // default to true + } else { + return enabledTabs.contains(context.getString(key)); + } + } } diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 9c56107034b..d9d8e60be29 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -372,6 +372,24 @@ feed_use_dedicated_fetch_method + feed_fetch_channel_tabs + fetch_channel_tabs_videos + fetch_channel_tabs_tracks + fetch_channel_tabs_shorts + fetch_channel_tabs_live + + @string/fetch_channel_tabs_videos + @string/fetch_channel_tabs_tracks + @string/fetch_channel_tabs_shorts + @string/fetch_channel_tabs_livestreams + + + @string/channel_tab_videos + @string/channel_tab_tracks + @string/channel_tab_shorts + @string/channel_tab_livestreams + + import_export_data_path import_data export_data diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 565c91b5471..44e8f5e7411 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -714,6 +714,8 @@ \nSo the choice boils down to what you prefer: speed or precise information. Show the following streams Show/Hide streams + Fetch channel tabs + Tabs to fetch when updating the feed. This option has no effect if a channel is updated using fast mode. This content is not yet supported by NewPipe.\n\nIt will hopefully be supported in a future version. Channel\'s avatar thumbnail Created by %s diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml index 8783ff1ed2d..73a849af75e 100644 --- a/app/src/main/res/xml/content_settings.xml +++ b/app/src/main/res/xml/content_settings.xml @@ -162,5 +162,15 @@ app:singleLineTitle="false" app:iconSpaceReserved="false" /> + + From dca32efadf42b2a140aef74964a0f187a8fbb099 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 19 Apr 2023 18:29:59 +0200 Subject: [PATCH 31/50] add channel banner placeholder --- app/src/main/res/layout/fragment_channel.xml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml index 15995f8f368..e1744739c8f 100644 --- a/app/src/main/res/layout/fragment_channel.xml +++ b/app/src/main/res/layout/fragment_channel.xml @@ -28,11 +28,10 @@ From 013d51345073034a42381709bd4772f0c7110fa5 Mon Sep 17 00:00:00 2001 From: Stypox Date: Fri, 21 Apr 2023 15:42:23 +0200 Subject: [PATCH 32/50] Add space above channel description (About tab) --- .../fragments/detail/BaseDescriptionFragment.java | 2 +- .../fragments/list/channel/ChannelAboutFragment.java | 9 +++++++++ app/src/main/res/layout/fragment_description.xml | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java index fbbfdf23f90..47f8598afe6 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java @@ -35,7 +35,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment { final CompositeDisposable descriptionDisposables = new CompositeDisposable(); - FragmentDescriptionBinding binding; + protected FragmentDescriptionBinding binding; @Override public View onCreateView(@NonNull final LayoutInflater inflater, diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java index ae04e8b00fd..271be39395a 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java @@ -3,7 +3,9 @@ import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT; import android.content.Context; +import android.os.Bundle; import android.view.LayoutInflater; +import android.view.View; import android.widget.LinearLayout; import androidx.annotation.Nullable; @@ -13,6 +15,7 @@ import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.fragments.detail.BaseDescriptionFragment; +import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.Localization; import java.util.List; @@ -33,6 +36,12 @@ public ChannelAboutFragment() { super(); } + @Override + protected void initViews(final View rootView, final Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + binding.constraintLayout.setPadding(0, DeviceUtils.dpToPx(8, requireContext()), 0, 0); + } + @Nullable @Override protected Description getDescription() { diff --git a/app/src/main/res/layout/fragment_description.xml b/app/src/main/res/layout/fragment_description.xml index 157b8f394f9..b20905d4ad2 100644 --- a/app/src/main/res/layout/fragment_description.xml +++ b/app/src/main/res/layout/fragment_description.xml @@ -8,6 +8,7 @@ android:scrollbars="vertical"> From 1061bce4f347def9b8f86d3bd2ad25e339ab713a Mon Sep 17 00:00:00 2001 From: Stypox Date: Fri, 21 Apr 2023 15:54:22 +0200 Subject: [PATCH 33/50] Add avatar and bannner URLs to channel About tab --- .../newpipe/fragments/list/channel/ChannelAboutFragment.java | 5 +++++ app/src/main/res/values/strings.xml | 2 ++ 2 files changed, 7 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java index 271be39395a..e78d5a92272 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java @@ -88,5 +88,10 @@ protected void setupMetadata(final LayoutInflater inflater, addMetadataItem(inflater, layout, false, R.string.metadata_subscribers, Localization.localizeNumber(context, channelInfo.getSubscriberCount())); } + + addMetadataItem(inflater, layout, true, R.string.metadata_avatar_url, + channelInfo.getAvatarUrl()); + addMetadataItem(inflater, layout, true, R.string.metadata_banner_url, + channelInfo.getBannerUrl()); } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 44e8f5e7411..e6fb35a1634 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -756,6 +756,8 @@ Support Host Thumbnail URL + Avatar URL + Banner URL Public Unlisted Private From c48e702a50bfcfc6329aa43a26ac9aaac71e2efb Mon Sep 17 00:00:00 2001 From: Stypox Date: Fri, 21 Apr 2023 17:05:28 +0200 Subject: [PATCH 34/50] Improve placeholder channel banner handling Now the placeholder gets hidden if there is no banner url or the user disabled images, to save space --- .../fragments/list/channel/ChannelFragment.java | 16 +++++++++++++--- .../org/schabi/newpipe/util/PicassoHelper.java | 6 +----- app/src/main/res/layout/fragment_channel.xml | 7 ++++--- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 46faaf277ab..f709fc22636 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.fragments.list.channel; +import static org.schabi.newpipe.extractor.utils.Utils.isBlank; import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor; import static org.schabi.newpipe.ktx.ViewUtils.animate; import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor; @@ -146,6 +147,10 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) { binding.tabLayout.setupWithViewPager(binding.viewPager); binding.channelTitleView.setText(name); + if (!PicassoHelper.getShouldLoadImages()) { + // do not waste space for the banner if it is not going to be loaded + binding.channelBannerImage.setImageDrawable(null); + } } @Override @@ -575,9 +580,14 @@ public void handleResult(@NonNull final ChannelInfo result) { currentInfo = result; setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName()); - binding.getRoot().setVisibility(View.VISIBLE); - PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG) - .into(binding.channelBannerImage); + if (PicassoHelper.getShouldLoadImages() && !isBlank(result.getBannerUrl())) { + PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG) + .into(binding.channelBannerImage); + } else { + // do not waste space for the banner, if the user disabled images or there is not one + binding.channelBannerImage.setImageDrawable(null); + } + PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG) .into(binding.channelAvatarView); PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG) diff --git a/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java b/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java index 750b8e799a2..ece0c7e8753 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java @@ -109,11 +109,7 @@ public static RequestCreator loadDetailsThumbnail(final String url) { } public static RequestCreator loadBanner(final String url) { - if (!shouldLoadImages || isBlank(url)) { - return picassoInstance.load((String) null); - } else { - return picassoInstance.load(url); - } + return loadImageDefault(url, R.drawable.placeholder_channel_banner); } public static RequestCreator loadPlaylistThumbnail(final String url) { diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml index e1744739c8f..cd3e371c512 100644 --- a/app/src/main/res/layout/fragment_channel.xml +++ b/app/src/main/res/layout/fragment_channel.xml @@ -28,10 +28,11 @@ From 604419dd1f35b55eee252b7baf25b32ef4f5b269 Mon Sep 17 00:00:00 2001 From: Stypox Date: Fri, 21 Apr 2023 17:08:43 +0200 Subject: [PATCH 35/50] Make channel banner placeholder match YouTube's size YouTube's "Desktop Max" thumbnails are 2560x423, while our previous placeholder banner was 2550x427. The extractor actually returns a lower resolution "Desktop Max" banner at 1060x175, but the ratio wrt 2560x423 is off by ~0.1% The PNG was optimized with OptiPNG --- .../placeholder_channel_banner.png | Bin 30565 -> 36256 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/src/main/res/drawable-nodpi/placeholder_channel_banner.png b/app/src/main/res/drawable-nodpi/placeholder_channel_banner.png index 12e70bb6dae6f1d68ba8fe521b9f88b0dbc43e58..cad11fa50410302df5c5f75fd4debbb464a4bbdf 100644 GIT binary patch literal 36256 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 JmgB13{|!4YKRf^c From 6b3a178f2a74c572ec2e90790f53291228ec4dfb Mon Sep 17 00:00:00 2001 From: Stypox Date: Tue, 25 Apr 2023 18:06:51 +0200 Subject: [PATCH 36/50] Show snackbar with feed loading errors --- .../schabi/newpipe/local/feed/FeedFragment.kt | 42 ++++++++++++------- .../schabi/newpipe/local/feed/FeedState.kt | 4 +- .../newpipe/local/feed/FeedViewModel.kt | 2 +- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index ea9a3f36e31..4f153dcf875 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -60,6 +60,7 @@ import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.databinding.FragmentFeedBinding import org.schabi.newpipe.error.ErrorInfo +import org.schabi.newpipe.error.ErrorUtil import org.schabi.newpipe.error.UserAction import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException @@ -453,24 +454,33 @@ class FeedFragment : BaseStateFragment() { if (t is FeedLoadService.RequestException && t.cause is ContentNotAvailableException ) { - Single.fromCallable { - NewPipeDatabase.getInstance(requireContext()).subscriptionDAO() - .getSubscription(t.subscriptionId) - }.subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { subscriptionEntity -> - handleFeedNotAvailable( - subscriptionEntity, - t.cause, - errors.subList(i + 1, errors.size) - ) - }, - { throwable -> Log.e(TAG, "Unable to process", throwable) } - ) - return // this will be called on the remaining errors by handleFeedNotAvailable() + disposables.add( + Single.fromCallable { + NewPipeDatabase.getInstance(requireContext()).subscriptionDAO() + .getSubscription(t.subscriptionId) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { subscriptionEntity -> + handleFeedNotAvailable( + subscriptionEntity, + t.cause, + errors.subList(i + 1, errors.size) + ) + }, + { throwable -> Log.e(TAG, "Unable to process", throwable) } + ) + ) + // this will be called on the remaining errors by handleFeedNotAvailable() + return@handleItemsErrors } } + + if (errors.isNotEmpty()) { + // if no error was a ContentNotAvailableException, show a general error snackbar + ErrorUtil.showSnackbar(this, ErrorInfo(errors, UserAction.REQUESTED_FEED, "")) + } } private fun handleFeedNotAvailable( diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt index 27613e83e9c..665ebbe4396 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt @@ -13,9 +13,9 @@ sealed class FeedState { data class LoadedState( val items: List, - val oldestUpdate: OffsetDateTime? = null, + val oldestUpdate: OffsetDateTime?, val notLoadedCount: Long, - val itemsErrors: List = emptyList() + val itemsErrors: List ) : FeedState() data class ErrorState( diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt index 58f9e9edc28..728570b17e0 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt @@ -86,7 +86,7 @@ class FeedViewModel( .subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) -> mutableStateLiveData.postValue( when (event) { - is IdleEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount) + is IdleEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount, listOf()) is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage) is SuccessResultEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount, event.itemsErrors) is ErrorResultEvent -> FeedState.ErrorState(event.error) From 1519527356936a1ebf4ac58154bc2e897139c519 Mon Sep 17 00:00:00 2001 From: Stypox Date: Tue, 25 Apr 2023 19:01:02 +0200 Subject: [PATCH 37/50] Fix loading feed when a channel tab is empty --- .../org/schabi/newpipe/local/feed/service/FeedLoadManager.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt index b5554970403..c9593e537b8 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt @@ -165,7 +165,9 @@ class FeedLoadManager(private val context: Context) { } .flatMap { (channelTabInfo, linkHandler) -> errors.addAll(channelTabInfo.errors) - if (channelTabInfo.relatedItems.isEmpty()) { + if (channelTabInfo.relatedItems.isEmpty() && + channelTabInfo.nextPage != null + ) { val infoItemsPage = getMoreChannelTabItems( subscriptionEntity.serviceId, linkHandler, channelTabInfo.nextPage From 6f23b56b06275d5d85721ab72ad7b47306c58f84 Mon Sep 17 00:00:00 2001 From: Stypox Date: Tue, 25 Apr 2023 19:11:30 +0200 Subject: [PATCH 38/50] Use consistent name for livestreams tab in settings keys --- app/src/main/res/values/settings_keys.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index d9d8e60be29..51abe14fbbf 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -278,7 +278,7 @@ show_channel_tabs_videos show_channel_tabs_tracks show_channel_tabs_shorts - show_channel_tabs_live + show_channel_tabs_livestreams show_channel_tabs_channels show_channel_tabs_playlists show_channel_tabs_albums @@ -376,7 +376,7 @@ fetch_channel_tabs_videos fetch_channel_tabs_tracks fetch_channel_tabs_shorts - fetch_channel_tabs_live + fetch_channel_tabs_livestreams @string/fetch_channel_tabs_videos @string/fetch_channel_tabs_tracks From 9e55014a133b7f77f65aadb456e33d5affd97dcf Mon Sep 17 00:00:00 2001 From: Stypox Date: Tue, 25 Apr 2023 19:18:34 +0200 Subject: [PATCH 39/50] Fix wrongly themed channel header Since it is embedded in the app bar and has red as background color, it should be themed in the same way as the toolbar. --- app/src/main/res/layout/fragment_channel.xml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml index cd3e371c512..f557e339696 100644 --- a/app/src/main/res/layout/fragment_channel.xml +++ b/app/src/main/res/layout/fragment_channel.xml @@ -5,10 +5,13 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + app:strokeWidth="2dp" + app:tint="@null" /> Date: Wed, 2 Aug 2023 22:45:53 +0200 Subject: [PATCH 40/50] Update NewPipeExtractor and adapt imports --- app/build.gradle | 2 +- .../newpipe/fragments/list/channel/ChannelTabFragment.java | 2 +- .../schabi/newpipe/local/subscription/SubscriptionManager.kt | 2 +- .../local/subscription/services/SubscriptionsImportService.java | 2 +- .../schabi/newpipe/player/playqueue/ChannelTabPlayQueue.java | 2 +- app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java | 2 +- app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d73cca4248f..051414ba07c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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.Theta-Dev:NewPipeExtractor:2ad496fc2b932dd89009f3892462014cb231f6ca' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:95a3cc0a173bba28c179f9f9503b1010ec6bff21' implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' /** Checkstyle **/ diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java index 86e429beade..6b2dd20bf90 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java @@ -13,7 +13,7 @@ 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.channel.tabs.ChannelTabInfo; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt index 9a8b53e90e9..3c11ce15204 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt @@ -14,7 +14,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionDAO import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.extractor.Info import org.schabi.newpipe.extractor.channel.ChannelInfo -import org.schabi.newpipe.extractor.channel.ChannelTabInfo +import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo import org.schabi.newpipe.extractor.feed.FeedInfo import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.local.feed.FeedDatabaseManager diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java index 66164807dc5..d624e1038e7 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java @@ -39,7 +39,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.channel.ChannelTabInfo; +import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo; import org.schabi.newpipe.extractor.subscription.SubscriptionItem; import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.streams.io.SharpInputStream; diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelTabPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelTabPlayQueue.java index e422a5c5254..a9eb2a19c7e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelTabPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelTabPlayQueue.java @@ -2,7 +2,7 @@ import org.schabi.newpipe.extractor.Page; -import org.schabi.newpipe.extractor.channel.ChannelTabInfo; +import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.util.ExtractorHelper; diff --git a/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java b/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java index 5db43886369..8e8d3849007 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java @@ -6,7 +6,7 @@ import androidx.annotation.StringRes; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.linkhandler.ChannelTabs; +import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import java.util.List; diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index 59a5df2054a..257a428fcbd 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -40,7 +40,7 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.channel.ChannelTabInfo; +import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo; import org.schabi.newpipe.extractor.comments.CommentsInfo; import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.kiosk.KioskInfo; From 5c7c38232347d90708ac740135d0130d82df9932 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Tue, 22 Aug 2023 12:39:27 +0200 Subject: [PATCH 41/50] Add missing `@Override` annotations to setupMetadata() implementations --- .../org/schabi/newpipe/fragments/detail/DescriptionFragment.java | 1 + .../newpipe/fragments/list/channel/ChannelAboutFragment.java | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java index ded4e907add..cb38d841645 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java @@ -73,6 +73,7 @@ public List getTags() { return streamInfo.getTags(); } + @Override protected void setupMetadata(final LayoutInflater inflater, final LinearLayout layout) { if (streamInfo.getUploadDate() != null) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java index e78d5a92272..4117533bd4f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java @@ -80,6 +80,7 @@ public List getTags() { return channelInfo.getTags(); } + @Override protected void setupMetadata(final LayoutInflater inflater, final LinearLayout layout) { final Context context = getContext(); From 6ab8716e695bf9595061570d5f3368d99cee1a42 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Tue, 22 Aug 2023 12:37:02 +0200 Subject: [PATCH 42/50] Extract actual feed loading code into separate method Increase readability --- .../local/feed/service/FeedLoadManager.kt | 197 +++++++++--------- 1 file changed, 104 insertions(+), 93 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt index c9593e537b8..b0969a76941 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt @@ -1,6 +1,7 @@ package org.schabi.newpipe.local.feed.service import android.content.Context +import android.content.SharedPreferences import androidx.preference.PreferenceManager import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Completable @@ -13,6 +14,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers import org.schabi.newpipe.R import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.subscription.NotificationMode +import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.extractor.Info import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.extractor.feed.FeedInfo @@ -108,99 +110,7 @@ class FeedLoadManager(private val context: Context) { .runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2) .filter { !cancelSignal.get() } .map { subscriptionEntity -> - var error: Throwable? = null - val storeOriginalErrorAndRethrow = { e: Throwable -> - // keep original to prevent blockingGet() from wrapping it into RuntimeException - error = e - throw e - } - - try { - // check for and load new streams - // either by using the dedicated feed method or by getting the channel info - var originalInfo: Info? = null - var streams: List? = null - val errors = ArrayList() - - if (useFeedExtractor) { - NewPipe.getService(subscriptionEntity.serviceId) - .getFeedExtractor(subscriptionEntity.url) - ?.also { feedExtractor -> - // the user wants to use a feed extractor and there is one, use it - val feedInfo = FeedInfo.getInfo(feedExtractor) - errors.addAll(feedInfo.errors) - originalInfo = feedInfo - streams = feedInfo.relatedItems - } - } - - if (originalInfo == null) { - // use the normal channel tabs extractor if either the user wants it, or - // the current service does not have a dedicated feed extractor - - val channelInfo = getChannelInfo( - subscriptionEntity.serviceId, - subscriptionEntity.url, true - ) - .onErrorReturn(storeOriginalErrorAndRethrow) - .blockingGet() - errors.addAll(channelInfo.errors) - originalInfo = channelInfo - - streams = channelInfo.tabs - .filter { tab -> - ChannelTabHelper.fetchFeedChannelTab( - context, - defaultSharedPreferences, - tab - ) - } - .map { - Pair( - getChannelTab(subscriptionEntity.serviceId, it, true) - .onErrorReturn(storeOriginalErrorAndRethrow) - .blockingGet(), - it - ) - } - .flatMap { (channelTabInfo, linkHandler) -> - errors.addAll(channelTabInfo.errors) - if (channelTabInfo.relatedItems.isEmpty() && - channelTabInfo.nextPage != null - ) { - val infoItemsPage = getMoreChannelTabItems( - subscriptionEntity.serviceId, - linkHandler, channelTabInfo.nextPage - ) - .blockingGet() - - errors.addAll(infoItemsPage.errors) - return@flatMap infoItemsPage.items - } else { - return@flatMap channelTabInfo.relatedItems - } - } - .filterIsInstance() - } - - return@map Notification.createOnNext( - FeedUpdateInfo( - subscriptionEntity, - originalInfo!!, - streams!!, - errors, - ) - ) - } catch (e: Throwable) { - val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}" - val wrapper = FeedLoadService.RequestException( - subscriptionEntity.uid, - request, - // do this to prevent blockingGet() from wrapping into RuntimeException - error ?: e - ) - return@map Notification.createOnError(wrapper) - } + loadStreams(subscriptionEntity, useFeedExtractor, defaultSharedPreferences) } .sequential() .observeOn(AndroidSchedulers.mainThread()) @@ -226,6 +136,107 @@ class FeedLoadManager(private val context: Context) { ) } + private fun loadStreams( + subscriptionEntity: SubscriptionEntity, + useFeedExtractor: Boolean, + defaultSharedPreferences: SharedPreferences + ): + Notification { + var error: Throwable? = null + val storeOriginalErrorAndRethrow = { e: Throwable -> + // keep original to prevent blockingGet() from wrapping it into RuntimeException + error = e + throw e + } + + try { + // check for and load new streams + // either by using the dedicated feed method or by getting the channel info + var originalInfo: Info? = null + var streams: List? = null + val errors = ArrayList() + + if (useFeedExtractor) { + NewPipe.getService(subscriptionEntity.serviceId) + .getFeedExtractor(subscriptionEntity.url) + ?.also { feedExtractor -> + // the user wants to use a feed extractor and there is one, use it + val feedInfo = FeedInfo.getInfo(feedExtractor) + errors.addAll(feedInfo.errors) + originalInfo = feedInfo + streams = feedInfo.relatedItems + } + } + + if (originalInfo == null) { + // use the normal channel tabs extractor if either the user wants it, or + // the current service does not have a dedicated feed extractor + + val channelInfo = getChannelInfo( + subscriptionEntity.serviceId, + subscriptionEntity.url, true + ) + .onErrorReturn(storeOriginalErrorAndRethrow) + .blockingGet() + errors.addAll(channelInfo.errors) + originalInfo = channelInfo + + streams = channelInfo.tabs + .filter { tab -> + ChannelTabHelper.fetchFeedChannelTab( + context, + defaultSharedPreferences, + tab + ) + } + .map { + Pair( + getChannelTab(subscriptionEntity.serviceId, it, true) + .onErrorReturn(storeOriginalErrorAndRethrow) + .blockingGet(), + it + ) + } + .flatMap { (channelTabInfo, linkHandler) -> + errors.addAll(channelTabInfo.errors) + if (channelTabInfo.relatedItems.isEmpty() && + channelTabInfo.nextPage != null + ) { + val infoItemsPage = getMoreChannelTabItems( + subscriptionEntity.serviceId, + linkHandler, channelTabInfo.nextPage + ) + .blockingGet() + + errors.addAll(infoItemsPage.errors) + return@flatMap infoItemsPage.items + } else { + return@flatMap channelTabInfo.relatedItems + } + } + .filterIsInstance() + } + + return Notification.createOnNext( + FeedUpdateInfo( + subscriptionEntity, + originalInfo!!, + streams!!, + errors, + ) + ) + } catch (e: Throwable) { + val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}" + val wrapper = FeedLoadService.RequestException( + subscriptionEntity.uid, + request, + // do this to prevent blockingGet() from wrapping into RuntimeException + error ?: e + ) + return Notification.createOnError(wrapper) + } + } + /** * Keep the feed and the stream tables small * to reduce loading times when trying to display the feed. From 89dc44be612224eb3bfc11579f66d1f4407de853 Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Tue, 22 Aug 2023 19:14:17 +0200 Subject: [PATCH 43/50] Always show the About tab and support having no description --- .../detail/BaseDescriptionFragment.java | 19 +++++++++++-------- .../list/channel/ChannelFragment.java | 4 +--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java index 47f8598afe6..3b1ede43270 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java @@ -129,10 +129,13 @@ private void enableDescriptionSelection() { private void disableDescriptionSelection() { // show description content again, otherwise some links are not clickable - TextLinkifier.fromDescription(binding.detailDescriptionView, - getDescription(), HtmlCompat.FROM_HTML_MODE_LEGACY, - getService(), getStreamUrl(), - descriptionDisposables, SET_LINK_MOVEMENT_METHOD); + final Description description = getDescription(); + if (description != null) { + TextLinkifier.fromDescription(binding.detailDescriptionView, + description, HtmlCompat.FROM_HTML_MODE_LEGACY, + getService(), getStreamUrl(), + descriptionDisposables, SET_LINK_MOVEMENT_METHOD); + } binding.detailDescriptionNoteView.setVisibility(View.GONE); binding.detailDescriptionView.setTextIsSelectable(false); @@ -144,10 +147,10 @@ private void disableDescriptionSelection() { } protected void addMetadataItem(final LayoutInflater inflater, - final LinearLayout layout, - final boolean linkifyContent, - @StringRes final int type, - @Nullable final String content) { + final LinearLayout layout, + final boolean linkifyContent, + @StringRes final int type, + @Nullable final String content) { if (isBlank(content)) { return; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index f709fc22636..6c0eb979223 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -470,9 +470,7 @@ private void updateTabs() { } } - final String description = currentInfo.getDescription(); - if (description != null && !description.isEmpty() - && ChannelTabHelper.showChannelTab( + if (ChannelTabHelper.showChannelTab( context, preferences, R.string.show_channel_tabs_about)) { tabAdapter.addFragment( ChannelAboutFragment.getInstance(currentInfo), From f2ee3859ab6031d821a49de8c57e64bf7483f1ce Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Tue, 22 Aug 2023 19:15:45 +0200 Subject: [PATCH 44/50] Hide the upload date element on the About tab This empty element should be always hidden for this tab, as there is no upload date available for channels. --- .../newpipe/fragments/list/channel/ChannelAboutFragment.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java index 4117533bd4f..d1afd51a0cd 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java @@ -84,6 +84,8 @@ public List getTags() { protected void setupMetadata(final LayoutInflater inflater, final LinearLayout layout) { final Context context = getContext(); + // There is no upload date available for channels, so hide the relevant UI element + binding.detailUploadDateView.setVisibility(View.GONE); if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) { addMetadataItem(inflater, layout, false, R.string.metadata_subscribers, From 8fbc8ffc7c57555bb617c7c5d6b3b158bf31ffa3 Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Tue, 22 Aug 2023 19:16:10 +0200 Subject: [PATCH 45/50] Remove unneeded German translation --- app/src/main/res/values-de/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1720d2b0a34..5283f1afa05 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -773,7 +773,6 @@ Wiedergabelisten Kanäle Alben - Info Tabs auf den Kanalseiten Welche Tabs auf den Kanalseiten angezeigt werden \ No newline at end of file From 0d9910cbbec39cd605aba774f2714e93db33ce5b Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Wed, 23 Aug 2023 23:23:26 +0200 Subject: [PATCH 46/50] Fix SubscriptionManagerTest tests The breakage of these tests is related to the channel tabs changes. The testRememberRecentStreams test method has been removed, as it doesn't seem to be relevant anymore to managing subscriptions. --- .../subscription/SubscriptionManagerTest.java | 41 +------------------ 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/app/src/androidTest/java/org/schabi/newpipe/local/subscription/SubscriptionManagerTest.java b/app/src/androidTest/java/org/schabi/newpipe/local/subscription/SubscriptionManagerTest.java index e71083d2cc6..213b679f009 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/local/subscription/SubscriptionManagerTest.java +++ b/app/src/androidTest/java/org/schabi/newpipe/local/subscription/SubscriptionManagerTest.java @@ -10,19 +10,13 @@ import org.junit.Test; import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.feed.model.FeedGroupEntity; -import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.localization.DateWrapper; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.testUtil.TestDatabase; import org.schabi.newpipe.testUtil.TrampolineSchedulerRule; import java.io.IOException; -import java.time.OffsetDateTime; -import java.util.Comparator; import java.util.List; public class SubscriptionManagerTest { @@ -58,7 +52,7 @@ public void testInsert() throws ExtractionException, IOException { final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/3blue1brown"); final SubscriptionEntity subscription = SubscriptionEntity.from(info); - manager.insertSubscription(subscription, info); + manager.insertSubscription(subscription); final SubscriptionEntity readSubscription = getAssertOneSubscriptionEntity(); // the uid has changed, since the uid is chosen upon inserting, but the rest should match @@ -76,7 +70,7 @@ public void testUpdateNotificationMode() throws ExtractionException, IOException final SubscriptionEntity subscription = SubscriptionEntity.from(info); subscription.setNotificationMode(0); - manager.insertSubscription(subscription, info); + manager.insertSubscription(subscription); manager.updateNotificationMode(subscription.getServiceId(), subscription.getUrl(), 1) .blockingAwait(); final SubscriptionEntity anotherSubscription = getAssertOneSubscriptionEntity(); @@ -85,35 +79,4 @@ public void testUpdateNotificationMode() throws ExtractionException, IOException assertEquals(subscription.getUrl(), anotherSubscription.getUrl()); assertEquals(1, anotherSubscription.getNotificationMode()); } - - @Test - public void testRememberRecentStreams() throws ExtractionException, IOException { - final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/Polyphia"); - final List relatedItems = List.of( - new StreamInfoItem(0, "a", "b", StreamType.VIDEO_STREAM), - new StreamInfoItem(1, "c", "d", StreamType.AUDIO_STREAM), - new StreamInfoItem(2, "e", "f", StreamType.AUDIO_LIVE_STREAM), - new StreamInfoItem(3, "g", "h", StreamType.LIVE_STREAM)); - relatedItems.forEach(item -> { - // these two fields must be non-null for the insert to succeed - item.setUploaderUrl(info.getUrl()); - item.setUploaderName(info.getName()); - // the upload date must not be too much in the past for the item to actually be inserted - item.setUploadDate(new DateWrapper(OffsetDateTime.now())); - }); - info.setRelatedItems(relatedItems); - final SubscriptionEntity subscription = SubscriptionEntity.from(info); - - manager.insertSubscription(subscription, info); - final List streams = database.streamDAO().getAll().blockingFirst(); - - assertEquals(4, streams.size()); - streams.sort(Comparator.comparing(StreamEntity::getServiceId)); - for (int i = 0; i < 4; i++) { - assertEquals(relatedItems.get(0).getServiceId(), streams.get(0).getServiceId()); - assertEquals(relatedItems.get(0).getUrl(), streams.get(0).getUrl()); - assertEquals(relatedItems.get(0).getName(), streams.get(0).getTitle()); - assertEquals(relatedItems.get(0).getStreamType(), streams.get(0).getStreamType()); - } - } } From 109d06b4bb407e0995754af418ae74cb0e159692 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Sun, 10 Sep 2023 01:11:00 +0200 Subject: [PATCH 47/50] Deduplicate code to initialize ClickListeners on playlist controls Add the separate utility class PlayButtonHelper to handle the initialization of the listeners. The ClickListeners on playlist controls had different behaviours. This commit fixes that. The commit also refactors the way how the app determines whether it is started for the first time. The previous version was not clean and recent in this PR caused it to fail. --- .../fragments/detail/VideoDetailFragment.java | 10 ++- .../list/channel/ChannelTabFragment.java | 33 ++----- .../playlist/PlaylistControlViewHolder.java | 13 +++ .../list/playlist/PlaylistFragment.java | 24 ++--- .../history/StatisticsPlaylistFragment.java | 26 +++--- .../local/playlist/LocalPlaylistFragment.java | 48 ++-------- .../newpipe/settings/NewPipeSettings.java | 20 ++--- .../schabi/newpipe/util/PlayButtonHelper.java | 90 +++++++++++++++++++ 8 files changed, 147 insertions(+), 117 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistControlViewHolder.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/PlayButtonHelper.java diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index def1774b765..686e102f1bf 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -112,6 +112,7 @@ import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.external_communication.ShareUtils; +import org.schabi.newpipe.util.PlayButtonHelper; import java.util.ArrayList; import java.util.Iterator; @@ -535,9 +536,11 @@ private void setOnLongClickListeners() { })); binding.detailControlsBackground.setOnLongClickListener(makeOnLongClickListener(info -> - openBackgroundPlayer(true))); + openBackgroundPlayer(true) + )); binding.detailControlsPopup.setOnLongClickListener(makeOnLongClickListener(info -> - openPopupPlayer(true))); + openPopupPlayer(true) + )); binding.detailControlsDownload.setOnLongClickListener(makeOnLongClickListener(info -> NavigationHelper.openDownloads(activity))); @@ -620,8 +623,7 @@ protected void initListeners() { final View.OnTouchListener controlsTouchListener = (view, motionEvent) -> { if (motionEvent.getAction() == MotionEvent.ACTION_DOWN - && PreferenceManager.getDefaultSharedPreferences(activity) - .getBoolean(getString(R.string.show_hold_to_append_key), true)) { + && PlayButtonHelper.shouldShowHoldToAppendTip(activity)) { animate(binding.touchAppendDetail, true, 250, AnimationType.ALPHA, 0, () -> animate(binding.touchAppendDetail, false, 1500, AnimationType.ALPHA, 1000)); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java index 6b2dd20bf90..27315a99109 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java @@ -17,12 +17,12 @@ import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; -import org.schabi.newpipe.player.PlayerType; +import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder; import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.util.ChannelTabHelper; import org.schabi.newpipe.util.ExtractorHelper; -import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.PlayButtonHelper; import java.util.List; import java.util.function.Supplier; @@ -31,7 +31,8 @@ import icepick.State; import io.reactivex.rxjava3.core.Single; -public class ChannelTabFragment extends BaseListInfoFragment { +public class ChannelTabFragment extends BaseListInfoFragment + implements PlaylistControlViewHolder { @State protected ListLinkHandler tabHandler; @@ -39,7 +40,6 @@ public class ChannelTabFragment extends BaseListInfoFragment 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; - }); + PlayButtonHelper.initPlaylistControlClickListener( + activity, playlistControlBinding, this); } } - private PlayQueue getPlayQueue() { + public PlayQueue getPlayQueue() { final List streamItems = infoListAdapter.getItemsList().stream() .filter(StreamInfoItem.class::isInstance) .map(StreamInfoItem.class::cast) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistControlViewHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistControlViewHolder.java new file mode 100644 index 00000000000..2a785146c2a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistControlViewHolder.java @@ -0,0 +1,13 @@ +package org.schabi.newpipe.fragments.list.playlist; + +import org.schabi.newpipe.player.playqueue.PlayQueue; + +/** + * Interface for {@code R.layout.playlist_control} view holders + * to give access to the play queue. + */ +public interface PlaylistControlViewHolder { + + PlayQueue getPlayQueue(); + +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 8dd77bed65a..2b7cf9446fb 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -43,7 +43,6 @@ import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.playlist.RemotePlaylistManager; -import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.util.ExtractorHelper; @@ -51,6 +50,7 @@ import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.external_communication.ShareUtils; +import org.schabi.newpipe.util.PlayButtonHelper; import java.util.ArrayList; import java.util.List; @@ -64,7 +64,8 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; -public class PlaylistFragment extends BaseListInfoFragment { +public class PlaylistFragment extends BaseListInfoFragment + implements PlaylistControlViewHolder { private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG"; @@ -332,25 +333,10 @@ public void handleResult(@NonNull final PlaylistInfo result) { .observeOn(AndroidSchedulers.mainThread()) .subscribe(getPlaylistBookmarkSubscriber()); - 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; - }); + PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this); } - private PlayQueue getPlayQueue() { + public PlayQueue getPlayQueue() { return getPlayQueue(0); } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index a20a80ae985..1fea7e1559c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -28,14 +28,16 @@ import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder; import org.schabi.newpipe.info_list.dialog.InfoItemDialog; +import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; import org.schabi.newpipe.local.BaseLocalListFragment; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.settings.HistorySettingsFragment; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; -import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; +import org.schabi.newpipe.util.PlayButtonHelper; import java.util.ArrayList; import java.util.Collections; @@ -49,7 +51,8 @@ import io.reactivex.rxjava3.disposables.Disposable; public class StatisticsPlaylistFragment - extends BaseLocalListFragment, Void> { + extends BaseLocalListFragment, Void> + implements PlaylistControlViewHolder { private final CompositeDisposable disposables = new CompositeDisposable(); @State Parcelable itemsListState; @@ -195,14 +198,9 @@ public void onDestroyView() { if (itemListAdapter != null) { itemListAdapter.unsetSelectedListener(); } - if (playlistControlBinding != null) { - playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null); - playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null); - playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null); - headerBinding = null; - playlistControlBinding = null; - } + headerBinding = null; + playlistControlBinding = null; if (databaseSubscription != null) { databaseSubscription.cancel(); @@ -276,12 +274,8 @@ public void handleResult(@NonNull final List result) { itemsListState = null; } - 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)); + PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this); + headerBinding.sortButton.setOnClickListener(view -> toggleSortMode()); hideLoading(); @@ -374,7 +368,7 @@ private void deleteEntry(final int index) { } } - private PlayQueue getPlayQueue() { + public PlayQueue getPlayQueue() { return getPlayQueue(0); } diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index 2a639a69f0d..0d8f8133440 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -22,7 +22,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; import androidx.viewbinding.ViewBinding; @@ -42,17 +41,18 @@ import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder; import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; import org.schabi.newpipe.local.BaseLocalListFragment; import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.external_communication.ShareUtils; +import org.schabi.newpipe.util.PlayButtonHelper; import java.util.ArrayList; import java.util.Collections; @@ -69,7 +69,8 @@ import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.subjects.PublishSubject; -public class LocalPlaylistFragment extends BaseLocalListFragment, Void> { +public class LocalPlaylistFragment extends BaseLocalListFragment, Void> + implements PlaylistControlViewHolder { // Save the list 10 seconds after the last change occurred private static final long SAVE_DEBOUNCE_MILLIS = 10000; private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12; @@ -265,14 +266,10 @@ public void onDestroyView() { if (itemListAdapter != null) { itemListAdapter.unsetSelectedListener(); } - if (playlistControlBinding != null) { - playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null); - playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null); - playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null); - headerBinding = null; - playlistControlBinding = null; - } + headerBinding = null; + playlistControlBinding = null; + if (databaseSubscription != null) { databaseSubscription.cancel(); @@ -498,38 +495,11 @@ public void handleResult(@NonNull final List result) { } setVideoCount(itemListAdapter.getItemsList().size()); - playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> { - NavigationHelper.playOnMainPlayer(activity, getPlayQueue()); - showHoldToAppendTipIfNeeded(); - }); - playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> { - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false); - showHoldToAppendTipIfNeeded(); - }); - playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> { - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false); - showHoldToAppendTipIfNeeded(); - }); - playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> { - NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP); - return true; - }); - - playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> { - NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO); - return true; - }); + PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this); hideLoading(); } - private void showHoldToAppendTipIfNeeded() { - if (PreferenceManager.getDefaultSharedPreferences(activity) - .getBoolean(getString(R.string.show_hold_to_append_key), true)) { - Toast.makeText(activity, R.string.hold_to_append, Toast.LENGTH_SHORT).show(); - } - } - /////////////////////////////////////////////////////////////////////////// // Fragment Error Handling /////////////////////////////////////////////////////////////////////////// @@ -853,7 +823,7 @@ private void setVideoCount(final long count) { } } - private PlayQueue getPlayQueue() { + public PlayQueue getPlayQueue() { return getPlayQueue(0); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index 2cca0807270..b85b95eb03b 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -44,21 +44,11 @@ public final class NewPipeSettings { private NewPipeSettings() { } public static void initSettings(final Context context) { - // check if there are entries in the prefs to determine whether this is the first app run - Boolean isFirstRun = null; - final Set prefsKeys = PreferenceManager.getDefaultSharedPreferences(context) - .getAll().keySet(); - for (final String key: prefsKeys) { - // ACRA stores some info in the prefs during app initialization - // which happens before this method is called. Therefore ignore ACRA-related keys. - if (!key.toLowerCase().startsWith("acra")) { - isFirstRun = false; - break; - } - } - if (isFirstRun == null) { - isFirstRun = true; - } + // check if the last used preference version is set + // to determine whether this is the first app run + final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(context) + .getInt(context.getString(R.string.last_used_preferences_version), -1); + final boolean isFirstRun = lastUsedPrefVersion == -1; // first run migrations, then setDefaultValues, since the latter requires the correct types SettingMigrations.runMigrationsIfNeeded(context, isFirstRun); diff --git a/app/src/main/java/org/schabi/newpipe/util/PlayButtonHelper.java b/app/src/main/java/org/schabi/newpipe/util/PlayButtonHelper.java new file mode 100644 index 00000000000..a0707c6566c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/PlayButtonHelper.java @@ -0,0 +1,90 @@ +package org.schabi.newpipe.util; + +import android.content.Context; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.PreferenceManager; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.PlaylistControlBinding; +import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder; +import org.schabi.newpipe.player.PlayerType; + +/** + * Utility class for play buttons and their respective click listeners. + */ +public final class PlayButtonHelper { + + private PlayButtonHelper() { + // utility class + } + + /** + *

Initialize {@link android.view.View.OnClickListener OnClickListener} + * and {@link android.view.View.OnLongClickListener OnLongClickListener} for playlist control + * buttons defined in {@code R.layout.playlist_control}.

+ * + * @param activity The activity to use for the {@link android.widget.Toast Toast}. + * @param playlistControlBinding The binding of the + * {@link R.layout.playlist_control playlist control layout}. + * @param fragment The fragment to get the play queue from. + */ + public static void initPlaylistControlClickListener( + @NonNull final AppCompatActivity activity, + @NonNull final PlaylistControlBinding playlistControlBinding, + @NonNull final PlaylistControlViewHolder fragment) { + // click listener + playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> { + NavigationHelper.playOnMainPlayer(activity, fragment.getPlayQueue()); + showHoldToAppendToastIfNeeded(activity); + }); + playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> { + NavigationHelper.playOnPopupPlayer(activity, fragment.getPlayQueue(), false); + showHoldToAppendToastIfNeeded(activity); + }); + playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> { + NavigationHelper.playOnBackgroundPlayer(activity, fragment.getPlayQueue(), false); + showHoldToAppendToastIfNeeded(activity); + }); + + // long click listener + playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> { + NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.POPUP); + return true; + }); + playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> { + NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.AUDIO); + return true; + }); + } + + /** + *

Show the "hold to append" toast if the corresponding preference is enabled.

+ * + * @param context The context to show the toast. + */ + private static void showHoldToAppendToastIfNeeded(@NonNull final Context context) { + if (shouldShowHoldToAppendTip(context)) { + Toast.makeText(context, R.string.hold_to_append, Toast.LENGTH_SHORT).show(); + } + + } + + /** + *

Check if the "hold to append" toast should be shown.

+ * + *

+ * The tip is shown if the corresponding preference is enabled. + * This is the default behaviour. + *

+ * + * @param context The context to get the preference. + * @return {@code true} if the tip should be shown, {@code false} otherwise. + */ + public static boolean shouldShowHoldToAppendTip(@NonNull final Context context) { + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.show_hold_to_append_key), true); + } +} From 57eaa1bbe1c837531d5bcfe6de41e130e61b97c3 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Mon, 18 Sep 2023 15:01:17 +0200 Subject: [PATCH 48/50] Apply review Co-Authored-By: Audric V <74829229+AudricV@users.noreply.github.com> --- .../detail/BaseDescriptionFragment.java | 4 ++-- .../fragments/detail/DescriptionFragment.java | 9 +++++++- .../fragments/list/BaseListInfoFragment.java | 2 -- .../list/channel/ChannelAboutFragment.java | 9 +++++++- .../list/channel/ChannelFragment.java | 12 +++++----- .../list/channel/ChannelTabFragment.java | 10 +++++++- .../playlist/PlaylistControlViewHolder.java | 2 -- .../local/feed/service/FeedLoadManager.kt | 3 +-- .../playqueue/AbstractInfoPlayQueue.java | 23 +++++++++++-------- .../schabi/newpipe/util/ExtractorHelper.java | 8 +++---- .../schabi/newpipe/util/PlayButtonHelper.java | 10 ++++---- .../main/res/layout/fragment_channel_tab.xml | 2 +- 12 files changed, 58 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java index 3b1ede43270..ae334b761c4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java @@ -34,7 +34,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable; public abstract class BaseDescriptionFragment extends BaseFragment { - final CompositeDisposable descriptionDisposables = new CompositeDisposable(); + private final CompositeDisposable descriptionDisposables = new CompositeDisposable(); protected FragmentDescriptionBinding binding; @Override @@ -75,7 +75,7 @@ public void onDestroy() { protected abstract int getServiceId(); /** - * Get the URL of the described video. Used for generating description links. + * Get the URL of the described video or audio, used to generate description links. * @return stream URL */ @Nullable diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java index cb38d841645..92219883b28 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java @@ -52,6 +52,9 @@ protected StreamingService getService() { @Override protected int getServiceId() { + if (streamInfo == null) { + return -1; + } return streamInfo.getServiceId(); } @@ -76,13 +79,17 @@ public List getTags() { @Override protected void setupMetadata(final LayoutInflater inflater, final LinearLayout layout) { - if (streamInfo.getUploadDate() != null) { + if (streamInfo != null && streamInfo.getUploadDate() != null) { binding.detailUploadDateView.setText(Localization .localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime())); } else { binding.detailUploadDateView.setVisibility(View.GONE); } + if (streamInfo == null) { + return; + } + addMetadataItem(inflater, layout, false, R.string.metadata_category, streamInfo.getCategory()); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java index d30dadfd1eb..e7e9f5aad1d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java @@ -233,8 +233,6 @@ public void handleResult(@NonNull final L result) { showListFooter(hasMoreItems()); } else { infoListAdapter.clearStreamItemList(); - // showEmptyState should be called only if there is no item as - // well as no header in infoListAdapter showEmptyState(); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java index d1afd51a0cd..543fd80f322 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelAboutFragment.java @@ -62,6 +62,9 @@ protected StreamingService getService() { @Override protected int getServiceId() { + if (channelInfo == null) { + return -1; + } return channelInfo.getServiceId(); } @@ -83,10 +86,14 @@ public List getTags() { @Override protected void setupMetadata(final LayoutInflater inflater, final LinearLayout layout) { - final Context context = getContext(); // There is no upload date available for channels, so hide the relevant UI element binding.detailUploadDateView.setVisibility(View.GONE); + if (channelInfo == null) { + return; + } + + final Context context = getContext(); if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) { addMetadataItem(inflater, layout, false, R.string.metadata_subscribers, Localization.localizeNumber(context, channelInfo.getSubscriberCount())); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 6c0eb979223..2c618988004 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -125,7 +125,7 @@ public void onCreate(final Bundle savedInstanceState) { } @Override - public void onAttach(final @NonNull Context context) { + public void onAttach(@NonNull final Context context) { super.onAttach(context); subscriptionManager = new SubscriptionManager(activity); } @@ -138,7 +138,7 @@ public View onCreateView(@NonNull final LayoutInflater inflater, return binding.getRoot(); } - @Override // called from onViewCreated in {@link BaseFragment#onViewCreated} + @Override // called from onViewCreated in BaseFragment.onViewCreated protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); @@ -202,7 +202,7 @@ public void onCreateOptionsMenu(@NonNull final Menu menu, } @Override - public void onPrepareOptionsMenu(final @NonNull Menu menu) { + public void onPrepareOptionsMenu(@NonNull final Menu menu) { super.onPrepareOptionsMenu(menu); menuRssButton = menu.findItem(R.id.menu_item_rss); menuNotifyButton = menu.findItem(R.id.menu_item_notify); @@ -210,7 +210,7 @@ public void onPrepareOptionsMenu(final @NonNull Menu menu) { } @Override - public boolean onOptionsItemSelected(final MenuItem item) { + public boolean onOptionsItemSelected(@NonNull final MenuItem item) { switch (item.getItemId()) { case R.id.menu_item_notify: final boolean value = !item.isChecked(); @@ -561,8 +561,8 @@ private void runWorker(final boolean forceLoad) { .subscribe(result -> { isLoading.set(false); handleResult(result); - }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM, - url == null ? "no url" : url, serviceId))); + }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL, + url == null ? "No URL" : url, serviceId))); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java index 27315a99109..8712ab4d914 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java @@ -34,12 +34,15 @@ public class ChannelTabFragment extends BaseListInfoFragment implements PlaylistControlViewHolder { + // states must be protected and not private for IcePick being able to access them @State protected ListLinkHandler tabHandler; @State protected String channelName; private PlaylistControlBinding playlistControlBinding; + + @NonNull public static ChannelTabFragment getInstance(final int serviceId, final ListLinkHandler tabHandler, final String channelName) { @@ -99,11 +102,16 @@ protected Single> loadMoreItemsLogic() { @Override public void setTitle(final String title) { + // The channel name is displayed as title in the toolbar. + // The title is always a description of the content of the tab fragment. + // It should be unique for each channel because multiple channel tabs + // can be added to the main page. Therefore, the channel name is used. + // Using the title variable would cause the title to be the same for all channel tabs. super.setTitle(channelName); } @Override - public void handleResult(final @NonNull ChannelTabInfo result) { + public void handleResult(@NonNull final ChannelTabInfo result) { super.handleResult(result); if (playlistControlBinding != null) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistControlViewHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistControlViewHolder.java index 2a785146c2a..e4705bb7188 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistControlViewHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistControlViewHolder.java @@ -7,7 +7,5 @@ * to give access to the play queue. */ public interface PlaylistControlViewHolder { - PlayQueue getPlayQueue(); - } diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt index b0969a76941..b86c856fc25 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt @@ -140,8 +140,7 @@ class FeedLoadManager(private val context: Context) { subscriptionEntity: SubscriptionEntity, useFeedExtractor: Boolean, defaultSharedPreferences: SharedPreferences - ): - Notification { + ): Notification { var error: Throwable? = null val storeOriginalErrorAndRethrow = { e: Throwable -> // keep original to prevent blockingGet() from wrapping it into RuntimeException diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java index a0fc88eae45..33ec390a567 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java @@ -29,9 +29,12 @@ abstract class AbstractInfoPlayQueue> protected AbstractInfoPlayQueue(final T info) { this(info.getServiceId(), info.getUrl(), info.getNextPage(), - info.getRelatedItems().stream().filter(StreamInfoItem.class::isInstance) - .map(StreamInfoItem.class::cast).collect( - Collectors.toList()), 0); + info.getRelatedItems() + .stream() + .filter(StreamInfoItem.class::isInstance) + .map(StreamInfoItem.class::cast) + .collect(Collectors.toList()), + 0); } protected AbstractInfoPlayQueue(final int serviceId, @@ -76,10 +79,11 @@ public void onSuccess(@NonNull final T result) { } nextPage = result.getNextPage(); - append(extractListItems(result.getRelatedItems().stream() + append(extractListItems(result.getRelatedItems() + .stream() .filter(StreamInfoItem.class::isInstance) - .map(StreamInfoItem.class::cast).collect( - Collectors.toList()))); + .map(StreamInfoItem.class::cast) + .collect(Collectors.toList()))); fetchReactor.dispose(); fetchReactor = null; @@ -114,10 +118,11 @@ public void onSuccess( } nextPage = result.getNextPage(); - append(extractListItems(result.getItems().stream() + append(extractListItems(result.getItems() + .stream() .filter(StreamInfoItem.class::isInstance) - .map(StreamInfoItem.class::cast).collect( - Collectors.toList()))); + .map(StreamInfoItem.class::cast) + .collect(Collectors.toList()))); fetchReactor.dispose(); fetchReactor = null; diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index 257a428fcbd..07d0f516de2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -135,10 +135,10 @@ public static Single getChannelTab(final int serviceId, ChannelTabInfo.getInfo(NewPipe.getService(serviceId), listLinkHandler))); } - public static Single> getMoreChannelTabItems(final int serviceId, - final ListLinkHandler - listLinkHandler, - final Page nextPage) { + public static Single> getMoreChannelTabItems( + final int serviceId, + final ListLinkHandler listLinkHandler, + final Page nextPage) { checkServiceId(serviceId); return Single.fromCallable(() -> ChannelTabInfo.getMoreItems(NewPipe.getService(serviceId), diff --git a/app/src/main/java/org/schabi/newpipe/util/PlayButtonHelper.java b/app/src/main/java/org/schabi/newpipe/util/PlayButtonHelper.java index a0707c6566c..9727c808300 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PlayButtonHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PlayButtonHelper.java @@ -22,13 +22,13 @@ private PlayButtonHelper() { } /** - *

Initialize {@link android.view.View.OnClickListener OnClickListener} + * Initialize {@link android.view.View.OnClickListener OnClickListener} * and {@link android.view.View.OnLongClickListener OnLongClickListener} for playlist control - * buttons defined in {@code R.layout.playlist_control}.

+ * buttons defined in {@link R.layout#playlist_control}. * * @param activity The activity to use for the {@link android.widget.Toast Toast}. * @param playlistControlBinding The binding of the - * {@link R.layout.playlist_control playlist control layout}. + * {@link R.layout#playlist_control playlist control layout}. * @param fragment The fragment to get the play queue from. */ public static void initPlaylistControlClickListener( @@ -61,7 +61,7 @@ public static void initPlaylistControlClickListener( } /** - *

Show the "hold to append" toast if the corresponding preference is enabled.

+ * Show the "hold to append" toast if the corresponding preference is enabled. * * @param context The context to show the toast. */ @@ -73,7 +73,7 @@ private static void showHoldToAppendToastIfNeeded(@NonNull final Context context } /** - *

Check if the "hold to append" toast should be shown.

+ * Check if the "hold to append" toast should be shown. * *

* The tip is shown if the corresponding preference is enabled. diff --git a/app/src/main/res/layout/fragment_channel_tab.xml b/app/src/main/res/layout/fragment_channel_tab.xml index dd114cb77d2..62795c7da6d 100644 --- a/app/src/main/res/layout/fragment_channel_tab.xml +++ b/app/src/main/res/layout/fragment_channel_tab.xml @@ -47,4 +47,4 @@ android:layout_alignParentTop="true" android:background="?attr/toolbar_shadow" /> - \ No newline at end of file + From 64da7a06c02647d856402f89d7115a83a50ade04 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Mon, 18 Sep 2023 15:52:29 +0200 Subject: [PATCH 49/50] Fix previous ActionBar title visible for a few miliseconds when opening ChannelFragment --- .../schabi/newpipe/fragments/list/channel/ChannelFragment.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 2c618988004..c1345180ba5 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -146,6 +146,7 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) { binding.viewPager.setAdapter(tabAdapter); binding.tabLayout.setupWithViewPager(binding.viewPager); + setTitle(name); binding.channelTitleView.setText(name); if (!PicassoHelper.getShouldLoadImages()) { // do not waste space for the banner if it is not going to be loaded From 031b893196cf9e1ce5016b9a836f8476bd142cde Mon Sep 17 00:00:00 2001 From: TobiGr Date: Mon, 18 Sep 2023 15:55:41 +0200 Subject: [PATCH 50/50] Remove unused content not supported TextView --- app/src/main/res/layout/fragment_channel_videos.xml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/src/main/res/layout/fragment_channel_videos.xml b/app/src/main/res/layout/fragment_channel_videos.xml index 9e22575391d..77d14b020db 100644 --- a/app/src/main/res/layout/fragment_channel_videos.xml +++ b/app/src/main/res/layout/fragment_channel_videos.xml @@ -49,15 +49,6 @@ android:text="@string/empty_view_no_videos" android:textSize="24sp" /> - -