diff --git a/app/build.gradle b/app/build.gradle index 75f966a9b..2d46fcc90 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,7 +57,7 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.TeamNewPipe:NewPipeExtractor:79b0a19d1af' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:8de53111d9' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 06d621016..4cd8e83f9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -35,12 +35,6 @@ - - @@ -183,6 +177,19 @@ + + + + + + + + + + + + + diff --git a/app/src/main/java/android/support/design/widget/FlingBehavior.java b/app/src/main/java/android/support/design/widget/FlingBehavior.java new file mode 100644 index 000000000..59eb08294 --- /dev/null +++ b/app/src/main/java/android/support/design/widget/FlingBehavior.java @@ -0,0 +1,116 @@ +package android.support.design.widget; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.design.animation.AnimationUtils; +import android.util.AttributeSet; +import android.view.View; + +// check this https://github.com/ToDou/appbarlayout-spring-behavior/blob/master/appbarspring/src/main/java/android/support/design/widget/AppBarFlingFixBehavior.java +public final class FlingBehavior extends AppBarLayout.Behavior { + + private ValueAnimator mOffsetAnimator; + private static final int MAX_OFFSET_ANIMATION_DURATION = 600; // ms + + public FlingBehavior() { + } + + public FlingBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) { + if (dy != 0) { + int val = child.getBottom(); + if (val != 0) { + int min, max; + if (dy < 0) { + // We're scrolling down + } else { + // We're scrolling up + if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) { + mOffsetAnimator.cancel(); + } + min = -child.getUpNestedPreScrollRange(); + max = 0; + consumed[1] = scroll(coordinatorLayout, child, dy, min, max); + } + } + } + } + + @Override + public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull AppBarLayout child, @NonNull View target, float velocityX, float velocityY) { + + if (velocityY != 0) { + if (velocityY < 0) { + // We're flinging down + int val = child.getBottom(); + if (val != 0) { + final int targetScroll = + +child.getDownNestedPreScrollRange(); + animateOffsetTo(coordinatorLayout, child, targetScroll, velocityY); + } + + } else { + // We're flinging up + int val = child.getBottom(); + if (val != 0) { + final int targetScroll = -child.getUpNestedPreScrollRange(); + if (getTopBottomOffsetForScrollingSibling() > targetScroll) { + animateOffsetTo(coordinatorLayout, child, targetScroll, velocityY); + } + } + } + } + + return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY); + } + + private void animateOffsetTo(final CoordinatorLayout coordinatorLayout, + final AppBarLayout child, final int offset, float velocity) { + final int distance = Math.abs(getTopBottomOffsetForScrollingSibling() - offset); + + final int duration; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 3 * Math.round(1000 * (distance / velocity)); + } else { + final float distanceRatio = (float) distance / child.getHeight(); + duration = (int) ((distanceRatio + 1) * 150); + } + + animateOffsetWithDuration(coordinatorLayout, child, offset, duration); + } + + private void animateOffsetWithDuration(final CoordinatorLayout coordinatorLayout, + final AppBarLayout child, final int offset, final int duration) { + final int currentOffset = getTopBottomOffsetForScrollingSibling(); + if (currentOffset == offset) { + if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) { + mOffsetAnimator.cancel(); + } + return; + } + + if (mOffsetAnimator == null) { + mOffsetAnimator = new ValueAnimator(); + mOffsetAnimator.setInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR); + mOffsetAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + setHeaderTopBottomOffset(coordinatorLayout, child, + (Integer) animator.getAnimatedValue()); + } + }); + } else { + mOffsetAnimator.cancel(); + } + + mOffsetAnimator.setDuration(Math.min(duration, MAX_OFFSET_ANIMATION_DURATION)); + mOffsetAnimator.setIntValues(currentOffset, offset); + mOffsetAnimator.start(); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/Downloader.java b/app/src/main/java/org/schabi/newpipe/Downloader.java index 32e8bd414..ff274a91a 100644 --- a/app/src/main/java/org/schabi/newpipe/Downloader.java +++ b/app/src/main/java/org/schabi/newpipe/Downloader.java @@ -3,18 +3,24 @@ package org.schabi.newpipe; import android.support.annotation.Nullable; import android.text.TextUtils; +import org.schabi.newpipe.extractor.DownloadRequest; +import org.schabi.newpipe.extractor.DownloadResponse; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.utils.Localization; import java.io.IOException; import java.io.InputStream; +import java.io.Serializable; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; @@ -139,13 +145,16 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader { private ResponseBody getBody(String siteUrl, Map customProperties) throws IOException, ReCaptchaException { final Request.Builder requestBuilder = new Request.Builder() - .method("GET", null).url(siteUrl) - .addHeader("User-Agent", USER_AGENT); + .method("GET", null).url(siteUrl); for (Map.Entry header : customProperties.entrySet()) { requestBuilder.addHeader(header.getKey(), header.getValue()); } + if (!customProperties.containsKey("User-Agent")) { + requestBuilder.header("User-Agent", USER_AGENT); + } + if (!TextUtils.isEmpty(mCookies)) { requestBuilder.addHeader("Cookie", mCookies); } @@ -177,4 +186,96 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader { public String download(String siteUrl) throws IOException, ReCaptchaException { return download(siteUrl, Collections.emptyMap()); } -} + + + @Override + public DownloadResponse get(String siteUrl, DownloadRequest request) throws IOException, ReCaptchaException { + final Request.Builder requestBuilder = new Request.Builder() + .method("GET", null).url(siteUrl); + + Map> requestHeaders = request.getRequestHeaders(); + // set custom headers in request + for (Map.Entry> pair : requestHeaders.entrySet()) { + for(String value : pair.getValue()){ + requestBuilder.addHeader(pair.getKey(), value); + } + } + + if (!requestHeaders.containsKey("User-Agent")) { + requestBuilder.header("User-Agent", USER_AGENT); + } + + if (!TextUtils.isEmpty(mCookies)) { + requestBuilder.addHeader("Cookie", mCookies); + } + + final Request okRequest = requestBuilder.build(); + final Response response = client.newCall(okRequest).execute(); + final ResponseBody body = response.body(); + + if (response.code() == 429) { + throw new ReCaptchaException("reCaptcha Challenge requested"); + } + + if (body == null) { + response.close(); + return null; + } + + return new DownloadResponse(body.string(), response.headers().toMultimap()); + } + + @Override + public DownloadResponse get(String siteUrl) throws IOException, ReCaptchaException { + return get(siteUrl, DownloadRequest.emptyRequest); + } + + @Override + public DownloadResponse post(String siteUrl, DownloadRequest request) throws IOException, ReCaptchaException { + + Map> requestHeaders = request.getRequestHeaders(); + if(null == requestHeaders.get("Content-Type") || requestHeaders.get("Content-Type").isEmpty()){ + // content type header is required. maybe throw an exception here + return null; + } + + String contentType = requestHeaders.get("Content-Type").get(0); + + RequestBody okRequestBody = null; + if(null != request.getRequestBody()){ + okRequestBody = RequestBody.create(MediaType.parse(contentType), request.getRequestBody()); + } + final Request.Builder requestBuilder = new Request.Builder() + .method("POST", okRequestBody).url(siteUrl); + + // set custom headers in request + for (Map.Entry> pair : requestHeaders.entrySet()) { + for(String value : pair.getValue()){ + requestBuilder.addHeader(pair.getKey(), value); + } + } + + if (!requestHeaders.containsKey("User-Agent")) { + requestBuilder.header("User-Agent", USER_AGENT); + } + + if (!TextUtils.isEmpty(mCookies)) { + requestBuilder.addHeader("Cookie", mCookies); + } + + final Request okRequest = requestBuilder.build(); + final Response response = client.newCall(okRequest).execute(); + final ResponseBody body = response.body(); + + if (response.code() == 429) { + throw new ReCaptchaException("reCaptcha Challenge requested"); + } + + if (body == null) { + response.close(); + return null; + } + + return new DownloadResponse(body.string(), response.headers().toMultimap()); + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..27cc3ec8a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java @@ -0,0 +1,73 @@ +package org.schabi.newpipe.fragments.detail; + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.List; + +public class TabAdaptor extends FragmentPagerAdapter { + + private final List mFragmentList = new ArrayList<>(); + private final List mFragmentTitleList = new ArrayList<>(); + private final FragmentManager fragmentManager; + + public TabAdaptor(FragmentManager fm) { + super(fm); + this.fragmentManager = fm; + } + + @Override + public Fragment getItem(int position) { + return mFragmentList.get(position); + } + + @Override + public int getCount() { + return mFragmentList.size(); + } + + public void addFragment(Fragment fragment, String title) { + mFragmentList.add(fragment); + mFragmentTitleList.add(title); + } + + public void clearAllItems() { + mFragmentList.clear(); + mFragmentTitleList.clear(); + } + + public void removeItem(int position){ + mFragmentList.remove(position == 0 ? 0 : position - 1); + mFragmentTitleList.remove(position == 0 ? 0 : position - 1); + } + + public void updateItem(int position, Fragment fragment){ + mFragmentList.set(position, fragment); + } + + public void updateItem(String title, Fragment fragment){ + int index = mFragmentTitleList.indexOf(title); + if(index != -1){ + updateItem(index, fragment); + } + } + + @Override + public int getItemPosition(Object object) { + if (mFragmentList.contains(object)) return mFragmentList.indexOf(object); + else return POSITION_NONE; + } + + public void notifyDataSetUpdate(){ + notifyDataSetChanged(); + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + fragmentManager.beginTransaction().remove((Fragment) object).commitNowAllowingStateLoss(); + } + +} 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 c346e1329..269125e27 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 @@ -10,11 +10,13 @@ import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.DrawableRes; -import android.support.annotation.FloatRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.design.widget.AppBarLayout; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; -import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; @@ -25,7 +27,6 @@ import android.text.method.LinkMovementMethod; import android.text.util.Linkify; import android.util.DisplayMetrics; import android.util.Log; -import android.util.TypedValue; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -33,19 +34,15 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.view.ViewParent; import android.widget.AdapterView; import android.widget.FrameLayout; -import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; -import android.widget.ScrollView; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; -import com.nirhart.parallaxscroll.views.ParallaxScrollView; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; @@ -57,6 +54,7 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.ServiceList; 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.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.AudioStream; @@ -64,21 +62,21 @@ import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; -import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.fragments.list.comments.CommentsFragment; +import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.player.MainVideoPlayer; import org.schabi.newpipe.player.PopupVideoPlayer; -import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.report.ErrorActivity; 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; @@ -86,11 +84,9 @@ import org.schabi.newpipe.util.InfoCache; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.StreamItemAdapter; import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; -import org.schabi.newpipe.util.ThemeHelper; import java.io.Serializable; import java.util.Collection; @@ -105,6 +101,7 @@ import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS; import static org.schabi.newpipe.util.AnimationUtils.animateView; public class VideoDetailFragment @@ -115,27 +112,27 @@ public class VideoDetailFragment View.OnLongClickListener { public static final String AUTO_PLAY = "auto_play"; - // Amount of videos to show on start - private static final int INITIAL_RELATED_VIDEOS = 8; - - private InfoItemBuilder infoItemBuilder = null; - private int updateFlags = 0; private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1; private static final int RESOLUTIONS_MENU_UPDATE_FLAG = 0x2; private static final int TOOLBAR_ITEMS_UPDATE_FLAG = 0x4; + private static final int COMMENTS_UPDATE_FLAG = 0x4; private boolean autoPlayEnabled; private boolean showRelatedStreams; - private boolean wasRelatedStreamsExpanded = false; + private boolean showComments; - @State protected int serviceId = Constants.NO_SERVICE_ID; - @State protected String name; - @State protected String url; + @State + protected int serviceId = Constants.NO_SERVICE_ID; + @State + protected String name; + @State + protected String url; private StreamInfo currentInfo; private Disposable currentWorker; - @NonNull private CompositeDisposable disposables = new CompositeDisposable(); + @NonNull + private CompositeDisposable disposables = new CompositeDisposable(); private List sortedVideoStreams; private int selectedVideoStreamIndex = -1; @@ -148,7 +145,6 @@ public class VideoDetailFragment private Spinner spinnerToolbar; - private ParallaxScrollView parallaxScrollRootView; private LinearLayout contentRootLayoutHiding; private View thumbnailBackgroundButton; @@ -157,7 +153,6 @@ public class VideoDetailFragment private View videoTitleRoot; private TextView videoTitleTextView; - @Nullable private ImageView videoTitleToggleArrow; private TextView videoCountView; @@ -182,10 +177,14 @@ public class VideoDetailFragment private ImageView thumbsDownImageView; private TextView thumbsDisabledTextView; - private TextView nextStreamTitle; - private LinearLayout relatedStreamRootLayout; - private LinearLayout relatedStreamsView; - private ImageButton relatedStreamExpandButton; + private static final String COMMENTS_TAB_TAG = "COMMENTS"; + private static final String RELATED_TAB_TAG = "NEXT VIDEO"; + + private AppBarLayout appBarLayout; + private ViewPager viewPager; + private TabAdaptor pageAdapter; + private TabLayout tabLayout; + private FrameLayout relatedStreamsLayout; /*////////////////////////////////////////////////////////////////////////*/ @@ -201,12 +200,17 @@ public class VideoDetailFragment //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreate(Bundle savedInstanceState) { + public void + onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(getString(R.string.show_next_video_key), true); + + showComments = PreferenceManager.getDefaultSharedPreferences(activity) + .getBoolean(getString(R.string.show_comments_key), true); + PreferenceManager.getDefaultSharedPreferences(activity) .registerOnSharedPreferenceChangeListener(this); } @@ -228,14 +232,16 @@ public class VideoDetailFragment if (updateFlags != 0) { if (!isLoading.get() && currentInfo != null) { - if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) initRelatedVideos(currentInfo); + if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) startLoading(false); if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) setupActionBar(currentInfo); + if ((updateFlags & COMMENTS_UPDATE_FLAG) != 0) startLoading(false); } if ((updateFlags & TOOLBAR_ITEMS_UPDATE_FLAG) != 0 && menu != null) { updateMenuItemVisibility(); } + updateFlags = 0; } @@ -292,6 +298,9 @@ public class VideoDetailFragment updateFlags |= RESOLUTIONS_MENU_UPDATE_FLAG; } else if (key.equals(getString(R.string.show_play_with_kodi_key))) { updateFlags |= TOOLBAR_ITEMS_UPDATE_FLAG; + } else if (key.equals(getString(R.string.show_comments_key))) { + showComments = sharedPreferences.getBoolean(key, true); + updateFlags |= COMMENTS_UPDATE_FLAG; } } @@ -301,7 +310,6 @@ public class VideoDetailFragment private static final String INFO_KEY = "info_key"; private static final String STACK_KEY = "stack_key"; - private static final String WAS_RELATED_EXPANDED_KEY = "was_related_expanded_key"; @Override public void onSaveInstanceState(Bundle outState) { @@ -310,10 +318,6 @@ public class VideoDetailFragment // Check if the next video label and video is visible, // if it is, include the two elements in the next check int nextCount = currentInfo != null && currentInfo.getNextVideo() != null ? 2 : 0; - if (relatedStreamsView != null - && relatedStreamsView.getChildCount() > INITIAL_RELATED_VIDEOS + nextCount) { - outState.putSerializable(WAS_RELATED_EXPANDED_KEY, true); - } if (!isLoading.get() && currentInfo != null && isVisible()) { outState.putSerializable(INFO_KEY, currentInfo); @@ -326,12 +330,11 @@ public class VideoDetailFragment protected void onRestoreInstanceState(@NonNull Bundle savedState) { super.onRestoreInstanceState(savedState); - wasRelatedStreamsExpanded = savedState.getBoolean(WAS_RELATED_EXPANDED_KEY, false); Serializable serializable = savedState.getSerializable(INFO_KEY); if (serializable instanceof StreamInfo) { //noinspection unchecked currentInfo = (StreamInfo) serializable; - InfoCache.getInstance().putInfo(serviceId, url, currentInfo); + InfoCache.getInstance().putInfo(serviceId, url, currentInfo, InfoItem.InfoType.STREAM); } serializable = savedState.getSerializable(STACK_KEY); @@ -339,6 +342,7 @@ public class VideoDetailFragment //noinspection unchecked stack.addAll((Collection) serializable); } + } /*////////////////////////////////////////////////////////////////////////// @@ -394,9 +398,6 @@ public class VideoDetailFragment case R.id.detail_title_root_layout: toggleTitleAndDescription(); break; - case R.id.detail_related_streams_expand: - toggleExpandRelatedVideos(currentInfo); - break; } } @@ -420,44 +421,17 @@ public class VideoDetailFragment } private void toggleTitleAndDescription() { - if (videoTitleToggleArrow != null) { //it is null for tablets - if (videoDescriptionRootLayout.getVisibility() == View.VISIBLE) { - videoTitleTextView.setMaxLines(1); - videoDescriptionRootLayout.setVisibility(View.GONE); - videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); - } else { - videoTitleTextView.setMaxLines(10); - videoDescriptionRootLayout.setVisibility(View.VISIBLE); - videoTitleToggleArrow.setImageResource(R.drawable.arrow_up); - } + if (videoDescriptionRootLayout.getVisibility() == View.VISIBLE) { + videoTitleTextView.setMaxLines(1); + videoDescriptionRootLayout.setVisibility(View.GONE); + videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); + } else { + videoTitleTextView.setMaxLines(10); + videoDescriptionRootLayout.setVisibility(View.VISIBLE); + videoTitleToggleArrow.setImageResource(R.drawable.arrow_up); } } - private void toggleExpandRelatedVideos(StreamInfo info) { - if (DEBUG) Log.d(TAG, "toggleExpandRelatedVideos() called with: info = [" + info + "]"); - if (!showRelatedStreams) return; - - int nextCount = info.getNextVideo() != null ? 2 : 0; - int initialCount = INITIAL_RELATED_VIDEOS + nextCount; - - if (relatedStreamsView.getChildCount() > initialCount) { - relatedStreamsView.removeViews(initialCount, - relatedStreamsView.getChildCount() - (initialCount)); - relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable( - activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand))); - return; - } - - for (int i = INITIAL_RELATED_VIDEOS; i < info.getRelatedStreams().size(); i++) { - InfoItem item = info.getRelatedStreams().get(i); - //Log.d(TAG, "i = " + i); - relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, item)); - } - relatedStreamExpandButton.setImageDrawable( - ContextCompat.getDrawable(activity, - ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.collapse))); - } - /*////////////////////////////////////////////////////////////////////////// // Init //////////////////////////////////////////////////////////////////////////*/ @@ -467,8 +441,6 @@ public class VideoDetailFragment super.initViews(rootView, savedInstanceState); spinnerToolbar = activity.findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner); - parallaxScrollRootView = rootView.findViewById(R.id.detail_main_content); - thumbnailBackgroundButton = rootView.findViewById(R.id.detail_thumbnail_root_layout); thumbnailImageView = rootView.findViewById(R.id.detail_thumbnail_image_view); thumbnailPlayButton = rootView.findViewById(R.id.detail_thumbnail_play_button); @@ -504,32 +476,23 @@ public class VideoDetailFragment uploaderTextView = rootView.findViewById(R.id.detail_uploader_text_view); uploaderThumb = rootView.findViewById(R.id.detail_uploader_thumbnail_view); - relatedStreamRootLayout = rootView.findViewById(R.id.detail_related_streams_root_layout); - nextStreamTitle = rootView.findViewById(R.id.detail_next_stream_title); - relatedStreamsView = rootView.findViewById(R.id.detail_related_streams_view); + 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); - relatedStreamExpandButton = rootView.findViewById(R.id.detail_related_streams_expand); + relatedStreamsLayout = rootView.findViewById(R.id.relatedStreamsLayout); - infoItemBuilder = new InfoItemBuilder(activity); setHeightThumbnail(); + + } @Override protected void initListeners() { super.initListeners(); - infoItemBuilder.setOnStreamSelectedListener(new OnClickGesture() { - @Override - public void selected(StreamInfoItem selectedItem) { - selectAndLoadVideo(selectedItem.getServiceId(), - selectedItem.getUrl(), - selectedItem.getName()); - } - - @Override - public void held(StreamInfoItem selectedItem) { - showStreamDialog(selectedItem); - } - }); videoTitleRoot.setOnClickListener(this); uploaderRootLayout.setOnClickListener(this); @@ -539,7 +502,6 @@ public class VideoDetailFragment detailControlsAddToPlaylist.setOnClickListener(this); detailControlsDownload.setOnClickListener(this); detailControlsDownload.setOnLongClickListener(this); - relatedStreamExpandButton.setOnClickListener(this); detailControlsBackground.setLongClickable(true); detailControlsPopup.setLongClickable(true); @@ -622,44 +584,6 @@ public class VideoDetailFragment } } - private void initRelatedVideos(StreamInfo info) { - if (relatedStreamsView.getChildCount() > 0) relatedStreamsView.removeAllViews(); - - if (info.getNextVideo() != null && showRelatedStreams) { - nextStreamTitle.setVisibility(View.VISIBLE); - relatedStreamsView.addView( - infoItemBuilder.buildView(relatedStreamsView, info.getNextVideo())); - relatedStreamsView.addView(getSeparatorView()); - setRelatedStreamsVisibility(View.VISIBLE); - } else { - nextStreamTitle.setVisibility(View.GONE); - setRelatedStreamsVisibility(View.GONE); - } - - if (info.getRelatedStreams() != null - && !info.getRelatedStreams().isEmpty() && showRelatedStreams) { - //long first = System.nanoTime(), each; - int to = info.getRelatedStreams().size() >= INITIAL_RELATED_VIDEOS - ? INITIAL_RELATED_VIDEOS - : info.getRelatedStreams().size(); - for (int i = 0; i < to; i++) { - InfoItem item = info.getRelatedStreams().get(i); - //each = System.nanoTime(); - relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, item)); - //if (DEBUG) Log.d(TAG, "each took " + ((System.nanoTime() - each) / 1000000L) + "ms"); - } - //if (DEBUG) Log.d(TAG, "Total time " + ((System.nanoTime() - first) / 1000000L) + "ms"); - - setRelatedStreamsVisibility(View.VISIBLE); - relatedStreamExpandButton.setVisibility(View.VISIBLE); - - relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable( - activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand))); - } else { - if (info.getNextVideo() == null) setRelatedStreamsVisibility(View.GONE); - relatedStreamExpandButton.setVisibility(View.GONE); - } - } /*////////////////////////////////////////////////////////////////////////// // Menu @@ -693,7 +617,7 @@ public class VideoDetailFragment @Override public boolean onOptionsItemSelected(MenuItem item) { - if(isLoading.get()) { + if (isLoading.get()) { // if is still loading block menu return true; } @@ -717,7 +641,7 @@ public class VideoDetailFragment NavigationHelper.playWithKore(activity, Uri.parse( url.replace("https", "http"))); } catch (Exception e) { - if(DEBUG) Log.i(TAG, "Failed to start kore", e); + if (DEBUG) Log.i(TAG, "Failed to start kore", e); showInstallKoreDialog(activity); } return true; @@ -731,7 +655,8 @@ public class VideoDetailFragment builder.setMessage(R.string.kore_not_found) .setPositiveButton(R.string.install, (DialogInterface dialog, int which) -> NavigationHelper.installKore(context)) - .setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {}); + .setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> { + }); builder.create().show(); } @@ -850,23 +775,16 @@ public class VideoDetailFragment setInitialData(info.getServiceId(), info.getUrl(), info.getName()); pushToStack(serviceId, url, name); showLoading(); + initTabs(); - Log.d(TAG, "prepareAndHandleInfo() called parallaxScrollRootView.getScrollY(): " - + parallaxScrollRootView.getScrollY()); - final boolean greaterThanThreshold = parallaxScrollRootView.getScrollY() > (int) - (getResources().getDisplayMetrics().heightPixels * .1f); + if (scrollToTop) appBarLayout.setExpanded(true, true); + handleResult(info); + showContent(); - if (scrollToTop) parallaxScrollRootView.smoothScrollTo(0, 0); - animateView(contentRootLayoutHiding, - false, - greaterThanThreshold ? 250 : 0, 0, () -> { - handleResult(info); - showContentWithAnimation(120, 0, .01f); - }); } protected void prepareAndLoadInfo() { - parallaxScrollRootView.smoothScrollTo(0, 0); + appBarLayout.setExpanded(true, true); pushToStack(serviceId, url, name); startLoading(false); } @@ -875,6 +793,7 @@ public class VideoDetailFragment public void startLoading(boolean forceLoad) { super.startLoading(forceLoad); + initTabs(); currentInfo = null; if (currentWorker != null) currentWorker.dispose(); @@ -884,12 +803,45 @@ public class VideoDetailFragment .subscribe((@NonNull StreamInfo result) -> { isLoading.set(false); currentInfo = result; - showContentWithAnimation(120, 0, 0); handleResult(result); + showContent(); }, (@NonNull Throwable throwable) -> { isLoading.set(false); onError(throwable); }); + + } + + private void initTabs() { + pageAdapter.clearAllItems(); + + if(shouldShowComments()){ + pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url, name), COMMENTS_TAB_TAG); + } + + if(showRelatedStreams && null == relatedStreamsLayout){ + //temp empty fragment. will be updated in handleResult + pageAdapter.addFragment(new Fragment(), RELATED_TAB_TAG); + } + + pageAdapter.notifyDataSetUpdate(); + + if(pageAdapter.getCount() < 2){ + tabLayout.setVisibility(View.GONE); + }else{ + tabLayout.setVisibility(View.VISIBLE); + } + } + + private boolean shouldShowComments() { + try { + return showComments && NewPipe.getService(serviceId) + .getServiceInfo() + .getMediaCapabilities() + .contains(COMMENTS); + } catch (ExtractionException e) { + return false; + } } /*////////////////////////////////////////////////////////////////////////// @@ -1009,24 +961,6 @@ public class VideoDetailFragment })); } - private View getSeparatorView() { - View separator = new View(activity); - LinearLayout.LayoutParams params = - new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1); - int m8 = (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()); - int m5 = (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()); - params.setMargins(m8, m5, m8, m5); - separator.setLayoutParams(params); - - TypedValue typedValue = new TypedValue(); - activity.getTheme().resolveAttribute(R.attr.separator_color, typedValue, true); - separator.setBackgroundColor(typedValue.data); - - return separator; - } - private void setHeightThumbnail() { final DisplayMetrics metrics = getResources().getDisplayMetrics(); boolean isPortrait = metrics.heightPixels > metrics.widthPixels; @@ -1038,50 +972,8 @@ public class VideoDetailFragment thumbnailImageView.setMinimumHeight(height); } - private void showContentWithAnimation(long duration, - long delay, - @FloatRange(from = 0.0f, to = 1.0f) - float translationPercent) { - int translationY = (int) (getResources().getDisplayMetrics().heightPixels * - (translationPercent > 0.0f ? translationPercent : .06f)); - - contentRootLayoutHiding.animate().setListener(null).cancel(); - contentRootLayoutHiding.setAlpha(0f); - contentRootLayoutHiding.setTranslationY(translationY); - contentRootLayoutHiding.setVisibility(View.VISIBLE); - contentRootLayoutHiding.animate() - .alpha(1f) - .translationY(0) - .setStartDelay(delay) - .setDuration(duration) - .setInterpolator(new FastOutSlowInInterpolator()) - .start(); - - uploaderRootLayout.animate().setListener(null).cancel(); - uploaderRootLayout.setAlpha(0f); - uploaderRootLayout.setTranslationY(translationY); - uploaderRootLayout.setVisibility(View.VISIBLE); - uploaderRootLayout.animate() - .alpha(1f) - .translationY(0) - .setStartDelay((long) (duration * .5f) + delay) - .setDuration(duration) - .setInterpolator(new FastOutSlowInInterpolator()) - .start(); - - if (showRelatedStreams) { - relatedStreamRootLayout.animate().setListener(null).cancel(); - relatedStreamRootLayout.setAlpha(0f); - relatedStreamRootLayout.setTranslationY(translationY); - relatedStreamRootLayout.setVisibility(View.VISIBLE); - relatedStreamRootLayout.animate() - .alpha(1f) - .translationY(0) - .setStartDelay((long) (duration * .8f) + delay) - .setDuration(duration) - .setInterpolator(new FastOutSlowInInterpolator()) - .start(); - } + private void showContent() { + AnimationUtils.slideUp(contentRootLayoutHiding,120, 96, 0.06f); } protected void setInitialData(int serviceId, String url, String name) { @@ -1116,7 +1008,7 @@ public class VideoDetailFragment public void showLoading() { super.showLoading(); - animateView(contentRootLayoutHiding, false, 200); + contentRootLayoutHiding.setVisibility(View.INVISIBLE); animateView(spinnerToolbar, false, 200); animateView(thumbnailPlayButton, false, 50); animateView(detailDurationView, false, 100); @@ -1126,17 +1018,17 @@ public class VideoDetailFragment animateView(videoTitleTextView, true, 0); videoDescriptionRootLayout.setVisibility(View.GONE); - if (videoTitleToggleArrow != null) { //phone - videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); - videoTitleToggleArrow.setVisibility(View.GONE); - } else { //tablet - final View related = (View) relatedStreamRootLayout.getParent(); - //don`t need to hide it if related streams are disabled - if (related.getVisibility() == View.VISIBLE) { - related.setVisibility(View.INVISIBLE); + videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); + videoTitleToggleArrow.setVisibility(View.GONE); + videoTitleRoot.setClickable(false); + + if(relatedStreamsLayout != null){ + if(showRelatedStreams){ + relatedStreamsLayout.setVisibility(View.INVISIBLE); + }else{ + relatedStreamsLayout.setVisibility(View.GONE); } } - videoTitleRoot.setClickable(false); imageLoader.cancelDisplayTask(thumbnailImageView); imageLoader.cancelDisplayTask(uploaderThumb); @@ -1149,6 +1041,19 @@ public class VideoDetailFragment super.handleResult(info); setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName()); + + if(showRelatedStreams){ + if(null == relatedStreamsLayout){ //phone + pageAdapter.updateItem(RELATED_TAB_TAG, RelatedVideosFragment.getInstance(currentInfo)); + pageAdapter.notifyDataSetUpdate(); + }else{ //tablet + getChildFragmentManager().beginTransaction() + .replace(R.id.relatedStreamsLayout, RelatedVideosFragment.getInstance(currentInfo)) + .commitNow(); + relatedStreamsLayout.setVisibility(View.VISIBLE); + } + } + //pushToStack(serviceId, url, name); animateView(thumbnailPlayButton, true, 200); @@ -1213,14 +1118,10 @@ public class VideoDetailFragment } videoDescriptionView.setVisibility(View.GONE); - if (videoTitleToggleArrow != null) { - videoTitleRoot.setClickable(true); - videoTitleToggleArrow.setVisibility(View.VISIBLE); - videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); - videoDescriptionRootLayout.setVisibility(View.GONE); - } else { - videoDescriptionRootLayout.setVisibility(View.VISIBLE); - } + videoTitleRoot.setClickable(true); + videoTitleToggleArrow.setVisibility(View.VISIBLE); + videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); + videoDescriptionRootLayout.setVisibility(View.GONE); if (!TextUtils.isEmpty(info.getUploadDate())) { videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate())); } @@ -1229,11 +1130,6 @@ public class VideoDetailFragment animateView(spinnerToolbar, true, 500); setupActionBar(info); initThumbnailViews(info); - initRelatedVideos(info); - if (wasRelatedStreamsExpanded) { - toggleExpandRelatedVideos(currentInfo); - wasRelatedStreamsExpanded = false; - } setTitleToUrl(info.getServiceId(), info.getUrl(), info.getName()); setTitleToUrl(info.getServiceId(), info.getOriginalUrl(), info.getName()); @@ -1268,11 +1164,6 @@ public class VideoDetailFragment // Only auto play in the first open autoPlayEnabled = false; } - - final ViewParent related = relatedStreamRootLayout.getParent(); - if (related instanceof ScrollView) { - ((ScrollView) related).scrollTo(0, 0); - } } @@ -1339,13 +1230,4 @@ public class VideoDetailFragment showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema); } - - private void setRelatedStreamsVisibility(int visibility) { - final ViewParent parent = relatedStreamRootLayout.getParent(); - if (parent instanceof ScrollView) { - ((ScrollView) parent).setVisibility(visibility); - } else { - relatedStreamRootLayout.setVisibility(visibility); - } - } } 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 0816334ea..b61fe0d02 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 @@ -22,6 +22,7 @@ import android.view.View; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; +import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.BaseStateFragment; @@ -220,6 +221,13 @@ public abstract class BaseListFragment extends BaseStateFragment implem } }); + infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture() { + @Override + public void selected(CommentsInfoItem selectedItem) { + onItemSelected(selectedItem); + } + }); + itemsList.clearOnScrollListeners(); itemsList.addOnScrollListener(new OnScrollBelowItemsListener() { @Override diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java new file mode 100644 index 000000000..956e6c1c8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java @@ -0,0 +1,149 @@ +package org.schabi.newpipe.fragments.list.comments; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.ListExtractor; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.comments.CommentsInfo; +import org.schabi.newpipe.fragments.list.BaseListInfoFragment; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.AnimationUtils; +import org.schabi.newpipe.util.ExtractorHelper; + +import io.reactivex.Single; +import io.reactivex.disposables.CompositeDisposable; + +public class CommentsFragment extends BaseListInfoFragment { + + 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; + } + + @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(); + } + + + /*////////////////////////////////////////////////////////////////////////// + // Load and handle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected Single loadMoreItemsLogic() { + return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPageUrl); + } + + @Override + protected Single 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); + + AnimationUtils.slideUp(getView(),120, 96, 0.06f); + + 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; + + hideLoading(); + showSnackBarError(exception, UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments); + return true; + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void setTitle(String title) { + return; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + return; + } + + @Override + protected boolean isGridLayout() { + return false; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 85bd0f232..a3b01f251 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -40,6 +40,7 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.search.SearchExtractor; import org.schabi.newpipe.extractor.search.SearchInfo; +import org.schabi.newpipe.util.FireTvUtils; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.local.history.HistoryRecordManager; @@ -470,6 +471,9 @@ public class SearchFragment if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) { showSuggestionsPanel(); } + if(FireTvUtils.isFireTv()){ + showKeyboardSearch(); + } }); searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> { @@ -520,7 +524,9 @@ public class SearchFragment if (DEBUG) { Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]"); } - if (event != null + if(actionId == EditorInfo.IME_ACTION_PREVIOUS){ + hideKeyboardSearch(); + } else if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) { search(searchEditText.getText().toString(), new String[0], ""); @@ -562,7 +568,7 @@ public class SearchFragment if (searchEditText.requestFocus()) { InputMethodManager imm = (InputMethodManager) activity.getSystemService( Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT); + imm.showSoftInput(searchEditText, InputMethodManager.SHOW_FORCED); } } @@ -572,8 +578,7 @@ public class SearchFragment InputMethodManager imm = (InputMethodManager) activity.getSystemService( Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), - InputMethodManager.HIDE_NOT_ALWAYS); + imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN); searchEditText.clearFocus(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedVideosFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedVideosFragment.java new file mode 100644 index 000000000..69a59c2f1 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedVideosFragment.java @@ -0,0 +1,208 @@ +package org.schabi.newpipe.fragments.list.videos; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.Switch; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.ListExtractor; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.fragments.list.BaseListInfoFragment; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.AnimationUtils; +import org.schabi.newpipe.util.RelatedStreamInfo; + +import java.io.Serializable; + +import io.reactivex.Single; +import io.reactivex.disposables.CompositeDisposable; + +public class RelatedVideosFragment extends BaseListInfoFragment implements SharedPreferences.OnSharedPreferenceChangeListener{ + + private CompositeDisposable disposables = new CompositeDisposable(); + private RelatedStreamInfo relatedStreamInfo; + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + private View headerRootLayout; + private Switch aSwitch; + + private boolean mIsVisibleToUser = false; + + public static RelatedVideosFragment getInstance(StreamInfo info) { + RelatedVideosFragment instance = new RelatedVideosFragment(); + 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_related_streams, container, false); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (disposables != null) disposables.clear(); + } + + protected View getListHeader(){ + if(relatedStreamInfo != null && relatedStreamInfo.getNextStream() != null){ + headerRootLayout = activity.getLayoutInflater().inflate(R.layout.related_streams_header, itemsList, false); + aSwitch = headerRootLayout.findViewById(R.id.autoplay_switch); + + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext()); + Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false); + aSwitch.setChecked(autoplay); + aSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean b) { + SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getContext()).edit(); + prefEdit.putBoolean(getString(R.string.auto_queue_key), b); + prefEdit.apply(); + } + }); + return headerRootLayout; + }else{ + return null; + } + } + + @Override + protected Single loadMoreItemsLogic() { + return Single.fromCallable(() -> ListExtractor.InfoItemsPage.emptyPage()); + } + + @Override + protected Single loadResult(boolean forceLoad) { + return Single.fromCallable(() -> relatedStreamInfo); + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void showLoading() { + super.showLoading(); + if(null != headerRootLayout) headerRootLayout.setVisibility(View.INVISIBLE); + } + + @Override + public void handleResult(@NonNull RelatedStreamInfo result) { + + super.handleResult(result); + + if(null != headerRootLayout) headerRootLayout.setVisibility(View.VISIBLE); + 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) { + return; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + return; + } + + private void setInitialData(StreamInfo info) { + super.setInitialData(info.getServiceId(), info.getUrl(), info.getName()); + if(this.relatedStreamInfo == null) this.relatedStreamInfo = RelatedStreamInfo.getInfo(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 RelatedStreamInfo){ + this.relatedStreamInfo = (RelatedStreamInfo) serializable; + } + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) { + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext()); + Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false); + if(null != aSwitch) aSwitch.setChecked(autoplay); + } + + @Override + protected boolean isGridLayout() { + return false; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java index f473e5d08..0e9fd3277 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java @@ -10,10 +10,13 @@ import com.nostra13.universalimageloader.core.ImageLoader; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; +import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; 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.PlaylistInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; @@ -50,6 +53,7 @@ public class InfoItemBuilder { private OnClickGesture onStreamSelectedListener; private OnClickGesture onChannelSelectedListener; private OnClickGesture onPlaylistSelectedListener; + private OnClickGesture onCommentsSelectedListener; public InfoItemBuilder(Context context) { this.context = context; @@ -73,6 +77,8 @@ public class InfoItemBuilder { return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent); case PLAYLIST: return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) : new PlaylistInfoItemHolder(this, parent); + case COMMENT: + return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent) : new CommentsInfoItemHolder(this, parent); default: Log.e(TAG, "Trollolo"); throw new RuntimeException("InfoType not expected = " + infoType.name()); @@ -111,4 +117,12 @@ public class InfoItemBuilder { this.onPlaylistSelectedListener = listener; } + public OnClickGesture getOnCommentsSelectedListener() { + return onCommentsSelectedListener; + } + + public void setOnCommentsSelectedListener(OnClickGesture onCommentsSelectedListener) { + this.onCommentsSelectedListener = onCommentsSelectedListener; + } + } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 15fdcad05..5e7095c7d 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -9,10 +9,13 @@ import android.view.ViewGroup; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; +import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; 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.ChannelGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder; @@ -63,6 +66,8 @@ public class InfoListAdapter extends RecyclerView.Adapter infoItemList; @@ -98,6 +103,10 @@ public class InfoListAdapter extends RecyclerView.Adapter listener) { + infoItemBuilder.setOnCommentsSelectedListener(listener); + } + public void useMiniItemVariants(boolean useMiniVariant) { this.useMiniVariant = useMiniVariant; } @@ -223,6 +232,8 @@ public class InfoListAdapter extends RecyclerView.Adapter + * ChannelInfoItemHolder .java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder { + + public final TextView itemTitleView; + + public CommentsInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { + super(infoItemBuilder, R.layout.list_comments_item, parent); + + itemTitleView = itemView.findViewById(R.id.itemTitleView); + } + + @Override + public void updateFromItem(final InfoItem infoItem) { + super.updateFromItem(infoItem); + + if (!(infoItem instanceof CommentsInfoItem)) return; + final CommentsInfoItem item = (CommentsInfoItem) infoItem; + + itemTitleView.setText(item.getAuthorName()); + } + +} 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 new file mode 100644 index 000000000..c2bc86691 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java @@ -0,0 +1,99 @@ +package org.schabi.newpipe.info_list.holder; + +import android.support.v7.app.AppCompatActivity; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.comments.CommentsInfoItem; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.util.ImageDisplayConstants; +import org.schabi.newpipe.util.NavigationHelper; + +import de.hdodenhof.circleimageview.CircleImageView; + +public class CommentsMiniInfoItemHolder extends InfoItemHolder { + public final CircleImageView itemThumbnailView; + private final TextView itemContentView; + private final TextView itemLikesCountView; + private final TextView itemDislikesCountView; + private final TextView itemPublishedTime; + + private static final int commentDefaultLines = 2; + private static final int commentExpandedLines = 1000; + + CommentsMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + super(infoItemBuilder, layoutId, parent); + + itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); + itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view); + itemDislikesCountView = itemView.findViewById(R.id.detail_thumbs_down_count_view); + itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime); + itemContentView = itemView.findViewById(R.id.itemCommentContentView); + } + + public CommentsMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { + this(infoItemBuilder, R.layout.list_comments_mini_item, parent); + } + + @Override + public void updateFromItem(final InfoItem infoItem) { + if (!(infoItem instanceof CommentsInfoItem)) return; + final CommentsInfoItem item = (CommentsInfoItem) infoItem; + + itemBuilder.getImageLoader() + .displayImage(item.getAuthorThumbnail(), + itemThumbnailView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); + + itemThumbnailView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + try { + final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext(); + NavigationHelper.openChannelFragment( + activity.getSupportFragmentManager(), + item.getServiceId(), + item.getAuthorEndpoint(), + item.getAuthorName()); + } catch (Exception e) { + ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e); + } + } + }); + + // ellipsize if not already ellipsized + if (null == itemContentView.getEllipsize()) { + itemContentView.setEllipsize(TextUtils.TruncateAt.END); + itemContentView.setMaxLines(commentDefaultLines); + } + + itemContentView.setText(item.getCommentText()); + if (null != item.getLikeCount()) { + itemLikesCountView.setText(String.valueOf(item.getLikeCount())); + } + itemPublishedTime.setText(item.getPublishedTime()); + + itemView.setOnClickListener(view -> { + toggleEllipsize(item.getCommentText()); + if (itemBuilder.getOnCommentsSelectedListener() != null) { + itemBuilder.getOnCommentsSelectedListener().selected(item); + } + }); + } + + private void toggleEllipsize(String text) { + // toggle ellipsize + if (null == itemContentView.getEllipsize()) { + itemContentView.setEllipsize(TextUtils.TruncateAt.END); + itemContentView.setMaxLines(commentDefaultLines); + } else { + itemContentView.setEllipsize(null); + itemContentView.setMaxLines(commentExpandedLines); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index f5c731ed9..8ea3d509c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -626,6 +626,7 @@ public final class PopupVideoPlayer extends Service { @Override public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { super.onLoadingComplete(imageUri, view, loadedImage); + if (playerImpl == null) return; // rebuild notification here since remote view does not release bitmaps, // causing memory leaks resetNotification(); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index 46d20c7e1..f148aed27 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -131,7 +131,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, private void onAudioFocusLossCanDuck() { Log.d(TAG, "onAudioFocusLossCanDuck() called"); // Set the volume to 1/10 on ducking - animateAudio(player.getVolume(), DUCK_AUDIO_TO); + player.setVolume(DUCK_AUDIO_TO); } private void animateAudio(final float from, final float to) { diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 19b728b3a..e1960247e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -45,7 +45,9 @@ import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MOD import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT; import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM; import static java.lang.annotation.RetentionPolicy.SOURCE; -import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.*; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP; public class PlayerHelper { private PlayerHelper() {} diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java index c62dc1088..7abebc49e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java @@ -13,6 +13,7 @@ import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerHelper; diff --git a/app/src/main/java/org/schabi/newpipe/report/UserAction.java b/app/src/main/java/org/schabi/newpipe/report/UserAction.java index 00a25ed8d..2b2369ad3 100644 --- a/app/src/main/java/org/schabi/newpipe/report/UserAction.java +++ b/app/src/main/java/org/schabi/newpipe/report/UserAction.java @@ -15,6 +15,7 @@ public enum UserAction { REQUESTED_CHANNEL("requested channel"), REQUESTED_PLAYLIST("requested playlist"), REQUESTED_KIOSK("requested kiosk"), + REQUESTED_COMMENTS("requested comments"), DELETE_FROM_HISTORY("delete from history"), PLAY_STREAM("Play stream"); diff --git a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java index 3c5f16929..6a398a8a2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java @@ -25,6 +25,7 @@ import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; import android.content.res.ColorStateList; import android.support.annotation.ColorInt; +import android.support.annotation.FloatRange; import android.support.v4.view.ViewCompat; import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.util.Log; @@ -363,4 +364,24 @@ public class AnimationUtils { }).start(); } } + + public static void slideUp(final View view, + long duration, + long delay, + @FloatRange(from = 0.0f, to = 1.0f) float translationPercent) { + int translationY = (int) (view.getResources().getDisplayMetrics().heightPixels * + (translationPercent)); + + view.animate().setListener(null).cancel(); + view.setAlpha(0f); + view.setTranslationY(translationY); + view.setVisibility(View.VISIBLE); + view.animate() + .alpha(1f) + .translationY(0) + .setStartDelay(delay) + .setDuration(duration) + .setInterpolator(new FastOutSlowInInterpolator()) + .start(); + } } 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 041f4933f..0f1c39473 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -29,11 +29,12 @@ import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.extractor.Info; +import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.SuggestionExtractor; import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.channel.ChannelInfoItem; +import org.schabi.newpipe.extractor.comments.CommentsInfo; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; @@ -62,7 +63,7 @@ public final class ExtractorHelper { } private static void checkServiceId(int serviceId) { - if(serviceId == Constants.NO_SERVICE_ID) { + if (serviceId == Constants.NO_SERVICE_ID) { throw new IllegalArgumentException("serviceId is NO_SERVICE_ID"); } } @@ -110,7 +111,7 @@ public final class ExtractorHelper { final String url, boolean forceLoad) { 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))); } @@ -118,29 +119,45 @@ public final class ExtractorHelper { final String url, boolean forceLoad) { 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))); } public static Single getMoreChannelItems(final int serviceId, - final String url, - final String nextStreamsUrl) { + final String url, + final String nextStreamsUrl) { checkServiceId(serviceId); return Single.fromCallable(() -> ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl)); } + public static Single 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 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 getPlaylistInfo(final int serviceId, final String url, boolean forceLoad) { 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))); } public static Single getMorePlaylistItems(final int serviceId, - final String url, - final String nextStreamsUrl) { + final String url, + final String nextStreamsUrl) { checkServiceId(serviceId); return Single.fromCallable(() -> PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl)); @@ -149,7 +166,7 @@ public final class ExtractorHelper { public static Single getKioskInfo(final int serviceId, final String url, 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))); } @@ -173,16 +190,17 @@ public final class ExtractorHelper { private static Single checkCache(boolean forceLoad, int serviceId, String url, + InfoItem.InfoType infoType, Single loadFromNetwork) { checkServiceId(serviceId); - loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info)); + loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info, infoType)); Single load; if (forceLoad) { - cache.removeInfo(serviceId, url); + cache.removeInfo(serviceId, url, infoType); load = loadFromNetwork; } else { - load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url), + load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, infoType), loadFromNetwork.toMaybe()) .firstElement() //Take the first valid .toSingle(); @@ -194,20 +212,20 @@ public final class ExtractorHelper { /** * Default implementation uses the {@link InfoCache} to get cached results */ - public static Maybe loadFromCache(final int serviceId, final String url) { + public static Maybe loadFromCache(final int serviceId, final String url, InfoItem.InfoType infoType) { checkServiceId(serviceId); return Maybe.defer(() -> { - //noinspection unchecked - I info = (I) cache.getFromKey(serviceId, url); - if (MainActivity.DEBUG) Log.d(TAG, "loadFromCache() called, info > " + info); + //noinspection unchecked + I info = (I) cache.getFromKey(serviceId, url, infoType); + if (MainActivity.DEBUG) Log.d(TAG, "loadFromCache() called, info > " + info); - // Only return info if it's not null (it is cached) - if (info != null) { - return Maybe.just(info); - } + // Only return info if it's not null (it is cached) + if (info != null) { + return Maybe.just(info); + } - return Maybe.empty(); - }); + return Maybe.empty(); + }); } /** diff --git a/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java b/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java new file mode 100644 index 000000000..69666463e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java @@ -0,0 +1,10 @@ +package org.schabi.newpipe.util; + +import org.schabi.newpipe.App; + +public class FireTvUtils { + public static boolean isFireTv(){ + final String AMAZON_FEATURE_FIRE_TV = "amazon.hardware.fire_tv"; + return App.getApp().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java index 318db37a1..23b134281 100644 --- a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java +++ b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java @@ -26,6 +26,7 @@ import android.util.Log; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.extractor.Info; +import org.schabi.newpipe.extractor.InfoItem; import java.util.Map; @@ -52,27 +53,27 @@ public final class InfoCache { } @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 + "]"); synchronized (lruCache) { - return getInfo(keyOf(serviceId, url)); + return getInfo(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 + "]"); final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId()); synchronized (lruCache) { 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 + "]"); synchronized (lruCache) { - lruCache.remove(keyOf(serviceId, url)); + lruCache.remove(keyOf(serviceId, url, infoType)); } } @@ -98,8 +99,8 @@ public final class InfoCache { } @NonNull - private static String keyOf(final int serviceId, @NonNull final String url) { - return serviceId + url; + private static String keyOf(final int serviceId, @NonNull final String url, @NonNull InfoItem.InfoType infoType) { + return serviceId + url + infoType.toString(); } private static void removeStaleCache() { 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 5df2e8be4..4b93600ce 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -33,6 +33,7 @@ import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.MainFragment; 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.local.bookmark.BookmarkFragment; import org.schabi.newpipe.local.feed.FeedFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; @@ -309,6 +310,18 @@ public class NavigationHelper { .commit(); } + public static void openCommentsFragment( + FragmentManager fragmentManager, + int serviceId, + String url, + String name) { + if (name == null) name = ""; + fragmentManager.beginTransaction().setCustomAnimations(R.anim.switch_service_in, R.anim.switch_service_out) + .replace(R.id.fragment_holder, CommentsFragment.getInstance(serviceId, url, name)) + .addToBackStack(null) + .commit(); + } + public static void openPlaylistFragment(FragmentManager fragmentManager, int serviceId, String url, diff --git a/app/src/main/java/org/schabi/newpipe/util/RelatedStreamInfo.java b/app/src/main/java/org/schabi/newpipe/util/RelatedStreamInfo.java new file mode 100644 index 000000000..6de663c13 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/RelatedStreamInfo.java @@ -0,0 +1,41 @@ +package org.schabi.newpipe.util; + +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.ListInfo; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class RelatedStreamInfo extends ListInfo { + + private StreamInfoItem nextStream; + + public RelatedStreamInfo(int serviceId, ListLinkHandler listUrlIdHandler, String name) { + super(serviceId, listUrlIdHandler, name); + } + + public static RelatedStreamInfo getInfo(StreamInfo info) { + ListLinkHandler handler = new ListLinkHandler(info.getOriginalUrl(), info.getUrl(), info.getId(), Collections.emptyList(), null); + RelatedStreamInfo relatedStreamInfo = new RelatedStreamInfo(info.getServiceId(), handler, info.getName()); + List streams = new ArrayList<>(); + if(info.getNextVideo() != null){ + streams.add(info.getNextVideo()); + } + streams.addAll(info.getRelatedStreams()); + relatedStreamInfo.setRelatedItems(streams); + relatedStreamInfo.setNextStream(info.getNextVideo()); + return relatedStreamInfo; + } + + public StreamInfoItem getNextStream() { + return nextStream; + } + + public void setNextStream(StreamInfoItem nextStream) { + this.nextStream = nextStream; + } +} diff --git a/app/src/main/res/drawable/default_dot.xml b/app/src/main/res/drawable/default_dot.xml new file mode 100644 index 000000000..3380dca3b --- /dev/null +++ b/app/src/main/res/drawable/default_dot.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selected_dot.xml b/app/src/main/res/drawable/selected_dot.xml new file mode 100644 index 000000000..017e99d43 --- /dev/null +++ b/app/src/main/res/drawable/selected_dot.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tab_selector.xml b/app/src/main/res/drawable/tab_selector.xml new file mode 100644 index 000000000..b7307674b --- /dev/null +++ b/app/src/main/res/drawable/tab_selector.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index 73939d60a..2df22ab48 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -8,107 +8,121 @@ android:focusableInTouchMode="true" android:orientation="horizontal"> - + android:layout_height="match_parent" + android:layout_weight="5" + android:fitsSystemWindows="true"> - - + android:fitsSystemWindows="true" + app:elevation="0dp" + app:layout_behavior="android.support.design.widget.FlingBehavior"> - - + app:layout_scrollFlags="scroll"> - + + android:background="@android:color/black" + android:clickable="true" + android:focusable="true" + android:foreground="?attr/selectableItemBackground" + app:layout_collapseMode="parallax"> - + - + - - + + + + + + + android:layout_height="wrap_content" + android:background="?android:windowBackground" + app:layout_scrollFlags="scroll"> @@ -126,6 +140,15 @@ tools:ignore="RtlHardcoded" tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed a ultricies ex. Integer sit amet sodales risus. Duis non mi et urna pretium bibendum. Nunc eleifend est quis ipsum porttitor egestas. Sed facilisis, nisl quis eleifend pellentesque, orci metus egestas dolor, at accumsan eros metus quis libero." /> + + @@ -209,17 +232,17 @@ tools:text="Uploader" /> + android:id="@+id/detail_uploader_subscribe" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical|right" + android:layout_marginRight="12dp" + android:text="@string/rss_button_title" + android:textSize="12sp" + android:theme="@style/RedButton" + android:drawableLeft="@drawable/ic_rss_feed_white_24dp" + tools:ignore="RtlHardcoded" + android:visibility="gone"/>--> @@ -401,7 +424,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="5dp" - android:orientation="vertical"> + android:orientation="vertical" + android:visibility="gone" + tools:visibility="visible"> + - - + - + + + + + + + + + - - - - - - - - - - + diff --git a/app/src/main/res/layout/fragment_comments.xml b/app/src/main/res/layout/fragment_comments.xml new file mode 100644 index 000000000..9ace63d4d --- /dev/null +++ b/app/src/main/res/layout/fragment_comments.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_related_streams.xml b/app/src/main/res/layout/fragment_related_streams.xml new file mode 100644 index 000000000..c12630392 --- /dev/null +++ b/app/src/main/res/layout/fragment_related_streams.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index 7c6568b67..906246bd0 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -1,6 +1,5 @@ - - + android:fitsSystemWindows="true"> - - + android:fitsSystemWindows="true" + app:elevation="0dp" + app:layout_behavior="android.support.design.widget.FlingBehavior"> - - + app:layout_scrollFlags="scroll"> - + + android:background="@android:color/black" + android:clickable="true" + android:focusable="true" + android:foreground="?attr/selectableItemBackground" + app:layout_collapseMode="parallax"> - + - + - - + + + + + + + android:layout_height="wrap_content" + android:background="?android:windowBackground" + app:layout_scrollFlags="scroll"> + tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed a ultricies ex. Integer sit amet sodales risus. Duis non mi et urna pretium bibendum. Nunc eleifend est quis ipsum porttitor egestas. Sed facilisis, nisl quis eleifend pellentesque, orci metus egestas dolor, at accumsan eros metus quis libero." /> + tools:ignore="ContentDescription,RtlHardcoded" /> @@ -150,7 +159,7 @@ android:layout_marginTop="@dimen/video_item_detail_error_panel_margin" android:indeterminate="true" android:visibility="gone" - tools:visibility="visible"/> + tools:visibility="visible" /> + tools:visibility="visible" /> + android:padding="6dp"> + tools:ignore="RtlHardcoded" /> + tools:text="Uploader" /> + tools:text="Published on Oct 2, 2009" /> + tools:text="Description Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed a ultricies ex. Integer sit amet sodales risus. Duis non mi et urna pretium bibendum." /> + android:background="?attr/separator_color" /> - - - - - - - - - - - + + + + + + + + + + + + diff --git a/app/src/main/res/layout/list_comments_item.xml b/app/src/main/res/layout/list_comments_item.xml new file mode 100644 index 000000000..a9b091329 --- /dev/null +++ b/app/src/main/res/layout/list_comments_item.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/list_comments_mini_item.xml b/app/src/main/res/layout/list_comments_mini_item.xml new file mode 100644 index 000000000..36f3e2e6e --- /dev/null +++ b/app/src/main/res/layout/list_comments_mini_item.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/related_streams_header.xml b/app/src/main/res/layout/related_streams_header.xml new file mode 100644 index 000000000..b98244b7e --- /dev/null +++ b/app/src/main/res/layout/related_streams_header.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/toolbar_search_layout.xml b/app/src/main/res/layout/toolbar_search_layout.xml index 797eea48e..9e9e51442 100644 --- a/app/src/main/res/layout/toolbar_search_layout.xml +++ b/app/src/main/res/layout/toolbar_search_layout.xml @@ -30,7 +30,9 @@ android:layout_width="48dp" android:layout_height="48dp" android:layout_gravity="right|center_vertical" - tools:ignore="RtlHardcoded"> + android:focusable="true" + tools:ignore="RtlHardcoded" + android:background="?attr/selectableItemBackground"> diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index fcc09872e..b6c214caf 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -138,6 +138,7 @@ show_search_suggestions show_play_with_kodi show_next_video + show_comments show_hold_to_append en GB diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 462b331d2..df9a1fcba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -77,6 +77,8 @@ Inexact seek allows the player to seek to positions faster with reduced precision Load thumbnails When off no thumbnails load, saving data and memory usage. Changes clear both in-memory and on-disk image cache. + Show comments + Disable to stop showing comments Image cache wiped Wipe cached metadata Remove all cached webpage data @@ -98,7 +100,8 @@ Resume on focus gain Continue playing after interruptions (e.g. phone calls) Download - Next + Up next + Autoplay Show \'Next\' and \'Similar\' videos Show \"Hold to append\" tip Show tip when background or popup button is pressed on video details page @@ -135,6 +138,7 @@ Playlist Playlists Videos + Comments Tracks Users Events @@ -238,6 +242,7 @@ User report No results @string/no_videos + @string/no_comments Nothing here but crickets Drag to reorder @@ -273,6 +278,12 @@ %s videos + No comments + + %s comment + %s comments + + Start Pause @@ -391,6 +402,7 @@ Warning: Could not import all files. This will override your current setup. Do you want to also import settings? + Could not load comments Kiosk diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml index 0254514ba..1a1a39e21 100644 --- a/app/src/main/res/xml/content_settings.xml +++ b/app/src/main/res/xml/content_settings.xml @@ -41,6 +41,13 @@ android:title="@string/download_thumbnail_title" android:summary="@string/download_thumbnail_summary"/> + +