From efd9a9c29d112d1acf85206aa6dd13538a9e906c Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sun, 1 Nov 2020 15:02:54 +0900 Subject: [PATCH] Update Discover tab --- app/build.gradle | 5 +- .../instagrabber/activities/MainActivity.java | 21 +- .../adapters/DiscoverTopicsAdapter.java | 53 ++ .../viewholder/TopicClusterViewHolder.java | 99 ++++ .../asyncs/DiscoverPostFetchService.java | 56 ++ .../asyncs/FeedPostFetchService.java | 8 +- .../asyncs/ProfilePostFetchService.java | 8 +- .../customviews/PostsRecyclerView.java | 3 +- .../customviews/helpers/PostFetcher.java | 18 +- .../helpers/RecyclerLazyLoaderAtBottom.java | 2 +- .../fragments/HashTagFragment.java | 68 +-- .../fragments/PostViewV2Fragment.java | 4 +- .../fragments/TopicPostsFragment.java | 363 +++++++++++++ .../fragments/main/DiscoverFragment.java | 364 +++++++------ .../fragments/main/FeedFragment.java | 16 +- .../instagrabber/models/TopicCluster.java | 57 ++ .../repositories/DiscoverRepository.java | 12 + .../awais/instagrabber/utils/Constants.java | 1 + .../instagrabber/utils/ResponseBodyUtils.java | 24 +- .../instagrabber/utils/SettingsHelper.java | 4 +- .../viewmodels/TopicClusterViewModel.java | 19 + .../webservices/DiscoverService.java | 501 ++++++++++++++++++ app/src/main/res/layout/fragment_discover.xml | 44 +- app/src/main/res/layout/fragment_profile.xml | 14 - .../main/res/layout/fragment_topic_posts.xml | 58 ++ .../main/res/layout/item_discover_topic.xml | 45 ++ app/src/main/res/menu/topic_posts_menu.xml | 9 + .../res/navigation/discover_nav_graph.xml | 76 ++- 28 files changed, 1662 insertions(+), 290 deletions(-) create mode 100644 app/src/main/java/awais/instagrabber/adapters/DiscoverTopicsAdapter.java create mode 100644 app/src/main/java/awais/instagrabber/adapters/viewholder/TopicClusterViewHolder.java create mode 100644 app/src/main/java/awais/instagrabber/asyncs/DiscoverPostFetchService.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/TopicPostsFragment.java create mode 100644 app/src/main/java/awais/instagrabber/models/TopicCluster.java create mode 100644 app/src/main/java/awais/instagrabber/repositories/DiscoverRepository.java create mode 100644 app/src/main/java/awais/instagrabber/viewmodels/TopicClusterViewModel.java create mode 100644 app/src/main/java/awais/instagrabber/webservices/DiscoverService.java create mode 100644 app/src/main/res/layout/fragment_topic_posts.xml create mode 100644 app/src/main/res/layout/item_discover_topic.xml create mode 100644 app/src/main/res/menu/topic_posts_menu.xml diff --git a/app/build.gradle b/app/build.gradle index d7c439a6..2d862706 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -49,7 +49,7 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10' def appcompat_version = "1.2.0" - def nav_version = "2.3.0" + def nav_version = '2.3.1' implementation 'com.google.android.material:material:1.3.0-alpha03' implementation 'com.google.android.exoplayer:exoplayer:2.12.0' @@ -61,9 +61,10 @@ dependencies { implementation "androidx.viewpager2:viewpager2:1.0.0" implementation "androidx.navigation:navigation-fragment:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version" - implementation "androidx.constraintlayout:constraintlayout:2.0.2" + implementation "androidx.constraintlayout:constraintlayout:2.0.4" implementation "androidx.preference:preference:1.1.1" implementation "androidx.work:work-runtime:2.4.0" + implementation 'androidx.palette:palette:1.0.0' implementation 'com.google.guava:guava:27.0.1-android' diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.java b/app/src/main/java/awais/instagrabber/activities/MainActivity.java index 05faef57..05708f09 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.java +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.java @@ -421,7 +421,7 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage R.id.main_nav_host, getIntent(), firstFragmentGraphIndex); - navControllerLiveData.observe(this, this::setupNavigation); + navControllerLiveData.observe(this, navController -> setupNavigation(binding.toolbar, navController)); currentNavControllerLiveData = navControllerLiveData; } @@ -446,8 +446,11 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage return mainNavList; } - private void setupNavigation(final NavController navController) { - NavigationUI.setupWithNavController(binding.toolbar, navController); + private void setupNavigation(final Toolbar toolbar, final NavController navController) { + if (navController == null) { + return; + } + NavigationUI.setupWithNavController(toolbar, navController); navController.addOnDestinationChangedListener((controller, destination, arguments) -> { // below is a hack to check if we are at the end of the current stack, to setup the search view binding.appBarLayout.setExpanded(true, true); @@ -640,4 +643,16 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage public int getNavHostContainerId() { return binding.mainNavHost.getId(); } + + public void setToolbar(final Toolbar toolbar) { + binding.appBarLayout.setVisibility(View.GONE); + setSupportActionBar(toolbar); + setupNavigation(toolbar, currentNavControllerLiveData.getValue()); + } + + public void resetToolbar() { + binding.appBarLayout.setVisibility(View.VISIBLE); + setSupportActionBar(binding.toolbar); + setupNavigation(binding.toolbar, currentNavControllerLiveData.getValue()); + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/DiscoverTopicsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/DiscoverTopicsAdapter.java new file mode 100644 index 00000000..fa173042 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/DiscoverTopicsAdapter.java @@ -0,0 +1,53 @@ +package awais.instagrabber.adapters; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; + +import awais.instagrabber.adapters.viewholder.TopicClusterViewHolder; +import awais.instagrabber.databinding.ItemDiscoverTopicBinding; +import awais.instagrabber.models.TopicCluster; + +public class DiscoverTopicsAdapter extends ListAdapter { + private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull final TopicCluster oldItem, @NonNull final TopicCluster newItem) { + return oldItem.getId().equals(newItem.getId()); + } + + @Override + public boolean areContentsTheSame(@NonNull final TopicCluster oldItem, @NonNull final TopicCluster newItem) { + return oldItem.getCoverMedia().getDisplayUrl().equals(newItem.getCoverMedia().getDisplayUrl()) + && oldItem.getTitle().equals(newItem.getTitle()); + } + }; + + private final OnTopicClickListener onTopicClickListener; + + public DiscoverTopicsAdapter(final OnTopicClickListener onTopicClickListener) { + super(DIFF_CALLBACK); + this.onTopicClickListener = onTopicClickListener; + } + + @NonNull + @Override + public TopicClusterViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { + final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); + final ItemDiscoverTopicBinding binding = ItemDiscoverTopicBinding.inflate(layoutInflater, parent, false); + return new TopicClusterViewHolder(binding, onTopicClickListener); + } + + @Override + public void onBindViewHolder(@NonNull final TopicClusterViewHolder holder, final int position) { + final TopicCluster topicCluster = getItem(position); + holder.bind(topicCluster); + } + + public interface OnTopicClickListener { + void onTopicClick(TopicCluster topicCluster, View root, View cover, View title, int titleColor, int backgroundColor); + } +} diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/TopicClusterViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/TopicClusterViewHolder.java new file mode 100644 index 00000000..6e7eaed3 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/TopicClusterViewHolder.java @@ -0,0 +1,99 @@ +package awais.instagrabber.adapters.viewholder; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.palette.graphics.Palette; +import androidx.recyclerview.widget.RecyclerView; + +import com.facebook.common.executors.CallerThreadExecutor; +import com.facebook.common.references.CloseableReference; +import com.facebook.datasource.DataSource; +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.imagepipeline.core.ImagePipeline; +import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber; +import com.facebook.imagepipeline.image.CloseableImage; +import com.facebook.imagepipeline.request.ImageRequest; +import com.facebook.imagepipeline.request.ImageRequestBuilder; + +import java.util.concurrent.atomic.AtomicInteger; + +import awais.instagrabber.R; +import awais.instagrabber.adapters.DiscoverTopicsAdapter; +import awais.instagrabber.databinding.ItemDiscoverTopicBinding; +import awais.instagrabber.models.TopicCluster; + +public class TopicClusterViewHolder extends RecyclerView.ViewHolder { + private final ItemDiscoverTopicBinding binding; + private final DiscoverTopicsAdapter.OnTopicClickListener onTopicClickListener; + + public TopicClusterViewHolder(@NonNull final ItemDiscoverTopicBinding binding, + final DiscoverTopicsAdapter.OnTopicClickListener onTopicClickListener) { + super(binding.getRoot()); + this.binding = binding; + this.onTopicClickListener = onTopicClickListener; + } + + public void bind(final TopicCluster topicCluster) { + if (topicCluster == null) { + return; + } + final AtomicInteger titleColor = new AtomicInteger(-1); + final AtomicInteger backgroundColor = new AtomicInteger(-1); + if (onTopicClickListener != null) { + itemView.setOnClickListener(v -> onTopicClickListener.onTopicClick( + topicCluster, + binding.getRoot(), + binding.cover, + binding.title, + titleColor.get(), + backgroundColor.get() + )); + } + // binding.title.setTransitionName("title-" + topicCluster.getId()); + binding.cover.setTransitionName("cover-" + topicCluster.getId()); + final ImageRequest imageRequest = ImageRequestBuilder + .newBuilderWithSource(Uri.parse(topicCluster.getCoverMedia().getDisplayUrl())) + .build(); + final ImagePipeline imagePipeline = Fresco.getImagePipeline(); + final DataSource> dataSource = imagePipeline + .fetchDecodedImage(imageRequest, CallerThreadExecutor.getInstance()); + dataSource.subscribe(new BaseBitmapDataSubscriber() { + @Override + public void onNewResultImpl(@Nullable Bitmap bitmap) { + if (dataSource.isFinished()) { + dataSource.close(); + } + if (bitmap != null) { + Palette.from(bitmap).generate(p -> { + final Palette.Swatch swatch = p.getDominantSwatch(); + final Resources resources = itemView.getResources(); + int titleTextColor = resources.getColor(R.color.white); + if (swatch != null) { + backgroundColor.set(swatch.getRgb()); + GradientDrawable gd = new GradientDrawable( + GradientDrawable.Orientation.TOP_BOTTOM, + new int[]{Color.TRANSPARENT, backgroundColor.get()}); + titleTextColor = swatch.getTitleTextColor(); + binding.background.setBackground(gd); + } + titleColor.set(titleTextColor); + binding.title.setTextColor(titleTextColor); + }); + } + } + + @Override + public void onFailureImpl(@NonNull DataSource dataSource) { + dataSource.close(); + } + }, CallerThreadExecutor.getInstance()); + binding.cover.setImageRequest(imageRequest); + binding.title.setText(topicCluster.getTitle()); + } +} diff --git a/app/src/main/java/awais/instagrabber/asyncs/DiscoverPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/DiscoverPostFetchService.java new file mode 100644 index 00000000..a8c83dc8 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/asyncs/DiscoverPostFetchService.java @@ -0,0 +1,56 @@ +package awais.instagrabber.asyncs; + +import java.util.List; + +import awais.instagrabber.customviews.helpers.PostFetcher; +import awais.instagrabber.interfaces.FetchListener; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.webservices.DiscoverService; +import awais.instagrabber.webservices.ServiceCallback; + +public class DiscoverPostFetchService implements PostFetcher.PostFetchService { + private static final String TAG = "DiscoverPostFetchService"; + private final DiscoverService discoverService; + private final DiscoverService.TopicalExploreRequest topicalExploreRequest; + private boolean moreAvailable = false; + + public DiscoverPostFetchService(final DiscoverService.TopicalExploreRequest topicalExploreRequest) { + this.topicalExploreRequest = topicalExploreRequest; + discoverService = DiscoverService.getInstance(); + } + + @Override + public void fetch(final FetchListener> fetchListener) { + discoverService.topicalExplore(topicalExploreRequest, new ServiceCallback() { + @Override + public void onSuccess(final DiscoverService.TopicalExploreResponse result) { + if (result == null) { + onFailure(new RuntimeException("result is null")); + return; + } + moreAvailable = result.isMoreAvailable(); + topicalExploreRequest.setMaxId(result.getNextMaxId()); + if (fetchListener != null) { + fetchListener.onResult(result.getItems()); + } + } + + @Override + public void onFailure(final Throwable t) { + if (fetchListener != null) { + fetchListener.onFailure(t); + } + } + }); + } + + @Override + public void reset() { + topicalExploreRequest.setMaxId(-1); + } + + @Override + public boolean hasNextPage() { + return moreAvailable; + } +} diff --git a/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java index a2b2778c..d2bba0ce 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java +++ b/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java @@ -20,8 +20,8 @@ public class FeedPostFetchService implements PostFetcher.PostFetchService { } @Override - public void fetch(final String cursor, final FetchListener> fetchListener) { - feedService.fetch(25, cursor, new ServiceCallback() { + public void fetch(final FetchListener> fetchListener) { + feedService.fetch(25, nextCursor, new ServiceCallback() { @Override public void onSuccess(final PostsFetchResponse result) { if (result == null) return; @@ -43,8 +43,8 @@ public class FeedPostFetchService implements PostFetcher.PostFetchService { } @Override - public String getNextCursor() { - return nextCursor; + public void reset() { + nextCursor = null; } @Override diff --git a/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java index 251b7ebf..3617cbd2 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java +++ b/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java @@ -23,8 +23,8 @@ public class ProfilePostFetchService implements PostFetcher.PostFetchService { } @Override - public void fetch(final String cursor, final FetchListener> fetchListener) { - profileService.fetchPosts(profileModel, 30, cursor, new ServiceCallback() { + public void fetch(final FetchListener> fetchListener) { + profileService.fetchPosts(profileModel, 30, nextCursor, new ServiceCallback() { @Override public void onSuccess(final PostsFetchResponse result) { if (result == null) return; @@ -46,8 +46,8 @@ public class ProfilePostFetchService implements PostFetcher.PostFetchService { } @Override - public String getNextCursor() { - return nextCursor; + public void reset() { + nextCursor = null; } @Override diff --git a/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java b/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java index 953167b0..f5b1acde 100644 --- a/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java +++ b/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java @@ -167,7 +167,7 @@ public class PostsRecyclerView extends RecyclerView { setNestedScrollingEnabled(true); lazyLoader = new RecyclerLazyLoaderAtBottom(layoutManager, (page) -> { if (postFetcher.hasMore()) { - postFetcher.fetchNextPage(); + postFetcher.fetch(); dispatchFetchStatus(); } }); @@ -204,6 +204,7 @@ public class PostsRecyclerView extends RecyclerView { public void refresh() { lazyLoader.resetState(); + postFetcher.reset(); postFetcher.fetch(); dispatchFetchStatus(); } diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/PostFetcher.java b/app/src/main/java/awais/instagrabber/customviews/helpers/PostFetcher.java index 75493bdd..35a840e6 100644 --- a/app/src/main/java/awais/instagrabber/customviews/helpers/PostFetcher.java +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/PostFetcher.java @@ -17,21 +17,17 @@ public class PostFetcher { } public void fetch() { - fetch(null); - } - - public void fetchNextPage() { - fetch(postFetchService.getNextCursor()); - } - - public void fetch(final String cursor) { fetching = true; - postFetchService.fetch(cursor, result -> { + postFetchService.fetch(result -> { fetching = false; fetchListener.onResult(result); }); } + public void reset() { + postFetchService.reset(); + } + public boolean isFetching() { return fetching; } @@ -41,9 +37,9 @@ public class PostFetcher { } public interface PostFetchService { - void fetch(String cursor, FetchListener> fetchListener); + void fetch(FetchListener> fetchListener); - String getNextCursor(); + void reset(); boolean hasNextPage(); } diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoaderAtBottom.java b/app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoaderAtBottom.java index 0af5e021..0c4168b6 100644 --- a/app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoaderAtBottom.java +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoaderAtBottom.java @@ -30,7 +30,7 @@ public final class RecyclerLazyLoaderAtBottom extends RecyclerView.OnScrollListe if (!recyclerView.canScrollVertically(RecyclerView.SCROLL_AXIS_HORIZONTAL) && newState == RecyclerView.SCROLL_STATE_IDLE) { if (!loading && lazyLoadListener != null) { loading = true; - new Handler().postDelayed(() -> lazyLoadListener.onLoadMore(++currentPage), 1000); + new Handler().postDelayed(() -> lazyLoadListener.onLoadMore(++currentPage), 500); } } } diff --git a/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java b/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java index 4ecefa83..07ccc788 100644 --- a/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java @@ -4,7 +4,6 @@ import android.content.Context; import android.graphics.Typeface; import android.os.AsyncTask; import android.os.Bundle; -import android.os.Handler; import android.text.SpannableStringBuilder; import android.text.style.RelativeSizeSpan; import android.text.style.StyleSpan; @@ -209,7 +208,7 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != null; final HashTagFragmentArgs fragmentArgs = HashTagFragmentArgs.fromBundle(getArguments()); hashtag = fragmentArgs.getHashtag(); - setTitle(); + // setTitle(); setupPosts(); fetchHashtagModel(); } @@ -284,6 +283,7 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe Toast.makeText(context, R.string.error_loading_profile, Toast.LENGTH_SHORT).show(); return; } + setTitle(); fetchPosts(); }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @@ -324,9 +324,35 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe binding.btnFollowTag.setOnClickListener(v -> { final String cookie = settingsHelper.getString(Constants.COOKIE); final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); - binding.btnFollowTag.setClickable(false); - if (!hashtagModel.getFollowing()) { - tagsService.follow(hashtag.substring(1), csrfToken, new ServiceCallback() { + if (csrfToken != null) { + binding.btnFollowTag.setClickable(false); + if (!hashtagModel.getFollowing()) { + tagsService.follow(hashtag.substring(1), csrfToken, new ServiceCallback() { + @Override + public void onSuccess(final Boolean result) { + binding.btnFollowTag.setClickable(true); + if (!result) { + Log.e(TAG, "onSuccess: result is false"); + return; + } + onRefresh(); + } + + @Override + public void onFailure(@NonNull final Throwable t) { + binding.btnFollowTag.setClickable(true); + Log.e(TAG, "onFailure: ", t); + final String message = t.getMessage(); + Snackbar.make(root, + message != null ? message + : getString(R.string.downloader_unknown_error), + BaseTransientBottomBar.LENGTH_LONG) + .show(); + } + }); + return; + } + tagsService.unfollow(hashtag.substring(1), csrfToken, new ServiceCallback() { @Override public void onSuccess(final Boolean result) { binding.btnFollowTag.setClickable(true); @@ -349,31 +375,7 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe .show(); } }); - return; } - tagsService.unfollow(hashtag.substring(1), csrfToken, new ServiceCallback() { - @Override - public void onSuccess(final Boolean result) { - binding.btnFollowTag.setClickable(true); - if (!result) { - Log.e(TAG, "onSuccess: result is false"); - return; - } - onRefresh(); - } - - @Override - public void onFailure(@NonNull final Throwable t) { - binding.btnFollowTag.setClickable(true); - Log.e(TAG, "onFailure: ", t); - final String message = t.getMessage(); - Snackbar.make(root, - message != null ? message - : getString(R.string.downloader_unknown_error), - BaseTransientBottomBar.LENGTH_LONG) - .show(); - } - }); }); } else { binding.btnFollowTag.setVisibility(View.GONE); @@ -425,7 +427,6 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe final NavDirections action = HashTagFragmentDirections .actionHashtagFragmentToStoryViewerFragment(-1, null, true, false, hashtagModel.getName(), hashtagModel.getName()); NavHostFragment.findNavController(this).navigate(action); - return; } }); } @@ -445,9 +446,10 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe private void setTitle() { final ActionBar actionBar = fragmentActivity.getSupportActionBar(); if (actionBar != null) { - Log.d(TAG, "setting title: " + hashtag); - final Handler handler = new Handler(); - handler.postDelayed(() -> actionBar.setTitle(hashtag), 200); + // Log.d(TAG, "setting title: " + hashtag); + actionBar.setTitle(hashtag); + // final Handler handler = new Handler(); + // handler.postDelayed(() -> , 1000); } } diff --git a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java index 992aa4b3..78882dff 100644 --- a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java @@ -920,7 +920,7 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { binding.playerControls.getRoot().setVisibility(View.GONE); binding.sliderParent.setVisibility(View.VISIBLE); binding.mediaCounter.setVisibility(View.VISIBLE); - if (sharedMainPostElement != null) { + if (!wasPaused && sharedMainPostElement != null) { addSharedElement(sharedMainPostElement, binding.sliderParent); } sliderItemsAdapter = new SliderItemsAdapter(onVerticalDragListener, binding.playerControls, true, new SliderCallbackAdapter() { @@ -1014,7 +1014,7 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { binding.sliderParent.setVisibility(View.GONE); binding.mediaCounter.setVisibility(View.GONE); // binding.playerControls.getRoot().setVisibility(View.VISIBLE); - if (sharedMainPostElement != null) { + if (!wasPaused && sharedMainPostElement != null) { final GenericDraweeHierarchy hierarchy = binding.videoPost.thumbnail.getHierarchy(); hierarchy.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP); addSharedElement(sharedMainPostElement, binding.videoPost.thumbnailParent); diff --git a/app/src/main/java/awais/instagrabber/fragments/TopicPostsFragment.java b/app/src/main/java/awais/instagrabber/fragments/TopicPostsFragment.java new file mode 100644 index 00000000..04be0526 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/TopicPostsFragment.java @@ -0,0 +1,363 @@ +package awais.instagrabber.fragments; + +import android.animation.ArgbEvaluator; +import android.content.Context; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.os.Bundle; +import android.os.Handler; +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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.PermissionChecker; +import androidx.core.graphics.ColorUtils; +import androidx.fragment.app.Fragment; +import androidx.navigation.NavController; +import androidx.navigation.NavDirections; +import androidx.navigation.fragment.NavHostFragment; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import androidx.transition.ChangeBounds; +import androidx.transition.TransitionInflater; +import androidx.transition.TransitionSet; + +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.drawee.controller.BaseControllerListener; +import com.facebook.drawee.interfaces.DraweeController; +import com.facebook.imagepipeline.image.ImageInfo; + +import awais.instagrabber.R; +import awais.instagrabber.activities.MainActivity; +import awais.instagrabber.adapters.FeedAdapterV2; +import awais.instagrabber.asyncs.DiscoverPostFetchService; +import awais.instagrabber.customviews.helpers.NestedCoordinatorLayout; +import awais.instagrabber.databinding.FragmentTopicPostsBinding; +import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; +import awais.instagrabber.fragments.main.DiscoverFragmentDirections; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.PostsLayoutPreferences; +import awais.instagrabber.models.TopicCluster; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.DownloadUtils; +import awais.instagrabber.utils.Utils; +import awais.instagrabber.webservices.DiscoverService; + +import static androidx.core.content.PermissionChecker.checkSelfPermission; +import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class TopicPostsFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { + private static final int STORAGE_PERM_REQUEST_CODE = 8020; + private MainActivity fragmentActivity; + private FragmentTopicPostsBinding binding; + private NestedCoordinatorLayout root; + private boolean shouldRefresh = true; + private TopicCluster topicCluster; + + private final FeedAdapterV2.FeedItemCallback feedItemCallback = new FeedAdapterV2.FeedItemCallback() { + @Override + public void onPostClick(final FeedModel feedModel, final View profilePicView, final View mainPostImage) { + openPostDialog(feedModel, profilePicView, mainPostImage, -1); + } + + @Override + public void onSliderClick(final FeedModel feedModel, final int position) { + openPostDialog(feedModel, null, null, position); + } + + @Override + public void onCommentsClick(final FeedModel feedModel) { + final NavDirections commentsAction = DiscoverFragmentDirections.actionGlobalCommentsViewerFragment( + feedModel.getShortCode(), + feedModel.getPostId(), + feedModel.getProfileModel().getId() + ); + NavHostFragment.findNavController(TopicPostsFragment.this).navigate(commentsAction); + } + + @Override + public void onDownloadClick(final FeedModel feedModel) { + final Context context = getContext(); + if (context == null) return; + if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { + showDownloadDialog(feedModel); + return; + } + requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); + } + + @Override + public void onHashtagClick(final String hashtag) { + final NavDirections action = DiscoverFragmentDirections.actionGlobalHashTagFragment(hashtag); + NavHostFragment.findNavController(TopicPostsFragment.this).navigate(action); + } + + @Override + public void onLocationClick(final FeedModel feedModel) { + final NavDirections action = DiscoverFragmentDirections.actionGlobalLocationFragment(feedModel.getLocationId()); + NavHostFragment.findNavController(TopicPostsFragment.this).navigate(action); + } + + @Override + public void onMentionClick(final String mention) { + navigateToProfile(mention.trim()); + } + + @Override + public void onNameClick(final FeedModel feedModel, final View profilePicView) { + navigateToProfile("@" + feedModel.getProfileModel().getUsername()); + } + + @Override + public void onProfilePicClick(final FeedModel feedModel, final View profilePicView) { + navigateToProfile("@" + feedModel.getProfileModel().getUsername()); + } + + @Override + public void onURLClick(final String url) { + Utils.openURL(getContext(), url); + } + + @Override + public void onEmailClick(final String emailId) { + Utils.openEmailAddress(getContext(), emailId); + } + + private void openPostDialog(final FeedModel feedModel, + final View profilePicView, + final View mainPostImage, + final int position) { + final PostViewV2Fragment.Builder builder = PostViewV2Fragment + .builder(feedModel); + if (position >= 0) { + builder.setPosition(position); + } + final PostViewV2Fragment fragment = builder + .setSharedProfilePicElement(profilePicView) + .setSharedMainPostElement(mainPostImage) + .build(); + fragment.show(getChildFragmentManager(), "post_view"); + } + }; + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + fragmentActivity = (MainActivity) requireActivity(); + final TransitionSet transitionSet = new TransitionSet(); + transitionSet.addTransition(new ChangeBounds()) + .addTransition(TransitionInflater.from(getContext()).inflateTransition(android.R.transition.move)) + .setDuration(200); + setSharedElementEnterTransition(transitionSet); + postponeEnterTransition(); + setHasOptionsMenu(true); + } + + @Nullable + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, + @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState) { + if (root != null) { + shouldRefresh = false; + return root; + } + binding = FragmentTopicPostsBinding.inflate(inflater, container, false); + root = binding.getRoot(); + return root; + } + + @Override + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + if (!shouldRefresh) return; + binding.swipeRefreshLayout.setOnRefreshListener(this); + init(); + shouldRefresh = false; + } + + @Override + public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { + inflater.inflate(R.menu.topic_posts_menu, menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull final MenuItem item) { + if (item.getItemId() == R.id.layout) { + showPostsLayoutPreferences(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onRefresh() { + binding.posts.refresh(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + resetToolbar(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + resetToolbar(); + } + + private void resetToolbar() { + fragmentActivity.resetToolbar(); + } + + private void init() { + if (getArguments() == null) return; + final TopicPostsFragmentArgs fragmentArgs = TopicPostsFragmentArgs.fromBundle(getArguments()); + topicCluster = fragmentArgs.getTopicCluster(); + setupToolbar(fragmentArgs.getTitleColor(), fragmentArgs.getBackgroundColor()); + setupPosts(); + } + + private void setupToolbar(final int titleColor, final int backgroundColor) { + if (topicCluster == null) { + return; + } + binding.cover.setTransitionName("cover-" + topicCluster.getId()); + fragmentActivity.setToolbar(binding.toolbar); + binding.collapsingToolbarLayout.setTitle(topicCluster.getTitle()); + final int collapsedTitleTextColor = ColorUtils.setAlphaComponent(titleColor, 0xFF); + final int expandedTitleTextColor = ColorUtils.setAlphaComponent(titleColor, 0x99); + binding.collapsingToolbarLayout.setExpandedTitleColor(expandedTitleTextColor); + binding.collapsingToolbarLayout.setCollapsedTitleTextColor(collapsedTitleTextColor); + binding.collapsingToolbarLayout.setContentScrimColor(backgroundColor); + final Drawable navigationIcon = binding.toolbar.getNavigationIcon(); + final Drawable overflowIcon = binding.toolbar.getOverflowIcon(); + if (navigationIcon != null && overflowIcon != null) { + final Drawable navDrawable = navigationIcon.mutate(); + final Drawable overflowDrawable = overflowIcon.mutate(); + navDrawable.setAlpha(0xFF); + overflowDrawable.setAlpha(0xFF); + final ArgbEvaluator argbEvaluator = new ArgbEvaluator(); + binding.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { + final int totalScrollRange = appBarLayout.getTotalScrollRange(); + final float current = totalScrollRange + verticalOffset; + final float fraction = current / totalScrollRange; + final int tempColor = (int) argbEvaluator.evaluate(fraction, collapsedTitleTextColor, expandedTitleTextColor); + navDrawable.setColorFilter(tempColor, PorterDuff.Mode.SRC_ATOP); + overflowDrawable.setColorFilter(tempColor, PorterDuff.Mode.SRC_ATOP); + + }); + } + final GradientDrawable gd = new GradientDrawable( + GradientDrawable.Orientation.TOP_BOTTOM, + new int[]{Color.TRANSPARENT, backgroundColor}); + binding.background.setBackground(gd); + setupCover(); + } + + private void setupCover() { + final String coverUrl = topicCluster.getCoverMedia().getDisplayUrl(); + final DraweeController controller = Fresco + .newDraweeControllerBuilder() + .setOldController(binding.cover.getController()) + .setUri(coverUrl) + .setControllerListener(new BaseControllerListener() { + + @Override + public void onFailure(final String id, final Throwable throwable) { + super.onFailure(id, throwable); + startPostponedEnterTransition(); + } + + @Override + public void onFinalImageSet(final String id, + @Nullable final ImageInfo imageInfo, + @Nullable final Animatable animatable) { + startPostponedEnterTransition(); + } + }) + .build(); + binding.cover.setController(controller); + } + + private void setupPosts() { + final DiscoverService.TopicalExploreRequest topicalExploreRequest = new DiscoverService.TopicalExploreRequest(); + topicalExploreRequest.setClusterId(topicCluster.getId()); + binding.posts.setViewModelStoreOwner(this) + .setLifeCycleOwner(this) + .setPostFetchService(new DiscoverPostFetchService(topicalExploreRequest)) + .setLayoutPreferences(PostsLayoutPreferences.fromJson(settingsHelper.getString(Constants.PREF_TOPIC_POSTS_LAYOUT))) + .addFetchStatusChangeListener(fetching -> updateSwipeRefreshState()) + .setFeedItemCallback(feedItemCallback) + .init(); + binding.swipeRefreshLayout.setRefreshing(true); + } + + private void updateSwipeRefreshState() { + binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching()); + } + + private void showDownloadDialog(final FeedModel feedModel) { + final Context context = getContext(); + if (context == null) return; + DownloadUtils.download(context, feedModel); + // switch (feedModel.getItemType()) { + // case MEDIA_TYPE_IMAGE: + // case MEDIA_TYPE_VIDEO: + // break; + // case MEDIA_TYPE_SLIDER: + // break; + // } + // final List postModelsToDownload = new ArrayList<>(); + // // if (!session) { + // final DialogInterface.OnClickListener clickListener = (dialog, which) -> { + // if (which == DialogInterface.BUTTON_NEGATIVE) { + // postModelsToDownload.addAll(postModels); + // } else if (which == DialogInterface.BUTTON_POSITIVE) { + // postModelsToDownload.add(postModels.get(childPosition)); + // } else { + // session = true; + // postModelsToDownload.add(postModels.get(childPosition)); + // } + // if (postModelsToDownload.size() > 0) { + // DownloadUtils.batchDownload(context, + // username, + // DownloadMethod.DOWNLOAD_POST_VIEWER, + // postModelsToDownload); + // } + // }; + // new AlertDialog.Builder(context) + // .setTitle(R.string.post_viewer_download_dialog_title) + // .setMessage(R.string.post_viewer_download_message) + // .setNeutralButton(R.string.post_viewer_download_session, clickListener) + // .setPositiveButton(R.string.post_viewer_download_current, clickListener) + // .setNegativeButton(R.string.post_viewer_download_album, clickListener).show(); + // } else { + // DownloadUtils.batchDownload(context, + // username, + // DownloadMethod.DOWNLOAD_POST_VIEWER, + // Collections.singletonList(postModels.get(childPosition))); + } + + private void navigateToProfile(final String username) { + final NavController navController = NavHostFragment.findNavController(this); + final Bundle bundle = new Bundle(); + bundle.putString("username", username); + navController.navigate(R.id.action_global_profileFragment, bundle); + } + + private void showPostsLayoutPreferences() { + final PostsLayoutPreferencesDialogFragment fragment = new PostsLayoutPreferencesDialogFragment( + Constants.PREF_TOPIC_POSTS_LAYOUT, + preferences -> new Handler().postDelayed(() -> binding.posts.setLayoutPreferences(preferences), 200)); + fragment.show(getChildFragmentManager(), "posts_layout_preferences"); + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java index 2939eb52..db91702c 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java @@ -1,126 +1,84 @@ package awais.instagrabber.fragments.main; -import android.content.Context; -import android.os.AsyncTask; import android.os.Bundle; +import android.util.Log; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Toast; import androidx.activity.OnBackPressedCallback; -import androidx.activity.OnBackPressedDispatcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; -import androidx.navigation.NavDirections; +import androidx.navigation.fragment.FragmentNavigator; import androidx.navigation.fragment.NavHostFragment; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - import awais.instagrabber.R; import awais.instagrabber.activities.MainActivity; -import awais.instagrabber.adapters.DiscoverAdapter; -import awais.instagrabber.asyncs.DiscoverFetcher; -import awais.instagrabber.asyncs.i.iTopicFetcher; +import awais.instagrabber.adapters.DiscoverTopicsAdapter; import awais.instagrabber.customviews.PrimaryActionModeCallback; -import awais.instagrabber.customviews.helpers.GridAutofitLayoutManager; import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration; -import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; import awais.instagrabber.databinding.FragmentDiscoverBinding; -import awais.instagrabber.interfaces.FetchListener; -import awais.instagrabber.models.DiscoverItemModel; -import awais.instagrabber.models.DiscoverTopicModel; -import awais.instagrabber.models.enums.DownloadMethod; -import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.Utils; -import awais.instagrabber.viewmodels.DiscoverItemViewModel; +import awais.instagrabber.viewmodels.TopicClusterViewModel; +import awais.instagrabber.webservices.DiscoverService; +import awais.instagrabber.webservices.ServiceCallback; public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { + private static final String TAG = "DiscoverFragment"; private MainActivity fragmentActivity; private CoordinatorLayout root; private FragmentDiscoverBinding binding; - private DiscoverAdapter discoverAdapter; - private RecyclerLazyLoader lazyLoader; - private boolean discoverHasMore = false; - private String[] topicIds; - private String rankToken; - private String currentTopic; - private String discoverEndMaxId; private ActionMode actionMode; - private DiscoverItemViewModel discoverItemViewModel; + private TopicClusterViewModel topicClusterViewModel; private boolean shouldRefresh = true; - private boolean isPullToRefresh; + private DiscoverService discoverService; - private final FetchListener topicFetchListener = new FetchListener() { - @Override - public void doBefore() {} - - @Override - public void onResult(final DiscoverTopicModel result) { - if (result != null) { - topicIds = result.getIds(); - rankToken = result.getToken(); - final Context context = getContext(); - if (context == null) return; - final ArrayAdapter spinnerArrayAdapter = new ArrayAdapter<>( - context, - android.R.layout.simple_spinner_dropdown_item, - result.getNames() - ); - binding.discoverType.setAdapter(spinnerArrayAdapter); - } - } - }; - private final FetchListener postsFetchListener = new FetchListener() { - @Override - public void doBefore() {} - - @Override - public void onResult(final DiscoverItemModel[] result) { - if (result == null || result.length <= 0) { - binding.discoverSwipeRefreshLayout.setRefreshing(false); - final Context context = getContext(); - if (context == null) return; - Toast.makeText(context, R.string.discover_empty, Toast.LENGTH_SHORT).show(); - return; - } - List current = discoverItemViewModel.getList().getValue(); - final List resultList = Arrays.asList(result); - current = current == null ? new ArrayList<>() : new ArrayList<>(current); // copy to modifiable list - if (isPullToRefresh) { - current = resultList; - isPullToRefresh = false; - } else { - current.addAll(resultList); - } - discoverItemViewModel.getList().postValue(current); - binding.discoverSwipeRefreshLayout.setRefreshing(false); - final DiscoverItemModel discoverItemModel = result[result.length - 1]; - if (discoverItemModel != null) { - discoverEndMaxId = discoverItemModel.getNextMaxId(); - discoverHasMore = discoverItemModel.hasMore(); - discoverItemModel.setMore(false, null); - } - } - }; + // private final FetchListener postsFetchListener = new FetchListener() { + // @Override + // public void doBefore() {} + // + // @Override + // public void onResult(final DiscoverItemModel[] result) { + // if (result == null || result.length <= 0) { + // binding.swipeRefreshLayout.setRefreshing(false); + // final Context context = getContext(); + // if (context == null) return; + // Toast.makeText(context, R.string.discover_empty, Toast.LENGTH_SHORT).show(); + // return; + // } + // List current = discoverItemViewModel.getList().getValue(); + // final List resultList = Arrays.asList(result); + // current = current == null ? new ArrayList<>() : new ArrayList<>(current); // copy to modifiable list + // if (isPullToRefresh) { + // current = resultList; + // isPullToRefresh = false; + // } else { + // current.addAll(resultList); + // } + // discoverItemViewModel.getList().postValue(current); + // binding.swipeRefreshLayout.setRefreshing(false); + // final DiscoverItemModel discoverItemModel = result[result.length - 1]; + // if (discoverItemModel != null) { + // discoverEndMaxId = discoverItemModel.getNextMaxId(); + // discoverHasMore = discoverItemModel.hasMore(); + // discoverItemModel.setMore(false, null); + // } + // } + // }; private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { setEnabled(false); remove(); - if (discoverAdapter == null) return; - discoverAdapter.clearSelection(); + // if (discoverAdapter == null) return; + // discoverAdapter.clearSelection(); } }; private final PrimaryActionModeCallback multiSelectAction = new PrimaryActionModeCallback( @@ -134,14 +92,14 @@ public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnR @Override public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) { if (item.getItemId() == R.id.action_download) { - if (discoverAdapter == null) return false; - final Context context = getContext(); - if (context == null) return false; - DownloadUtils.batchDownload(context, - null, - DownloadMethod.DOWNLOAD_DISCOVER, - discoverAdapter.getSelectedModels()); - checkAndResetAction(); + // if (discoverAdapter == null) return false; + // final Context context = getContext(); + // if (context == null) return false; + // DownloadUtils.batchDownload(context, + // null, + // DownloadMethod.DOWNLOAD_DISCOVER, + // discoverAdapter.getSelectedModels()); + // checkAndResetAction(); return true; } return false; @@ -152,6 +110,7 @@ public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnR public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); fragmentActivity = (MainActivity) requireActivity(); + discoverService = DiscoverService.getInstance(); } @Override @@ -170,96 +129,159 @@ public class DiscoverFragment extends Fragment implements SwipeRefreshLayout.OnR @Override public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { if (!shouldRefresh) return; - binding.discoverSwipeRefreshLayout.setOnRefreshListener(this); - setupExplore(); + binding.swipeRefreshLayout.setOnRefreshListener(this); + init(); shouldRefresh = false; } + private void init() { + // setExitSharedElementCallback(new SharedElementCallback() { + // @Override + // public void onSharedElementsArrived(final List sharedElementNames, + // final List sharedElements, + // final OnSharedElementsReadyListener listener) { + // super.onSharedElementsArrived(sharedElementNames, sharedElements, listener); + // Log.d(TAG, "onSharedElementsArrived: sharedElementNames: " + sharedElementNames); + // } + // + // @Override + // public void onSharedElementEnd(final List sharedElementNames, + // final List sharedElements, + // final List sharedElementSnapshots) { + // super.onSharedElementEnd(sharedElementNames, sharedElements, sharedElementSnapshots); + // Log.d(TAG, "onSharedElementEnd: sharedElementNames: " + sharedElementNames); + // } + // + // @Override + // public void onSharedElementStart(final List sharedElementNames, + // final List sharedElements, + // final List sharedElementSnapshots) { + // super.onSharedElementStart(sharedElementNames, sharedElements, sharedElementSnapshots); + // Log.d(TAG, "onSharedElementStart: sharedElementNames: " + sharedElementNames); + // } + // }); + setupTopics(); + fetchTopics(); + } + @Override public void onRefresh() { - isPullToRefresh = true; - discoverEndMaxId = null; - lazyLoader.resetState(); - fetchPosts(); - } - - private void setupExplore() { - discoverItemViewModel = new ViewModelProvider(fragmentActivity).get(DiscoverItemViewModel.class); - final Context context = getContext(); - if (context == null) return; - final GridAutofitLayoutManager layoutManager = new GridAutofitLayoutManager(context, Utils.convertDpToPx(110)); - binding.discoverPosts.setLayoutManager(layoutManager); - binding.discoverPosts.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(4))); - binding.discoverType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int pos, long id) { - if (topicIds == null || topicIds.length <= 0) return; - currentTopic = topicIds[pos]; - onRefresh(); - } - - @Override - public void onNothingSelected(AdapterView parent) {} - }); - discoverAdapter = new DiscoverAdapter((model, position) -> { - if (discoverAdapter.isSelecting()) { - if (actionMode == null) return; - final String title = getString(R.string.number_selected, discoverAdapter.getSelectedModels().size()); - actionMode.setTitle(title); - return; - } - if (checkAndResetAction()) return; - final List discoverItemModels = discoverItemViewModel.getList().getValue(); - if (discoverItemModels == null || discoverItemModels.size() == 0) return; - if (discoverItemModels.get(0) == null) return; - final String postId = discoverItemModels.get(0).getPostId(); - final boolean isId = postId != null; - final String[] idsOrShortCodes = new String[discoverItemModels.size()]; - for (int i = 0; i < discoverItemModels.size(); i++) { - idsOrShortCodes[i] = isId ? discoverItemModels.get(i).getPostId() - : discoverItemModels.get(i).getShortCode(); - } - final NavDirections action = DiscoverFragmentDirections.actionGlobalPostViewFragment( - position, - idsOrShortCodes, - isId); - NavHostFragment.findNavController(this).navigate(action); - }, (model, position) -> { - if (!discoverAdapter.isSelecting()) { - checkAndResetAction(); - return true; - } - final OnBackPressedDispatcher onBackPressedDispatcher = fragmentActivity.getOnBackPressedDispatcher(); - if (onBackPressedCallback.isEnabled()) { - return true; - } - actionMode = fragmentActivity.startActionMode(multiSelectAction); - final String title = getString(R.string.number_selected, 1); - actionMode.setTitle(title); - onBackPressedDispatcher.addCallback(getViewLifecycleOwner(), onBackPressedCallback); - return true; - }); - binding.discoverPosts.setAdapter(discoverAdapter); - discoverItemViewModel.getList().observe(fragmentActivity, discoverAdapter::submitList); - lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { - if (discoverHasMore) { - fetchPosts(); - } - }, 3); - binding.discoverPosts.addOnScrollListener(lazyLoader); fetchTopics(); } - private void fetchTopics() { - new iTopicFetcher(topicFetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + public void setupTopics() { + topicClusterViewModel = new ViewModelProvider(fragmentActivity).get(TopicClusterViewModel.class); + binding.topicsRecyclerView.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(2))); + final DiscoverTopicsAdapter adapter = new DiscoverTopicsAdapter((topicCluster, root, cover, title, titleColor, backgroundColor) -> { + final FragmentNavigator.Extras.Builder builder = new FragmentNavigator.Extras.Builder() + .addSharedElement(cover, "cover-" + topicCluster.getId()); + // .addSharedElement(title, "title-" + topicCluster.getId()); + final DiscoverFragmentDirections.ActionDiscoverFragmentToTopicPostsFragment action = DiscoverFragmentDirections + .actionDiscoverFragmentToTopicPostsFragment(topicCluster, titleColor, backgroundColor); + NavHostFragment.findNavController(this).navigate(action, builder.build()); + }); + binding.topicsRecyclerView.setAdapter(adapter); + topicClusterViewModel.getList().observe(getViewLifecycleOwner(), adapter::submitList); } - private void fetchPosts() { - binding.discoverSwipeRefreshLayout.setRefreshing(true); - new DiscoverFetcher(currentTopic, discoverEndMaxId, rankToken, postsFetchListener, false) - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + private void setupExplore() { + // discoverItemViewModel = new ViewModelProvider(fragmentActivity).get(DiscoverItemViewModel.class); + // final Context context = getContext(); + // if (context == null) return; + // final GridAutofitLayoutManager layoutManager = new GridAutofitLayoutManager(context, Utils.convertDpToPx(110)); + // binding.postsRecyclerView.setLayoutManager(layoutManager); + // binding.postsRecyclerView.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(4))); + // binding.discoverType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + // @Override + // public void onItemSelected(AdapterView parent, View view, int pos, long id) { + // if (topicIds == null || topicIds.length <= 0) return; + // currentTopic = topicIds[pos]; + // onRefresh(); + // } + // + // @Override + // public void onNothingSelected(AdapterView parent) {} + // }); + // discoverAdapter = new DiscoverAdapter((model, position) -> { + // if (discoverAdapter.isSelecting()) { + // if (actionMode == null) return; + // final String title = getString(R.string.number_selected, discoverAdapter.getSelectedModels().size()); + // actionMode.setTitle(title); + // return; + // } + // if (checkAndResetAction()) return; + // final List discoverItemModels = discoverItemViewModel.getList().getValue(); + // if (discoverItemModels == null || discoverItemModels.size() == 0) return; + // if (discoverItemModels.get(0) == null) return; + // final String postId = discoverItemModels.get(0).getPostId(); + // final boolean isId = postId != null; + // final String[] idsOrShortCodes = new String[discoverItemModels.size()]; + // for (int i = 0; i < discoverItemModels.size(); i++) { + // idsOrShortCodes[i] = isId ? discoverItemModels.get(i).getPostId() + // : discoverItemModels.get(i).getShortCode(); + // } + // final NavDirections action = DiscoverFragmentDirections.actionGlobalPostViewFragment( + // position, + // idsOrShortCodes, + // isId); + // NavHostFragment.findNavController(this).navigate(action); + // }, (model, position) -> { + // if (!discoverAdapter.isSelecting()) { + // checkAndResetAction(); + // return true; + // } + // final OnBackPressedDispatcher onBackPressedDispatcher = fragmentActivity.getOnBackPressedDispatcher(); + // if (onBackPressedCallback.isEnabled()) { + // return true; + // } + // actionMode = fragmentActivity.startActionMode(multiSelectAction); + // final String title = getString(R.string.number_selected, 1); + // actionMode.setTitle(title); + // onBackPressedDispatcher.addCallback(getViewLifecycleOwner(), onBackPressedCallback); + // return true; + // }); + // binding.postsRecyclerView.setAdapter(discoverAdapter); + // discoverItemViewModel.getList().observe(fragmentActivity, discoverAdapter::submitList); + // lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { + // if (discoverHasMore) { + // fetchPosts(); + // } + // }, 3); + // binding.postsRecyclerView.addOnScrollListener(lazyLoader); + // binding.postsRecyclerView.setViewModelStoreOwner(this) + // .setLifeCycleOwner(this) + // .setPostFetchService(new DiscoverPostFetchService()) + // .setLayoutPreferences(PostsLayoutPreferences.fromJson(settingsHelper.getString(Constants.PREF_PROFILE_POSTS_LAYOUT))) + // .addFetchStatusChangeListener(fetching -> updateSwipeRefreshState()) + // .setFeedItemCallback(feedItemCallback) + // .init(); + // binding.swipeRefreshLayout.setRefreshing(true); } + private void fetchTopics() { + // new iTopicFetcher(topicFetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + binding.swipeRefreshLayout.setRefreshing(true); + discoverService.topicalExplore(new DiscoverService.TopicalExploreRequest(), new ServiceCallback() { + @Override + public void onSuccess(final DiscoverService.TopicalExploreResponse result) { + topicClusterViewModel.getList().postValue(result.getClusters()); + binding.swipeRefreshLayout.setRefreshing(false); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "onFailure", t); + binding.swipeRefreshLayout.setRefreshing(false); + } + }); + } + + // private void fetchPosts() { + // binding.swipeRefreshLayout.setRefreshing(true); + // new DiscoverFetcher(currentTopic, discoverEndMaxId, rankToken, postsFetchListener, false) + // .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + // } + private boolean checkAndResetAction() { if (!onBackPressedCallback.isEnabled() && actionMode == null) { return false; diff --git a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java index 7f414633..e79967c9 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java @@ -58,17 +58,10 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre private CoordinatorLayout root; private FragmentFeedBinding binding; private StoriesService storiesService; - // private boolean feedHasNextPage = false; - // private String feedEndCursor = null; - // private FeedViewModel feedViewModel; - // private VideoAwareRecyclerScroller videoAwareRecyclerScroller; private boolean shouldRefresh = true; - // private boolean isPullToRefresh; private FeedStoriesViewModel feedStoriesViewModel; - // private StaggeredGridLayoutManager gridLayoutManager; private boolean storiesFetching; - // private final boolean shouldAutoPlay = settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS); private final FeedAdapterV2.FeedItemCallback feedItemCallback = new FeedAdapterV2.FeedItemCallback() { @Override public void onPostClick(final FeedModel feedModel, final View profilePicView, final View mainPostImage) { @@ -226,15 +219,11 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre @Override public void onRefresh() { - // isPullToRefresh = true; - // feedEndCursor = null; binding.feedRecyclerView.refresh(); fetchStories(); } private void setupFeed() { - final Context context = getContext(); - if (context == null) return; binding.feedRecyclerView.setViewModelStoreOwner(this) .setLifeCycleOwner(this) .setPostFetchService(new FeedPostFetchService()) @@ -331,8 +320,9 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre } private void showPostsLayoutPreferences() { - final PostsLayoutPreferencesDialogFragment fragment = new PostsLayoutPreferencesDialogFragment(Constants.PREF_POSTS_LAYOUT, preferences -> new Handler() - .postDelayed(() -> binding.feedRecyclerView.setLayoutPreferences(preferences), 200)); + final PostsLayoutPreferencesDialogFragment fragment = new PostsLayoutPreferencesDialogFragment( + Constants.PREF_POSTS_LAYOUT, + preferences -> new Handler().postDelayed(() -> binding.feedRecyclerView.setLayoutPreferences(preferences), 200)); fragment.show(getChildFragmentManager(), "posts_layout_preferences"); } } diff --git a/app/src/main/java/awais/instagrabber/models/TopicCluster.java b/app/src/main/java/awais/instagrabber/models/TopicCluster.java new file mode 100644 index 00000000..6356e8c0 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/TopicCluster.java @@ -0,0 +1,57 @@ +package awais.instagrabber.models; + +import java.io.Serializable; + +public class TopicCluster implements Serializable { + private String id; + private String title; + private String type; + private boolean canMute; + private boolean isMuted; + private int rankedPosition; + private FeedModel coverMedia; + + public TopicCluster(final String id, + final String title, + final String type, + final boolean canMute, + final boolean isMuted, + final int rankedPosition, + final FeedModel coverMedia) { + this.id = id; + this.title = title; + this.type = type; + this.canMute = canMute; + this.isMuted = isMuted; + this.rankedPosition = rankedPosition; + this.coverMedia = coverMedia; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getType() { + return type; + } + + public boolean isCanMute() { + return canMute; + } + + public boolean isMuted() { + return isMuted; + } + + public int getRankedPosition() { + return rankedPosition; + } + + public FeedModel getCoverMedia() { + return coverMedia; + } +} diff --git a/app/src/main/java/awais/instagrabber/repositories/DiscoverRepository.java b/app/src/main/java/awais/instagrabber/repositories/DiscoverRepository.java new file mode 100644 index 00000000..020884d0 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/DiscoverRepository.java @@ -0,0 +1,12 @@ +package awais.instagrabber.repositories; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.QueryMap; + +public interface DiscoverRepository { + @GET("/api/v1/discover/topical_explore/") + Call topicalExplore(@QueryMap Map queryParams); +} diff --git a/app/src/main/java/awais/instagrabber/utils/Constants.java b/app/src/main/java/awais/instagrabber/utils/Constants.java index e448802b..c09064e6 100644 --- a/app/src/main/java/awais/instagrabber/utils/Constants.java +++ b/app/src/main/java/awais/instagrabber/utils/Constants.java @@ -89,4 +89,5 @@ public final class Constants { public static final String SHARED_PREFERENCES_NAME = "settings"; public static final String PREF_POSTS_LAYOUT = "posts_layout"; public static final String PREF_PROFILE_POSTS_LAYOUT = "profile_posts_layout"; + public static final String PREF_TOPIC_POSTS_LAYOUT = "topic_posts_layout"; } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java index f5879d76..75b13d29 100644 --- a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java @@ -99,7 +99,7 @@ public final class ResponseBodyUtils { if (Utils.logCollector != null) Utils.logCollector.appendException(e, LogCollector.LogFile.UTILS, "getLowQualityImage", new Pair<>("resourcesNull", resources == null)); - if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); + if (BuildConfig.DEBUG) Log.e(TAG, "Error in getLowQualityImage", e); } return src; } @@ -152,12 +152,24 @@ public final class ResponseBodyUtils { } public static String getVideoUrl(@NonNull final JSONObject mediaObj) { - String thumbnail = null; - final JSONArray imageVersions = mediaObj.optJSONArray("video_versions"); - if (imageVersions != null) { - thumbnail = getItemThumbnail(imageVersions).url; + String url = null; + final JSONArray videoVersions = mediaObj.optJSONArray("video_versions"); + if (videoVersions == null) { + return null; } - return thumbnail; + int largestWidth = 0; + for (int i = 0; i < videoVersions.length(); i++) { + final JSONObject videoVersionJson = videoVersions.optJSONObject(i); + if (videoVersionJson == null) { + continue; + } + final int width = videoVersionJson.optInt("width"); + if (largestWidth == 0 || width > largestWidth) { + largestWidth = width; + url = videoVersionJson.optString("url"); + } + } + return url; } @Nullable diff --git a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java index 4121edf0..52903987 100755 --- a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java +++ b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java @@ -32,6 +32,7 @@ import static awais.instagrabber.utils.Constants.PREF_DARK_THEME; import static awais.instagrabber.utils.Constants.PREF_LIGHT_THEME; import static awais.instagrabber.utils.Constants.PREF_POSTS_LAYOUT; import static awais.instagrabber.utils.Constants.PREF_PROFILE_POSTS_LAYOUT; +import static awais.instagrabber.utils.Constants.PREF_TOPIC_POSTS_LAYOUT; import static awais.instagrabber.utils.Constants.PREV_INSTALL_VERSION; import static awais.instagrabber.utils.Constants.SHOW_QUICK_ACCESS_DIALOG; import static awais.instagrabber.utils.Constants.SKIPPED_VERSION; @@ -115,7 +116,8 @@ public final class SettingsHelper { @StringDef( {APP_LANGUAGE, APP_THEME, COOKIE, FOLDER_PATH, DATE_TIME_FORMAT, DATE_TIME_SELECTION, CUSTOM_DATE_TIME_FORMAT, - DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME, PREF_POSTS_LAYOUT, PREF_PROFILE_POSTS_LAYOUT}) + DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME, PREF_POSTS_LAYOUT, PREF_PROFILE_POSTS_LAYOUT, + PREF_TOPIC_POSTS_LAYOUT}) public @interface StringSettings {} @StringDef({DOWNLOAD_USER_FOLDER, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, SHOW_QUICK_ACCESS_DIALOG, MUTED_VIDEOS, diff --git a/app/src/main/java/awais/instagrabber/viewmodels/TopicClusterViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/TopicClusterViewModel.java new file mode 100644 index 00000000..3e8a169f --- /dev/null +++ b/app/src/main/java/awais/instagrabber/viewmodels/TopicClusterViewModel.java @@ -0,0 +1,19 @@ +package awais.instagrabber.viewmodels; + +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import java.util.List; + +import awais.instagrabber.models.TopicCluster; + +public class TopicClusterViewModel extends ViewModel { + private MutableLiveData> list; + + public MutableLiveData> getList() { + if (list == null) { + list = new MutableLiveData<>(); + } + return list; + } +} diff --git a/app/src/main/java/awais/instagrabber/webservices/DiscoverService.java b/app/src/main/java/awais/instagrabber/webservices/DiscoverService.java new file mode 100644 index 00000000..107eb49c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/webservices/DiscoverService.java @@ -0,0 +1,501 @@ +package awais.instagrabber.webservices; + +import androidx.annotation.NonNull; + +import com.google.common.collect.ImmutableBiMap; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.PostChild; +import awais.instagrabber.models.ProfileModel; +import awais.instagrabber.models.TopicCluster; +import awais.instagrabber.models.enums.MediaItemType; +import awais.instagrabber.repositories.DiscoverRepository; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.ResponseBodyUtils; +import awais.instagrabber.utils.TextUtils; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; + +public class DiscoverService extends BaseService { + + private static final String TAG = "DiscoverService"; + + private final DiscoverRepository repository; + + private static DiscoverService instance; + + private DiscoverService() { + final Retrofit retrofit = getRetrofitBuilder() + .baseUrl("https://i.instagram.com") + .build(); + repository = retrofit.create(DiscoverRepository.class); + } + + public static DiscoverService getInstance() { + if (instance == null) { + instance = new DiscoverService(); + } + return instance; + } + + public void topicalExplore(@NonNull final TopicalExploreRequest request, + final ServiceCallback callback) { + final ImmutableBiMap.Builder builder = ImmutableBiMap.builder() + .put("module", "explore_popular"); + if (!TextUtils.isEmpty(request.getModule())) { + builder.put("module", request.getModule()); + } + if (!TextUtils.isEmpty(request.getClusterId())) { + builder.put("cluster_id", request.getClusterId()); + } + if (request.getMaxId() >= 0) { + builder.put("max_id", String.valueOf(request.getMaxId())); + } + final Call req = repository.topicalExplore(builder.build()); + req.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + if (callback == null) { + return; + } + final String body = response.body(); + if (TextUtils.isEmpty(body)) { + callback.onSuccess(null); + return; + } + try { + final TopicalExploreResponse topicalExploreResponse = parseTopicalExploreResponse(body); + callback.onSuccess(topicalExploreResponse); + } catch (JSONException e) { + callback.onFailure(e); + // Log.e(TAG, "Error parsing topicalExplore response", e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + callback.onFailure(t); + } + }); + } + + private TopicalExploreResponse parseTopicalExploreResponse(@NonNull final String body) throws JSONException { + final JSONObject root = new JSONObject(body); + final boolean moreAvailable = root.optBoolean("more_available"); + final int nextMaxId = root.optInt("next_max_id", -1); + final int numResults = root.optInt("num_results"); + final String status = root.optString("status"); + final JSONArray clustersJson = root.optJSONArray("clusters"); + final List clusters = parseClusters(clustersJson); + final JSONArray itemsJson = root.optJSONArray("items"); + final List items = parseItems(itemsJson); + return new TopicalExploreResponse( + moreAvailable, + nextMaxId, + numResults, + status, + clusters, + items + ); + } + + private List parseClusters(final JSONArray clustersJson) throws JSONException { + if (clustersJson == null) { + return Collections.emptyList(); + } + final List clusters = new ArrayList<>(); + for (int i = 0; i < clustersJson.length(); i++) { + final JSONObject clusterJson = clustersJson.getJSONObject(i); + final String id = clusterJson.optString("id"); + final String title = clusterJson.optString("title"); + if (id == null || title == null) { + continue; + } + final String type = clusterJson.optString("type"); + final boolean canMute = clusterJson.optBoolean("can_mute"); + final boolean isMuted = clusterJson.optBoolean("is_muted"); + final JSONObject coverMediaJson = clusterJson.optJSONObject("cover_media"); + final int rankedPosition = clusterJson.optInt("ranked_position"); + final FeedModel feedModel = parseClusterCover(coverMediaJson); + final TopicCluster topicCluster = new TopicCluster( + id, + title, + type, + canMute, + isMuted, + rankedPosition, + feedModel + ); + clusters.add(topicCluster); + } + return clusters; + } + + private FeedModel parseClusterCover(final JSONObject coverMediaJson) throws JSONException { + if (coverMediaJson == null) { + return null; + } + ProfileModel profileModel = null; + if (coverMediaJson.has("user")) { + final JSONObject user = coverMediaJson.getJSONObject("user"); + profileModel = new ProfileModel( + user.optBoolean("is_private"), + false, + user.optBoolean("is_verified"), + user.getString("pk"), + user.getString(Constants.EXTRAS_USERNAME), + user.optString("full_name"), + null, + null, + user.getString("profile_pic_url"), + null, + 0, + 0, + 0, + false, + false, + false, + false); + } + final String resourceUrl = ResponseBodyUtils.getHighQualityImage(coverMediaJson); + final String thumbnailUrl = ResponseBodyUtils.getLowQualityImage(coverMediaJson); + final int width = coverMediaJson.optInt("original_width"); + final int height = coverMediaJson.optInt("original_height"); + return new FeedModel.Builder() + .setProfileModel(profileModel) + .setItemType(MediaItemType.MEDIA_TYPE_IMAGE) + .setViewCount(0) + .setPostId(coverMediaJson.getString(Constants.EXTRAS_ID)) + .setDisplayUrl(resourceUrl) + .setThumbnailUrl(thumbnailUrl) + .setShortCode(coverMediaJson.getString("code")) + .setPostCaption(null) + .setCommentsCount(0) + .setTimestamp(coverMediaJson.optLong("taken_at", -1)) + .setLiked(false) + .setBookmarked(false) + .setLikesCount(0) + .setLocationName(null) + .setLocationId(null) + .setImageHeight(height) + .setImageWidth(width) + .build(); + } + + private List parseItems(final JSONArray items) throws JSONException { + if (items == null) { + return Collections.emptyList(); + } + final List feedModels = new ArrayList<>(); + for (int i = 0; i < items.length(); i++) { + final JSONObject itemJson = items.optJSONObject(i); + if (itemJson == null) { + continue; + } + final JSONObject mediaJson = itemJson.optJSONObject("media"); + final FeedModel feedModel = parseClusterItemMedia(mediaJson); + if (feedModel != null) { + feedModels.add(feedModel); + } + } + return feedModels; + } + + private FeedModel parseClusterItemMedia(final JSONObject mediaJson) throws JSONException { + if (mediaJson == null) { + return null; + } + ProfileModel profileModel = null; + if (mediaJson.has("user")) { + final JSONObject user = mediaJson.getJSONObject("user"); + final JSONObject friendshipStatus = user.optJSONObject("friendship_status"); + boolean following = false; + boolean restricted = false; + boolean requested = false; + if (friendshipStatus != null) { + following = friendshipStatus.optBoolean("following"); + requested = friendshipStatus.optBoolean("outgoing_request"); + restricted = friendshipStatus.optBoolean("is_restricted"); + } + profileModel = new ProfileModel( + user.optBoolean("is_private"), + false, // if you can see it then you def follow + user.optBoolean("is_verified"), + user.getString("pk"), + user.getString(Constants.EXTRAS_USERNAME), + user.optString("full_name"), + null, + null, + user.getString("profile_pic_url"), + null, + 0, + 0, + 0, + following, + restricted, + false, + requested); + } + final JSONObject captionJson = mediaJson.optJSONObject("caption"); + final JSONObject locationJson = mediaJson.optJSONObject("location"); + final MediaItemType mediaType = ResponseBodyUtils.getMediaItemType(mediaJson.optInt("media_type")); + final FeedModel.Builder feedModelBuilder = new FeedModel.Builder() + .setItemType(mediaType) + .setProfileModel(profileModel) + .setPostId(mediaJson.getString(Constants.EXTRAS_ID)) + .setThumbnailUrl(mediaType != MediaItemType.MEDIA_TYPE_SLIDER ? ResponseBodyUtils.getLowQualityImage(mediaJson) : null) + .setShortCode(mediaJson.getString("code")) + .setPostCaption(captionJson != null ? captionJson.optString("text") : null) + .setCommentsCount(mediaJson.optInt("comment_count")) + .setTimestamp(mediaJson.optLong("taken_at", -1)) + .setLiked(mediaJson.optBoolean("has_liked")) + // .setBookmarked() + .setLikesCount(mediaJson.optInt("like_count")) + .setLocationName(locationJson != null ? locationJson.optString("name") : null) + .setLocationId(locationJson != null ? String.valueOf(locationJson.optInt("pk")) : null) + .setImageHeight(mediaJson.optInt("original_height")) + .setImageWidth(mediaJson.optInt("original_width")); + switch (mediaType) { + case MEDIA_TYPE_VIDEO: + final long videoViews = mediaJson.optLong("view_count", 0); + feedModelBuilder.setViewCount(videoViews) + .setDisplayUrl(ResponseBodyUtils.getVideoUrl(mediaJson)); + break; + case MEDIA_TYPE_IMAGE: + feedModelBuilder.setDisplayUrl(ResponseBodyUtils.getHighQualityImage(mediaJson)); + break; + case MEDIA_TYPE_SLIDER: + final List childPosts = getChildPosts(mediaJson); + feedModelBuilder.setSliderItems(childPosts); + break; + } + return feedModelBuilder.build(); + } + + private List getChildPosts(final JSONObject mediaJson) throws JSONException { + if (mediaJson == null) { + return Collections.emptyList(); + } + final JSONArray carouselMedia = mediaJson.optJSONArray("carousel_media"); + if (carouselMedia == null) { + return Collections.emptyList(); + } + final List children = new ArrayList<>(); + for (int i = 0; i < carouselMedia.length(); i++) { + final JSONObject childJson = carouselMedia.optJSONObject(i); + final PostChild childPost = getChildPost(childJson); + if (childPost != null) { + children.add(childPost); + } + } + return children; + } + + private PostChild getChildPost(final JSONObject childJson) throws JSONException { + if (childJson == null) { + return null; + } + final MediaItemType mediaType = ResponseBodyUtils.getMediaItemType(childJson.optInt("media_type")); + final PostChild.Builder builder = new PostChild.Builder(); + switch (mediaType) { + case MEDIA_TYPE_VIDEO: + builder.setDisplayUrl(ResponseBodyUtils.getVideoUrl(childJson)); + break; + case MEDIA_TYPE_IMAGE: + builder.setDisplayUrl(ResponseBodyUtils.getHighQualityImage(childJson)); + break; + } + return builder.setItemType(mediaType) + .setPostId(childJson.getString("id")) + .setThumbnailUrl(ResponseBodyUtils.getLowQualityImage(childJson)) + .setHeight(childJson.optInt("original_height")) + .setWidth(childJson.optInt("original_width")) + .build(); + } + + public static class TopicalExploreRequest { + + private String module; + private String clusterId; + private int maxId = -1; + + public TopicalExploreRequest() {} + + public TopicalExploreRequest(final String module, final String clusterId, final int maxId) { + this.module = module; + this.clusterId = clusterId; + this.maxId = maxId; + } + + public String getModule() { + return module; + } + + public TopicalExploreRequest setModule(final String module) { + this.module = module; + return this; + } + + public String getClusterId() { + return clusterId; + } + + public TopicalExploreRequest setClusterId(final String clusterId) { + this.clusterId = clusterId; + return this; + } + + public int getMaxId() { + return maxId; + } + + public TopicalExploreRequest setMaxId(final int maxId) { + this.maxId = maxId; + return this; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final TopicalExploreRequest that = (TopicalExploreRequest) o; + return maxId == that.maxId && + Objects.equals(module, that.module) && + Objects.equals(clusterId, that.clusterId); + } + + @Override + public int hashCode() { + return Objects.hash(module, clusterId, maxId); + } + + @Override + public String toString() { + return "TopicalExploreRequest{" + + "module='" + module + '\'' + + ", clusterId='" + clusterId + '\'' + + ", maxId=" + maxId + + '}'; + } + } + + public static class TopicalExploreResponse { + + private boolean moreAvailable; + private int nextMaxId; + private int numResults; + private String status; + private List clusters; + private List items; + + public TopicalExploreResponse() {} + + public TopicalExploreResponse(final boolean moreAvailable, + final int nextMaxId, + final int numResults, + final String status, + final List clusters, final List items) { + this.moreAvailable = moreAvailable; + this.nextMaxId = nextMaxId; + this.numResults = numResults; + this.status = status; + this.clusters = clusters; + this.items = items; + } + + public boolean isMoreAvailable() { + return moreAvailable; + } + + public TopicalExploreResponse setMoreAvailable(final boolean moreAvailable) { + this.moreAvailable = moreAvailable; + return this; + } + + public int getNextMaxId() { + return nextMaxId; + } + + public TopicalExploreResponse setNextMaxId(final int nextMaxId) { + this.nextMaxId = nextMaxId; + return this; + } + + public int getNumResults() { + return numResults; + } + + public TopicalExploreResponse setNumResults(final int numResults) { + this.numResults = numResults; + return this; + } + + public String getStatus() { + return status; + } + + public TopicalExploreResponse setStatus(final String status) { + this.status = status; + return this; + } + + public List getClusters() { + return clusters; + } + + public TopicalExploreResponse setClusters(final List clusters) { + this.clusters = clusters; + return this; + } + + public List getItems() { + return items; + } + + public TopicalExploreResponse setItems(final List items) { + this.items = items; + return this; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final TopicalExploreResponse that = (TopicalExploreResponse) o; + return moreAvailable == that.moreAvailable && + nextMaxId == that.nextMaxId && + numResults == that.numResults && + Objects.equals(status, that.status) && + Objects.equals(clusters, that.clusters) && + Objects.equals(items, that.items); + } + + @Override + public int hashCode() { + return Objects.hash(moreAvailable, nextMaxId, numResults, status, clusters, items); + } + + @Override + public String toString() { + return "TopicalExploreResponse{" + + "moreAvailable=" + moreAvailable + + ", nextMaxId=" + nextMaxId + + ", numResults=" + numResults + + ", status='" + status + '\'' + + ", clusters=" + clusters + + ", items=" + items + + '}'; + } + } +} diff --git a/app/src/main/res/layout/fragment_discover.xml b/app/src/main/res/layout/fragment_discover.xml index 24a6a326..ae5b93dd 100644 --- a/app/src/main/res/layout/fragment_discover.xml +++ b/app/src/main/res/layout/fragment_discover.xml @@ -7,37 +7,39 @@ android:animateLayoutChanges="true" android:background="?attr/colorSurface"> - + + + + + - + + + + - - - + + + + + + + + + app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" + app:spanCount="2" + tools:itemCount="10" + tools:listitem="@layout/item_discover_topic" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml index 12b71350..cdc5993d 100644 --- a/app/src/main/res/layout/fragment_profile.xml +++ b/app/src/main/res/layout/fragment_profile.xml @@ -285,20 +285,6 @@ tools:listitem="@layout/item_feed_photo" /> - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_discover_topic.xml b/app/src/main/res/layout/item_discover_topic.xml new file mode 100644 index 00000000..51c3831c --- /dev/null +++ b/app/src/main/res/layout/item_discover_topic.xml @@ -0,0 +1,45 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/topic_posts_menu.xml b/app/src/main/res/menu/topic_posts_menu.xml new file mode 100644 index 00000000..1ced72b6 --- /dev/null +++ b/app/src/main/res/menu/topic_posts_menu.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/discover_nav_graph.xml b/app/src/main/res/navigation/discover_nav_graph.xml index 82e87b5e..00894c28 100644 --- a/app/src/main/res/navigation/discover_nav_graph.xml +++ b/app/src/main/res/navigation/discover_nav_graph.xml @@ -5,9 +5,40 @@ android:id="@+id/discover_nav_graph" app:startDestination="@id/discoverFragment"> - + + + + + + - + + + + + + + + + + + + + + + + + + + + + tools:layout="@layout/fragment_discover"> + + + + + + + + + \ No newline at end of file