added comments fragment

This commit is contained in:
Ritvik Saraf 2018-09-23 07:02:19 +05:30
parent 5cc73555cd
commit 7047b62442
10 changed files with 451 additions and 77 deletions

View file

@ -55,7 +55,7 @@ dependencies {
exclude module: 'support-annotations' exclude module: 'support-annotations'
} }
implementation 'com.github.yausername:NewPipeExtractor:e04787f' implementation 'com.github.yausername:NewPipeExtractor:c1199c8'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.8.9' testImplementation 'org.mockito:mockito-core:2.8.9'

View file

@ -54,9 +54,12 @@ import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.download.DownloadDialog; import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.comments.CommentsInfo; import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.AudioStream;
@ -131,6 +134,7 @@ public class VideoDetailFragment
private boolean autoPlayEnabled; private boolean autoPlayEnabled;
private boolean showRelatedStreams; private boolean showRelatedStreams;
private boolean showComments; private boolean showComments;
private boolean isCommentsSupported;
private boolean wasRelatedStreamsExpanded = false; private boolean wasRelatedStreamsExpanded = false;
@State @State
@ -141,6 +145,7 @@ public class VideoDetailFragment
protected String url; protected String url;
private StreamInfo currentInfo; private StreamInfo currentInfo;
private CommentsInfo commentsInfo;
private Disposable currentWorker; private Disposable currentWorker;
@NonNull @NonNull
private CompositeDisposable disposables = new CompositeDisposable(); private CompositeDisposable disposables = new CompositeDisposable();
@ -242,6 +247,7 @@ public class VideoDetailFragment
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
if (currentWorker != null) currentWorker.dispose(); if (currentWorker != null) currentWorker.dispose();
if (commentsDisposable != null) commentsDisposable.dispose();
} }
@Override @Override
@ -253,7 +259,7 @@ public class VideoDetailFragment
if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0)
initRelatedVideos(currentInfo); initRelatedVideos(currentInfo);
if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) setupActionBar(currentInfo); if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) setupActionBar(currentInfo);
if ((updateFlags & COMMENTS_UPDATE_FLAG) != 0) initComments(currentInfo.getCommentsInfo()); if ((updateFlags & COMMENTS_UPDATE_FLAG) != 0) initComments(commentsInfo);
} }
if ((updateFlags & TOOLBAR_ITEMS_UPDATE_FLAG) != 0 if ((updateFlags & TOOLBAR_ITEMS_UPDATE_FLAG) != 0
@ -267,6 +273,8 @@ public class VideoDetailFragment
// Check if it was loading when the fragment was stopped/paused, // Check if it was loading when the fragment was stopped/paused,
if (wasLoading.getAndSet(false)) { if (wasLoading.getAndSet(false)) {
selectAndLoadVideo(serviceId, url, name); selectAndLoadVideo(serviceId, url, name);
}else{
loadComments(false);
} }
} }
@ -277,8 +285,10 @@ public class VideoDetailFragment
.unregisterOnSharedPreferenceChangeListener(this); .unregisterOnSharedPreferenceChangeListener(this);
if (currentWorker != null) currentWorker.dispose(); if (currentWorker != null) currentWorker.dispose();
if (commentsDisposable != null) commentsDisposable.dispose();
if (disposables != null) disposables.clear(); if (disposables != null) disposables.clear();
currentWorker = null; currentWorker = null;
commentsDisposable = null;
disposables = null; disposables = null;
} }
@ -359,7 +369,7 @@ public class VideoDetailFragment
if (serializable instanceof StreamInfo) { if (serializable instanceof StreamInfo) {
//noinspection unchecked //noinspection unchecked
currentInfo = (StreamInfo) serializable; currentInfo = (StreamInfo) serializable;
InfoCache.getInstance().putInfo(serviceId, url, currentInfo); InfoCache.getInstance().putInfo(serviceId, url, currentInfo, InfoItem.InfoType.STREAM);
} }
serializable = savedState.getSerializable(STACK_KEY); serializable = savedState.getSerializable(STACK_KEY);
@ -367,6 +377,7 @@ public class VideoDetailFragment
//noinspection unchecked //noinspection unchecked
stack.addAll((Collection<? extends StackItem>) serializable); stack.addAll((Collection<? extends StackItem>) serializable);
} }
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -425,7 +436,7 @@ public class VideoDetailFragment
toggleExpandRelatedVideos(currentInfo); toggleExpandRelatedVideos(currentInfo);
break; break;
case R.id.detail_comments_expand: case R.id.detail_comments_expand:
toggleExpandComments(currentInfo.getCommentsInfo()); toggleExpandComments(commentsInfo);
break; break;
} }
} }
@ -493,40 +504,33 @@ public class VideoDetailFragment
if (!showComments || null == info) return; if (!showComments || null == info) return;
int initialCount = INITIAL_COMMENTS; int initialCount = INITIAL_COMMENTS;
int currentCount = commentsView.getChildCount();
if (commentsView.getChildCount() > initialCount && commentsView.getChildCount() >= info.getComments().size() && !info.hasMoreComments()) { //collapse
if (currentCount > initialCount && !info.hasNextPage()) {
commentsView.removeViews(initialCount, commentsView.removeViews(initialCount,
commentsView.getChildCount() - (initialCount)); currentCount - (initialCount));
commentsExpandButton.setImageDrawable(ContextCompat.getDrawable( commentsExpandButton.setImageDrawable(ContextCompat.getDrawable(
activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand))); activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand)));
return; return;
} }
//Log.d(TAG, "toggleExpandRelatedVideos() called with: info = [" + info + "], from = [" + INITIAL_RELATED_VIDEOS + "]"); if(currentCount < info.getRelatedItems().size()){
int currentCount = commentsView.getChildCount(); //expand
for (int i = currentCount; i < info.getComments().size(); i++) { for (int i = currentCount; i < info.getRelatedItems().size(); i++) {
CommentsInfoItem item = info.getComments().get(i); CommentsInfoItem item = info.getRelatedItems().get(i);
//Log.d(TAG, "i = " + i);
commentsView.addView(infoItemBuilder.buildView(commentsView, item)); commentsView.addView(infoItemBuilder.buildView(commentsView, item));
} }
if(!info.hasNextPage()){
if (info.hasMoreComments()) {
loadMoreComments(info);
} else {
commentsExpandButton.setImageDrawable( commentsExpandButton.setImageDrawable(
ContextCompat.getDrawable(activity, ContextCompat.getDrawable(activity,
ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.collapse))); ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.collapse)));
} }
}else{
NavigationHelper.openCommentsFragment(getFragmentManager(), serviceId, url, name);
}
} }
private void loadMoreComments(CommentsInfo info) {
if (commentsDisposable != null) commentsDisposable.dispose();
commentsDisposable = Single.fromCallable(() -> {
CommentsInfo.loadMoreComments(info);
return info.getComments();
}).subscribeOn(Schedulers.io()).doOnError(e -> info.addError(e)).subscribe();
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Init // Init
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -573,23 +577,17 @@ public class VideoDetailFragment
uploaderTextView = rootView.findViewById(R.id.detail_uploader_text_view); uploaderTextView = rootView.findViewById(R.id.detail_uploader_text_view);
uploaderThumb = rootView.findViewById(R.id.detail_uploader_thumbnail_view); uploaderThumb = rootView.findViewById(R.id.detail_uploader_thumbnail_view);
tabHost = (TabHost) rootView.findViewById(R.id.tab_host); StreamingService service = null;
tabHost.setup(); try {
service = NewPipe.getService(serviceId);
} catch (ExtractionException e) {
onError(e);
}
TabHost.TabSpec commentsTab = tabHost.newTabSpec(COMMENTS_TAB_TAG); if (service.isCommentsSupported()) {
commentsTab.setContent(R.id.detail_comments_root_layout); isCommentsSupported = true;
commentsTab.setIndicator(getString(R.string.comments)); initTabs(rootView);
}
TabHost.TabSpec relatedVideosTab = tabHost.newTabSpec(RELATED_TAB_TAG);
relatedVideosTab.setContent(R.id.detail_related_streams_root_layout);
relatedVideosTab.setIndicator(getString(R.string.next_video_title));
tabHost.addTab(commentsTab);
tabHost.addTab(relatedVideosTab);
//show comments tab by default
tabHost.setCurrentTabByTag(COMMENTS_TAB_TAG);
relatedStreamRootLayout = rootView.findViewById(R.id.detail_related_streams_root_layout); relatedStreamRootLayout = rootView.findViewById(R.id.detail_related_streams_root_layout);
nextStreamTitle = rootView.findViewById(R.id.detail_next_stream_title); nextStreamTitle = rootView.findViewById(R.id.detail_next_stream_title);
@ -606,6 +604,25 @@ public class VideoDetailFragment
} }
private void initTabs(View rootView) {
tabHost = (TabHost) rootView.findViewById(R.id.tab_host);
tabHost.setup();
TabHost.TabSpec commentsTab = tabHost.newTabSpec(COMMENTS_TAB_TAG);
commentsTab.setContent(R.id.detail_comments_root_layout);
commentsTab.setIndicator(getString(R.string.comments));
TabHost.TabSpec relatedVideosTab = tabHost.newTabSpec(RELATED_TAB_TAG);
relatedVideosTab.setContent(R.id.detail_related_streams_root_layout);
relatedVideosTab.setIndicator(getString(R.string.next_video_title));
tabHost.addTab(commentsTab);
tabHost.addTab(relatedVideosTab);
//show comments tab by default
tabHost.setCurrentTabByTag(COMMENTS_TAB_TAG);
}
@Override @Override
protected void initListeners() { protected void initListeners() {
super.initListeners(); super.initListeners();
@ -751,22 +768,25 @@ public class VideoDetailFragment
} }
private void showRelatedStreamsIfSelected() { private void showRelatedStreamsIfSelected() {
if (tabHost.getCurrentTabTag().contentEquals(RELATED_TAB_TAG)) { if (null == tabHost || tabHost.getCurrentTabTag().contentEquals(RELATED_TAB_TAG)) {
relatedStreamRootLayout.setVisibility(View.VISIBLE); relatedStreamRootLayout.setVisibility(View.VISIBLE);
} }
} }
private void initComments(CommentsInfo info) { private void initComments(CommentsInfo info) {
if(null == info) return;
if (commentsView.getChildCount() > 0) commentsView.removeAllViews(); if (commentsView.getChildCount() > 0) commentsView.removeAllViews();
if (null != info && info.getComments() != null List<CommentsInfoItem> initialComments = info.getRelatedItems();
&& !info.getComments().isEmpty() && showComments) { if (null != info && initialComments != null
&& !initialComments.isEmpty() && showComments) {
//long first = System.nanoTime(), each; //long first = System.nanoTime(), each;
int to = info.getComments().size() >= INITIAL_RELATED_VIDEOS int to = initialComments.size() >= INITIAL_COMMENTS
? INITIAL_RELATED_VIDEOS ? INITIAL_COMMENTS
: info.getComments().size(); : initialComments.size();
for (int i = 0; i < to; i++) { for (int i = 0; i < to; i++) {
InfoItem item = info.getComments().get(i); InfoItem item = initialComments.get(i);
//each = System.nanoTime(); //each = System.nanoTime();
commentsView.addView(infoItemBuilder.buildView(commentsView, item)); commentsView.addView(infoItemBuilder.buildView(commentsView, item));
//if (DEBUG) Log.d(TAG, "each took " + ((System.nanoTime() - each) / 1000000L) + "ms"); //if (DEBUG) Log.d(TAG, "each took " + ((System.nanoTime() - each) / 1000000L) + "ms");
@ -774,10 +794,13 @@ public class VideoDetailFragment
//if (DEBUG) Log.d(TAG, "Total time " + ((System.nanoTime() - first) / 1000000L) + "ms"); //if (DEBUG) Log.d(TAG, "Total time " + ((System.nanoTime() - first) / 1000000L) + "ms");
showCommentsIfSelected(); showCommentsIfSelected();
if(initialComments.size() > INITIAL_COMMENTS){
commentsExpandButton.setVisibility(View.VISIBLE); commentsExpandButton.setVisibility(View.VISIBLE);
commentsExpandButton.setImageDrawable(ContextCompat.getDrawable( commentsExpandButton.setImageDrawable(ContextCompat.getDrawable(
activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand))); activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand)));
}else{
commentsExpandButton.setVisibility(View.GONE);
}
} else { } else {
commentsRootLayout.setVisibility(View.GONE); commentsRootLayout.setVisibility(View.GONE);
} }
@ -785,11 +808,16 @@ public class VideoDetailFragment
} }
private void showCommentsIfSelected() { private void showCommentsIfSelected() {
if (tabHost.getCurrentTabTag().contentEquals(COMMENTS_TAB_TAG)) { if (null == tabHost || tabHost.getCurrentTabTag().contentEquals(COMMENTS_TAB_TAG)) {
commentsRootLayout.setVisibility(View.VISIBLE); commentsRootLayout.setVisibility(View.VISIBLE);
} }
} }
private void clearComments(){
if (commentsView.getChildCount() > 0) commentsView.removeAllViews();
commentsExpandButton.setVisibility(View.GONE);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Menu // Menu
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -988,6 +1016,7 @@ public class VideoDetailFragment
protected void prepareAndLoadInfo() { protected void prepareAndLoadInfo() {
parallaxScrollRootView.smoothScrollTo(0, 0); parallaxScrollRootView.smoothScrollTo(0, 0);
pushToStack(serviceId, url, name); pushToStack(serviceId, url, name);
//clearComments();
startLoading(false); startLoading(false);
} }
@ -1010,6 +1039,27 @@ public class VideoDetailFragment
isLoading.set(false); isLoading.set(false);
onError(throwable); onError(throwable);
}); });
loadComments(forceLoad);
}
private void loadComments(boolean forceLoad) {
if(isCommentsSupported && showComments){
commentsInfo = null;
if (commentsDisposable != null) commentsDisposable.dispose();
commentsDisposable = ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((@NonNull CommentsInfo result) -> {
commentsInfo = result;
showCommentsWithAnimation(120, 0,0);
initComments(commentsInfo);
}, (@NonNull Throwable throwable) -> {
onError(throwable);
});
}
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -1199,7 +1249,7 @@ public class VideoDetailFragment
.setInterpolator(new FastOutSlowInInterpolator()) .setInterpolator(new FastOutSlowInInterpolator())
.start(); .start();
if (showRelatedStreams && tabHost.getCurrentTabTag().contentEquals(RELATED_TAB_TAG)) { if (showRelatedStreams && (null == tabHost || tabHost.getCurrentTabTag().contentEquals(RELATED_TAB_TAG))) {
relatedStreamRootLayout.animate().setListener(null).cancel(); relatedStreamRootLayout.animate().setListener(null).cancel();
relatedStreamRootLayout.setAlpha(0f); relatedStreamRootLayout.setAlpha(0f);
relatedStreamRootLayout.setTranslationY(translationY); relatedStreamRootLayout.setTranslationY(translationY);
@ -1213,7 +1263,15 @@ public class VideoDetailFragment
.start(); .start();
} }
if (showComments && tabHost.getCurrentTabTag().contentEquals(COMMENTS_TAB_TAG)) { }
private void showCommentsWithAnimation(long duration,
long delay,
@FloatRange(from = 0.0f, to = 1.0f) float translationPercent) {
int translationY = (int) (getResources().getDisplayMetrics().heightPixels *
(translationPercent > 0.0f ? translationPercent : .06f));
if (showComments && (null == tabHost || tabHost.getCurrentTabTag().contentEquals(COMMENTS_TAB_TAG))) {
commentsRootLayout.animate().setListener(null).cancel(); commentsRootLayout.animate().setListener(null).cancel();
commentsRootLayout.setAlpha(0f); commentsRootLayout.setAlpha(0f);
commentsRootLayout.setTranslationY(translationY); commentsRootLayout.setTranslationY(translationY);
@ -1360,7 +1418,6 @@ public class VideoDetailFragment
setupActionBar(info); setupActionBar(info);
initThumbnailViews(info); initThumbnailViews(info);
initRelatedVideos(info); initRelatedVideos(info);
initComments(info.getCommentsInfo());
if (wasRelatedStreamsExpanded) { if (wasRelatedStreamsExpanded) {
toggleExpandRelatedVideos(currentInfo); toggleExpandRelatedVideos(currentInfo);

View file

@ -0,0 +1,201 @@
package org.schabi.newpipe.fragments.list.comments;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v7.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.comments.CommentsInfo;
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.info_list.InfoItemDialog;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.subscription.SubscriptionService;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
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 java.util.ArrayList;
import java.util.Collections;
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 CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
private CompositeDisposable disposables = new CompositeDisposable();
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private boolean mIsVisibleToUser = false;
public static CommentsFragment getInstance(int serviceId, String url, String name) {
CommentsFragment instance = new CommentsFragment();
instance.setInitialData(serviceId, url, name);
return instance;
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mIsVisibleToUser = isVisibleToUser;
if(activity != null
&& useAsFrontPage
&& isVisibleToUser) {
setTitle(currentInfo != null ? currentInfo.getName() : name);
}
}
@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_comments, container, false);
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposables != null) disposables.clear();
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
/*//////////////////////////////////////////////////////////////////////////
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPageUrl);
}
@Override
protected Single<CommentsInfo> loadResult(boolean forceLoad) {
return ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad);
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
}
@Override
public void handleResult(@NonNull CommentsInfo result) {
super.handleResult(result);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, 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_COMMENTS,
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;
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
onUnrecoverableError(exception,
UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(serviceId),
url,
errorId);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setTitle(String title) {
super.setTitle(title);
}
}

View file

@ -13,6 +13,8 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
@ -57,6 +59,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
private static final int CHANNEL_HOLDER_TYPE = 0x201; private static final int CHANNEL_HOLDER_TYPE = 0x201;
private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300; private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300;
private static final int PLAYLIST_HOLDER_TYPE = 0x301; private static final int PLAYLIST_HOLDER_TYPE = 0x301;
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
private static final int COMMENT_HOLDER_TYPE = 0x401;
private final InfoItemBuilder infoItemBuilder; private final InfoItemBuilder infoItemBuilder;
private final ArrayList<InfoItem> infoItemList; private final ArrayList<InfoItem> infoItemList;
@ -216,6 +220,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE; return useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
case PLAYLIST: case PLAYLIST:
return useMiniVariant ? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE; return useMiniVariant ? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE;
case COMMENT:
return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE;
default: default:
Log.e(TAG, "Trollolo"); Log.e(TAG, "Trollolo");
return -1; return -1;
@ -224,7 +230,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
@Override @Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) { public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]"); if (DEBUG)
Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
switch (type) { switch (type) {
case HEADER_TYPE: case HEADER_TYPE:
return new HFHolder(header); return new HFHolder(header);
@ -242,6 +249,10 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return new PlaylistMiniInfoItemHolder(infoItemBuilder, parent); return new PlaylistMiniInfoItemHolder(infoItemBuilder, parent);
case PLAYLIST_HOLDER_TYPE: case PLAYLIST_HOLDER_TYPE:
return new PlaylistInfoItemHolder(infoItemBuilder, parent); return new PlaylistInfoItemHolder(infoItemBuilder, parent);
case MINI_COMMENT_HOLDER_TYPE:
return new CommentsMiniInfoItemHolder(infoItemBuilder, parent);
case COMMENT_HOLDER_TYPE:
return new CommentsInfoItemHolder(infoItemBuilder, parent);
default: default:
Log.e(TAG, "Trollolo"); Log.e(TAG, "Trollolo");
return new FallbackViewHolder(new View(parent.getContext())); return new FallbackViewHolder(new View(parent.getContext()));

View file

@ -15,6 +15,7 @@ public enum UserAction {
REQUESTED_CHANNEL("requested channel"), REQUESTED_CHANNEL("requested channel"),
REQUESTED_PLAYLIST("requested playlist"), REQUESTED_PLAYLIST("requested playlist"),
REQUESTED_KIOSK("requested kiosk"), REQUESTED_KIOSK("requested kiosk"),
REQUESTED_COMMENTS("requested comments"),
DELETE_FROM_HISTORY("delete from history"), DELETE_FROM_HISTORY("delete from history"),
PLAY_STREAM("Play stream"); PLAY_STREAM("Play stream");

View file

@ -29,9 +29,11 @@ import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
@ -109,7 +111,7 @@ public final class ExtractorHelper {
final String url, final String url,
boolean forceLoad) { boolean forceLoad) {
checkServiceId(serviceId); checkServiceId(serviceId);
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() -> return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.STREAM, Single.fromCallable(() ->
StreamInfo.getInfo(NewPipe.getService(serviceId), url))); StreamInfo.getInfo(NewPipe.getService(serviceId), url)));
} }
@ -117,7 +119,7 @@ public final class ExtractorHelper {
final String url, final String url,
boolean forceLoad) { boolean forceLoad) {
checkServiceId(serviceId); checkServiceId(serviceId);
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() -> return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.CHANNEL, Single.fromCallable(() ->
ChannelInfo.getInfo(NewPipe.getService(serviceId), url))); ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
} }
@ -129,11 +131,27 @@ public final class ExtractorHelper {
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl)); ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl));
} }
public static Single<CommentsInfo> getCommentsInfo(final int serviceId,
final String url,
boolean forceLoad) {
checkServiceId(serviceId);
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.COMMENT, Single.fromCallable(() ->
CommentsInfo.getInfo(NewPipe.getService(serviceId), url)));
}
public static Single<InfoItemsPage> getMoreCommentItems(final int serviceId,
final CommentsInfo info,
final String nextPageUrl) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPageUrl));
}
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId, public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId,
final String url, final String url,
boolean forceLoad) { boolean forceLoad) {
checkServiceId(serviceId); checkServiceId(serviceId);
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() -> return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, Single.fromCallable(() ->
PlaylistInfo.getInfo(NewPipe.getService(serviceId), url))); PlaylistInfo.getInfo(NewPipe.getService(serviceId), url)));
} }
@ -149,7 +167,7 @@ public final class ExtractorHelper {
final String url, final String url,
final String contentCountry, final String contentCountry,
boolean forceLoad) { boolean forceLoad) {
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() -> return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, Single.fromCallable(() ->
KioskInfo.getInfo(NewPipe.getService(serviceId), url, contentCountry))); KioskInfo.getInfo(NewPipe.getService(serviceId), url, contentCountry)));
} }
@ -174,16 +192,17 @@ public final class ExtractorHelper {
private static <I extends Info> Single<I> checkCache(boolean forceLoad, private static <I extends Info> Single<I> checkCache(boolean forceLoad,
int serviceId, int serviceId,
String url, String url,
InfoItem.InfoType infoType,
Single<I> loadFromNetwork) { Single<I> loadFromNetwork) {
checkServiceId(serviceId); checkServiceId(serviceId);
loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info)); loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info, infoType));
Single<I> load; Single<I> load;
if (forceLoad) { if (forceLoad) {
cache.removeInfo(serviceId, url); cache.removeInfo(serviceId, url, infoType);
load = loadFromNetwork; load = loadFromNetwork;
} else { } else {
load = Maybe.concat(ExtractorHelper.<I>loadFromCache(serviceId, url), load = Maybe.concat(ExtractorHelper.<I>loadFromCache(serviceId, url, infoType),
loadFromNetwork.toMaybe()) loadFromNetwork.toMaybe())
.firstElement() //Take the first valid .firstElement() //Take the first valid
.toSingle(); .toSingle();
@ -195,11 +214,11 @@ public final class ExtractorHelper {
/** /**
* Default implementation uses the {@link InfoCache} to get cached results * Default implementation uses the {@link InfoCache} to get cached results
*/ */
public static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url) { public static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url, InfoItem.InfoType infoType) {
checkServiceId(serviceId); checkServiceId(serviceId);
return Maybe.defer(() -> { return Maybe.defer(() -> {
//noinspection unchecked //noinspection unchecked
I info = (I) cache.getFromKey(serviceId, url); I info = (I) cache.getFromKey(serviceId, url, infoType);
if (MainActivity.DEBUG) Log.d(TAG, "loadFromCache() called, info > " + info); if (MainActivity.DEBUG) Log.d(TAG, "loadFromCache() called, info > " + info);
// Only return info if it's not null (it is cached) // Only return info if it's not null (it is cached)

View file

@ -26,6 +26,7 @@ import android.util.Log;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.InfoItem;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -55,27 +56,27 @@ public final class InfoCache {
} }
@Nullable @Nullable
public Info getFromKey(int serviceId, @NonNull String url) { public Info getFromKey(int serviceId, @NonNull String url, @NonNull InfoItem.InfoType infoType) {
if (DEBUG) Log.d(TAG, "getFromKey() called with: serviceId = [" + serviceId + "], url = [" + url + "]"); if (DEBUG) Log.d(TAG, "getFromKey() called with: serviceId = [" + serviceId + "], url = [" + url + "]");
synchronized (lruCache) { synchronized (lruCache) {
return getInfo(lruCache, keyOf(serviceId, url)); return getInfo(lruCache, keyOf(serviceId, url, infoType));
} }
} }
public void putInfo(int serviceId, @NonNull String url, @NonNull Info info) { public void putInfo(int serviceId, @NonNull String url, @NonNull Info info, @NonNull InfoItem.InfoType infoType) {
if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]"); if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]");
final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId()); final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId());
synchronized (lruCache) { synchronized (lruCache) {
final CacheData data = new CacheData(info, expirationMillis); final CacheData data = new CacheData(info, expirationMillis);
lruCache.put(keyOf(serviceId, url), data); lruCache.put(keyOf(serviceId, url, infoType), data);
} }
} }
public void removeInfo(int serviceId, @NonNull String url) { public void removeInfo(int serviceId, @NonNull String url, @NonNull InfoItem.InfoType infoType) {
if (DEBUG) Log.d(TAG, "removeInfo() called with: serviceId = [" + serviceId + "], url = [" + url + "]"); if (DEBUG) Log.d(TAG, "removeInfo() called with: serviceId = [" + serviceId + "], url = [" + url + "]");
synchronized (lruCache) { synchronized (lruCache) {
lruCache.remove(keyOf(serviceId, url)); lruCache.remove(keyOf(serviceId, url, infoType));
} }
} }
@ -101,8 +102,8 @@ public final class InfoCache {
} }
@NonNull @NonNull
private static String keyOf(final int serviceId, @NonNull final String url) { private static String keyOf(final int serviceId, @NonNull final String url, @NonNull InfoItem.InfoType infoType) {
return serviceId + url; return serviceId + url + infoType.toString();
} }
private static void removeStaleCache(@NonNull final LruCache<String, CacheData> cache) { private static void removeStaleCache(@NonNull final LruCache<String, CacheData> cache) {

View file

@ -34,6 +34,7 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.channel.ChannelFragment; import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.local.bookmark.BookmarkFragment; import org.schabi.newpipe.local.bookmark.BookmarkFragment;
import org.schabi.newpipe.local.feed.FeedFragment; import org.schabi.newpipe.local.feed.FeedFragment;
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
@ -331,6 +332,18 @@ public class NavigationHelper {
.commit(); .commit();
} }
public static void openCommentsFragment(
FragmentManager fragmentManager,
int serviceId,
String url,
String name) {
if (name == null) name = "";
defaultTransaction(fragmentManager)
.replace(R.id.fragment_holder, CommentsFragment.getInstance(serviceId, url, name))
.addToBackStack(null)
.commit();
}
public static void openPlaylistFragment(FragmentManager fragmentManager, public static void openPlaylistFragment(FragmentManager fragmentManager,
int serviceId, int serviceId,
String url, String url,

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/items_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
tools:listitem="@layout/list_comments_item"/>
<ProgressBar
android:id="@+id/loading_progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible"/>
<LinearLayout
android:id="@+id/empty_state_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical"
android:paddingTop="90dp"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="10dp"
android:fontFamily="monospace"
android:text="(╯°-°)╯"
android:textSize="35sp"
tools:ignore="HardcodedText,UnusedAttribute"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/empty_view_no_videos"
android:textSize="24sp"/>
</LinearLayout>
<!--ERROR PANEL-->
<include
android:id="@+id/error_panel"
layout="@layout/error_retry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="50dp"
android:visibility="gone"
tools:visibility="visible"/>
<View
android:layout_width="match_parent"
android:layout_height="4dp"
android:background="?attr/toolbar_shadow_drawable"
android:layout_alignParentTop="true"/>
</RelativeLayout>

View file

@ -495,6 +495,7 @@
android:src="?attr/expand" android:src="?attr/expand"
android:textAlignment="center" android:textAlignment="center"
android:textAllCaps="true" android:textAllCaps="true"
android:visibility="gone"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
</LinearLayout> </LinearLayout>