From 5539606a46911e1cc98953cab839c6b5eca11ed1 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sun, 8 Mar 2020 15:06:45 +0100 Subject: [PATCH 01/22] Add initial support for tabs --- app/build.gradle | 4 +- .../org/schabi/newpipe/RouterActivity.java | 18 +- .../detail/ChannelDetailFragment.java | 557 ++++++++++++++++++ .../list/channel/ChannelFragment.java | 465 --------------- .../list/channel/ChannelTabFragment.java | 167 ++++++ .../player/playqueue/ChannelPlayQueue.java | 74 +-- .../org/schabi/newpipe/settings/tabs/Tab.java | 6 +- .../schabi/newpipe/util/ExtractorHelper.java | 7 +- .../schabi/newpipe/util/NavigationHelper.java | 4 +- app/src/main/res/layout/channel_header.xml | 95 --- .../res/layout/fragment_channel_detail.xml | 147 +++++ ...t_channel.xml => fragment_channel_tab.xml} | 7 +- 12 files changed, 932 insertions(+), 619 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java delete mode 100644 app/src/main/res/layout/channel_header.xml create mode 100644 app/src/main/res/layout/fragment_channel_detail.xml rename app/src/main/res/layout/{fragment_channel.xml => fragment_channel_tab.xml} (93%) diff --git a/app/build.gradle b/app/build.gradle index ec6b36d43da..ab8af7d3024 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -92,7 +92,7 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.TeamNewPipe:NewPipeExtractor:65a7eda' + implementation 'com.github.wb9688:NewPipeExtractor:5daba8dd3c34bc1318a7062976ec172779e58b5b' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' @@ -164,4 +164,4 @@ static String getGitWorkingBranch() { // git was not found return "" } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 1be6e096a22..f7d1b3187dd 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -9,12 +9,6 @@ import android.content.pm.PackageManager; import android.os.Bundle; import android.preference.PreferenceManager; -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; import android.text.TextUtils; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; @@ -26,6 +20,12 @@ import android.widget.RadioGroup; import android.widget.Toast; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.NotificationCompat; import androidx.fragment.app.FragmentManager; import org.schabi.newpipe.download.DownloadDialog; @@ -38,7 +38,6 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; @@ -71,6 +70,8 @@ import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO; import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; +//import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; + /** * Get the url from the intent and open it in the chosen preferred player */ @@ -591,7 +592,8 @@ public Consumer getResultHandler(Choice choice) { } if (info instanceof ChannelInfo || info instanceof PlaylistInfo) { - playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info); +// playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info); + playQueue = new PlaylistPlayQueue((PlaylistInfo) info); if (playerChoice.equals(videoPlayerKey)) { NavigationHelper.playOnMainPlayer(this, playQueue, true); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java new file mode 100644 index 00000000000..2ddc5a00a1a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java @@ -0,0 +1,557 @@ +package org.schabi.newpipe.fragments.detail; + +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.TextUtils; +import android.util.Log; +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 android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.viewpager.widget.ViewPager; + +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.tabs.TabLayout; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.ReCaptchaActivity; +import org.schabi.newpipe.extractor.InfoItem; +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.exceptions.ContentNotAvailableException; +import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; +import org.schabi.newpipe.fragments.BackPressable; +import org.schabi.newpipe.fragments.BaseStateFragment; +import org.schabi.newpipe.fragments.list.channel.ChannelTabFragment; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.ImageDisplayConstants; +import org.schabi.newpipe.util.InfoCache; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.ShareUtils; + +import java.io.Serializable; +import java.util.Collection; +import java.util.LinkedList; + +import icepick.State; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +public class ChannelDetailFragment + extends BaseStateFragment + implements BackPressable, + SharedPreferences.OnSharedPreferenceChangeListener, + View.OnClickListener, + View.OnLongClickListener { + + private int updateFlags = 0; + + @State + protected int serviceId = Constants.NO_SERVICE_ID; + @State + protected String name; + @State + protected String url; + + private ChannelInfo currentInfo; + private Disposable currentWorker; + @NonNull + private CompositeDisposable disposables = new CompositeDisposable(); + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + private Menu menu; + + private AppBarLayout appBarLayout; + private ViewPager viewPager; + private TabAdaptor pageAdapter; + private TabLayout tabLayout; + + private MenuItem menuRssButton; + + private ImageView headerChannelBanner; + private ImageView headerAvatarView; + private TextView headerTitleView; + private TextView headerSubscribersTextView; + private Button headerSubscribeButton; + + + /*////////////////////////////////////////////////////////////////////////*/ + + public static ChannelDetailFragment getInstance(int serviceId, String videoUrl, String name) { + ChannelDetailFragment instance = new ChannelDetailFragment(); + instance.setInitialData(serviceId, videoUrl, name); + return instance; + } + + /*////////////////////////////////////////////////////////////////////////// + // Fragment's Lifecycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void + onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + + PreferenceManager.getDefaultSharedPreferences(activity) + .registerOnSharedPreferenceChangeListener(this); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_channel_detail, container, false); + } + + @Override + public void onPause() { + super.onPause(); + if (currentWorker != null) currentWorker.dispose(); + PreferenceManager.getDefaultSharedPreferences(getContext()) + .edit() + .putString(getString(R.string.stream_info_selected_tab_key), pageAdapter.getItemTitle(viewPager.getCurrentItem())) + .apply(); + } + + @Override + public void onResume() { + super.onResume(); + + if (updateFlags != 0) { + updateFlags = 0; + } + + // Check if it was loading when the fragment was stopped/paused, + if (wasLoading.getAndSet(false)) { + selectAndLoadVideo(serviceId, url, name); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + PreferenceManager.getDefaultSharedPreferences(activity) + .unregisterOnSharedPreferenceChangeListener(this); + + if (currentWorker != null) currentWorker.dispose(); + if (disposables != null) disposables.clear(); + currentWorker = null; + disposables = null; + } + + @Override + public void onDestroyView() { + if (DEBUG) Log.d(TAG, "onDestroyView() called"); + super.onDestroyView(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + switch (requestCode) { + case ReCaptchaActivity.RECAPTCHA_REQUEST: + if (resultCode == Activity.RESULT_OK) { + NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, name); + } else Log.e(TAG, "ReCaptcha failed"); + break; + default: + Log.e(TAG, "Request code from activity not supported [" + requestCode + "]"); + break; + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {} + + /*////////////////////////////////////////////////////////////////////////// + // State Saving + //////////////////////////////////////////////////////////////////////////*/ + + private static final String INFO_KEY = "info_key"; + private static final String STACK_KEY = "stack_key"; + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + if (!isLoading.get() && currentInfo != null && isVisible()) { + outState.putSerializable(INFO_KEY, currentInfo); + } + + outState.putSerializable(STACK_KEY, stack); + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle savedState) { + super.onRestoreInstanceState(savedState); + + Serializable serializable = savedState.getSerializable(INFO_KEY); + if (serializable instanceof ChannelInfo) { + currentInfo = (ChannelInfo) serializable; + InfoCache.getInstance().putInfo(serviceId, url, currentInfo, InfoItem.InfoType.CHANNEL); + } + + serializable = savedState.getSerializable(STACK_KEY); + if (serializable instanceof Collection) { + //noinspection unchecked + stack.addAll((Collection) serializable); + } + + } + + /*////////////////////////////////////////////////////////////////////////// + // OnClick + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onClick(View v) { + if (isLoading.get() || currentInfo == null) return; + + switch (v.getId()) { + } + } + + @Override + public boolean onLongClick(View v) { + if (isLoading.get() || currentInfo == null) return false; + + switch (v.getId()) {} + + return true; + } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + appBarLayout = rootView.findViewById(R.id.appbarlayout); + viewPager = rootView.findViewById(R.id.viewpager); + pageAdapter = new TabAdaptor(getChildFragmentManager()); + viewPager.setAdapter(pageAdapter); + tabLayout = rootView.findViewById(R.id.tablayout); + tabLayout.setupWithViewPager(viewPager); + + headerChannelBanner = rootView.findViewById(R.id.channel_banner_image); + headerAvatarView = rootView.findViewById(R.id.channel_avatar_view); + headerTitleView = rootView.findViewById(R.id.channel_title_view); + headerSubscribersTextView = rootView.findViewById(R.id.channel_subscriber_view); + headerSubscribeButton = rootView.findViewById(R.id.channel_subscribe_button); + } + + @Override + protected void initListeners() { + super.initListeners(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Menu + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + this.menu = menu; + super.onCreateOptionsMenu(menu, inflater); + ActionBar supportActionBar = activity.getSupportActionBar(); + if (useAsFrontPage && supportActionBar != null) { + supportActionBar.setDisplayHomeAsUpEnabled(false); + } else { + inflater.inflate(R.menu.menu_channel, menu); + + if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + + "], inflater = [" + inflater + "]"); + menuRssButton = menu.findItem(R.id.menu_item_rss); + + if (currentInfo != null) menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl())); + } + } + + private void openRssFeed() { + final ChannelInfo info = currentInfo; + if (info != null) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl())); + startActivity(intent); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == R.id.action_settings) { + NavigationHelper.openSettings(requireContext()); + return true; + } + + if (isLoading.get()) { + // if still loading, block menu buttons related to video info + return true; + } + + switch (id) { + case R.id.menu_item_share: { + if (currentInfo != null) { + ShareUtils.shareUrl(requireContext(), currentInfo.getName(), currentInfo.getOriginalUrl()); + } + return true; + } + case R.id.menu_item_openInBrowser: { + if (currentInfo != null) { + ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getOriginalUrl()); + } + return true; + } + case R.id.menu_item_rss: + openRssFeed(); + break; + default: + return super.onOptionsItemSelected(item); + } + return true; + } + + private void setupActionBar(final ChannelInfo info) { + if (DEBUG) Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]"); + } + + /*////////////////////////////////////////////////////////////////////////// + // OwnStack + //////////////////////////////////////////////////////////////////////////*/ + + /** + * Stack that contains the "navigation history".
+ * The peek is the current video. + */ + protected final LinkedList stack = new LinkedList<>(); + + public void pushToStack(int serviceId, String videoUrl, String name) { + if (DEBUG) { + Log.d(TAG, "pushToStack() called with: serviceId = [" + + serviceId + "], videoUrl = [" + videoUrl + "], name = [" + name + "]"); + } + + if (stack.size() > 0 + && stack.peek().getServiceId() == serviceId + && stack.peek().getUrl().equals(videoUrl)) { + Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = [" + + serviceId + "], videoUrl == peek.getUrl = [" + videoUrl + "]"); + return; + } else { + Log.d(TAG, "pushToStack() wasn't equal"); + } + + stack.push(new StackItem(serviceId, videoUrl, name)); + } + + @Override + public boolean onBackPressed() { + if (DEBUG) Log.d(TAG, "onBackPressed() called"); + // That means that we are on the start of the stack, + // return false to let the MainActivity handle the onBack + if (stack.size() <= 1) return false; + // Remove top + stack.pop(); + // Get stack item from the new top + StackItem peek = stack.peek(); + + selectAndLoadVideo(peek.getServiceId(), + peek.getUrl(), + !TextUtils.isEmpty(peek.getTitle()) + ? peek.getTitle() + : ""); + return true; + } + + /*////////////////////////////////////////////////////////////////////////// + // Info loading and handling + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected void doInitialLoadLogic() { + if (currentInfo == null) prepareAndLoadInfo(); + else prepareAndHandleInfo(currentInfo, false); + } + + public void selectAndLoadVideo(int serviceId, String videoUrl, String name) { + setInitialData(serviceId, videoUrl, name); + prepareAndLoadInfo(); + } + + public void prepareAndHandleInfo(final ChannelInfo info, boolean scrollToTop) { + if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = [" + + info + "], scrollToTop = [" + scrollToTop + "]"); + + setInitialData(info.getServiceId(), info.getUrl(), info.getName()); + pushToStack(serviceId, url, name); + showLoading(); + initTabs(); + + if (scrollToTop) appBarLayout.setExpanded(true, true); + handleResult(info); + } + + protected void prepareAndLoadInfo() { + appBarLayout.setExpanded(true, true); + pushToStack(serviceId, url, name); + startLoading(false); + } + + @Override + public void startLoading(boolean forceLoad) { + super.startLoading(forceLoad); + + initTabs(); + currentInfo = null; + if (currentWorker != null) currentWorker.dispose(); + + currentWorker = ExtractorHelper.getChannelInfo(serviceId, url, forceLoad) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe((@NonNull ChannelInfo result) -> { + isLoading.set(false); + currentInfo = result; + handleResult(result); + }, (@NonNull Throwable throwable) -> { + isLoading.set(false); + onError(throwable); + }); + + } + + private void initTabs() { + pageAdapter.clearAllItems(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + protected void setInitialData(int serviceId, String url, String name) { + this.serviceId = serviceId; + this.url = url; + this.name = !TextUtils.isEmpty(name) ? name : ""; + } + + @Override + public void showError(String message, boolean showRetryButton) { + showError(message, showRetryButton, R.drawable.not_available_monkey); + } + + protected void showError(String message, boolean showRetryButton, @DrawableRes int imageError) { + super.showError(message, showRetryButton); + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void showLoading() { + super.showLoading(); + + headerTitleView.setText(name != null ? name : ""); + + imageLoader.cancelDisplayTask(headerAvatarView); + headerAvatarView.setImageBitmap(null); + + imageLoader.cancelDisplayTask(headerChannelBanner); + headerChannelBanner.setImageBitmap(null); + } + + private void initThumbnailViews(@NonNull ChannelInfo info) { + if (!TextUtils.isEmpty(info.getAvatarUrl())) { + imageLoader.displayImage(info.getAvatarUrl(), headerAvatarView, + ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); + } + + if (!TextUtils.isEmpty(info.getBannerUrl())) { + imageLoader.displayImage(info.getBannerUrl(), headerChannelBanner, + ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); + } + } + + @Override + public void handleResult(@NonNull ChannelInfo info) { + super.handleResult(info); + + setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName()); + + for (ChannelTabInfo tabInfo : currentInfo.getTabs()) { + pageAdapter.addFragment(ChannelTabFragment.getInstance(tabInfo), tabInfo.getName()); + } + + pageAdapter.notifyDataSetUpdate(); + + if (pageAdapter.getCount() < 2) { + tabLayout.setVisibility(View.GONE); + } else { + tabLayout.setVisibility(View.VISIBLE); + } + + pushToStack(serviceId, url, name); + + setupActionBar(info); + initThumbnailViews(info); + + activity.invalidateOptionsMenu(); + + setTitle(info.getName()); + + if (!info.getErrors().isEmpty()) { + showSnackBarError(info.getErrors(), + UserAction.REQUESTED_STREAM, + NewPipe.getNameOfService(info.getServiceId()), + info.getUrl(), + 0); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Stream Results + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected boolean onError(Throwable exception) { + if (super.onError(exception)) return true; + + else if (exception instanceof ContentNotAvailableException) { + showError(getString(R.string.content_not_available), false); + } else { + int errorId = exception instanceof YoutubeStreamExtractor.DecryptException + ? R.string.youtube_signature_decryption_error + : exception instanceof ParsingException + ? R.string.parsing_error + : R.string.general_error; + onUnrecoverableError(exception, + UserAction.REQUESTED_STREAM, + NewPipe.getNameOfService(serviceId), + url, + errorId); + } + + return true; + } +} 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 deleted file mode 100644 index 3615b092250..00000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ /dev/null @@ -1,465 +0,0 @@ -package org.schabi.newpipe.fragments.list.channel; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.appcompat.app.ActionBar; -import android.text.TextUtils; -import android.util.Log; -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 android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.jakewharton.rxbinding2.view.RxView; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.subscription.SubscriptionEntity; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.ListExtractor; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.fragments.list.BaseListInfoFragment; -import org.schabi.newpipe.local.subscription.SubscriptionManager; -import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; -import org.schabi.newpipe.player.playqueue.PlayQueue; -import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.AnimationUtils; -import org.schabi.newpipe.util.ExtractorHelper; -import org.schabi.newpipe.util.ImageDisplayConstants; -import org.schabi.newpipe.util.Localization; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.ShareUtils; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import io.reactivex.Observable; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Action; -import io.reactivex.functions.Consumer; -import io.reactivex.functions.Function; -import io.reactivex.schedulers.Schedulers; - -import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor; -import static org.schabi.newpipe.util.AnimationUtils.animateTextColor; -import static org.schabi.newpipe.util.AnimationUtils.animateView; - -public class ChannelFragment extends BaseListInfoFragment { - - private final CompositeDisposable disposables = new CompositeDisposable(); - private Disposable subscribeButtonMonitor; - private SubscriptionManager subscriptionManager; - - /*////////////////////////////////////////////////////////////////////////// - // Views - //////////////////////////////////////////////////////////////////////////*/ - - private View headerRootLayout; - private ImageView headerChannelBanner; - private ImageView headerAvatarView; - private TextView headerTitleView; - private TextView headerSubscribersTextView; - private Button headerSubscribeButton; - private View playlistCtrl; - - private LinearLayout headerPlayAllButton; - private LinearLayout headerPopupButton; - private LinearLayout headerBackgroundButton; - - private MenuItem menuRssButton; - - public static ChannelFragment getInstance(int serviceId, String url, String name) { - ChannelFragment instance = new ChannelFragment(); - instance.setInitialData(serviceId, url, name); - return instance; - } - - /*////////////////////////////////////////////////////////////////////////// - // LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void setUserVisibleHint(boolean isVisibleToUser) { - super.setUserVisibleHint(isVisibleToUser); - if (activity != null - && useAsFrontPage - && isVisibleToUser) { - setTitle(currentInfo != null ? currentInfo.getName() : name); - } - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - subscriptionManager = new SubscriptionManager(activity); - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_channel, container, false); - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (disposables != null) disposables.clear(); - if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Init - //////////////////////////////////////////////////////////////////////////*/ - - protected View getListHeader() { - headerRootLayout = activity.getLayoutInflater().inflate(R.layout.channel_header, itemsList, false); - headerChannelBanner = headerRootLayout.findViewById(R.id.channel_banner_image); - headerAvatarView = headerRootLayout.findViewById(R.id.channel_avatar_view); - headerTitleView = headerRootLayout.findViewById(R.id.channel_title_view); - headerSubscribersTextView = headerRootLayout.findViewById(R.id.channel_subscriber_view); - headerSubscribeButton = headerRootLayout.findViewById(R.id.channel_subscribe_button); - playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control); - - - headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button); - headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button); - headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button); - - return headerRootLayout; - } - - /*////////////////////////////////////////////////////////////////////////// - // Menu - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - ActionBar supportActionBar = activity.getSupportActionBar(); - if (useAsFrontPage && supportActionBar != null) { - supportActionBar.setDisplayHomeAsUpEnabled(false); - } else { - inflater.inflate(R.menu.menu_channel, menu); - - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + - "], inflater = [" + inflater + "]"); - menuRssButton = menu.findItem(R.id.menu_item_rss); - } - } - - private void openRssFeed() { - final ChannelInfo info = currentInfo; - if (info != null) { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl())); - startActivity(intent); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_settings: - NavigationHelper.openSettings(requireContext()); - break; - case R.id.menu_item_rss: - openRssFeed(); - break; - case R.id.menu_item_openInBrowser: - if (currentInfo != null) { - ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getOriginalUrl()); - } - break; - case R.id.menu_item_share: - if (currentInfo != null) { - ShareUtils.shareUrl(requireContext(), name, currentInfo.getOriginalUrl()); - } - break; - default: - return super.onOptionsItemSelected(item); - } - return true; - } - - /*////////////////////////////////////////////////////////////////////////// - // Channel Subscription - //////////////////////////////////////////////////////////////////////////*/ - - private static final int BUTTON_DEBOUNCE_INTERVAL = 100; - - private void monitorSubscription(final ChannelInfo info) { - final Consumer onError = (Throwable throwable) -> { - animateView(headerSubscribeButton, false, 100); - showSnackBarError(throwable, UserAction.SUBSCRIPTION, - NewPipe.getNameOfService(currentInfo.getServiceId()), - "Get subscription status", - 0); - }; - - final Observable> observable = subscriptionManager.subscriptionTable() - .getSubscriptionFlowable(info.getServiceId(), info.getUrl()) - .toObservable(); - - disposables.add(observable - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getSubscribeUpdateMonitor(info), onError)); - - disposables.add(observable - // Some updates are very rapid (when calling the updateSubscription(info), for example) - // so only update the UI for the latest emission ("sync" the subscribe button's state) - .debounce(100, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe((List subscriptionEntities) -> - updateSubscribeButton(!subscriptionEntities.isEmpty()) - , onError)); - - } - - private Function mapOnSubscribe(final SubscriptionEntity subscription, 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) -> - onUnrecoverableError(throwable, - UserAction.SUBSCRIPTION, - NewPipe.getNameOfService(info.getServiceId()), - "Updating Subscription for " + info.getUrl(), - R.string.subscription_update_failed); - - 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) -> - onUnrecoverableError(throwable, - UserAction.SUBSCRIPTION, - NewPipe.getNameOfService(currentInfo.getServiceId()), - "Subscription Change", - R.string.subscription_change_failed); - - /* 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!"); - SubscriptionEntity channel = new SubscriptionEntity(); - channel.setServiceId(info.getServiceId()); - channel.setUrl(info.getUrl()); - channel.setData(info.getName(), - info.getAvatarUrl(), - info.getDescription(), - info.getSubscriberCount()); - subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel, info)); - } else { - if (DEBUG) Log.d(TAG, "Found subscription to this channel!"); - final SubscriptionEntity subscription = subscriptionEntities.get(0); - subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription)); - } - }; - } - - private void updateSubscribeButton(boolean isSubscribed) { - if (DEBUG) Log.d(TAG, "updateSubscribeButton() called with: isSubscribed = [" + isSubscribed + "]"); - - boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE; - int backgroundDuration = isButtonVisible ? 300 : 0; - int textDuration = isButtonVisible ? 200 : 0; - - int subscribeBackground = ContextCompat.getColor(activity, R.color.subscribe_background_color); - int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color); - int subscribedBackground = ContextCompat.getColor(activity, R.color.subscribed_background_color); - int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color); - - if (!isSubscribed) { - headerSubscribeButton.setText(R.string.subscribe_button_title); - animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground, subscribeBackground); - animateTextColor(headerSubscribeButton, textDuration, subscribedText, subscribeText); - } else { - headerSubscribeButton.setText(R.string.subscribed_button_title); - animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground, subscribedBackground); - animateTextColor(headerSubscribeButton, textDuration, subscribeText, subscribedText); - } - - animateView(headerSubscribeButton, AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, true, 100); - } - - /*////////////////////////////////////////////////////////////////////////// - // Load and handle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected Single loadMoreItemsLogic() { - return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPageUrl); - } - - @Override - protected Single loadResult(boolean forceLoad) { - return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad); - } - - /*////////////////////////////////////////////////////////////////////////// - // Contract - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void showLoading() { - super.showLoading(); - - imageLoader.cancelDisplayTask(headerChannelBanner); - imageLoader.cancelDisplayTask(headerAvatarView); - animateView(headerSubscribeButton, false, 100); - } - - @Override - public void handleResult(@NonNull ChannelInfo result) { - super.handleResult(result); - - headerRootLayout.setVisibility(View.VISIBLE); - imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner, - ImageDisplayConstants.DISPLAY_BANNER_OPTIONS); - imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView, - ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); - - headerSubscribersTextView.setVisibility(View.VISIBLE); - if (result.getSubscriberCount() >= 0) { - headerSubscribersTextView.setText(Localization.shortSubscriberCount(activity, result.getSubscriberCount())); - } else { - headerSubscribersTextView.setText(R.string.subscribers_count_not_available); - } - - if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl())); - - playlistCtrl.setVisibility(View.VISIBLE); - - if (!result.getErrors().isEmpty()) { - showSnackBarError(result.getErrors(), UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); - } - - if (disposables != null) disposables.clear(); - if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); - updateSubscription(result); - monitorSubscription(result); - - headerPlayAllButton.setOnClickListener( - view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); - headerPopupButton.setOnClickListener( - view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); - headerBackgroundButton.setOnClickListener( - view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); - } - - private PlayQueue getPlayQueue() { - return getPlayQueue(0); - } - - private PlayQueue getPlayQueue(final int index) { - final List streamItems = new ArrayList<>(); - for (InfoItem i : infoListAdapter.getItemsList()) { - if (i instanceof StreamInfoItem) { - streamItems.add((StreamInfoItem) i); - } - } - return new ChannelPlayQueue( - currentInfo.getServiceId(), - currentInfo.getUrl(), - currentInfo.getNextPageUrl(), - streamItems, - index - ); - } - - @Override - public void handleNextItems(ListExtractor.InfoItemsPage result) { - super.handleNextItems(result); - - if (!result.getErrors().isEmpty()) { - showSnackBarError(result.getErrors(), - UserAction.REQUESTED_CHANNEL, - NewPipe.getNameOfService(serviceId), - "Get next page of: " + url, - R.string.general_error); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // OnError - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected boolean onError(Throwable exception) { - if (super.onError(exception)) return true; - - if (exception instanceof ContentNotAvailableException) { - showError(getString(R.string.content_not_available), false); - } else { - int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; - onUnrecoverableError(exception, - UserAction.REQUESTED_CHANNEL, - NewPipe.getNameOfService(serviceId), - url, - errorId); - } - return true; - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void setTitle(String title) { - super.setTitle(title); - if (!useAsFrontPage) headerTitleView.setText(title); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java new file mode 100644 index 00000000000..e9287340a23 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java @@ -0,0 +1,167 @@ +package org.schabi.newpipe.fragments.list.channel; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +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.extractor.ListExtractor; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.channel.ChannelTabInfo; +import org.schabi.newpipe.fragments.list.BaseListInfoFragment; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.AnimationUtils; + +import java.io.Serializable; + +import io.reactivex.Single; +import io.reactivex.disposables.CompositeDisposable; + +import static org.schabi.newpipe.util.ExtractorHelper.getMoreChannelTabItems; + +public class ChannelTabFragment extends BaseListInfoFragment implements SharedPreferences.OnSharedPreferenceChangeListener{ + + private CompositeDisposable disposables = new CompositeDisposable(); + private ChannelTabInfo relatedStreamInfo; + + public static ChannelTabFragment getInstance(ChannelTabInfo info) { + ChannelTabFragment instance = new ChannelTabFragment(); + instance.setInitialData(info); + return instance; + } + + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + mIsVisibleToUser = isVisibleToUser; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_channel_tab, container, false); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (disposables != null) disposables.clear(); + } + + @Override + protected Single loadMoreItemsLogic() { + return getMoreChannelTabItems(relatedStreamInfo, currentNextPageUrl); + } + + @Override + protected Single loadResult(boolean forceLoad) { + return Single.fromCallable(() -> relatedStreamInfo); + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void showLoading() { + super.showLoading(); + } + + @Override + public void handleResult(@NonNull ChannelTabInfo result) { + super.handleResult(result); + + AnimationUtils.slideUp(getView(),120, 96, 0.06f); + + if (!result.getErrors().isEmpty()) { + showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); + } + + if (disposables != null) disposables.clear(); + } + + @Override + public void handleNextItems(ListExtractor.InfoItemsPage result) { + super.handleNextItems(result); + + if (!result.getErrors().isEmpty()) { + showSnackBarError(result.getErrors(), + UserAction.REQUESTED_STREAM, + NewPipe.getNameOfService(serviceId), + "Get next page of: " + url, + R.string.general_error); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // OnError + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected boolean onError(Throwable exception) { + if (super.onError(exception)) return true; + + hideLoading(); + showSnackBarError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, R.string.general_error); + return true; + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void setTitle(String title) {} + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {} + + private void setInitialData(ChannelTabInfo info) { + super.setInitialData(info.getServiceId(), info.getUrl(), info.getName()); + if(this.relatedStreamInfo == null) this.relatedStreamInfo = info; + } + + + private static final String INFO_KEY = "related_info_key"; + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putSerializable(INFO_KEY, relatedStreamInfo); + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle savedState) { + super.onRestoreInstanceState(savedState); + if (savedState != null) { + Serializable serializable = savedState.getSerializable(INFO_KEY); + if (serializable instanceof ChannelTabInfo){ + this.relatedStreamInfo = (ChannelTabInfo) serializable; + } + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {} + + @Override + protected boolean isGridLayout() { + return false; + } +} 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 index 5a2e34d31df..84b3bd4d595 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java @@ -11,40 +11,40 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; -public final class ChannelPlayQueue extends AbstractInfoPlayQueue { - public ChannelPlayQueue(final ChannelInfoItem item) { - super(item); - } - - public ChannelPlayQueue(final ChannelInfo info) { - this(info.getServiceId(), info.getUrl(), info.getNextPageUrl(), info.getRelatedItems(), 0); - } - - public ChannelPlayQueue(final int serviceId, - final String url, - final String nextPageUrl, - final List streams, - final int index) { - super(serviceId, url, nextPageUrl, 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.nextUrl) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getNextPageObserver()); - } - } -} +//public final class ChannelPlayQueue extends AbstractInfoPlayQueue { +// public ChannelPlayQueue(final ChannelInfoItem item) { +// super(item); +// } +// +// public ChannelPlayQueue(final ChannelInfo info) { +// this(info.getServiceId(), info.getUrl(), info.getNextPageUrl(), info.getRelatedItems(), 0); +// } +// +// public ChannelPlayQueue(final int serviceId, +// final String url, +// final String nextPageUrl, +// final List streams, +// final int index) { +// super(serviceId, url, nextPageUrl, 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.nextUrl) +// .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 cc40298b998..9f4a527582b 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 @@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.fragments.BlankFragment; -import org.schabi.newpipe.fragments.list.channel.ChannelFragment; +import org.schabi.newpipe.fragments.detail.ChannelDetailFragment; import org.schabi.newpipe.fragments.list.kiosk.DefaultKioskFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.local.bookmark.BookmarkFragment; @@ -402,8 +402,8 @@ public int getTabIconRes(Context context) { } @Override - public ChannelFragment getFragment(Context context) { - return ChannelFragment.getInstance(channelServiceId, channelUrl, channelName); + public ChannelDetailFragment getFragment(Context context) { + return ChannelDetailFragment.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 cf44772237a..91f4f8e1efb 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -35,6 +35,7 @@ import org.schabi.newpipe.extractor.NewPipe; 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.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ParsingException; @@ -128,12 +129,10 @@ public static Single getChannelInfo(final int serviceId, ChannelInfo.getInfo(NewPipe.getService(serviceId), url))); } - public static Single getMoreChannelItems(final int serviceId, - final String url, + public static Single getMoreChannelTabItems(final ChannelTabInfo tabInfo, final String nextStreamsUrl) { - checkServiceId(serviceId); return Single.fromCallable(() -> - ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl)); + ChannelTabInfo.getMoreItems(tabInfo, nextStreamsUrl)); } public static Single> getFeedInfoFallbackToChannelInfo(final int serviceId, diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index b6f73dac741..59c749eaf0a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -33,8 +33,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.MainFragment; +import org.schabi.newpipe.fragments.detail.ChannelDetailFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; -import org.schabi.newpipe.fragments.list.channel.ChannelFragment; import org.schabi.newpipe.fragments.list.comments.CommentsFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; @@ -316,7 +316,7 @@ public static void openChannelFragment( String name) { if (name == null) name = ""; defaultTransaction(fragmentManager) - .replace(R.id.fragment_holder, ChannelFragment.getInstance(serviceId, url, name)) + .replace(R.id.fragment_holder, ChannelDetailFragment.getInstance(serviceId, url, name)) .addToBackStack(null) .commit(); } diff --git a/app/src/main/res/layout/channel_header.xml b/app/src/main/res/layout/channel_header.xml deleted file mode 100644 index 4a0e261c54b..00000000000 --- a/app/src/main/res/layout/channel_header.xml +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_channel_detail.xml b/app/src/main/res/layout/fragment_channel_detail.xml new file mode 100644 index 00000000000..95c9e40aaf0 --- /dev/null +++ b/app/src/main/res/layout/fragment_channel_detail.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel_tab.xml similarity index 93% rename from app/src/main/res/layout/fragment_channel.xml rename to app/src/main/res/layout/fragment_channel_tab.xml index f6f8afaa3c8..30d34004167 100644 --- a/app/src/main/res/layout/fragment_channel.xml +++ b/app/src/main/res/layout/fragment_channel_tab.xml @@ -25,9 +25,9 @@ android:id="@+id/empty_state_view" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_centerInParent="true" + android:layout_centerHorizontal="true" android:orientation="vertical" - android:paddingTop="90dp" + android:paddingTop="85dp" android:visibility="gone" tools:visibility="visible"> @@ -65,6 +65,7 @@ android:layout_width="match_parent" android:layout_height="4dp" android:background="?attr/toolbar_shadow_drawable" - android:layout_alignParentTop="true"/> + android:layout_alignParentTop="true" + android:visibility="gone"/> From 31650edc02c9b16ba0b757c3f8ee5b4abb6c1734 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Mon, 9 Mar 2020 11:25:24 +0100 Subject: [PATCH 02/22] Show subscriber count --- .../fragments/detail/ChannelDetailFragment.java | 15 ++++++++++----- .../main/res/layout/fragment_channel_detail.xml | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java index 2ddc5a00a1a..8ca1fa5ab9e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java @@ -43,6 +43,7 @@ import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.InfoCache; +import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ShareUtils; @@ -475,10 +476,7 @@ public void showLoading() { headerTitleView.setText(name != null ? name : ""); imageLoader.cancelDisplayTask(headerAvatarView); - headerAvatarView.setImageBitmap(null); - imageLoader.cancelDisplayTask(headerChannelBanner); - headerChannelBanner.setImageBitmap(null); } private void initThumbnailViews(@NonNull ChannelInfo info) { @@ -489,7 +487,7 @@ private void initThumbnailViews(@NonNull ChannelInfo info) { if (!TextUtils.isEmpty(info.getBannerUrl())) { imageLoader.displayImage(info.getBannerUrl(), headerChannelBanner, - ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); + ImageDisplayConstants.DISPLAY_BANNER_OPTIONS); } } @@ -499,7 +497,7 @@ public void handleResult(@NonNull ChannelInfo info) { setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName()); - for (ChannelTabInfo tabInfo : currentInfo.getTabs()) { + for (ChannelTabInfo tabInfo : info.getTabs()) { pageAdapter.addFragment(ChannelTabFragment.getInstance(tabInfo), tabInfo.getName()); } @@ -511,6 +509,13 @@ public void handleResult(@NonNull ChannelInfo info) { tabLayout.setVisibility(View.VISIBLE); } + headerSubscribersTextView.setVisibility(View.VISIBLE); + if (info.getSubscriberCount() >= 0) { + headerSubscribersTextView.setText(Localization.shortSubscriberCount(activity, info.getSubscriberCount())); + } else { + headerSubscribersTextView.setText(R.string.subscribers_count_not_available); + } + pushToStack(serviceId, url, name); setupActionBar(info); diff --git a/app/src/main/res/layout/fragment_channel_detail.xml b/app/src/main/res/layout/fragment_channel_detail.xml index 95c9e40aaf0..fa11a1c9e5d 100644 --- a/app/src/main/res/layout/fragment_channel_detail.xml +++ b/app/src/main/res/layout/fragment_channel_detail.xml @@ -27,7 +27,7 @@ android:id="@+id/detail_content_root_layout" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?android:windowBackground" + android:background="?attr/contrast_background_color" app:layout_scrollFlags="scroll"> Date: Fri, 13 Mar 2020 20:09:54 +0100 Subject: [PATCH 03/22] Show subscribe button --- .../detail/ChannelDetailFragment.java | 174 +++++++++++++++++- .../local/subscription/SubscriptionManager.kt | 6 +- .../schabi/newpipe/util/ExtractorHelper.java | 3 +- 3 files changed, 177 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java index 8ca1fa5ab9e..c64877f32de 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.fragments.detail; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; @@ -21,13 +22,16 @@ import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; +import androidx.core.content.ContextCompat; import androidx.viewpager.widget.ViewPager; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.tabs.TabLayout; +import com.jakewharton.rxbinding2.view.RxView; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; +import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelInfo; @@ -38,7 +42,9 @@ import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.list.channel.ChannelTabFragment; +import org.schabi.newpipe.local.subscription.SubscriptionManager; import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; @@ -50,13 +56,23 @@ import java.io.Serializable; import java.util.Collection; import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; import icepick.State; +import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Action; +import io.reactivex.functions.Consumer; +import io.reactivex.functions.Function; import io.reactivex.schedulers.Schedulers; +import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor; +import static org.schabi.newpipe.util.AnimationUtils.animateTextColor; +import static org.schabi.newpipe.util.AnimationUtils.animateView; + public class ChannelDetailFragment extends BaseStateFragment implements BackPressable, @@ -64,6 +80,10 @@ public class ChannelDetailFragment View.OnClickListener, View.OnLongClickListener { + private CompositeDisposable disposables = new CompositeDisposable(); + private Disposable subscribeButtonMonitor; + private SubscriptionManager subscriptionManager; + private int updateFlags = 0; @State @@ -75,8 +95,6 @@ public class ChannelDetailFragment private ChannelInfo currentInfo; private Disposable currentWorker; - @NonNull - private CompositeDisposable disposables = new CompositeDisposable(); /*////////////////////////////////////////////////////////////////////////// // Views @@ -120,6 +138,12 @@ public static ChannelDetailFragment getInstance(int serviceId, String videoUrl, .registerOnSharedPreferenceChangeListener(this); } + @Override + public void onAttach(Context context) { + super.onAttach(context); + subscriptionManager = new SubscriptionManager(activity); + } + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_channel_detail, container, false); @@ -157,6 +181,8 @@ public void onDestroy() { if (currentWorker != null) currentWorker.dispose(); if (disposables != null) disposables.clear(); + if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); + currentWorker = null; disposables = null; } @@ -386,6 +412,143 @@ public boolean onBackPressed() { return true; } + /*////////////////////////////////////////////////////////////////////////// + // Channel Subscription + //////////////////////////////////////////////////////////////////////////*/ + + private static final int BUTTON_DEBOUNCE_INTERVAL = 100; + + private void monitorSubscription(final ChannelInfo info) { + final Consumer onError = (Throwable throwable) -> { + animateView(headerSubscribeButton, false, 100); + showSnackBarError(throwable, UserAction.SUBSCRIPTION, + NewPipe.getNameOfService(currentInfo.getServiceId()), + "Get subscription status", + 0); + }; + + final Observable> observable = subscriptionManager.subscriptionTable() + .getSubscriptionFlowable(info.getServiceId(), info.getUrl()) + .toObservable(); + + disposables.add(observable + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getSubscribeUpdateMonitor(info), onError)); + + disposables.add(observable + // Some updates are very rapid (when calling the updateSubscription(info), for example) + // so only update the UI for the latest emission ("sync" the subscribe button's state) + .debounce(100, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe((List subscriptionEntities) -> + updateSubscribeButton(!subscriptionEntities.isEmpty()) + , onError)); + + } + + private Function mapOnSubscribe(final SubscriptionEntity subscription, 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) -> + onUnrecoverableError(throwable, + UserAction.SUBSCRIPTION, + NewPipe.getNameOfService(info.getServiceId()), + "Updating Subscription for " + info.getUrl(), + R.string.subscription_update_failed); + + 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) -> + onUnrecoverableError(throwable, + UserAction.SUBSCRIPTION, + NewPipe.getNameOfService(currentInfo.getServiceId()), + "Subscription Change", + R.string.subscription_change_failed); + + /* 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, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]"); + if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); + + if (subscriptionEntities.isEmpty()) { + if (DEBUG) Log.d(TAG, "No subscription to this channel!"); + SubscriptionEntity channel = new SubscriptionEntity(); + channel.setServiceId(info.getServiceId()); + channel.setUrl(info.getUrl()); + channel.setData(info.getName(), + info.getAvatarUrl(), + info.getDescription(), + info.getSubscriberCount()); + subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel, info)); + } else { + if (DEBUG) Log.d(TAG, "Found subscription to this channel!"); + final SubscriptionEntity subscription = subscriptionEntities.get(0); + subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription)); + } + }; + } + + private void updateSubscribeButton(boolean isSubscribed) { + if (DEBUG) Log.d(TAG, "updateSubscribeButton() called with: isSubscribed = [" + isSubscribed + "]"); + + boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE; + int backgroundDuration = isButtonVisible ? 300 : 0; + int textDuration = isButtonVisible ? 200 : 0; + + int subscribeBackground = ContextCompat.getColor(activity, R.color.subscribe_background_color); + int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color); + int subscribedBackground = ContextCompat.getColor(activity, R.color.subscribed_background_color); + int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color); + + if (!isSubscribed) { + headerSubscribeButton.setText(R.string.subscribe_button_title); + animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground, subscribeBackground); + animateTextColor(headerSubscribeButton, textDuration, subscribedText, subscribeText); + } else { + headerSubscribeButton.setText(R.string.subscribed_button_title); + animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground, subscribedBackground); + animateTextColor(headerSubscribeButton, textDuration, subscribeText, subscribedText); + } + + animateView(headerSubscribeButton, AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, true, 100); + } + /*////////////////////////////////////////////////////////////////////////// // Info loading and handling //////////////////////////////////////////////////////////////////////////*/ @@ -477,6 +640,8 @@ public void showLoading() { imageLoader.cancelDisplayTask(headerAvatarView); imageLoader.cancelDisplayTask(headerChannelBanner); + + animateView(headerSubscribeButton, false, 100); } private void initThumbnailViews(@NonNull ChannelInfo info) { @@ -516,6 +681,11 @@ public void handleResult(@NonNull ChannelInfo info) { headerSubscribersTextView.setText(R.string.subscribers_count_not_available); } + if (disposables != null) disposables.clear(); + if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); + updateSubscription(info); + monitorSubscription(info); + pushToStack(serviceId, url, name); setupActionBar(info); 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 92ab8cb0ce7..626d299fd1d 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 @@ -27,7 +27,7 @@ class SubscriptionManager(context: Context) { database.runInTransaction { infoList.forEachIndexed { index, info -> - feedDatabaseManager.upsertAll(listEntities[index].uid, info.relatedItems) + feedDatabaseManager.upsertAll(listEntities[index].uid, info.tabs[0].relatedItems as List) } } @@ -39,7 +39,7 @@ class SubscriptionManager(context: Context) { Completable.fromRunnable { it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount) subscriptionTable.update(it) - feedDatabaseManager.upsertAll(it.uid, info.relatedItems) + feedDatabaseManager.upsertAll(it.uid, info.tabs[0].relatedItems as List) } } @@ -64,7 +64,7 @@ class SubscriptionManager(context: Context) { fun insertSubscription(subscriptionEntity: SubscriptionEntity, info: ChannelInfo) { database.runInTransaction { val subscriptionId = subscriptionTable.insert(subscriptionEntity) - feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems) + feedDatabaseManager.upsertAll(subscriptionId, info.tabs[0].relatedItems as 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 91f4f8e1efb..20521a392f1 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -59,6 +59,7 @@ import io.reactivex.Maybe; import io.reactivex.Single; +import io.reactivex.SingleSource; public final class ExtractorHelper { private static final String TAG = ExtractorHelper.class.getSimpleName(); @@ -148,7 +149,7 @@ public static Single> getFeedInfoFallbackToChannelInfo( return FeedInfo.getInfo(feedExtractor); }); - return maybeFeedInfo.switchIfEmpty(getChannelInfo(serviceId, url, true)); + return maybeFeedInfo.switchIfEmpty((SingleSource) getChannelInfo(serviceId, url, true)); } public static Single getCommentsInfo(final int serviceId, From 6473bfc356af05989f7fece811bee4449f3ba5a5 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sat, 14 Mar 2020 08:56:40 +0100 Subject: [PATCH 04/22] Show 'No items' instead of 'No videos' --- app/src/main/res/layout/fragment_channel_tab.xml | 2 +- app/src/main/res/values-nl/strings.xml | 1 + app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/fragment_channel_tab.xml b/app/src/main/res/layout/fragment_channel_tab.xml index 30d34004167..752f8c106ed 100644 --- a/app/src/main/res/layout/fragment_channel_tab.xml +++ b/app/src/main/res/layout/fragment_channel_tab.xml @@ -45,7 +45,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:text="@string/empty_view_no_videos" + android:text="@string/empty_view_no_items" android:textSize="24sp"/> diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 35faa9ddec9..589be28e25d 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -204,6 +204,7 @@ %s weergave %s weergaven + Geen items Geen video\'s %s video diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a1e9d28d11b..ecae5f514f6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -247,6 +247,7 @@ Report error User report No results + @string/no_items @string/no_videos @string/no_comments Nothing here but crickets @@ -283,6 +284,7 @@ %s listener %s listeners + No items No videos %s video From 9e0dc0f289e6dfe5ceddc99f42d6d72a9572a3bf Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sat, 14 Mar 2020 09:19:28 +0100 Subject: [PATCH 05/22] Show tabs at the top instead of bottom --- .../main/res/layout/fragment_channel_detail.xml | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/layout/fragment_channel_detail.xml b/app/src/main/res/layout/fragment_channel_detail.xml index fa11a1c9e5d..f75d19aae8e 100644 --- a/app/src/main/res/layout/fragment_channel_detail.xml +++ b/app/src/main/res/layout/fragment_channel_detail.xml @@ -123,8 +123,12 @@ - + + - - - From 76e5f8cc8bf4d26653b3816969822299ce74d247 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sat, 14 Mar 2020 16:19:09 +0100 Subject: [PATCH 06/22] Display tab names --- .../fragments/detail/ChannelDetailFragment.java | 11 ++++++++++- .../schabi/newpipe/fragments/detail/TabAdaptor.java | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java index c64877f32de..6cb45e6c386 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java @@ -21,6 +21,7 @@ import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.core.content.ContextCompat; import androidx.viewpager.widget.ViewPager; @@ -278,7 +279,15 @@ protected void initViews(View rootView, Bundle savedInstanceState) { appBarLayout = rootView.findViewById(R.id.appbarlayout); viewPager = rootView.findViewById(R.id.viewpager); - pageAdapter = new TabAdaptor(getChildFragmentManager()); + pageAdapter = new TabAdaptor(getChildFragmentManager()) { + @Nullable + public CharSequence getPageTitle(int position) { + if (position < 0 || position >= mFragmentTitleList.size()) { + return null; + } + return mFragmentTitleList.get(position); + } + }; viewPager.setAdapter(pageAdapter); tabLayout = rootView.findViewById(R.id.tablayout); tabLayout.setupWithViewPager(viewPager); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java index d86226e9266..a898e6d6f6a 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java @@ -12,7 +12,7 @@ public class TabAdaptor extends FragmentPagerAdapter { private final List mFragmentList = new ArrayList<>(); - private final List mFragmentTitleList = new ArrayList<>(); + protected final List mFragmentTitleList = new ArrayList<>(); private final FragmentManager fragmentManager; public TabAdaptor(FragmentManager fm) { From a3c03ab1e00bf626b3025b2610dcda5868bce4fc Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sat, 14 Mar 2020 16:32:29 +0100 Subject: [PATCH 07/22] Fix crash when trying to open item from a channel in the homepage --- app/src/main/java/org/schabi/newpipe/BaseFragment.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/BaseFragment.java b/app/src/main/java/org/schabi/newpipe/BaseFragment.java index d4795cde272..f0f4b2d598a 100644 --- a/app/src/main/java/org/schabi/newpipe/BaseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/BaseFragment.java @@ -115,6 +115,8 @@ public void setTitle(String title) { protected FragmentManager getFM() { return getParentFragment() == null ? getFragmentManager() - : getParentFragment().getFragmentManager(); + : (getParentFragment().getParentFragment() == null + ? getParentFragment().getFragmentManager() + : getParentFragment().getParentFragment().getFragmentManager()); } } From 845a75a8dd63fcc972d83fb9c6d78d73dde7baa2 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sat, 14 Mar 2020 16:36:02 +0100 Subject: [PATCH 08/22] Don't call cancelDisplayTask for the images --- .../schabi/newpipe/fragments/detail/ChannelDetailFragment.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java index 6cb45e6c386..3a20ece7db0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java @@ -647,9 +647,6 @@ public void showLoading() { headerTitleView.setText(name != null ? name : ""); - imageLoader.cancelDisplayTask(headerAvatarView); - imageLoader.cancelDisplayTask(headerChannelBanner); - animateView(headerSubscribeButton, false, 100); } From d76fee8cc23286653f8d08eb9c2d625af5f5c950 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sat, 14 Mar 2020 20:13:12 +0100 Subject: [PATCH 09/22] Localize channel tab names --- .../detail/ChannelDetailFragment.java | 27 ++++++++++++++++++- app/src/main/res/values-nl/strings.xml | 7 +++++ app/src/main/res/values/strings.xml | 11 +++++++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java index 3a20ece7db0..ab4c7099da8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java @@ -669,7 +669,32 @@ public void handleResult(@NonNull ChannelInfo info) { setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName()); for (ChannelTabInfo tabInfo : info.getTabs()) { - pageAdapter.addFragment(ChannelTabFragment.getInstance(tabInfo), tabInfo.getName()); + String name; + switch (tabInfo.getName()) { + default: + case "Videos": + name = getString(R.string.channel_tab_videos); + break; + case "Playlists": + name = getString(R.string.channel_tab_playlists); + break; + case "Popular tracks": + name = getString(R.string.channel_tab_popular_tracks); + break; + case "Tracks": + name = getString(R.string.channel_tab_tracks); + break; + case "Albums": + name = getString(R.string.channel_tab_albums); + break; + case "Reposts": + name = getString(R.string.channel_tab_reposts); + break; + case "Events": + name = getString(R.string.channel_tab_events); + break; + } + pageAdapter.addFragment(ChannelTabFragment.getInstance(tabInfo), name); } pageAdapter.notifyDataSetUpdate(); diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 589be28e25d..1cc91dff26a 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -481,4 +481,11 @@ \nNota: niet alle toestellen zijn compatibel Wis data Verander de downloadmappen om effect te bekomen + Video\'s + Afspeellijsten + Populaire tracks + Tracks + Albums + Reposts + Evenementen \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ecae5f514f6..53bc9ce8793 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -646,4 +646,13 @@ Enable fast mode Disable fast mode Do you think feed loading is too slow? If so, try enabling fast loading (you can change it in settings or by pressing the button below).\n\nNewPipe offers two feed loading strategies:\n• Fetching the whole subscription channel, which is slow but complete.\n• Using a dedicated service endpoint, which is fast but usually not complete.\n\nThe difference between the two is that the fast one usually lacks some information, like the item\'s duration or type (can\'t distinguish between live videos and normal ones) and it may return less items.\n\nYouTube is an example of a service that offers this fast method with its RSS feed.\n\nSo the choice boils down to what you prefer: speed or precise information. - \ No newline at end of file + + + Videos + Playlists + Popular tracks + Tracks + Albums + Reposts + Events + From bcc15cb9e66934a7770fabc1464be527872163e2 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sat, 14 Mar 2020 20:14:48 +0100 Subject: [PATCH 10/22] Change background color of tabs --- app/src/main/res/layout/fragment_channel_detail.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/fragment_channel_detail.xml b/app/src/main/res/layout/fragment_channel_detail.xml index f75d19aae8e..e0d8b428a20 100644 --- a/app/src/main/res/layout/fragment_channel_detail.xml +++ b/app/src/main/res/layout/fragment_channel_detail.xml @@ -126,7 +126,8 @@ + android:layout_height="wrap_content" + android:background="?attr/contrast_background_color" /> From 2f216143f5bdf56d847f458d0c4015b97143d482 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sun, 15 Mar 2020 10:36:47 +0100 Subject: [PATCH 11/22] Avoid duplicated translations --- app/src/main/res/values-nl/strings.xml | 7 ++----- app/src/main/res/values/strings.xml | 8 ++++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 1cc91dff26a..0cfcca3ab59 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -401,7 +401,8 @@ Standaardwaarden Kanalen Afspeellijsten - Nummers + Video\'s + Tracks Gebruikers Afmelden Nieuw tabblad @@ -481,11 +482,7 @@ \nNota: niet alle toestellen zijn compatibel Wis data Verander de downloadmappen om effect te bekomen - Video\'s - Afspeellijsten Populaire tracks - Tracks Albums Reposts - Evenementen \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 53bc9ce8793..53cd1bdda7d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -648,11 +648,11 @@ Do you think feed loading is too slow? If so, try enabling fast loading (you can change it in settings or by pressing the button below).\n\nNewPipe offers two feed loading strategies:\n• Fetching the whole subscription channel, which is slow but complete.\n• Using a dedicated service endpoint, which is fast but usually not complete.\n\nThe difference between the two is that the fast one usually lacks some information, like the item\'s duration or type (can\'t distinguish between live videos and normal ones) and it may return less items.\n\nYouTube is an example of a service that offers this fast method with its RSS feed.\n\nSo the choice boils down to what you prefer: speed or precise information. - Videos - Playlists + @string/videos_string + @string/playlists Popular tracks - Tracks + @string/tracks Albums Reposts - Events + @string/events From 7a1402502733243d13219d3d0476e1b904561c94 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sun, 15 Mar 2020 11:52:41 +0100 Subject: [PATCH 12/22] Clean up the code --- .../java/org/schabi/newpipe/MainActivity.java | 2 +- .../detail/ChannelDetailFragment.java | 130 +++++------------- .../fragments/detail/VideoDetailFragment.java | 2 +- .../fragments/list/BaseListFragment.java | 11 +- .../list/channel/ChannelTabFragment.java | 32 ++--- .../list/playlist/PlaylistFragment.java | 2 +- .../holder/CommentsMiniInfoItemHolder.java | 2 +- .../subscription/SubscriptionFragment.kt | 2 +- .../schabi/newpipe/util/NavigationHelper.java | 9 +- 9 files changed, 60 insertions(+), 132 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 1d7a930ba85..e5c7cfd7d2d 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -643,7 +643,7 @@ private void handleIntent(Intent intent) { NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(), serviceId, url, title, autoPlay); break; case CHANNEL: - NavigationHelper.openChannelFragment(getSupportFragmentManager(), + NavigationHelper.openChannelDetailFragment(getSupportFragmentManager(), serviceId, url, title); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java index ab4c7099da8..e1c1817986f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java @@ -3,8 +3,6 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.text.TextUtils; @@ -19,7 +17,6 @@ import android.widget.ImageView; import android.widget.TextView; -import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; @@ -38,8 +35,7 @@ import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelTabInfo; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; -import org.schabi.newpipe.extractor.exceptions.ParsingException; -import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.list.channel.ChannelTabFragment; @@ -76,10 +72,7 @@ public class ChannelDetailFragment extends BaseStateFragment - implements BackPressable, - SharedPreferences.OnSharedPreferenceChangeListener, - View.OnClickListener, - View.OnLongClickListener { + implements BackPressable { private CompositeDisposable disposables = new CompositeDisposable(); private Disposable subscribeButtonMonitor; @@ -101,15 +94,11 @@ public class ChannelDetailFragment // Views //////////////////////////////////////////////////////////////////////////*/ - private Menu menu; - private AppBarLayout appBarLayout; private ViewPager viewPager; private TabAdaptor pageAdapter; private TabLayout tabLayout; - private MenuItem menuRssButton; - private ImageView headerChannelBanner; private ImageView headerAvatarView; private TextView headerTitleView; @@ -119,9 +108,9 @@ public class ChannelDetailFragment /*////////////////////////////////////////////////////////////////////////*/ - public static ChannelDetailFragment getInstance(int serviceId, String videoUrl, String name) { + public static ChannelDetailFragment getInstance(int serviceId, String channelUrl, String name) { ChannelDetailFragment instance = new ChannelDetailFragment(); - instance.setInitialData(serviceId, videoUrl, name); + instance.setInitialData(serviceId, channelUrl, name); return instance; } @@ -133,15 +122,14 @@ public static ChannelDetailFragment getInstance(int serviceId, String videoUrl, public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - PreferenceManager.getDefaultSharedPreferences(activity) - .registerOnSharedPreferenceChangeListener(this); + setHasOptionsMenu(true); } @Override public void onAttach(Context context) { super.onAttach(context); + subscriptionManager = new SubscriptionManager(activity); } @@ -153,6 +141,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @Override public void onPause() { super.onPause(); + if (currentWorker != null) currentWorker.dispose(); PreferenceManager.getDefaultSharedPreferences(getContext()) .edit() @@ -170,15 +159,13 @@ public void onResume() { // Check if it was loading when the fragment was stopped/paused, if (wasLoading.getAndSet(false)) { - selectAndLoadVideo(serviceId, url, name); + selectAndLoadChannel(serviceId, url, name); } } @Override public void onDestroy() { super.onDestroy(); - PreferenceManager.getDefaultSharedPreferences(activity) - .unregisterOnSharedPreferenceChangeListener(this); if (currentWorker != null) currentWorker.dispose(); if (disposables != null) disposables.clear(); @@ -197,21 +184,15 @@ public void onDestroyView() { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - switch (requestCode) { - case ReCaptchaActivity.RECAPTCHA_REQUEST: - if (resultCode == Activity.RESULT_OK) { - NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, name); - } else Log.e(TAG, "ReCaptcha failed"); - break; - default: - Log.e(TAG, "Request code from activity not supported [" + requestCode + "]"); - break; + if (requestCode == ReCaptchaActivity.RECAPTCHA_REQUEST) { + if (resultCode == Activity.RESULT_OK) { + NavigationHelper.openChannelDetailFragment(getFragmentManager(), serviceId, url, name); + } else Log.e(TAG, "ReCaptcha failed"); + } else { + Log.e(TAG, "Request code from activity not supported [" + requestCode + "]"); } } - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {} - /*////////////////////////////////////////////////////////////////////////// // State Saving //////////////////////////////////////////////////////////////////////////*/ @@ -248,27 +229,6 @@ protected void onRestoreInstanceState(@NonNull Bundle savedState) { } - /*////////////////////////////////////////////////////////////////////////// - // OnClick - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onClick(View v) { - if (isLoading.get() || currentInfo == null) return; - - switch (v.getId()) { - } - } - - @Override - public boolean onLongClick(View v) { - if (isLoading.get() || currentInfo == null) return false; - - switch (v.getId()) {} - - return true; - } - /*////////////////////////////////////////////////////////////////////////// // Init //////////////////////////////////////////////////////////////////////////*/ @@ -299,18 +259,12 @@ public CharSequence getPageTitle(int position) { headerSubscribeButton = rootView.findViewById(R.id.channel_subscribe_button); } - @Override - protected void initListeners() { - super.initListeners(); - } - /*////////////////////////////////////////////////////////////////////////// // Menu //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - this.menu = menu; + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); ActionBar supportActionBar = activity.getSupportActionBar(); if (useAsFrontPage && supportActionBar != null) { @@ -320,20 +274,12 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); - menuRssButton = menu.findItem(R.id.menu_item_rss); + MenuItem menuRssButton = menu.findItem(R.id.menu_item_rss); if (currentInfo != null) menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl())); } } - private void openRssFeed() { - final ChannelInfo info = currentInfo; - if (info != null) { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl())); - startActivity(intent); - } - } - @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); @@ -343,7 +289,7 @@ public boolean onOptionsItemSelected(MenuItem item) { } if (isLoading.get()) { - // if still loading, block menu buttons related to video info + // if still loading, block menu buttons related to channel info return true; } @@ -361,7 +307,9 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } case R.id.menu_item_rss: - openRssFeed(); + if (currentInfo != null) { + ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getFeedUrl()); + } break; default: return super.onOptionsItemSelected(item); @@ -379,27 +327,27 @@ private void setupActionBar(final ChannelInfo info) { /** * Stack that contains the "navigation history".
- * The peek is the current video. + * The peek is the current channel. */ - protected final LinkedList stack = new LinkedList<>(); + private final LinkedList stack = new LinkedList<>(); - public void pushToStack(int serviceId, String videoUrl, String name) { + private void pushToStack(int serviceId, String channelUrl, String name) { if (DEBUG) { Log.d(TAG, "pushToStack() called with: serviceId = [" - + serviceId + "], videoUrl = [" + videoUrl + "], name = [" + name + "]"); + + serviceId + "], channelUrl = [" + channelUrl + "], name = [" + name + "]"); } if (stack.size() > 0 && stack.peek().getServiceId() == serviceId - && stack.peek().getUrl().equals(videoUrl)) { + && stack.peek().getUrl().equals(channelUrl)) { Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = [" - + serviceId + "], videoUrl == peek.getUrl = [" + videoUrl + "]"); + + serviceId + "], channelUrl == peek.getUrl = [" + channelUrl + "]"); return; } else { Log.d(TAG, "pushToStack() wasn't equal"); } - stack.push(new StackItem(serviceId, videoUrl, name)); + stack.push(new StackItem(serviceId, channelUrl, name)); } @Override @@ -413,7 +361,7 @@ public boolean onBackPressed() { // Get stack item from the new top StackItem peek = stack.peek(); - selectAndLoadVideo(peek.getServiceId(), + selectAndLoadChannel(peek.getServiceId(), peek.getUrl(), !TextUtils.isEmpty(peek.getTitle()) ? peek.getTitle() @@ -565,28 +513,27 @@ private void updateSubscribeButton(boolean isSubscribed) { @Override protected void doInitialLoadLogic() { if (currentInfo == null) prepareAndLoadInfo(); - else prepareAndHandleInfo(currentInfo, false); + else prepareAndHandleInfo(currentInfo); } - public void selectAndLoadVideo(int serviceId, String videoUrl, String name) { - setInitialData(serviceId, videoUrl, name); + private void selectAndLoadChannel(int serviceId, String channelUrl, String name) { + setInitialData(serviceId, channelUrl, name); prepareAndLoadInfo(); } - public void prepareAndHandleInfo(final ChannelInfo info, boolean scrollToTop) { + private void prepareAndHandleInfo(final ChannelInfo info) { if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = [" - + info + "], scrollToTop = [" + scrollToTop + "]"); + + info + "], scrollToTop = [" + false + "]"); setInitialData(info.getServiceId(), info.getUrl(), info.getName()); pushToStack(serviceId, url, name); showLoading(); initTabs(); - if (scrollToTop) appBarLayout.setExpanded(true, true); handleResult(info); } - protected void prepareAndLoadInfo() { + private void prepareAndLoadInfo() { appBarLayout.setExpanded(true, true); pushToStack(serviceId, url, name); startLoading(false); @@ -628,15 +575,6 @@ protected void setInitialData(int serviceId, String url, String name) { this.name = !TextUtils.isEmpty(name) ? name : ""; } - @Override - public void showError(String message, boolean showRetryButton) { - showError(message, showRetryButton, R.drawable.not_available_monkey); - } - - protected void showError(String message, boolean showRetryButton, @DrawableRes int imageError) { - super.showError(message, showRetryButton); - } - /*////////////////////////////////////////////////////////////////////////// // Contract //////////////////////////////////////////////////////////////////////////*/ 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 86198650c68..c099b83cc83 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 @@ -396,7 +396,7 @@ public void onClick(View v) { Log.w(TAG, "Can't open channel because we got no channel URL"); } else { try { - NavigationHelper.openChannelFragment( + NavigationHelper.openChannelDetailFragment( getFragmentManager(), currentInfo.getServiceId(), currentInfo.getUploaderUrl(), diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index d55bf3f409a..307939d0df7 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -7,16 +7,17 @@ import android.content.res.Resources; import android.os.Bundle; import android.preference.PreferenceManager; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; + import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.View; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; @@ -214,7 +215,7 @@ public void held(StreamInfoItem selectedItem) { public void selected(ChannelInfoItem selectedItem) { try { onItemSelected(selectedItem); - NavigationHelper.openChannelFragment(getFM(), + NavigationHelper.openChannelDetailFragment(getFM(), selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); 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 e9287340a23..735dda141ca 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 @@ -1,7 +1,5 @@ package org.schabi.newpipe.fragments.list.channel; -import android.content.Context; -import android.content.SharedPreferences; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; @@ -27,7 +25,7 @@ import static org.schabi.newpipe.util.ExtractorHelper.getMoreChannelTabItems; -public class ChannelTabFragment extends BaseListInfoFragment implements SharedPreferences.OnSharedPreferenceChangeListener{ +public class ChannelTabFragment extends BaseListInfoFragment { private CompositeDisposable disposables = new CompositeDisposable(); private ChannelTabInfo relatedStreamInfo; @@ -45,12 +43,8 @@ public static ChannelTabFragment getInstance(ChannelTabInfo info) { @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); - mIsVisibleToUser = isVisibleToUser; - } - @Override - public void onAttach(Context context) { - super.onAttach(context); + mIsVisibleToUser = isVisibleToUser; } @Override @@ -61,6 +55,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c @Override public void onDestroy() { super.onDestroy(); + if (disposables != null) disposables.clear(); } @@ -78,11 +73,6 @@ protected Single loadResult(boolean forceLoad) { // Contract //////////////////////////////////////////////////////////////////////////*/ - @Override - public void showLoading() { - super.showLoading(); - } - @Override public void handleResult(@NonNull ChannelTabInfo result) { super.handleResult(result); @@ -119,6 +109,7 @@ protected boolean onError(Throwable exception) { hideLoading(); showSnackBarError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, R.string.general_error); + return true; } @@ -134,32 +125,29 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {} private void setInitialData(ChannelTabInfo info) { super.setInitialData(info.getServiceId(), info.getUrl(), info.getName()); + if(this.relatedStreamInfo == null) this.relatedStreamInfo = info; } - private static final String INFO_KEY = "related_info_key"; @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); + outState.putSerializable(INFO_KEY, relatedStreamInfo); } @Override protected void onRestoreInstanceState(@NonNull Bundle savedState) { super.onRestoreInstanceState(savedState); - if (savedState != null) { - Serializable serializable = savedState.getSerializable(INFO_KEY); - if (serializable instanceof ChannelTabInfo){ - this.relatedStreamInfo = (ChannelTabInfo) serializable; - } + + Serializable serializable = savedState.getSerializable(INFO_KEY); + if (serializable instanceof ChannelTabInfo) { + this.relatedStreamInfo = (ChannelTabInfo) serializable; } } - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {} - @Override protected boolean isGridLayout() { return false; 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 a992cd7baf2..8328a8ccd03 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 @@ -267,7 +267,7 @@ public void handleResult(@NonNull final PlaylistInfo result) { if (!TextUtils.isEmpty(result.getUploaderUrl())) { headerUploaderLayout.setOnClickListener(v -> { try { - NavigationHelper.openChannelFragment(getFragmentManager(), + NavigationHelper.openChannelDetailFragment(getFragmentManager(), result.getServiceId(), result.getUploaderUrl(), result.getUploaderName()); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java index 58f1ab90d60..9892a44aee1 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java @@ -85,7 +85,7 @@ public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager h if(StringUtil.isBlank(item.getAuthorEndpoint())) return; try { final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext(); - NavigationHelper.openChannelFragment( + NavigationHelper.openChannelDetailFragment( activity.getSupportFragmentManager(), item.getServiceId(), item.getAuthorEndpoint(), diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index 98e20a02f62..e3be905ba06 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -304,7 +304,7 @@ class SubscriptionFragment : BaseStateFragment() { } private val listenerChannelItem = object : OnClickGesture() { - override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(fm, + override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelDetailFragment(fm, selectedItem.serviceId, selectedItem.url, selectedItem.name) override fun held(selectedItem: ChannelInfoItem) = showLongTapDialog(selectedItem) diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 59c749eaf0a..ae91dcc7beb 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -8,14 +8,15 @@ import android.net.Uri; import android.os.Build; import android.preference.PreferenceManager; +import android.util.Log; +import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; -import androidx.appcompat.app.AlertDialog; -import android.util.Log; -import android.widget.Toast; import com.nostra13.universalimageloader.core.ImageLoader; @@ -309,7 +310,7 @@ public static void openVideoDetailFragment(FragmentManager fragmentManager, int .commit(); } - public static void openChannelFragment( + public static void openChannelDetailFragment( FragmentManager fragmentManager, int serviceId, String url, From 3a5fb0c060eab88a10ea5d7f7ebc5620167f679c Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sun, 15 Mar 2020 11:53:00 +0100 Subject: [PATCH 13/22] Use the proper error handling for channels --- app/build.gradle | 2 +- .../fragments/detail/ChannelDetailFragment.java | 11 +++-------- .../fragments/list/channel/ChannelTabFragment.java | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ab8af7d3024..69744527317 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -92,7 +92,7 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.wb9688:NewPipeExtractor:5daba8dd3c34bc1318a7062976ec172779e58b5b' + implementation 'com.github.wb9688:NewPipeExtractor:a27979794e9a0eb6cd2087d22dc8f0304073b3e9' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java index e1c1817986f..8aa53e07c17 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java @@ -681,21 +681,16 @@ public void handleResult(@NonNull ChannelInfo info) { protected boolean onError(Throwable exception) { if (super.onError(exception)) return true; - else if (exception instanceof ContentNotAvailableException) { + if (exception instanceof ContentNotAvailableException) { showError(getString(R.string.content_not_available), false); } else { - int errorId = exception instanceof YoutubeStreamExtractor.DecryptException - ? R.string.youtube_signature_decryption_error - : exception instanceof ParsingException - ? R.string.parsing_error - : R.string.general_error; + int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; onUnrecoverableError(exception, - UserAction.REQUESTED_STREAM, + UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), url, errorId); } - 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 735dda141ca..8131da38c9c 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 @@ -108,7 +108,7 @@ protected boolean onError(Throwable exception) { if (super.onError(exception)) return true; hideLoading(); - showSnackBarError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, R.string.general_error); + showSnackBarError(exception, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), url, R.string.general_error); return true; } From 56030263b4abb6535f471fdf731025a87b43c336 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sun, 15 Mar 2020 12:04:35 +0100 Subject: [PATCH 14/22] Make tabs scrollable --- app/src/main/res/layout/fragment_channel_detail.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/fragment_channel_detail.xml b/app/src/main/res/layout/fragment_channel_detail.xml index e0d8b428a20..79eaa4e13a9 100644 --- a/app/src/main/res/layout/fragment_channel_detail.xml +++ b/app/src/main/res/layout/fragment_channel_detail.xml @@ -127,7 +127,8 @@ android:id="@+id/tablayout" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?attr/contrast_background_color" /> + android:background="?attr/contrast_background_color" + app:tabMode="scrollable" /> From f1c2744ddac444c386df17138e053293308117dc Mon Sep 17 00:00:00 2001 From: Stypox Date: Sun, 15 Mar 2020 14:13:19 +0100 Subject: [PATCH 15/22] Fix location of loading animation and network error Signed-off-by: wb9688 --- .../res/layout/fragment_channel_detail.xml | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/app/src/main/res/layout/fragment_channel_detail.xml b/app/src/main/res/layout/fragment_channel_detail.xml index 79eaa4e13a9..ce289047531 100644 --- a/app/src/main/res/layout/fragment_channel_detail.xml +++ b/app/src/main/res/layout/fragment_channel_detail.xml @@ -100,6 +100,20 @@ tools:ignore="RtlHardcoded" tools:visibility="visible"/> + + + + + + - - Date: Sun, 15 Mar 2020 14:33:32 +0100 Subject: [PATCH 16/22] Put REQUESTED_CHANNEL instead of REQUESTED_STREAM in the errors --- .../newpipe/fragments/detail/ChannelDetailFragment.java | 9 ++------- .../fragments/list/channel/ChannelTabFragment.java | 4 ++-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java index 8aa53e07c17..f461af263f7 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java @@ -4,7 +4,6 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.preference.PreferenceManager; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; @@ -143,10 +142,6 @@ public void onPause() { super.onPause(); if (currentWorker != null) currentWorker.dispose(); - PreferenceManager.getDefaultSharedPreferences(getContext()) - .edit() - .putString(getString(R.string.stream_info_selected_tab_key), pageAdapter.getItemTitle(viewPager.getCurrentItem())) - .apply(); } @Override @@ -666,7 +661,7 @@ public void handleResult(@NonNull ChannelInfo info) { if (!info.getErrors().isEmpty()) { showSnackBarError(info.getErrors(), - UserAction.REQUESTED_STREAM, + UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(info.getServiceId()), info.getUrl(), 0); @@ -674,7 +669,7 @@ public void handleResult(@NonNull ChannelInfo info) { } /*////////////////////////////////////////////////////////////////////////// - // Stream Results + // Channel Results //////////////////////////////////////////////////////////////////////////*/ @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 8131da38c9c..eadaa97ee18 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 @@ -80,7 +80,7 @@ public void handleResult(@NonNull ChannelTabInfo result) { AnimationUtils.slideUp(getView(),120, 96, 0.06f); if (!result.getErrors().isEmpty()) { - showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); + showSnackBarError(result.getErrors(), UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); } if (disposables != null) disposables.clear(); @@ -92,7 +92,7 @@ public void handleNextItems(ListExtractor.InfoItemsPage result) { if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), - UserAction.REQUESTED_STREAM, + UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), "Get next page of: " + url, R.string.general_error); From 344b4e7b6f26a5af008d0f2aef8e42b5f9dd0cae Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sun, 15 Mar 2020 15:53:30 +0100 Subject: [PATCH 17/22] Add playlist control buttons for certain tabs --- app/build.gradle | 2 +- .../list/channel/ChannelTabFragment.java | 67 ++++++++++++++++-- .../player/playqueue/ChannelPlayQueue.java | 69 +++++++++---------- .../main/res/layout/fragment_channel_tab.xml | 3 + 4 files changed, 96 insertions(+), 45 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 69744527317..ac731df5f9e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -92,7 +92,7 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.wb9688:NewPipeExtractor:a27979794e9a0eb6cd2087d22dc8f0304073b3e9' + implementation 'com.github.wb9688:NewPipeExtractor:c0565e767d2be11dda3fe0e727ec47e1394da518' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' 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 eadaa97ee18..0f556e285a2 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 @@ -6,19 +6,27 @@ import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelTabInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; +import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; +import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.AnimationUtils; +import org.schabi.newpipe.util.NavigationHelper; import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; import io.reactivex.Single; import io.reactivex.disposables.CompositeDisposable; @@ -28,7 +36,13 @@ public class ChannelTabFragment extends BaseListInfoFragment { private CompositeDisposable disposables = new CompositeDisposable(); - private ChannelTabInfo relatedStreamInfo; + private ChannelTabInfo channelTabInfo; + + private View playlistCtrl; + private LinearLayout headerPlayAllButton; + private LinearLayout headerPopupButton; + private LinearLayout headerBackgroundButton; + public static ChannelTabFragment getInstance(ChannelTabInfo info) { ChannelTabFragment instance = new ChannelTabFragment(); @@ -61,12 +75,24 @@ public void onDestroy() { @Override protected Single loadMoreItemsLogic() { - return getMoreChannelTabItems(relatedStreamInfo, currentNextPageUrl); + return getMoreChannelTabItems(channelTabInfo, currentNextPageUrl); } @Override protected Single loadResult(boolean forceLoad) { - return Single.fromCallable(() -> relatedStreamInfo); + return Single.fromCallable(() -> channelTabInfo); + } + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + playlistCtrl = rootView.findViewById(R.id.playlist_control); + playlistCtrl.setVisibility(View.GONE); + + headerPlayAllButton = playlistCtrl.findViewById(R.id.playlist_ctrl_play_all_button); + headerPopupButton = playlistCtrl.findViewById(R.id.playlist_ctrl_play_popup_button); + headerBackgroundButton = playlistCtrl.findViewById(R.id.playlist_ctrl_play_bg_button); } /*////////////////////////////////////////////////////////////////////////// @@ -84,6 +110,35 @@ public void handleResult(@NonNull ChannelTabInfo result) { } if (disposables != null) disposables.clear(); + + switch (result.getName()) { + case "Videos": + case "Tracks": + case "Popular tracks": + case "Events": + playlistCtrl.setVisibility(View.VISIBLE); + + headerPlayAllButton.setOnClickListener( + view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue(0), false)); + headerPopupButton.setOnClickListener( + view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(0), false)); + headerBackgroundButton.setOnClickListener( + view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(0), false)); + } + } + + private PlayQueue getPlayQueue(final int index) { + final List streamItems = new ArrayList<>(); + for (InfoItem i : infoListAdapter.getItemsList()) { + if (i instanceof StreamInfoItem) { + streamItems.add((StreamInfoItem) i); + } + } + return new ChannelPlayQueue( + currentInfo, + streamItems, + index + ); } @Override @@ -126,7 +181,7 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {} private void setInitialData(ChannelTabInfo info) { super.setInitialData(info.getServiceId(), info.getUrl(), info.getName()); - if(this.relatedStreamInfo == null) this.relatedStreamInfo = info; + if (this.channelTabInfo == null) this.channelTabInfo = info; } private static final String INFO_KEY = "related_info_key"; @@ -135,7 +190,7 @@ private void setInitialData(ChannelTabInfo info) { public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putSerializable(INFO_KEY, relatedStreamInfo); + outState.putSerializable(INFO_KEY, channelTabInfo); } @Override @@ -144,7 +199,7 @@ protected void onRestoreInstanceState(@NonNull Bundle savedState) { Serializable serializable = savedState.getSerializable(INFO_KEY); if (serializable instanceof ChannelTabInfo) { - this.relatedStreamInfo = (ChannelTabInfo) serializable; + this.channelTabInfo = (ChannelTabInfo) serializable; } } 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 index 84b3bd4d595..bdc9d8d95b0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java @@ -1,50 +1,43 @@ package org.schabi.newpipe.player.playqueue; - import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; +import org.schabi.newpipe.extractor.channel.ChannelTabInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.util.ExtractorHelper; import java.util.List; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; -//public final class ChannelPlayQueue extends AbstractInfoPlayQueue { -// public ChannelPlayQueue(final ChannelInfoItem item) { -// super(item); -// } -// -// public ChannelPlayQueue(final ChannelInfo info) { -// this(info.getServiceId(), info.getUrl(), info.getNextPageUrl(), info.getRelatedItems(), 0); -// } -// -// public ChannelPlayQueue(final int serviceId, -// final String url, -// final String nextPageUrl, -// final List streams, -// final int index) { -// super(serviceId, url, nextPageUrl, 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.nextUrl) -// .subscribeOn(Schedulers.io()) -// .observeOn(AndroidSchedulers.mainThread()) -// .subscribe(getNextPageObserver()); -// } -// } -//} +public final class ChannelPlayQueue extends AbstractInfoPlayQueue { + private ChannelTabInfo channelTabInfo; + + public ChannelPlayQueue(final ChannelTabInfo channelTabInfo, final List streams, final int index) { + super(channelTabInfo.getServiceId(), channelTabInfo.getUrl(), channelTabInfo.getNextPageUrl(), streams, index); + + this.channelTabInfo = channelTabInfo; + } + + @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((Consumer) getHeadListObserver()); + } else { + ExtractorHelper.getMoreChannelTabItems(this.channelTabInfo, this.nextUrl) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getNextPageObserver()); + } + } +} diff --git a/app/src/main/res/layout/fragment_channel_tab.xml b/app/src/main/res/layout/fragment_channel_tab.xml index 752f8c106ed..e8a5092acbe 100644 --- a/app/src/main/res/layout/fragment_channel_tab.xml +++ b/app/src/main/res/layout/fragment_channel_tab.xml @@ -5,10 +5,13 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + From c0a601ab585480fe4f314f8007d3772e65e8cdd6 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sun, 15 Mar 2020 17:04:35 +0100 Subject: [PATCH 18/22] Remove buggy custom stack implementation from ChannelDetailFragment --- .../detail/ChannelDetailFragment.java | 72 ++----------------- 1 file changed, 4 insertions(+), 68 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java index f461af263f7..348741965f1 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/ChannelDetailFragment.java @@ -35,7 +35,6 @@ import org.schabi.newpipe.extractor.channel.ChannelTabInfo; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.list.channel.ChannelTabFragment; import org.schabi.newpipe.local.subscription.SubscriptionManager; @@ -50,8 +49,6 @@ import org.schabi.newpipe.util.ShareUtils; import java.io.Serializable; -import java.util.Collection; -import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -70,8 +67,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView; public class ChannelDetailFragment - extends BaseStateFragment - implements BackPressable { + extends BaseStateFragment { private CompositeDisposable disposables = new CompositeDisposable(); private Disposable subscribeButtonMonitor; @@ -202,8 +198,6 @@ public void onSaveInstanceState(Bundle outState) { if (!isLoading.get() && currentInfo != null && isVisible()) { outState.putSerializable(INFO_KEY, currentInfo); } - - outState.putSerializable(STACK_KEY, stack); } @Override @@ -215,13 +209,6 @@ protected void onRestoreInstanceState(@NonNull Bundle savedState) { currentInfo = (ChannelInfo) serializable; InfoCache.getInstance().putInfo(serviceId, url, currentInfo, InfoItem.InfoType.CHANNEL); } - - serializable = savedState.getSerializable(STACK_KEY); - if (serializable instanceof Collection) { - //noinspection unchecked - stack.addAll((Collection) serializable); - } - } /*////////////////////////////////////////////////////////////////////////// @@ -316,54 +303,6 @@ private void setupActionBar(final ChannelInfo info) { if (DEBUG) Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]"); } - /*////////////////////////////////////////////////////////////////////////// - // OwnStack - //////////////////////////////////////////////////////////////////////////*/ - - /** - * Stack that contains the "navigation history".
- * The peek is the current channel. - */ - private final LinkedList stack = new LinkedList<>(); - - private void pushToStack(int serviceId, String channelUrl, String name) { - if (DEBUG) { - Log.d(TAG, "pushToStack() called with: serviceId = [" - + serviceId + "], channelUrl = [" + channelUrl + "], name = [" + name + "]"); - } - - if (stack.size() > 0 - && stack.peek().getServiceId() == serviceId - && stack.peek().getUrl().equals(channelUrl)) { - Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = [" - + serviceId + "], channelUrl == peek.getUrl = [" + channelUrl + "]"); - return; - } else { - Log.d(TAG, "pushToStack() wasn't equal"); - } - - stack.push(new StackItem(serviceId, channelUrl, name)); - } - - @Override - public boolean onBackPressed() { - if (DEBUG) Log.d(TAG, "onBackPressed() called"); - // That means that we are on the start of the stack, - // return false to let the MainActivity handle the onBack - if (stack.size() <= 1) return false; - // Remove top - stack.pop(); - // Get stack item from the new top - StackItem peek = stack.peek(); - - selectAndLoadChannel(peek.getServiceId(), - peek.getUrl(), - !TextUtils.isEmpty(peek.getTitle()) - ? peek.getTitle() - : ""); - return true; - } - /*////////////////////////////////////////////////////////////////////////// // Channel Subscription //////////////////////////////////////////////////////////////////////////*/ @@ -521,7 +460,6 @@ private void prepareAndHandleInfo(final ChannelInfo info) { + info + "], scrollToTop = [" + false + "]"); setInitialData(info.getServiceId(), info.getUrl(), info.getName()); - pushToStack(serviceId, url, name); showLoading(); initTabs(); @@ -530,7 +468,6 @@ private void prepareAndHandleInfo(final ChannelInfo info) { private void prepareAndLoadInfo() { appBarLayout.setExpanded(true, true); - pushToStack(serviceId, url, name); startLoading(false); } @@ -558,6 +495,7 @@ public void startLoading(boolean forceLoad) { private void initTabs() { pageAdapter.clearAllItems(); + pageAdapter.notifyDataSetUpdate(); } /*////////////////////////////////////////////////////////////////////////// @@ -630,14 +568,14 @@ public void handleResult(@NonNull ChannelInfo info) { pageAdapter.addFragment(ChannelTabFragment.getInstance(tabInfo), name); } - pageAdapter.notifyDataSetUpdate(); - if (pageAdapter.getCount() < 2) { tabLayout.setVisibility(View.GONE); } else { tabLayout.setVisibility(View.VISIBLE); } + pageAdapter.notifyDataSetUpdate(); + headerSubscribersTextView.setVisibility(View.VISIBLE); if (info.getSubscriberCount() >= 0) { headerSubscribersTextView.setText(Localization.shortSubscriberCount(activity, info.getSubscriberCount())); @@ -650,8 +588,6 @@ public void handleResult(@NonNull ChannelInfo info) { updateSubscription(info); monitorSubscription(info); - pushToStack(serviceId, url, name); - setupActionBar(info); initThumbnailViews(info); From 2b7d6970eaaa638e52290130f551b03f80fa42f4 Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sun, 15 Mar 2020 17:15:49 +0100 Subject: [PATCH 19/22] Also show playlist controls for reposts, as it ignores playlists --- .../newpipe/fragments/list/channel/ChannelTabFragment.java | 1 + 1 file changed, 1 insertion(+) 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 0f556e285a2..651e25f8b1c 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 @@ -115,6 +115,7 @@ public void handleResult(@NonNull ChannelTabInfo result) { case "Videos": case "Tracks": case "Popular tracks": + case "Reposts": case "Events": playlistCtrl.setVisibility(View.VISIBLE); From f63eb02ac14915131d60933db249aba16d6ce22b Mon Sep 17 00:00:00 2001 From: wb9688 Date: Sun, 15 Mar 2020 18:30:36 +0100 Subject: [PATCH 20/22] Fix updating feed --- .../schabi/newpipe/local/feed/service/FeedLoadService.kt | 2 +- .../java/org/schabi/newpipe/util/ExtractorHelper.java | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) 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 294a7fcd5e7..8e76ac27c80 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 @@ -211,7 +211,7 @@ class FeedLoadService : Service() { } else { ExtractorHelper .getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true) - .blockingGet() + .blockingGet().tabs[0] } as ListInfo return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo)) 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 20521a392f1..b428ccec90f 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -47,7 +47,6 @@ import org.schabi.newpipe.extractor.search.SearchInfo; import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; @@ -59,7 +58,6 @@ import io.reactivex.Maybe; import io.reactivex.Single; -import io.reactivex.SingleSource; public final class ExtractorHelper { private static final String TAG = ExtractorHelper.class.getSimpleName(); @@ -136,9 +134,8 @@ public static Single getMoreChannelTabItems(final ChannelTabInfo ChannelTabInfo.getMoreItems(tabInfo, nextStreamsUrl)); } - public static Single> getFeedInfoFallbackToChannelInfo(final int serviceId, - final String url) { - final Maybe> maybeFeedInfo = Maybe.fromCallable(() -> { + 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); @@ -149,7 +146,7 @@ public static Single> getFeedInfoFallbackToChannelInfo( return FeedInfo.getInfo(feedExtractor); }); - return maybeFeedInfo.switchIfEmpty((SingleSource) getChannelInfo(serviceId, url, true)); + return maybeFeedInfo.switchIfEmpty(Single.fromCallable(() -> getChannelInfo(serviceId, url, true).blockingGet().getTabs().get(0))); } public static Single getCommentsInfo(final int serviceId, From 93bc0e7b3a6e6bbfbaa18f5900a6add33e1e390f Mon Sep 17 00:00:00 2001 From: wb9688 Date: Thu, 19 Mar 2020 13:53:28 +0100 Subject: [PATCH 21/22] Lazy-load tabs --- app/build.gradle | 2 +- .../schabi/newpipe/fragments/BaseStateFragment.java | 13 ++++++++++--- .../fragments/list/channel/ChannelTabFragment.java | 7 ++++++- .../local/subscription/SubscriptionManager.kt | 9 ++++++++- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ac731df5f9e..decc7bc6b07 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -92,7 +92,7 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.wb9688:NewPipeExtractor:c0565e767d2be11dda3fe0e727ec47e1394da518' + implementation 'com.github.wb9688:NewPipeExtractor:a657d9763bcded5d40efc7f45ead234b63772398' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java index 8e328266e4e..10f8acbb1eb 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java @@ -2,8 +2,6 @@ import android.content.Intent; import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; import android.util.Log; import android.view.View; import android.widget.Button; @@ -11,6 +9,9 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + import com.jakewharton.rxbinding2.view.RxView; import org.schabi.newpipe.BaseFragment; @@ -52,7 +53,6 @@ public abstract class BaseStateFragment extends BaseFragment implements ViewC @Override public void onViewCreated(View rootView, Bundle savedInstanceState) { super.onViewCreated(rootView, savedInstanceState); - doInitialLoadLogic(); } @Override @@ -61,6 +61,13 @@ public void onPause() { wasLoading.set(isLoading.get()); } + @Override + public void onStart() { + super.onStart(); + + doInitialLoadLogic(); + } + /*////////////////////////////////////////////////////////////////////////// // Init 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 651e25f8b1c..6d7e93340c3 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 @@ -18,6 +18,7 @@ import org.schabi.newpipe.extractor.channel.ChannelTabInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; +import org.schabi.newpipe.local.subscription.SubscriptionManager; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.report.UserAction; @@ -80,7 +81,7 @@ protected Single loadMoreItemsLogic() { @Override protected Single loadResult(boolean forceLoad) { - return Single.fromCallable(() -> channelTabInfo); + return Single.fromCallable(() -> channelTabInfo.loadTab()); } @Override @@ -126,6 +127,10 @@ public void handleResult(@NonNull ChannelTabInfo result) { headerBackgroundButton.setOnClickListener( view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(0), false)); } + + if (channelTabInfo.isUsedForFeed()) { + new SubscriptionManager(activity).updateChannelTabInfo(currentInfo); + } } private PlayQueue getPlayQueue(final int index) { 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 626d299fd1d..eaca42324aa 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 @@ -9,6 +9,7 @@ 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.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 @@ -39,7 +40,13 @@ class SubscriptionManager(context: Context) { Completable.fromRunnable { it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount) subscriptionTable.update(it) - feedDatabaseManager.upsertAll(it.uid, info.tabs[0].relatedItems as List) + } + } + + fun updateChannelTabInfo(info: ChannelTabInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url) + .flatMapCompletable { + Completable.fromRunnable { + feedDatabaseManager.upsertAll(it.uid, info.relatedItems as List) } } From 9fed618e02fae570c13a5503b49c720277421d4e Mon Sep 17 00:00:00 2001 From: wb9688 Date: Thu, 19 Mar 2020 14:28:02 +0100 Subject: [PATCH 22/22] Fix updating feed --- .../schabi/newpipe/local/feed/service/FeedLoadService.kt | 5 ++++- .../newpipe/local/subscription/SubscriptionManager.kt | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) 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 8e76ac27c80..9827944d28a 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 @@ -46,6 +46,7 @@ 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.channel.ChannelTabInfo import org.schabi.newpipe.extractor.exceptions.ReCaptchaException import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.local.feed.FeedDatabaseManager @@ -211,7 +212,7 @@ class FeedLoadService : Service() { } else { ExtractorHelper .getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true) - .blockingGet().tabs[0] + .blockingGet().tabs[0].loadTab() } as ListInfo return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo)) @@ -304,6 +305,8 @@ class FeedLoadService : Service() { val subscriptionId = notification.value!!.first val info = notification.value!!.second + if (info is ChannelTabInfo) info.loadTab() + feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems) subscriptionManager.updateFromInfo(subscriptionId, info) 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 eaca42324aa..3bee3c140a2 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 @@ -28,7 +28,7 @@ class SubscriptionManager(context: Context) { database.runInTransaction { infoList.forEachIndexed { index, info -> - feedDatabaseManager.upsertAll(listEntities[index].uid, info.tabs[0].relatedItems as List) + feedDatabaseManager.upsertAll(listEntities[index].uid, info.tabs[0].loadTab().relatedItems as List) } } @@ -46,7 +46,7 @@ class SubscriptionManager(context: Context) { fun updateChannelTabInfo(info: ChannelTabInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url) .flatMapCompletable { Completable.fromRunnable { - feedDatabaseManager.upsertAll(it.uid, info.relatedItems as List) + feedDatabaseManager.upsertAll(it.uid, info.loadTab().relatedItems as List) } } @@ -71,7 +71,7 @@ class SubscriptionManager(context: Context) { fun insertSubscription(subscriptionEntity: SubscriptionEntity, info: ChannelInfo) { database.runInTransaction { val subscriptionId = subscriptionTable.insert(subscriptionEntity) - feedDatabaseManager.upsertAll(subscriptionId, info.tabs[0].relatedItems as List) + feedDatabaseManager.upsertAll(subscriptionId, info.tabs[0].loadTab().relatedItems as List) } }