From 0a67e859e0fcbeea32411fe5d48b0581665dda05 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Tue, 27 Oct 2020 20:33:21 +0900 Subject: [PATCH] Add PostsRecyclerView to ProfileFragment --- .../asyncs/FeedPostFetchService.java | 6 +- .../asyncs/ProfilePostFetchService.java | 57 ++ .../customviews/PostsRecyclerView.java | 20 +- .../PostsLayoutPreferencesDialogFragment.java | 13 +- .../fragments/PostViewV2Fragment.java | 11 +- .../fragments/main/FeedFragment.java | 2 +- .../fragments/main/ProfileFragment.java | 517 +++++++++++------- .../models/PostsLayoutPreferences.java | 9 +- .../repositories/ProfileRepository.java | 6 + ...hResponse.java => PostsFetchResponse.java} | 4 +- .../awais/instagrabber/utils/Constants.java | 1 + .../instagrabber/utils/SettingsHelper.java | 3 +- .../instagrabber/webservices/FeedService.java | 17 +- .../webservices/ProfileService.java | 216 ++++++++ app/src/main/res/layout/fragment_feed.xml | 6 - app/src/main/res/layout/fragment_profile.xml | 24 +- app/src/main/res/menu/feed_menu.xml | 2 +- app/src/main/res/menu/profile_menu.xml | 12 +- app/src/main/res/values/strings.xml | 1 + 19 files changed, 662 insertions(+), 265 deletions(-) create mode 100644 app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java rename app/src/main/java/awais/instagrabber/repositories/responses/{FeedFetchResponse.java => PostsFetchResponse.java} (78%) diff --git a/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java index 5236a120..a2b2778c 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java +++ b/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java @@ -5,7 +5,7 @@ import java.util.List; import awais.instagrabber.customviews.helpers.PostFetcher; import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.models.FeedModel; -import awais.instagrabber.repositories.responses.FeedFetchResponse; +import awais.instagrabber.repositories.responses.PostsFetchResponse; import awais.instagrabber.webservices.FeedService; import awais.instagrabber.webservices.ServiceCallback; @@ -21,9 +21,9 @@ public class FeedPostFetchService implements PostFetcher.PostFetchService { @Override public void fetch(final String cursor, final FetchListener> fetchListener) { - feedService.fetch(25, cursor, new ServiceCallback() { + feedService.fetch(25, cursor, new ServiceCallback() { @Override - public void onSuccess(final FeedFetchResponse result) { + public void onSuccess(final PostsFetchResponse result) { if (result == null) return; nextCursor = result.getNextCursor(); hasNextPage = result.hasNextPage(); diff --git a/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java new file mode 100644 index 00000000..251b7ebf --- /dev/null +++ b/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java @@ -0,0 +1,57 @@ +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.models.ProfileModel; +import awais.instagrabber.repositories.responses.PostsFetchResponse; +import awais.instagrabber.webservices.ProfileService; +import awais.instagrabber.webservices.ServiceCallback; + +public class ProfilePostFetchService implements PostFetcher.PostFetchService { + private static final String TAG = "ProfilePostFetchService"; + private final ProfileService profileService; + private final ProfileModel profileModel; + private String nextCursor; + private boolean hasNextPage; + + public ProfilePostFetchService(final ProfileModel profileModel) { + this.profileModel = profileModel; + profileService = ProfileService.getInstance(); + } + + @Override + public void fetch(final String cursor, final FetchListener> fetchListener) { + profileService.fetchPosts(profileModel, 30, cursor, new ServiceCallback() { + @Override + public void onSuccess(final PostsFetchResponse result) { + if (result == null) return; + nextCursor = result.getNextCursor(); + hasNextPage = result.hasNextPage(); + if (fetchListener != null) { + fetchListener.onResult(result.getFeedModels()); + } + } + + @Override + public void onFailure(final Throwable t) { + // Log.e(TAG, "onFailure: ", t); + if (fetchListener != null) { + fetchListener.onFailure(t); + } + } + }); + } + + @Override + public String getNextCursor() { + return nextCursor; + } + + @Override + public boolean hasNextPage() { + return hasNextPage; + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java b/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java index 3895b27c..953167b0 100644 --- a/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java +++ b/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java @@ -25,7 +25,6 @@ import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtBottom; import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.PostsLayoutPreferences; -import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.FeedViewModel; @@ -130,16 +129,8 @@ public class PostsRecyclerView extends RecyclerView { throw new IllegalArgumentException("PostFetchService cannot be null"); } if (layoutPreferences == null) { - layoutPreferences = PostsLayoutPreferences.builder() - .setType(PostsLayoutPreferences.PostsLayoutType.GRID) - .setColCount(3) - .setAvatarVisible(true) - .setNameVisible(false) - .setProfilePicSize(PostsLayoutPreferences.ProfilePicSize.TINY) - .setHasGap(true) - .setHasRoundedCorners(true) - .build(); - Utils.settingsHelper.putString(Constants.PREF_POSTS_LAYOUT, layoutPreferences.getJson()); + layoutPreferences = PostsLayoutPreferences.builder().build(); + // Utils.settingsHelper.putString(Constants.PREF_POSTS_LAYOUT, layoutPreferences.getJson()); } gridSpacingItemDecoration = new GridSpacingItemDecoration(Utils.convertDpToPx(2)); initTransition(); @@ -156,9 +147,6 @@ public class PostsRecyclerView extends RecyclerView { private void initLayoutManager() { layoutManager = new StaggeredGridLayoutManager(layoutPreferences.getColCount(), StaggeredGridLayoutManager.VERTICAL); - if (layoutPreferences.getHasGap()) { - addItemDecoration(gridSpacingItemDecoration); - } setLayoutManager(layoutManager); } @@ -172,7 +160,9 @@ public class PostsRecyclerView extends RecyclerView { feedViewModel = new ViewModelProvider(viewModelStoreOwner).get(FeedViewModel.class); feedViewModel.getList().observe(lifeCycleOwner, feedAdapter::submitList); postFetcher = new PostFetcher(postFetchService, fetchListener); - addItemDecoration(gridSpacingItemDecoration); + if (layoutPreferences.getHasGap()) { + addItemDecoration(gridSpacingItemDecoration); + } setHasFixedSize(true); setNestedScrollingEnabled(true); lazyLoader = new RecyclerLazyLoaderAtBottom(layoutManager, (page) -> { diff --git a/app/src/main/java/awais/instagrabber/dialogs/PostsLayoutPreferencesDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/PostsLayoutPreferencesDialogFragment.java index bee6256b..9e35ae54 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/PostsLayoutPreferencesDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/PostsLayoutPreferencesDialogFragment.java @@ -16,20 +16,21 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import awais.instagrabber.R; import awais.instagrabber.databinding.DialogPostLayoutPreferencesBinding; import awais.instagrabber.models.PostsLayoutPreferences; -import awais.instagrabber.utils.Constants; import static awais.instagrabber.utils.Utils.settingsHelper; public class PostsLayoutPreferencesDialogFragment extends DialogFragment { - private final PostsLayoutPreferences.Builder preferencesBuilder; - @NonNull private final OnApplyListener onApplyListener; + private final PostsLayoutPreferences.Builder preferencesBuilder; + private final String layoutPreferenceKey; private DialogPostLayoutPreferencesBinding binding; private Context context; - public PostsLayoutPreferencesDialogFragment(@NonNull final OnApplyListener onApplyListener) { - final PostsLayoutPreferences preferences = PostsLayoutPreferences.fromJson(settingsHelper.getString(Constants.PREF_POSTS_LAYOUT)); + public PostsLayoutPreferencesDialogFragment(final String layoutPreferenceKey, + @NonNull final OnApplyListener onApplyListener) { + this.layoutPreferenceKey = layoutPreferenceKey; + final PostsLayoutPreferences preferences = PostsLayoutPreferences.fromJson(settingsHelper.getString(layoutPreferenceKey)); this.preferencesBuilder = PostsLayoutPreferences.builder().mergeFrom(preferences); this.onApplyListener = onApplyListener; } @@ -50,7 +51,7 @@ public class PostsLayoutPreferencesDialogFragment extends DialogFragment { .setPositiveButton(R.string.apply, (dialog, which) -> { final PostsLayoutPreferences preferences = preferencesBuilder.build(); final String json = preferences.getJson(); - settingsHelper.putString(Constants.PREF_POSTS_LAYOUT, json); + settingsHelper.putString(layoutPreferenceKey, json); onApplyListener.onApply(preferences); }) .create(); diff --git a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java index 7ef0d79b..992aa4b3 100644 --- a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java @@ -417,7 +417,9 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { public void onPause() { super.onPause(); wasPaused = true; - captionState = bottomSheetBehavior.getState(); + if (bottomSheetBehavior != null) { + captionState = bottomSheetBehavior.getState(); + } } @Override @@ -494,7 +496,9 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { binding.postImage.setLayoutParams(new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); binding.postImage.requestLayout(); - bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + if (bottomSheetBehavior != null) { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } return; } if (destView == binding.sliderParent) { @@ -778,6 +782,9 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { private void setupCaption() { final CharSequence postCaption = feedModel.getPostCaption(); + if (TextUtils.isEmpty(postCaption)) { + return; + } binding.caption.addOnHashtagListener(autoLinkItem -> { final NavController navController = NavHostFragment.findNavController(this); final Bundle bundle = new Bundle(); 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 16b75d2b..7f414633 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java @@ -331,7 +331,7 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre } private void showPostsLayoutPreferences() { - final PostsLayoutPreferencesDialogFragment fragment = new PostsLayoutPreferencesDialogFragment(preferences -> new Handler() + 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/fragments/main/ProfileFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java index d5fd5b61..0296eb6c 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -24,16 +24,17 @@ import android.widget.TextView; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; -import androidx.activity.OnBackPressedDispatcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.content.PermissionChecker; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavController; import androidx.navigation.NavDirections; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; @@ -46,7 +47,6 @@ import com.facebook.imagepipeline.image.ImageInfo; import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; -import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; @@ -54,24 +54,25 @@ import java.util.List; import awais.instagrabber.ProfileNavGraphDirections; import awais.instagrabber.R; import awais.instagrabber.activities.MainActivity; +import awais.instagrabber.adapters.FeedAdapterV2; import awais.instagrabber.adapters.HighlightsAdapter; import awais.instagrabber.adapters.PostsAdapter; import awais.instagrabber.asyncs.HighlightsFetcher; -import awais.instagrabber.asyncs.PostsFetcher; import awais.instagrabber.asyncs.ProfileFetcher; +import awais.instagrabber.asyncs.ProfilePostFetchService; import awais.instagrabber.asyncs.UsernameFetcher; import awais.instagrabber.asyncs.direct_messages.CreateThreadAction; import awais.instagrabber.customviews.PrimaryActionModeCallback; import awais.instagrabber.customviews.PrimaryActionModeCallback.CallbacksHelper; -import awais.instagrabber.customviews.helpers.GridAutofitLayoutManager; -import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration; import awais.instagrabber.customviews.helpers.NestedCoordinatorLayout; -import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; import awais.instagrabber.databinding.FragmentProfileBinding; +import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; import awais.instagrabber.dialogs.ProfilePicDialogFragment; +import awais.instagrabber.fragments.PostViewV2Fragment; import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.interfaces.MentionClickListener; -import awais.instagrabber.models.PostModel; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.PostsLayoutPreferences; import awais.instagrabber.models.ProfileModel; import awais.instagrabber.models.StoryModel; import awais.instagrabber.models.enums.DownloadMethod; @@ -86,17 +87,17 @@ import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.HighlightsViewModel; -import awais.instagrabber.viewmodels.PostsViewModel; import awais.instagrabber.webservices.FriendshipService; import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.StoriesService; -import awaisomereport.LogCollector; -import static awais.instagrabber.utils.Utils.logCollector; +import static androidx.core.content.PermissionChecker.checkSelfPermission; +import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; import static awais.instagrabber.utils.Utils.settingsHelper; public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { private static final String TAG = "ProfileFragment"; + private static final int STORAGE_PERM_REQUEST_CODE = 8020; private MainActivity fragmentActivity; private CoordinatorLayout root; @@ -105,21 +106,18 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe private String cookie; private String username; private ProfileModel profileModel; - private PostsViewModel postsViewModel; private PostsAdapter postsAdapter; private ActionMode actionMode; private Handler usernameSettingHandler; private FriendshipService friendshipService; private StoriesService storiesService; - private boolean shouldRefresh = true, hasStories = false; - private boolean hasNextPage; - private String endCursor; - private AsyncTask> currentlyExecuting; - private boolean isPullToRefresh; + private boolean shouldRefresh = true; + private boolean hasStories = false; private HighlightsAdapter highlightsAdapter; private HighlightsViewModel highlightsViewModel; private MenuItem blockMenuItem; private MenuItem restrictMenuItem; + private boolean highlightsFetching; private final Runnable usernameSettingRunnable = () -> { final ActionBar actionBar = fragmentActivity.getSupportActionBar(); @@ -165,36 +163,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe return false; } }); - private final FetchListener> postsFetchListener = new FetchListener>() { - @Override - public void onResult(final List result) { - binding.swipeRefreshLayout.setRefreshing(false); - if (result == null || result.isEmpty()) { - binding.privatePage1.setImageResource(R.drawable.ic_cancel); - binding.privatePage2.setText(R.string.empty_acc); - binding.privatePage.setVisibility(View.VISIBLE); - return; - } else { - binding.privatePage.setVisibility(View.GONE); - } - binding.mainPosts.post(() -> binding.mainPosts.setVisibility(View.VISIBLE)); - final List postModels = postsViewModel.getList().getValue(); - List finalList = postModels == null || postModels.isEmpty() ? new ArrayList<>() - : new ArrayList<>(postModels); - if (isPullToRefresh) { - finalList = result; - isPullToRefresh = false; - } else { - finalList.addAll(result); - } - postsViewModel.getList().postValue(finalList); - final PostModel lastPostModel = result.get(result.size() - 1); - if (lastPostModel == null) return; - endCursor = lastPostModel.getEndCursor(); - hasNextPage = lastPostModel.hasNextPage(); - lastPostModel.setPageCursor(false, null); - } - }; private final MentionClickListener mentionClickListener = (view, text, isHashtag, isLocation) -> { Log.d(TAG, "action..."); if (isHashtag) { @@ -213,6 +181,92 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe action.setUsername("@" + text); NavHostFragment.findNavController(this).navigate(action); }; + 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 = FeedFragmentDirections.actionGlobalCommentsViewerFragment( + feedModel.getShortCode(), + feedModel.getPostId(), + feedModel.getProfileModel().getId() + ); + NavHostFragment.findNavController(ProfileFragment.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 = FeedFragmentDirections.actionGlobalHashTagFragment(hashtag); + NavHostFragment.findNavController(ProfileFragment.this).navigate(action); + } + + @Override + public void onLocationClick(final FeedModel feedModel) { + final NavDirections action = FeedFragmentDirections.actionGlobalLocationFragment(feedModel.getLocationId()); + NavHostFragment.findNavController(ProfileFragment.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"); + } + }; + private boolean postsSetupDone = false; @Override public void onCreate(@Nullable final Bundle savedInstanceState) { @@ -267,7 +321,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe @Override public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { inflater.inflate(R.menu.profile_menu, menu); - // favMenuItem = menu.findItem(R.id.favourites); blockMenuItem = menu.findItem(R.id.block); if (blockMenuItem != null) { blockMenuItem.setVisible(false); @@ -280,6 +333,10 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe @Override public boolean onOptionsItemSelected(@NonNull final MenuItem item) { + if (item.getItemId() == R.id.layout) { + showPostsLayoutPreferences(); + return true; + } if (item.getItemId() == R.id.restrict) { if (!isLoggedIn) return false; final String action = profileModel.getRestricted() ? "Unrestrict" : "Restrict"; @@ -346,10 +403,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe @Override public void onRefresh() { - isPullToRefresh = true; - endCursor = null; fetchProfileDetails(); - fetchPosts(); } @Override @@ -359,9 +413,9 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe if (usernameSettingHandler != null) { usernameSettingHandler.removeCallbacks(usernameSettingRunnable); } - if (postsViewModel != null) { - postsViewModel.getList().postValue(Collections.emptyList()); - } + // if (postsViewModel != null) { + // postsViewModel.getList().postValue(Collections.emptyList()); + // } if (highlightsViewModel != null) { highlightsViewModel.getList().postValue(Collections.emptyList()); } @@ -385,7 +439,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe return; } binding.swipeRefreshLayout.setEnabled(true); - setupPosts(); setupHighlights(); setupCommonListeners(); fetchUsername(); @@ -444,87 +497,18 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe Toast.makeText(context, R.string.error_loading_profile, Toast.LENGTH_SHORT).show(); return; } + if (!postsSetupDone) { + setupPosts(); + } else { + binding.postsRecyclerView.refresh(); + } binding.isVerified.setVisibility(profileModel.isVerified() ? View.VISIBLE : View.GONE); final String profileId = profileModel.getId(); - final String myId = CookieUtils.getUserIdFromCookie(cookie); if (isLoggedIn) { - storiesService.getUserStory(profileId, - profileModel.getUsername(), - false, - false, - false, - new ServiceCallback>() { - @Override - public void onSuccess(final List storyModels) { - if (storyModels != null && !storyModels.isEmpty()) { - binding.mainProfileImage.setStoriesBorder(); - hasStories = true; - } - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error", t); - } - }); - new HighlightsFetcher(profileId, - result -> { - if (result != null) { - binding.highlightsList.setVisibility(View.VISIBLE); - highlightsViewModel.getList().postValue(result); - } else binding.highlightsList.setVisibility(View.GONE); - }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - if (profileId.equals(myId)) { - binding.btnTagged.setVisibility(View.VISIBLE); - binding.btnSaved.setVisibility(View.VISIBLE); - binding.btnLiked.setVisibility(View.VISIBLE); - binding.btnDM.setVisibility(View.GONE); - binding.btnSaved.setText(R.string.saved); - } else { - binding.btnTagged.setVisibility(View.GONE); - binding.btnSaved.setVisibility(View.GONE); - binding.btnLiked.setVisibility(View.GONE); - binding.btnDM.setVisibility(View.VISIBLE); // maybe there is a judgment mechanism? - binding.btnFollow.setVisibility(View.VISIBLE); - if (profileModel.getFollowing()) { - binding.btnFollow.setText(R.string.unfollow); - binding.btnFollow.setIconResource(R.drawable.ic_outline_person_add_disabled_24); - } else if (profileModel.getRequested()) { - binding.btnFollow.setText(R.string.cancel); - binding.btnFollow.setIconResource(R.drawable.ic_outline_person_add_disabled_24); - } else { - binding.btnFollow.setText(R.string.follow); - binding.btnFollow.setIconResource(R.drawable.ic_outline_person_add_24); - } - if (restrictMenuItem != null) { - restrictMenuItem.setVisible(true); - if (profileModel.getRestricted()) { - restrictMenuItem.setTitle(R.string.unrestrict); - } else { - restrictMenuItem.setTitle(R.string.restrict); - } - } - binding.btnTagged.setVisibility(profileModel.isReallyPrivate() ? View.GONE : View.VISIBLE); - if (blockMenuItem != null) { - blockMenuItem.setVisible(true); - if (profileModel.getBlocked()) { - blockMenuItem.setTitle(R.string.unblock); - } else { - blockMenuItem.setTitle(R.string.block); - } - } - } - } else { - if (!profileModel.isReallyPrivate() && restrictMenuItem != null) { - restrictMenuItem.setVisible(true); - if (profileModel.getRestricted()) { - restrictMenuItem.setTitle(R.string.unrestrict); - } else { - restrictMenuItem.setTitle(R.string.restrict); - } - } + fetchStoryAndHighlights(profileId); } + setupButtons(profileId, myId); if (!profileId.equals(myId)) { binding.favCb.setVisibility(View.VISIBLE); final boolean isFav = Utils.dataBox.getFavorite(username.substring(1), FavoriteType.USER) != null; @@ -572,8 +556,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe : profileModel.getName()); CharSequence biography = profileModel.getBiography(); - // binding.mainBiography.setCaptionIsExpandable(true); - // binding.mainBiography.setCaptionIsExpanded(true); if (TextUtils.hasMentions(biography)) { biography = TextUtils.getMentionText(biography); binding.mainBiography.setText(biography, TextView.BufferType.SPANNABLE); @@ -618,7 +600,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe } binding.swipeRefreshLayout.setRefreshing(true); - binding.mainPosts.setVisibility(View.VISIBLE); + binding.postsRecyclerView.setVisibility(View.VISIBLE); fetchPosts(); } else { binding.mainFollowers.setClickable(false); @@ -628,10 +610,94 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe binding.privatePage1.setImageResource(R.drawable.lock); binding.privatePage2.setText(R.string.priv_acc); binding.privatePage.setVisibility(View.VISIBLE); - binding.mainPosts.setVisibility(View.GONE); + binding.postsRecyclerView.setVisibility(View.GONE); + } + } + + private void setupButtons(final String profileId, final String myId) { + if (isLoggedIn) { + if (profileId.equals(myId)) { + binding.btnTagged.setVisibility(View.VISIBLE); + binding.btnSaved.setVisibility(View.VISIBLE); + binding.btnLiked.setVisibility(View.VISIBLE); + binding.btnDM.setVisibility(View.GONE); + binding.btnSaved.setText(R.string.saved); + return; + } + binding.btnTagged.setVisibility(View.GONE); + binding.btnSaved.setVisibility(View.GONE); + binding.btnLiked.setVisibility(View.GONE); + binding.btnDM.setVisibility(View.VISIBLE); // maybe there is a judgment mechanism? + binding.btnFollow.setVisibility(View.VISIBLE); + if (profileModel.getFollowing()) { + binding.btnFollow.setText(R.string.unfollow); + binding.btnFollow.setIconResource(R.drawable.ic_outline_person_add_disabled_24); + } else if (profileModel.getRequested()) { + binding.btnFollow.setText(R.string.cancel); + binding.btnFollow.setIconResource(R.drawable.ic_outline_person_add_disabled_24); + } else { + binding.btnFollow.setText(R.string.follow); + binding.btnFollow.setIconResource(R.drawable.ic_outline_person_add_24); + } + if (restrictMenuItem != null) { + restrictMenuItem.setVisible(true); + if (profileModel.getRestricted()) { + restrictMenuItem.setTitle(R.string.unrestrict); + } else { + restrictMenuItem.setTitle(R.string.restrict); + } + } + binding.btnTagged.setVisibility(profileModel.isReallyPrivate() ? View.GONE : View.VISIBLE); + if (blockMenuItem != null) { + blockMenuItem.setVisible(true); + if (profileModel.getBlocked()) { + blockMenuItem.setTitle(R.string.unblock); + } else { + blockMenuItem.setTitle(R.string.block); + } + } + return; + } + if (!profileModel.isReallyPrivate() && restrictMenuItem != null) { + restrictMenuItem.setVisible(true); + if (profileModel.getRestricted()) { + restrictMenuItem.setTitle(R.string.unrestrict); + } else { + restrictMenuItem.setTitle(R.string.restrict); + } } } + private void fetchStoryAndHighlights(final String profileId) { + storiesService.getUserStory(profileId, + profileModel.getUsername(), + false, + false, + false, + new ServiceCallback>() { + @Override + public void onSuccess(final List storyModels) { + if (storyModels != null && !storyModels.isEmpty()) { + binding.mainProfileImage.setStoriesBorder(); + hasStories = true; + } + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "Error", t); + } + }); + new HighlightsFetcher(profileId, + result -> { + highlightsFetching = false; + if (result != null) { + binding.highlightsList.setVisibility(View.VISIBLE); + highlightsViewModel.getList().postValue(result); + } else binding.highlightsList.setVisibility(View.GONE); + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + private void setupCommonListeners() { final String userIdFromCookie = CookieUtils.getUserIdFromCookie(cookie); @@ -781,62 +847,60 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe } private void setupPosts() { - postsViewModel = new ViewModelProvider(this).get(PostsViewModel.class); - final Context context = getContext(); - if (context == null) return; - final GridAutofitLayoutManager layoutManager = new GridAutofitLayoutManager(context, Utils.convertDpToPx(110)); - binding.mainPosts.setLayoutManager(layoutManager); - binding.mainPosts.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(4))); - postsAdapter = new PostsAdapter((postModel, position) -> { - if (postsAdapter.isSelecting()) { - if (actionMode == null) return; - final String title = getString(R.string.number_selected, - postsAdapter.getSelectedModels().size()); - actionMode.setTitle(title); - return; - } - if (checkAndResetAction()) return; - final List postModels = postsViewModel.getList().getValue(); - if (postModels == null || postModels.size() == 0) return; - if (postModels.get(0) == null) return; - final String postId = isLoggedIn ? postModels.get(0).getPostId() : postModels.get(0).getShortCode(); - final boolean isId = isLoggedIn && postId != null; - final String[] idsOrShortCodes = new String[postModels.size()]; - for (int i = 0; i < postModels.size(); i++) { - idsOrShortCodes[i] = isId ? postModels.get(i).getPostId() - : postModels.get(i).getShortCode(); - } - final NavDirections action = ProfileFragmentDirections.actionGlobalPostViewFragment( - position, - idsOrShortCodes, - isId); - NavHostFragment.findNavController(this).navigate(action); + binding.postsRecyclerView.setViewModelStoreOwner(this) + .setLifeCycleOwner(this) + .setPostFetchService(new ProfilePostFetchService(profileModel)) + .setLayoutPreferences(PostsLayoutPreferences.fromJson(settingsHelper.getString(Constants.PREF_PROFILE_POSTS_LAYOUT))) + .addFetchStatusChangeListener(fetching -> updateSwipeRefreshState()) + .setFeedItemCallback(feedItemCallback) + .init(); + binding.swipeRefreshLayout.setRefreshing(true); + postsSetupDone = true; + // postsAdapter = new PostsAdapter((postModel, position) -> { + // if (postsAdapter.isSelecting()) { + // if (actionMode == null) return; + // final String title = getString(R.string.number_selected, + // postsAdapter.getSelectedModels().size()); + // actionMode.setTitle(title); + // return; + // } + // if (checkAndResetAction()) return; + // final List postModels = postsViewModel.getList().getValue(); + // if (postModels == null || postModels.size() == 0) return; + // if (postModels.get(0) == null) return; + // final String postId = isLoggedIn ? postModels.get(0).getPostId() : postModels.get(0).getShortCode(); + // final boolean isId = isLoggedIn && postId != null; + // final String[] idsOrShortCodes = new String[postModels.size()]; + // for (int i = 0; i < postModels.size(); i++) { + // idsOrShortCodes[i] = isId ? postModels.get(i).getPostId() + // : postModels.get(i).getShortCode(); + // } + // final NavDirections action = ProfileFragmentDirections.actionGlobalPostViewFragment( + // position, + // idsOrShortCodes, + // isId); + // NavHostFragment.findNavController(this).navigate(action); + // + // }, (model, position) -> { + // if (!postsAdapter.isSelecting()) { + // checkAndResetAction(); + // return true; + // } + // if (onBackPressedCallback.isEnabled()) { + // return true; + // } + // final OnBackPressedDispatcher onBackPressedDispatcher = fragmentActivity.getOnBackPressedDispatcher(); + // onBackPressedCallback.setEnabled(true); + // actionMode = fragmentActivity.startActionMode(multiSelectAction); + // final String title = getString(R.string.number_selected, 1); + // actionMode.setTitle(title); + // onBackPressedDispatcher.addCallback(getViewLifecycleOwner(), onBackPressedCallback); + // return true; + // }); + } - }, (model, position) -> { - if (!postsAdapter.isSelecting()) { - checkAndResetAction(); - return true; - } - if (onBackPressedCallback.isEnabled()) { - return true; - } - final OnBackPressedDispatcher onBackPressedDispatcher = fragmentActivity.getOnBackPressedDispatcher(); - onBackPressedCallback.setEnabled(true); - actionMode = fragmentActivity.startActionMode(multiSelectAction); - final String title = getString(R.string.number_selected, 1); - actionMode.setTitle(title); - onBackPressedDispatcher.addCallback(getViewLifecycleOwner(), onBackPressedCallback); - return true; - }); - postsViewModel.getList().observe(fragmentActivity, postsAdapter::submitList); - binding.mainPosts.setAdapter(postsAdapter); - final RecyclerLazyLoader lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { - if (!hasNextPage) return; - binding.swipeRefreshLayout.setRefreshing(true); - fetchPosts(); - endCursor = null; - }); - binding.mainPosts.addOnScrollListener(lazyLoader); + private void updateSwipeRefreshState() { + binding.swipeRefreshLayout.setRefreshing(binding.postsRecyclerView.isFetching() || highlightsFetching); } private void setupHighlights() { @@ -855,22 +919,11 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe } private void fetchPosts() { - stopCurrentExecutor(); + // stopCurrentExecutor(); binding.swipeRefreshLayout.setRefreshing(true); - currentlyExecuting = new PostsFetcher(profileModel.getId(), PostItemType.MAIN, endCursor, postsFetchListener) - .setUsername(profileModel.getUsername()) - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - public void stopCurrentExecutor() { - if (currentlyExecuting != null) { - try { - currentlyExecuting.cancel(true); - } catch (final Exception e) { - if (logCollector != null) logCollector.appendException(e, LogCollector.LogFile.MAIN_HELPER, "stopCurrentExecutor"); - Log.e(TAG, "", e); - } - } + // currentlyExecuting = new PostsFetcher(profileModel.getId(), PostItemType.MAIN, endCursor, postsFetchListener) + // .setUsername(profileModel.getUsername()) + // .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private boolean checkAndResetAction() { @@ -887,4 +940,60 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe } return true; } + + 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 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 showPostsLayoutPreferences() { + final PostsLayoutPreferencesDialogFragment fragment = new PostsLayoutPreferencesDialogFragment( + Constants.PREF_PROFILE_POSTS_LAYOUT, + preferences -> new Handler().postDelayed(() -> binding.postsRecyclerView.setLayoutPreferences(preferences), 200)); + fragment.show(getChildFragmentManager(), "posts_layout_preferences"); + } } diff --git a/app/src/main/java/awais/instagrabber/models/PostsLayoutPreferences.java b/app/src/main/java/awais/instagrabber/models/PostsLayoutPreferences.java index eee16129..aae54fc3 100644 --- a/app/src/main/java/awais/instagrabber/models/PostsLayoutPreferences.java +++ b/app/src/main/java/awais/instagrabber/models/PostsLayoutPreferences.java @@ -15,10 +15,10 @@ public final class PostsLayoutPreferences { public static class Builder { private PostsLayoutType type = PostsLayoutType.GRID; - private int colCount = 2; - private boolean isAvatarVisible = false; + private int colCount = 3; + private boolean isAvatarVisible = true; private boolean isNameVisible = false; - private ProfilePicSize profilePicSize = ProfilePicSize.REGULAR; + private ProfilePicSize profilePicSize = ProfilePicSize.SMALL; private boolean hasRoundedCorners = true; private boolean hasGap = true; @@ -87,6 +87,9 @@ public final class PostsLayoutPreferences { } public Builder mergeFrom(final PostsLayoutPreferences preferences) { + if (preferences == null) { + return this; + } setColCount(preferences.getColCount()); setAvatarVisible(preferences.isAvatarVisible()); setNameVisible(preferences.isNameVisible()); diff --git a/app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java b/app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java index 62245dda..53ae0470 100644 --- a/app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java @@ -1,11 +1,17 @@ package awais.instagrabber.repositories; +import java.util.Map; + import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Path; +import retrofit2.http.QueryMap; public interface ProfileRepository { @GET("api/v1/users/{uid}/info/") Call getUserInfo(@Path("uid") final String uid); + + @GET("/graphql/query/") + Call fetch(@QueryMap Map queryMap); } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/FeedFetchResponse.java b/app/src/main/java/awais/instagrabber/repositories/responses/PostsFetchResponse.java similarity index 78% rename from app/src/main/java/awais/instagrabber/repositories/responses/FeedFetchResponse.java rename to app/src/main/java/awais/instagrabber/repositories/responses/PostsFetchResponse.java index f3aa0f50..56ce0570 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/FeedFetchResponse.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/PostsFetchResponse.java @@ -4,12 +4,12 @@ import java.util.List; import awais.instagrabber.models.FeedModel; -public class FeedFetchResponse { +public class PostsFetchResponse { private List feedModels; private boolean hasNextPage; private String nextCursor; - public FeedFetchResponse(final List feedModels, final boolean hasNextPage, final String nextCursor) { + public PostsFetchResponse(final List feedModels, final boolean hasNextPage, final String nextCursor) { this.feedModels = feedModels; this.hasNextPage = hasNextPage; this.nextCursor = nextCursor; diff --git a/app/src/main/java/awais/instagrabber/utils/Constants.java b/app/src/main/java/awais/instagrabber/utils/Constants.java index f32d7db2..e448802b 100644 --- a/app/src/main/java/awais/instagrabber/utils/Constants.java +++ b/app/src/main/java/awais/instagrabber/utils/Constants.java @@ -88,4 +88,5 @@ public final class Constants { public static final String DEFAULT_HASH_TAG_PIC = "https://www.instagram.com/static/images/hashtag/search-hashtag-default-avatar.png/1d8417c9a4f5.png"; 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"; } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java index cfcedd7b..4121edf0 100755 --- a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java +++ b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java @@ -31,6 +31,7 @@ import static awais.instagrabber.utils.Constants.MUTED_VIDEOS; 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.PREV_INSTALL_VERSION; import static awais.instagrabber.utils.Constants.SHOW_QUICK_ACCESS_DIALOG; import static awais.instagrabber.utils.Constants.SKIPPED_VERSION; @@ -114,7 +115,7 @@ 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}) + DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME, PREF_POSTS_LAYOUT, PREF_PROFILE_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/webservices/FeedService.java b/app/src/main/java/awais/instagrabber/webservices/FeedService.java index fca0b210..84b567a8 100644 --- a/app/src/main/java/awais/instagrabber/webservices/FeedService.java +++ b/app/src/main/java/awais/instagrabber/webservices/FeedService.java @@ -25,7 +25,7 @@ import awais.instagrabber.models.PostChild; import awais.instagrabber.models.ProfileModel; import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.repositories.FeedRepository; -import awais.instagrabber.repositories.responses.FeedFetchResponse; +import awais.instagrabber.repositories.responses.PostsFetchResponse; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.ResponseBodyUtils; import awais.instagrabber.utils.TextUtils; @@ -58,7 +58,7 @@ public class FeedService extends BaseService { public void fetch(final int maxItemsToLoad, final String cursor, - final ServiceCallback callback) { + final ServiceCallback callback) { if (loadFromMock) { final Handler handler = new Handler(); handler.postDelayed(() -> { @@ -96,9 +96,9 @@ public class FeedService extends BaseService { public void onResponse(@NonNull final Call call, @NonNull final Response response) { try { // Log.d(TAG, "onResponse: body: " + response.body()); - final FeedFetchResponse feedFetchResponse = parseResponse(response); + final PostsFetchResponse postsFetchResponse = parseResponse(response); if (callback != null) { - callback.onSuccess(feedFetchResponse); + callback.onSuccess(postsFetchResponse); } } catch (JSONException e) { Log.e(TAG, "onResponse", e); @@ -119,16 +119,16 @@ public class FeedService extends BaseService { } @NonNull - private FeedFetchResponse parseResponse(@NonNull final Response response) throws JSONException { + private PostsFetchResponse parseResponse(@NonNull final Response response) throws JSONException { if (TextUtils.isEmpty(response.body())) { Log.e(TAG, "parseResponse: feed response body is empty with status code: " + response.code()); - return new FeedFetchResponse(Collections.emptyList(), false, null); + return new PostsFetchResponse(Collections.emptyList(), false, null); } return parseResponseBody(response.body()); } @NonNull - private FeedFetchResponse parseResponseBody(@NonNull final String body) + private PostsFetchResponse parseResponseBody(@NonNull final String body) throws JSONException { final List feedModels = new ArrayList<>(); final JSONObject timelineFeed = new JSONObject(body) @@ -161,7 +161,6 @@ public class FeedService extends BaseService { final String displayUrl = feedItem.optString("display_url"); if (TextUtils.isEmpty(displayUrl)) continue; final String resourceUrl; - if (isVideo) { resourceUrl = feedItem.getString("video_url"); } else { @@ -265,7 +264,7 @@ public class FeedService extends BaseService { final FeedModel feedModel = feedModelBuilder.build(); feedModels.add(feedModel); } - return new FeedFetchResponse(feedModels, hasNextPage, endCursor); + return new PostsFetchResponse(feedModels, hasNextPage, endCursor); } @NonNull diff --git a/app/src/main/java/awais/instagrabber/webservices/ProfileService.java b/app/src/main/java/awais/instagrabber/webservices/ProfileService.java index 251442f1..38a4ce22 100644 --- a/app/src/main/java/awais/instagrabber/webservices/ProfileService.java +++ b/app/src/main/java/awais/instagrabber/webservices/ProfileService.java @@ -4,12 +4,26 @@ import android.util.Log; import androidx.annotation.NonNull; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.PostChild; +import awais.instagrabber.models.ProfileModel; +import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.repositories.ProfileRepository; +import awais.instagrabber.repositories.responses.PostsFetchResponse; import awais.instagrabber.repositories.responses.UserInfo; import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.ResponseBodyUtils; +import awais.instagrabber.utils.TextUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -19,6 +33,7 @@ public class ProfileService extends BaseService { private static final String TAG = "ProfileService"; private final ProfileRepository repository; + private final ProfileRepository wwwRepository; private static ProfileService instance; @@ -26,7 +41,11 @@ public class ProfileService extends BaseService { final Retrofit retrofit = getRetrofitBuilder() .baseUrl("https://i.instagram.com") .build(); + final Retrofit wwwRetrofit = getRetrofitBuilder() + .baseUrl("https://www.instagram.com") + .build(); repository = retrofit.create(ProfileRepository.class); + wwwRepository = wwwRetrofit.create(ProfileRepository.class); } public static ProfileService getInstance() { @@ -66,4 +85,201 @@ public class ProfileService extends BaseService { } }); } + + + public void fetchPosts(final ProfileModel profileModel, + final int postsPerPage, + final String cursor, + final ServiceCallback callback) { + final Map queryMap = new HashMap<>(); + queryMap.put("query_hash", "18a7b935ab438c4514b1f742d8fa07a7"); + queryMap.put("variables", "{" + + "\"id\":\"" + profileModel.getId() + "\"," + + "\"first\":" + postsPerPage + "," + + "\"after\":\"" + (cursor == null ? "" : cursor) + "\"" + + "}"); + final Call request = wwwRepository.fetch(queryMap); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + try { + // Log.d(TAG, "onResponse: body: " + response.body()); + final PostsFetchResponse postsFetchResponse = parseResponse(profileModel, response); + if (callback != null) { + callback.onSuccess(postsFetchResponse); + } + } catch (JSONException e) { + Log.e(TAG, "onResponse", e); + if (callback != null) { + callback.onFailure(e); + } + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + } + + private PostsFetchResponse parseResponse(final ProfileModel profileModel, final Response response) throws JSONException { + if (TextUtils.isEmpty(response.body())) { + Log.e(TAG, "parseResponse: feed response body is empty with status code: " + response.code()); + return new PostsFetchResponse(Collections.emptyList(), false, null); + } + return parseResponseBody(profileModel, response.body()); + } + + private PostsFetchResponse parseResponseBody(final ProfileModel profileModel, final String body) throws JSONException { + // Log.d(TAG, "parseResponseBody: body: " + body); + final List feedModels = new ArrayList<>(); + // return new FeedFetchResponse(feedModels, false, null); + final JSONObject mediaPosts = new JSONObject(body) + .getJSONObject("data") + .getJSONObject(Constants.EXTRAS_USER) + .getJSONObject("edge_owner_to_timeline_media"); + final String endCursor; + final boolean hasNextPage; + final JSONObject pageInfo = mediaPosts.getJSONObject("page_info"); + if (pageInfo.has("has_next_page")) { + hasNextPage = pageInfo.getBoolean("has_next_page"); + endCursor = hasNextPage ? pageInfo.getString("end_cursor") : null; + } else { + hasNextPage = false; + endCursor = null; + } + final JSONArray edges = mediaPosts.getJSONArray("edges"); + for (int i = 0; i < edges.length(); ++i) { + final JSONObject mediaNode = edges.getJSONObject(i).getJSONObject("node"); + final String mediaType = mediaNode.optString("__typename"); + if (mediaType.isEmpty() || "GraphSuggestedUserFeedUnit".equals(mediaType)) + continue; + final boolean isVideo = mediaNode.getBoolean("is_video"); + final long videoViews = mediaNode.optLong("video_view_count", 0); + + final String displayUrl = mediaNode.optString("display_url"); + if (TextUtils.isEmpty(displayUrl)) continue; + final String resourceUrl; + + if (isVideo) { + resourceUrl = mediaNode.getString("video_url"); + } else { + resourceUrl = mediaNode.has("display_resources") ? ResponseBodyUtils.getHighQualityImage(mediaNode) : displayUrl; + } + JSONObject tempJsonObject = mediaNode.optJSONObject("edge_media_preview_comment"); + final long commentsCount = tempJsonObject != null ? tempJsonObject.optLong("count") : 0; + tempJsonObject = mediaNode.optJSONObject("edge_media_preview_like"); + final long likesCount = tempJsonObject != null ? tempJsonObject.optLong("count") : 0; + tempJsonObject = mediaNode.optJSONObject("edge_media_to_caption"); + final JSONArray captions = tempJsonObject != null ? tempJsonObject.getJSONArray("edges") : null; + String captionText = null; + if (captions != null && captions.length() > 0) { + if ((tempJsonObject = captions.optJSONObject(0)) != null && + (tempJsonObject = tempJsonObject.optJSONObject("node")) != null) { + captionText = tempJsonObject.getString("text"); + } + } + final JSONObject location = mediaNode.optJSONObject("location"); + // Log.d(TAG, "location: " + (location == null ? null : location.toString())); + String locationId = null; + String locationName = null; + if (location != null) { + locationName = location.optString("name"); + if (location.has("id")) { + locationId = location.getString("id"); + } else if (location.has("pk")) { + locationId = location.getString("pk"); + } + // Log.d(TAG, "locationId: " + locationId); + } + int height = 0; + int width = 0; + final JSONObject dimensions = mediaNode.optJSONObject("dimensions"); + if (dimensions != null) { + height = dimensions.optInt("height"); + width = dimensions.optInt("width"); + } + String thumbnailUrl = null; + try { + thumbnailUrl = mediaNode.getJSONArray("display_resources") + .getJSONObject(0) + .getString("src"); + } catch (JSONException ignored) {} + final FeedModel.Builder builder = new FeedModel.Builder() + .setProfileModel(profileModel) + .setItemType(isVideo ? MediaItemType.MEDIA_TYPE_VIDEO + : MediaItemType.MEDIA_TYPE_IMAGE) + .setViewCount(videoViews) + .setPostId(mediaNode.getString(Constants.EXTRAS_ID)) + .setDisplayUrl(resourceUrl) + .setThumbnailUrl(thumbnailUrl != null ? thumbnailUrl : displayUrl) + .setShortCode(mediaNode.getString(Constants.EXTRAS_SHORTCODE)) + .setPostCaption(captionText) + .setCommentsCount(commentsCount) + .setTimestamp(mediaNode.optLong("taken_at_timestamp", -1)) + .setLiked(mediaNode.getBoolean("viewer_has_liked")) + .setBookmarked(mediaNode.getBoolean("viewer_has_saved")) + .setLikesCount(likesCount) + .setLocationName(locationName) + .setLocationId(locationId) + .setImageHeight(height) + .setImageWidth(width); + final boolean isSlider = "GraphSidecar".equals(mediaType) && mediaNode.has("edge_sidecar_to_children"); + if (isSlider) { + builder.setItemType(MediaItemType.MEDIA_TYPE_SLIDER); + final JSONObject sidecar = mediaNode.optJSONObject("edge_sidecar_to_children"); + if (sidecar != null) { + final JSONArray children = sidecar.optJSONArray("edges"); + if (children != null) { + final List sliderItems = getSliderItems(children); + builder.setSliderItems(sliderItems); + } + } + } + final FeedModel feedModel = builder.build(); + feedModels.add(feedModel); + // DownloadUtils.checkExistence(downloadDir, customDir, isSlider, model); + } + return new PostsFetchResponse(feedModels, hasNextPage, endCursor); + } + + @NonNull + private List getSliderItems(final JSONArray children) throws JSONException { + final List sliderItems = new ArrayList<>(); + for (int j = 0; j < children.length(); ++j) { + final JSONObject childNode = children.optJSONObject(j).getJSONObject("node"); + final boolean isChildVideo = childNode.optBoolean("is_video"); + int height = 0; + int width = 0; + final JSONObject dimensions = childNode.optJSONObject("dimensions"); + if (dimensions != null) { + height = dimensions.optInt("height"); + width = dimensions.optInt("width"); + } + String thumbnailUrl = null; + try { + thumbnailUrl = childNode.getJSONArray("display_resources") + .getJSONObject(0) + .getString("src"); + } catch (JSONException ignored) {} + final PostChild sliderItem = new PostChild.Builder() + .setItemType(isChildVideo ? MediaItemType.MEDIA_TYPE_VIDEO + : MediaItemType.MEDIA_TYPE_IMAGE) + .setPostId(childNode.getString(Constants.EXTRAS_ID)) + .setDisplayUrl(isChildVideo ? childNode.getString("video_url") + : childNode.getString("display_url")) + .setThumbnailUrl(thumbnailUrl != null ? thumbnailUrl + : childNode.getString("display_url")) + .setVideoViews(childNode.optLong("video_view_count", 0)) + .setHeight(height) + .setWidth(width) + .build(); + // Log.d(TAG, "getSliderItems: sliderItem: " + sliderItem); + sliderItems.add(sliderItem); + } + return sliderItems; + } } diff --git a/app/src/main/res/layout/fragment_feed.xml b/app/src/main/res/layout/fragment_feed.xml index bf80a5a5..b50138eb 100644 --- a/app/src/main/res/layout/fragment_feed.xml +++ b/app/src/main/res/layout/fragment_feed.xml @@ -37,10 +37,4 @@ android:clipToPadding="false" tools:listitem="@layout/item_feed_photo" /> - - - - - - \ 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 510783fc..12b71350 100644 --- a/app/src/main/res/layout/fragment_profile.xml +++ b/app/src/main/res/layout/fragment_profile.xml @@ -272,19 +272,33 @@ + app:layout_behavior="@string/appbar_scrolling_view_behavior"> - + tools:listitem="@layout/item_feed_photo" /> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/profile_menu.xml b/app/src/main/res/menu/profile_menu.xml index 75df9c5e..e5b86650 100644 --- a/app/src/main/res/menu/profile_menu.xml +++ b/app/src/main/res/menu/profile_menu.xml @@ -1,13 +1,11 @@ - - - - - - - + + Download item %d of %d Delete Comment + Layout %d like %d likes