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 | 571 +++-------------- .../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, 997 insertions(+), 543 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 396f119b2..22ac7d67d 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 8a0b49249..6989552f2 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 { +public class ChannelFragment extends BaseStateFragment { + @State + protected int serviceId = Constants.NO_SERVICE_ID; + @State + protected String name; + @State + protected String url; - private static final int BUTTON_DEBOUNCE_INTERVAL = 100; - private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG"; + private ChannelInfo currentInfo; + private Disposable currentWorker; - private final CompositeDisposable disposables = new CompositeDisposable(); - private Disposable subscribeButtonMonitor; - - private boolean channelContentNotSupported = false; + private MenuItem menuRssButton; /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ - private SubscriptionManager subscriptionManager; - - private FragmentChannelBinding channelBinding; - private ChannelHeaderBinding headerBinding; - private PlaylistControlBinding playlistControlBinding; - - private MenuItem menuRssButton; - private MenuItem menuNotifyButton; + private FragmentChannelBinding binding; + private TabAdapter tabAdapter; public static ChannelFragment getInstance(final int serviceId, final String url, final String name) { @@ -100,15 +59,13 @@ public class ChannelFragment extends BaseListInfoFragment 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 //////////////////////////////////////////////////////////////////////////*/ @@ -176,19 +109,14 @@ public class ChannelFragment extends BaseListInfoFragment onError = (Throwable throwable) -> { - animate(headerBinding.channelSubscribeButton, false, 100); - showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET, - "Get subscription status", currentInfo)); - }; + private void updateTabs() { + tabAdapter.clearAllItems(); - final Observable> observable = subscriptionManager - .subscriptionTable() - .getSubscriptionFlowable(info.getServiceId(), info.getUrl()) - .toObservable(); + if (currentInfo != null) { + tabAdapter.addFragment(ChannelVideosFragment.getInstance(currentInfo), "Videos"); - disposables.add(observable - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getSubscribeUpdateMonitor(info), onError)); - - disposables.add(observable - .map(List::isEmpty) - .distinctUntilChanged() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(isEmpty -> updateSubscribeButton(!isEmpty), onError)); - - disposables.add(observable - .map(List::isEmpty) - .distinctUntilChanged() - .skip(1) // channel has just been opened - .filter(x -> NotificationHelper.areNewStreamsNotificationsEnabled(requireContext())) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(isEmpty -> { - if (!isEmpty) { - showNotifySnackbar(); - } - }, onError)); - } - - 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()); + for (final ChannelTabHandler tab : currentInfo.getTabs()) { + tabAdapter.addFragment( + ChannelTabFragment.getInstance(serviceId, tab), tab.getTab().name()); } - }; - final Consumer onError = (@NonNull Throwable throwable) -> - showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_UPDATE, - "Updating subscription for " + info.getUrl(), info)); + final String description = currentInfo.getDescription(); + if (!description.isEmpty()) { + tabAdapter.addFragment(ChannelInfoFragment.getInstance(description), "Info"); + } + } - disposables.add(subscriptionManager.updateChannelInfo(info) + tabAdapter.notifyDataSetUpdate(); + + for (int i = 0; i < tabAdapter.getCount(); i++) { + binding.tabLayout.getTabAt(i).setText(tabAdapter.getItemTitle(i)); + } + } + + @Override + public void startLoading(final boolean forceLoad) { + super.startLoading(forceLoad); + + currentInfo = null; + updateTabs(); + if (currentWorker != null) { + currentWorker.dispose(); + } + + runWorker(forceLoad); + } + + private void runWorker(final boolean forceLoad) { + currentWorker = ExtractorHelper.getChannelInfo(serviceId, url, forceLoad) .subscribeOn(Schedulers.io()) .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); - } - - private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) { - if (menuNotifyButton == null) { - return; - } - if (subscription != null) { - menuNotifyButton.setEnabled( - NotificationHelper.areNewStreamsNotificationsEnabled(requireContext()) - ); - menuNotifyButton.setChecked( - subscription.getNotificationMode() == NotificationMode.ENABLED - ); - } - - menuNotifyButton.setVisible(subscription != null); - } - - private void setNotify(final boolean isEnabled) { - disposables.add( - subscriptionManager - .updateNotificationMode( - currentInfo.getServiceId(), - currentInfo.getUrl(), - isEnabled ? NotificationMode.ENABLED : NotificationMode.DISABLED) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe() - ); - } - - /** - * Show a snackbar with the option to enable notifications on new streams for this channel. - */ - private void showNotifySnackbar() { - Snackbar.make(itemsList, R.string.you_successfully_subscribed, Snackbar.LENGTH_LONG) - .setAction(R.string.get_notified, v -> setNotify(true)) - .setActionTextColor(Color.YELLOW) - .show(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Load and handle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected Single> loadMoreItemsLogic() { - return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage); + .subscribe(result -> { + isLoading.set(false); + handleResult(result); + }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM, + url == null ? "no url" : url, serviceId))); } @Override - protected Single loadResult(final boolean forceLoad) { - return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad); - } + public void handleResult(@NonNull final ChannelInfo info) { + super.handleResult(info); - /*////////////////////////////////////////////////////////////////////////// - // OnClick - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onClick(final View v) { - if (isLoading.get() || currentInfo == null) { - return; - } - - switch (v.getId()) { - case R.id.sub_channel_avatar_view: - case R.id.sub_channel_title_view: - if (!TextUtils.isEmpty(currentInfo.getParentChannelUrl())) { - try { - NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(), - currentInfo.getParentChannelUrl(), - currentInfo.getParentChannelName()); - } catch (final Exception e) { - ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e); - } - } else if (DEBUG) { - Log.i(TAG, "Can't open parent channel because we got no channel URL"); - } - break; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Contract - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void showLoading() { - super.showLoading(); - PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG); - animate(headerBinding.channelSubscribeButton, false, 100); - } - - @Override - public void handleResult(@NonNull final ChannelInfo result) { - super.handleResult(result); - - headerBinding.getRoot().setVisibility(View.VISIBLE); - PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG) - .into(headerBinding.channelBannerImage); - PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG) - .into(headerBinding.channelAvatarView); - PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG) - .into(headerBinding.subChannelAvatarView); - - headerBinding.channelSubscriberView.setVisibility(View.VISIBLE); - if (result.getSubscriberCount() >= 0) { - headerBinding.channelSubscriberView.setText(Localization - .shortSubscriberCount(activity, result.getSubscriberCount())); - } else { - headerBinding.channelSubscriberView.setText(R.string.subscribers_count_not_available); - } - - if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) { - headerBinding.subChannelTitleView.setText(String.format( - getString(R.string.channel_created_by), - currentInfo.getParentChannelName()) - ); - headerBinding.subChannelTitleView.setVisibility(View.VISIBLE); - headerBinding.subChannelAvatarView.setVisibility(View.VISIBLE); - } else { - headerBinding.subChannelTitleView.setVisibility(View.GONE); - } - - if (menuRssButton != null) { - menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl())); - } - - // PlaylistControls should be visible only if there is some item in - // infoListAdapter other than header - if (infoListAdapter.getItemCount() != 1) { - playlistControlBinding.getRoot().setVisibility(View.VISIBLE); - } else { - playlistControlBinding.getRoot().setVisibility(View.GONE); - } - - channelContentNotSupported = false; - for (final Throwable throwable : result.getErrors()) { - if (throwable instanceof ContentNotSupportedException) { - channelContentNotSupported = true; - showContentNotSupportedIfNeeded(); - break; - } - } - - disposables.clear(); - if (subscribeButtonMonitor != null) { - subscribeButtonMonitor.dispose(); - } - updateSubscription(result); - monitorSubscription(result); - - playlistControlBinding.playlistCtrlPlayAllButton - .setOnClickListener(view -> NavigationHelper - .playOnMainPlayer(activity, getPlayQueue())); - playlistControlBinding.playlistCtrlPlayPopupButton - .setOnClickListener(view -> NavigationHelper - .playOnPopupPlayer(activity, getPlayQueue(), false)); - playlistControlBinding.playlistCtrlPlayBgButton - .setOnClickListener(view -> NavigationHelper - .playOnBackgroundPlayer(activity, getPlayQueue(), false)); - - playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> { - NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP); - return true; - }); - - playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> { - NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO); - return true; - }); - } - - private void showContentNotSupportedIfNeeded() { - // channelBinding might not be initialized when handleResult() is called - // (e.g. after rotating the screen, #6696) - if (!channelContentNotSupported || channelBinding == null) { - return; - } - - channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE); - channelBinding.channelKaomoji.setText("(︶︹︺)"); - channelBinding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f); - channelBinding.emptyStateMessage.setVisibility(View.GONE); - } - - private PlayQueue getPlayQueue() { - final List 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 000000000..c9273f528 --- /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 000000000..12514a55c --- /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 000000000..9f8c83ce7 --- /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 7e3f5d0c8..a06bf32d4 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.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.fragments.BlankFragment; -import org.schabi.newpipe.fragments.list.channel.ChannelFragment; +import org.schabi.newpipe.fragments.list.channel.ChannelVideosFragment; import org.schabi.newpipe.fragments.list.kiosk.DefaultKioskFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; @@ -432,8 +432,8 @@ public abstract class Tab { } @Override - public ChannelFragment getFragment(final Context context) { - return ChannelFragment.getInstance(channelServiceId, channelUrl, channelName); + public ChannelVideosFragment getFragment(final Context context) { + return ChannelVideosFragment.getInstance(channelServiceId, channelUrl, channelName); } @Override 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 d5d472d6f..b4648c79b 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.NewPipe; import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.channel.ChannelInfo; +import org.schabi.newpipe.extractor.channel.ChannelTabInfo; import org.schabi.newpipe.extractor.comments.CommentsInfo; import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.feed.FeedExtractor; import org.schabi.newpipe.extractor.feed.FeedInfo; import org.schabi.newpipe.extractor.kiosk.KioskInfo; +import org.schabi.newpipe.extractor.linkhandler.ChannelTabHandler; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.search.SearchInfo; import org.schabi.newpipe.extractor.stream.StreamInfo; @@ -151,6 +153,25 @@ public final class ExtractorHelper { return maybeFeedInfo.switchIfEmpty(getChannelInfo(serviceId, url, true)); } + public static Single 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 @@ public final class ExtractorHelper { 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 @@ public final class ExtractorHelper { /** * 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 final class ExtractorHelper { * Formats the text contained in the meta info list as HTML and puts it into the text view, * while also making the separator visible. If the list is null or empty, or the user chose not * to see meta information, both the text view and the separator are hidden - * @param metaInfos a list of meta information, can be null or empty - * @param metaInfoTextView the text view in which to show the formatted HTML + * + * @param 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 final class ExtractorHelper { 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 714b9d4f9..d938f71a7 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 000000000..fbb8e355b --- /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 000000000..519156296 --- /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 000000000..2dfb2fbf6 --- /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 000000000..a3b2e7ae0 --- /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 6989552f2..4938d1c00 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.NonNull; 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.channel.ChannelInfo; 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 class ChannelFragment extends BaseStateFragment { 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 @@ public class ChannelFragment extends BaseStateFragment { @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 class ChannelFragment extends BaseStateFragment { + "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 @@ public class ChannelFragment extends BaseStateFragment { } } + 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 class ChannelFragment extends BaseStateFragment { 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 12514a55c..21613d717 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 @@ package org.schabi.newpipe.fragments.list.channel; 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 class ChannelTabFragment extends BaseListInfoFragment - - - 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 4938d1c00..4cd8313fc 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.databinding.FragmentChannelBinding; 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 @@ public class ChannelFragment extends BaseStateFragment { // 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 @@ public class ChannelFragment extends BaseStateFragment { } @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 f147e4f9d..23655dee2 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 class ChannelVideosFragment extends BaseListInfoFragment + + + + + + + + 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 4cd8313fc..6e7473b1e 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 @@ public class ChannelFragment extends BaseStateFragment { 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 c9273f528..2ab4ce419 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 class ChannelInfoFragment extends BaseFragment { 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 21613d717..5a26371b7 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 @@ package org.schabi.newpipe.fragments.list.channel; 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 fbb8e355b..8cbadba1f 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 dfc26a4e2..c5260fa03 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 a37bfeb82..7d65807b7 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 519156296..dd114cb77 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 8cbadba1f..c9648e01f 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 6e7473b1e..f71791d8e 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 android.view.ViewGroup; 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.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.Constants; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -268,13 +271,22 @@ public class ChannelFragment extends BaseStateFragment { 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 2ab4ce419..6e7e49876 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 @@ public class ChannelInfoFragment extends BaseFragment { 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 000000000..983daf349 --- /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 c5260fa03..76abdfbe2 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 56fc19eed..00c501643 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 7d65807b7..fd0971761 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 fddb966c8..8783ff1ed 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 f71791d8e..51625d202 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.NonNull; 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 @@ public class ChannelFragment extends BaseStateFragment { 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 @@ public class ChannelFragment extends BaseStateFragment { 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 @@ public class ChannelFragment extends BaseStateFragment { } 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 @@ public class ChannelFragment extends BaseStateFragment { 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 6e7e49876..ba1faab8f 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 @@ public class ChannelInfoFragment extends BaseFragment { 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 5a26371b7..1ce55df81 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 class ChannelTabFragment extends BaseListInfoFragment 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 0147e9c08..029339cef 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 @@ public final class ChannelTabs { 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 final class ChannelTabs { 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 76abdfbe2..1720d2b0a 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 00c501643..9746d7889 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 fd0971761..87577a81a 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 029339cef..b861824d5 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 @@ public final class ChannelTabs { 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 51625d202..d7955eb9d 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.ChannelTabs; 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.disposables.Disposable; 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 @@ public class ChannelFragment extends BaseStateFragment { 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 @@ public class ChannelFragment extends BaseStateFragment { 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 @@ public class ChannelFragment extends BaseStateFragment { 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 @@ public class ChannelFragment extends BaseStateFragment { } } + /*////////////////////////////////////////////////////////////////////////// + // 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 b4648c79b..bf99ae3d3 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 final class ExtractorHelper { 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 d7955eb9d..322093781 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.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.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 @@ public class ChannelFragment extends BaseStateFragment 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 1ce55df81..d00cb5cf9 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.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 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 322093781..f0810a03c 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 @@ public class ChannelFragment extends BaseStateFragment 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 @@ public class ChannelFragment extends BaseStateFragment 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 f0810a03c..8b6c0084e 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 @@ public class ChannelFragment extends BaseStateFragment 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 d00cb5cf9..3f400bdf8 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 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 22ac7d67d..10dd4bef0 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 | 143 ++++-------- 3 files changed, 286 insertions(+), 249 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 000000000..d8aea1a03 --- /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 d364c0c0f..cf99365dc 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 class DescriptionFragment extends BaseFragment { 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 + protected StreamingService getService() { + if (streamInfo == null) { + return null; + } + return streamInfo.getService(); } @Override - public void onDestroy() { - descriptionDisposables.clear(); - super.onDestroy(); + protected int getServiceId() { + return streamInfo.getServiceId(); } + @Nullable + @Override + protected String getStreamUrl() { + if (streamInfo == null) { + return null; + } + return streamInfo.getUrl(); + } - private void setupUploadDate() { + @Nullable + @Override + public List getTags() { + if (streamInfo == null) { + return null; + } + return streamInfo.getTags(); + } + + 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 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; - } - - // 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, - 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); - } - - private void setupMetadata(final LayoutInflater inflater, - final LinearLayout layout) { addMetadataItem(inflater, layout, false, R.string.metadata_category, streamInfo.getCategory()); @@ -153,67 +106,6 @@ public class DescriptionFragment extends BaseFragment { 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 ba1faab8f..70b182a75 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 class ChannelInfoFragment extends BaseFragment { 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(); - } - - @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 Description getDescription() { + if (channelInfo == null) { + return null; } + return new Description(channelInfo.getDescription(), Description.PLAIN_TEXT); } - private void setupMetadata(final LayoutInflater inflater, - final LinearLayout layout) { + @Nullable + @Override + protected StreamingService getService() { + if (channelInfo == null) { + return null; + } + return channelInfo.getService(); + } + + @Override + protected int getServiceId() { + return channelInfo.getServiceId(); + } + + @Nullable + @Override + protected String getStreamUrl() { + return null; + } + + @Nullable + @Override + public List getTags() { + if (channelInfo == null) { + return null; + } + return channelInfo.getTags(); + } + + protected 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, + addMetadataItem(inflater, layout, false, 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; } } 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 10dd4bef0..b7430287a 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 c9648e01f..000000000 --- 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 8b6c0084e..96f2522eb 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 class ChannelFragment extends BaseStateFragment 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 d8aea1a03..fbbfdf23f 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 java.util.List; 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 cf99365dc..ded4e907a 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 java.util.List; 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 70b182a75..ae04e8b00 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.R; 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 96f2522eb..95aa2c45a 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 @@ public class ChannelFragment extends BaseStateFragment 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 9746d7889..d32fbce0c 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 87577a81a..259689231 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 95aa2c45a..96de433f5 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 @@ public class ChannelFragment extends BaseStateFragment @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 | 411 +++++++++++++---- .../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 | 242 +++++++--- app/src/main/res/values-land/dimens.xml | 1 - app/src/main/res/values/dimens.xml | 1 - 8 files changed, 541 insertions(+), 673 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 96de433f5..9de143518 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 android.view.ViewGroup; 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 class ChannelFragment extends BaseStateFragment 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 @@ public class ChannelFragment extends BaseStateFragment 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 @@ public class ChannelFragment extends BaseStateFragment 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 class ChannelFragment extends BaseStateFragment if (currentWorker != null) { currentWorker.dispose(); } - if (subscriptionMonitor != null) { - subscriptionMonitor.dispose(); - } disposables.clear(); binding = null; } - /*////////////////////////////////////////////////////////////////////////// + + /*////////////////////////////////////////////////////////////////////////// // Menu //////////////////////////////////////////////////////////////////////////*/ @@ -164,8 +195,6 @@ public class ChannelFragment extends BaseStateFragment } menuRssButton = menu.findItem(R.id.menu_item_rss); menuNotifyButton = menu.findItem(R.id.menu_item_notify); - updateRssButton(); - monitorSubscription(); } @Override @@ -201,37 +230,168 @@ public class ChannelFragment extends BaseStateFragment 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 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 Function mapOnSubscribe(final SubscriptionEntity subscription, + final ChannelInfo info) { + return (@NonNull Object o) -> { + subscriptionManager.insertSubscription(subscription, info); + return o; + }; } - private Consumer> getSubscribeUpdateMonitor() { - return (List subscriptionEntities) -> { - if (subscriptionEntities.isEmpty()) { - updateNotifyButton(null); - } else { - final SubscriptionEntity subscription = subscriptionEntities.get(0); - updateNotifyButton(subscription); + 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 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(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) { @@ -263,52 +423,48 @@ public class ChannelFragment extends BaseStateFragment ); } + /** + * 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"); + if (currentInfo != null && !channelContentNotSupported) { + tabAdapter.addFragment(new ChannelVideosFragment(currentInfo), "Videos"); - final Context context = getContext(); - final SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(context); + final Context context = requireContext(); + 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))); - } - } - - 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 @@ public class ChannelFragment extends BaseStateFragment } } + /*////////////////////////////////////////////////////////////////////////// // State Saving //////////////////////////////////////////////////////////////////////////*/ @@ -336,11 +493,7 @@ public class ChannelFragment extends BaseStateFragment @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 class ChannelFragment extends BaseStateFragment 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 @@ public class ChannelFragment extends BaseStateFragment 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 a38b913d6..a2d50836b 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,109 +1,61 @@ 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; - - /*////////////////////////////////////////////////////////////////////////// - // Views - //////////////////////////////////////////////////////////////////////////*/ - - 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; - } - 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; - } + /*////////////////////////////////////////////////////////////////////////// + // 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(); @@ -112,22 +64,12 @@ public class ChannelVideosFragment extends BaseListInfoFragment 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 @@ public class ChannelVideosFragment extends BaseListInfoFragment= 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 class ChannelVideosFragment 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.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 class ChannelVideosFragment extends BaseListInfoFragment streamItems = infoListAdapter.getItemsList().stream() .filter(StreamInfoItem.class::isInstance) @@ -514,14 +154,4 @@ public class ChannelVideosFragment extends BaseListInfoFragment - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml index db77391bc..29d9143c5 100644 --- a/app/src/main/res/layout/fragment_channel.xml +++ b/app/src/main/res/layout/fragment_channel.xml @@ -1,75 +1,207 @@ - - + app:elevation="0dp"> - + + + + + + + + + + + + + + + + + + + + + - - - - + app:layout_behavior="@string/appbar_scrolling_view_behavior"> - + + + + + android:layout_centerInParent="true" + android:indeterminate="true" + android:visibility="gone" + tools:visibility="visible" /> - + android:layout_centerInParent="true" + android:orientation="vertical" + android:paddingTop="90dp" + android:visibility="gone" + tools:visibility="visible"> - + - - - \ No newline at end of file + + + + + + + + \ 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 77e18695d..46244b3c9 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 e47b72c9a..0e5fd126f 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 9de143518..2947cdb16 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 class ChannelFragment extends BaseStateFragment 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 @@ public class ChannelFragment extends BaseStateFragment 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 29d9143c5..15995f8f3 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 b7430287a..1f924b12f 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 70c377de9..c59dc7532 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.PrivateContentException; 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.local.dialog.PlaylistDialog; 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 class RouterActivity extends AppCompatActivity { } 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 c73ae8be0..d30dadfd1 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.InfoItem; 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 abstract class BaseListInfoFragment }, 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 @@ public class ChannelFragment extends BaseStateFragment 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 @@ public class ChannelFragment extends BaseStateFragment 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 a2d50836b..000000000 --- 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 5aca3ad26..782f5ee47 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 fec50a579..be2c2490e 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 bde301b92..f960040de 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 5f72a6b84..12fbe8d41 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 b17f49801..9a8b53e90 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 af598b106..66164807d 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.content.Intent; 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.R; 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 org.schabi.newpipe.util.ExtractorHelper; 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 @@ public class SubscriptionsImportService extends BaseImportExportService { .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 @@ public class SubscriptionsImportService extends BaseImportExportService { } private Subscriber> getSubscriber() { - return new Subscriber>() { + return new Subscriber<>() { @Override public void onSubscribe(final Subscription s) { subscription = s; @@ -254,10 +264,11 @@ public class SubscriptionsImportService extends BaseImportExportService { }; } - 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 @@ public class SubscriptionsImportService extends BaseImportExportService { }; } - 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 e51ee4720..a0fc88eae 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 android.util.Log; 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 java.util.stream.Collectors; 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 @@ abstract class AbstractInfoPlayQueue> } 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 @@ abstract class AbstractInfoPlayQueue> }; } - SingleObserver> getNextPageObserver() { + SingleObserver> getNextPageObserver() { return new SingleObserver<>() { @Override public void onSubscribe(@NonNull final Disposable d) { @@ -101,13 +108,16 @@ abstract class AbstractInfoPlayQueue> @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 1e1fef85e..000000000 --- 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 000000000..e422a5c52 --- /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 b5375075f..7e3f5d0c8 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.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.fragments.BlankFragment; -import org.schabi.newpipe.fragments.list.channel.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 abstract class Tab { } @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 fb384e076..974445a96 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 androidx.annotation.StringRes; 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 @@ public final class ChannelTabHelper { @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 d8d68f0e4..59a5df205 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.R; 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 final class ExtractorHelper { 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 d32fbce0c..9c5610703 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 259689231..565c91b54 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 3a8d46a4d..46faaf277 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 @@ public class ChannelFragment extends BaseStateFragment 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 3f400bdf8..df3aced50 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.ListExtractor; 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 @@ import io.reactivex.rxjava3.core.Single; 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 974445a96..d10e0a3dd 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 final class ChannelTabHelper { case ChannelTabs.SHORTS: case ChannelTabs.LIVESTREAMS: return true; + default: + return false; } - return false; } /** @@ -61,8 +62,9 @@ public final class ChannelTabHelper { 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 final class ChannelTabHelper { 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 df3aced50..86e429bea 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.NonNull; 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 @@ public class ChannelTabFragment extends BaseListInfoFragment 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 1f924b12f..d73cca424 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 be2c2490e..b55549704 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 d10e0a3dd..5db438863 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 @@ public final class ChannelTabHelper { } } + @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 final class ChannelTabHelper { } 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 9c5610703..d9d8e60be 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 565c91b54..44e8f5e74 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 8783ff1ed..73a849af7 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 15995f8f3..e1744739c 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 fbbfdf23f..47f8598af 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 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable; 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 ae04e8b00..271be3939 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 @@ package org.schabi.newpipe.fragments.list.channel; 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.StreamingService; 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 class ChannelAboutFragment extends BaseDescriptionFragment { 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 157b8f394..b20905d4a 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 271be3939..e78d5a922 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 @@ public class ChannelAboutFragment extends BaseDescriptionFragment { 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 44e8f5e74..e6fb35a16 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 46faaf277..f709fc226 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 @@ public class ChannelFragment extends BaseStateFragment 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 class ChannelFragment extends BaseStateFragment 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 750b8e799..ece0c7e87 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 final class PicassoHelper { } 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 e1744739c..cd3e371c5 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 ea9a3f36e..4f153dcf8 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 27613e83e..665ebbe43 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 58f9e9edc..728570b17 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 b55549704..c9593e537 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 d9d8e60be..51abe14fb 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 cd3e371c5..f557e3396 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 d73cca424..051414ba0 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 86e429bea..6b2dd20bf 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.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.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 9a8b53e90..3c11ce152 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 66164807d..d624e1038 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.R; 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 e422a5c52..a9eb2a19c 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 @@ package org.schabi.newpipe.player.playqueue; 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 5db438863..8e8d38490 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 android.content.SharedPreferences; 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 59a5df205..257a428fc 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.MetaInfo; 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 ded4e907a..cb38d8416 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 class DescriptionFragment extends BaseDescriptionFragment { 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 e78d5a922..4117533bd 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 class ChannelAboutFragment extends BaseDescriptionFragment { 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 c9593e537..b0969a769 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 47f8598af..3b1ede432 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 @@ public abstract class BaseDescriptionFragment extends BaseFragment { 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 @@ public abstract class BaseDescriptionFragment extends BaseFragment { } 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 f709fc226..6c0eb9792 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 @@ public class ChannelFragment extends BaseStateFragment } } - 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 4117533bd..d1afd51a0 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 class ChannelAboutFragment extends BaseDescriptionFragment { 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 1720d2b0a..5283f1afa 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 e71083d2c..213b679f0 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.Rule; 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 class SubscriptionManagerTest { 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 class SubscriptionManagerTest { 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 class SubscriptionManagerTest { 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 def1774b7..686e102f1 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.StreamTypeUtil; 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 @@ public final class VideoDetailFragment })); 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 @@ public final class VideoDetailFragment 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 6b2dd20bf..27315a991 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.channel.tabs.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.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 java.util.stream.Collectors; 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 000000000..2a785146c --- /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 8dd77bed6..2b7cf9446 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.InfoItemDialog; 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.Localization; 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.core.Single; 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 class PlaylistFragment 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() { 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 a20a80ae9..1fea7e155 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.databinding.StatisticPlaylistControlBinding; 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.CompositeDisposable; 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 class StatisticsPlaylistFragment 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 class StatisticsPlaylistFragment 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 @@ public class StatisticsPlaylistFragment } } - 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 2a639a69f..0d8f81334 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 android.widget.Toast; 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.databinding.PlaylistControlBinding; 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.disposables.Disposable; 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 class LocalPlaylistFragment extends BaseLocalListFragment { - 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 @@ public class LocalPlaylistFragment extends BaseLocalListFragment 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 000000000..a0707c656 --- /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 3b1ede432..ae334b761 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 java.util.List; 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 abstract class BaseDescriptionFragment extends BaseFragment { 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 cb38d8416..92219883b 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 @@ public class DescriptionFragment extends BaseDescriptionFragment { @Override protected int getServiceId() { + if (streamInfo == null) { + return -1; + } return streamInfo.getServiceId(); } @@ -76,13 +79,17 @@ public class DescriptionFragment extends BaseDescriptionFragment { @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 d30dadfd1..e7e9f5aad 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 abstract class BaseListInfoFragment } @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 class ChannelFragment extends BaseStateFragment 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 class ChannelFragment extends BaseStateFragment } @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 class ChannelFragment extends BaseStateFragment } @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 @@ public class ChannelFragment extends BaseStateFragment .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 27315a991..8712ab4d9 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 @@ import io.reactivex.rxjava3.core.Single; 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 @@ public class ChannelTabFragment extends BaseListInfoFragment { + ): 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 a0fc88eae..33ec390a5 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 @@ abstract class AbstractInfoPlayQueue> } 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 @@ abstract class AbstractInfoPlayQueue> } 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 257a428fc..07d0f516d 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 final class ExtractorHelper { 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 a0707c656..9727c8083 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 @@ public final class 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 final class PlayButtonHelper { } /** - *

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 @@ public final class PlayButtonHelper { } /** - *

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 dd114cb77..62795c7da 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 2c6189880..c1345180b 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 @@ public class ChannelFragment extends BaseStateFragment 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 9e2257539..77d14b020 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" /> - -