From ab4306ac0d9b6dff9bd6fdadf41cd2580303f9b2 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Mon, 2 Nov 2020 22:00:07 +0900 Subject: [PATCH] Add Posts view to SavedViewerFragment --- .../asyncs/SavedPostFetchService.java | 71 +++ .../customviews/RemixDrawerLayout.java | 199 -------- .../fragments/SavedViewerFragment.java | 444 ++++++++++------- .../repositories/ProfileRepository.java | 9 + .../awais/instagrabber/utils/Constants.java | 3 + .../instagrabber/utils/SettingsHelper.java | 6 +- .../webservices/DiscoverService.java | 2 +- .../webservices/ProfileService.java | 241 +++++++++ .../instagrabber/webservices/TagsService.java | 1 + app/src/main/res/layout/fragment_saved.xml | 28 +- app/src/main/res/layout/layout_feed_view.xml | 49 -- .../main/res/layout/layout_profile_view.xml | 463 ------------------ app/src/main/res/menu/saved_viewer_menu.xml | 9 + .../main/res/navigation/profile_nav_graph.xml | 19 + 14 files changed, 621 insertions(+), 923 deletions(-) create mode 100644 app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java delete mode 100755 app/src/main/java/awais/instagrabber/customviews/RemixDrawerLayout.java delete mode 100644 app/src/main/res/layout/layout_feed_view.xml delete mode 100644 app/src/main/res/layout/layout_profile_view.xml create mode 100644 app/src/main/res/menu/saved_viewer_menu.xml diff --git a/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java new file mode 100644 index 00000000..36c668d1 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java @@ -0,0 +1,71 @@ +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.enums.PostItemType; +import awais.instagrabber.webservices.ProfileService; +import awais.instagrabber.webservices.ProfileService.SavedPostsFetchResponse; +import awais.instagrabber.webservices.ServiceCallback; + +public class SavedPostFetchService implements PostFetcher.PostFetchService { + private final ProfileService profileService; + private final String profileId; + private final PostItemType type; + + private String nextMaxId; + private boolean moreAvailable; + + public SavedPostFetchService(final String profileId, final PostItemType type) { + this.profileId = profileId; + this.type = type; + profileService = ProfileService.getInstance(); + } + + @Override + public void fetch(final FetchListener> fetchListener) { + final ServiceCallback callback = new ServiceCallback() { + @Override + public void onSuccess(final SavedPostsFetchResponse result) { + if (result == null) return; + nextMaxId = result.getNextMaxId(); + moreAvailable = result.isMoreAvailable(); + if (fetchListener != null) { + fetchListener.onResult(result.getItems()); + } + } + + @Override + public void onFailure(final Throwable t) { + // Log.e(TAG, "onFailure: ", t); + if (fetchListener != null) { + fetchListener.onFailure(t); + } + } + }; + switch (type) { + case LIKED: + profileService.fetchLiked(nextMaxId, callback); + break; + case TAGGED: + profileService.fetchTagged(profileId, nextMaxId, callback); + break; + case SAVED: + default: + profileService.fetchSaved(nextMaxId, callback); + break; + } + } + + @Override + public void reset() { + nextMaxId = null; + } + + @Override + public boolean hasNextPage() { + return moreAvailable; + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/RemixDrawerLayout.java b/app/src/main/java/awais/instagrabber/customviews/RemixDrawerLayout.java deleted file mode 100755 index ebfc4e80..00000000 --- a/app/src/main/java/awais/instagrabber/customviews/RemixDrawerLayout.java +++ /dev/null @@ -1,199 +0,0 @@ -package awais.instagrabber.customviews; - -import android.app.Activity; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.res.Configuration; -import android.graphics.Rect; -import android.os.Build; -import android.util.AttributeSet; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.widget.FrameLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.view.GravityCompat; -import androidx.core.view.ViewCompat; -import androidx.recyclerview.widget.RecyclerView; -import androidx.viewpager.widget.PagerAdapter; -import androidx.viewpager.widget.ViewPager; - -import awais.instagrabber.R; - -public final class RemixDrawerLayout extends MouseDrawer implements MouseDrawer.DrawerListener { - private final FrameLayout frameLayout; - private View drawerView; - private RecyclerView highlightsList, feedPosts, feedStories; - private float startX; - - public RemixDrawerLayout(@NonNull final Context context) { - this(context, null); - } - - public RemixDrawerLayout(@NonNull final Context context, @Nullable final AttributeSet attrs) { - this(context, attrs, 0); - } - - public RemixDrawerLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyle) { - super(context, attrs, defStyle); - - super.setDrawerElevation(getDrawerElevation()); - - addDrawerListener(this); - - frameLayout = new FrameLayout(context); - frameLayout.setPadding(0, 0, 0, 0); - super.addView(frameLayout); - } - - @Override - public void addView(@NonNull final View child, final ViewGroup.LayoutParams params) { - child.setLayoutParams(params); - addView(child); - } - - @Override - public void addView(@NonNull final View child) { - if (child.getTag() != null) super.addView(child); - else frameLayout.addView(child); - } - - @Override - public boolean onInterceptTouchEvent(@NonNull final MotionEvent ev) { - final float x = ev.getX(); - final float y = ev.getY(); - - // another one of my own weird hack thingies to make this app work - if (feedPosts == null) feedPosts = findViewById(R.id.feedPosts); - if (feedPosts != null) { - for (int i = 0; i < feedPosts.getChildCount(); ++i) { - final View viewHolder = feedPosts.getChildAt(i); - final View mediaList = viewHolder.findViewById(R.id.media_list); - if (mediaList instanceof ViewPager) { - final ViewPager viewPager = (ViewPager) mediaList; - - final Rect rect = new Rect(); - viewPager.getGlobalVisibleRect(rect); - - final boolean touchIsInMediaList = rect.contains((int) x, (int) y); - if (touchIsInMediaList) { - final PagerAdapter adapter = viewPager.getAdapter(); - final int count = adapter != null ? adapter.getCount() : 0; - if (count < 1 || viewPager.getCurrentItem() != count - 1) return false; - break; - } - } - } - } - - // thanks to Fede @ https://stackoverflow.com/questions/6920137/android-viewpager-and-horizontalscrollview/7258579#7258579 - if (highlightsList == null) highlightsList = findViewById(R.id.highlightsList); - if (highlightsList != null) { - final Boolean result = handleHorizontalRecyclerView(ev, highlightsList); - if (result != null) { - return result; - } - } - if (feedStories == null) feedStories = findViewById(R.id.feedStories); - if (feedStories != null) { - final Boolean result = handleHorizontalRecyclerView(ev, feedStories); - if (result != null) { - return result; - } - } - return super.onInterceptTouchEvent(ev); - } - - private Boolean handleHorizontalRecyclerView(@NonNull final MotionEvent ev, final RecyclerView view) { - final float x = ev.getX(); - final float y = ev.getY(); - final boolean touchIsInRecycler = x >= view.getLeft() && x < view.getRight() - && y >= view.getTop() && view.getBottom() > y; - - if (touchIsInRecycler) { - final int action = ev.getActionMasked(); - - if (action == MotionEvent.ACTION_CANCEL) return super.onInterceptTouchEvent(ev); - - if (action == MotionEvent.ACTION_DOWN) startX = x; - else if (action == MotionEvent.ACTION_MOVE) { - final int scrollRange = view.computeHorizontalScrollRange(); - final int scrollOffset = view.computeHorizontalScrollOffset(); - final boolean scrollable = scrollRange > view.getWidth(); - final boolean draggingFromRight = startX > x; - - if (scrollOffset < 1) { - if (!scrollable) return super.onInterceptTouchEvent(ev); - else if (!draggingFromRight) return super.onInterceptTouchEvent(ev); - } else if (scrollable && draggingFromRight && scrollRange - scrollOffset == view.computeHorizontalScrollExtent()) { - return super.onInterceptTouchEvent(ev); - } - - return false; - } - } - return null; - } - - @Override - public void onDrawerSlide(@NonNull final View view, @EdgeGravity final int gravity, final float slideOffset) { - drawerView = view; - final int absHorizGravity = getDrawerViewAbsoluteGravity(GravityCompat.START); - final int childAbsGravity = getDrawerViewAbsoluteGravity(drawerView); - - final Window window = getActivity(getContext()).getWindow(); - final boolean isRtl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL - || window.getDecorView().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL - || getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); - - final int drawerViewWidth = drawerView.getWidth(); - - // for (int i = 0; i < frameLayout.getChildCount(); i++) { - // final View child = frameLayout.getChildAt(i); - // - // final boolean isLeftDrawer = isRtl == (childAbsGravity != absHorizGravity); - // float width = isLeftDrawer ? drawerViewWidth : -drawerViewWidth; - // - // child.setX(width * slideOffset); - // } - - final boolean isLeftDrawer = isRtl == (childAbsGravity != absHorizGravity); - float width = isLeftDrawer ? drawerViewWidth : -drawerViewWidth; - - frameLayout.setX(width * (isRtl ? -slideOffset : slideOffset)); - } - - @Override - public void openDrawer(@NonNull final View drawerView, final boolean animate) { - super.openDrawer(drawerView, animate); - post(() -> onDrawerSlide(drawerView, Gravity.NO_GRAVITY, isDrawerOpen(drawerView) ? 1f : 0f)); - } - - @Override - protected void onConfigurationChanged(final Configuration newConfig) { - super.onConfigurationChanged(newConfig); - if (drawerView != null) onDrawerSlide(drawerView, Gravity.NO_GRAVITY, isDrawerOpen(drawerView) ? 1f : 0f); - } - - private static Activity getActivity(final Context context) { - if (context != null) { - if (context instanceof Activity) return (Activity) context; - if (context instanceof ContextWrapper) - return getActivity(((ContextWrapper) context).getBaseContext()); - } - return null; - } - - final int getDrawerViewAbsoluteGravity(final int gravity) { - return GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(this)) & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK; - } - - final int getDrawerViewAbsoluteGravity(@NonNull final View drawerView) { - final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; - return getDrawerViewAbsoluteGravity(gravity); - } -} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/SavedViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/SavedViewerFragment.java index efaeb320..112e0fd4 100644 --- a/app/src/main/java/awais/instagrabber/fragments/SavedViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/SavedViewerFragment.java @@ -2,81 +2,69 @@ package awais.instagrabber.fragments; import android.content.Context; import android.content.pm.PackageManager; -import android.os.AsyncTask; import android.os.Bundle; -import android.util.Log; +import android.os.Handler; import android.view.ActionMode; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.LinearLayout; -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.AppCompatActivity; +import androidx.core.content.PermissionChecker; import androidx.fragment.app.Fragment; -import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavController; import androidx.navigation.NavDirections; import androidx.navigation.fragment.NavHostFragment; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import awais.instagrabber.BuildConfig; import awais.instagrabber.R; -import awais.instagrabber.adapters.PostsAdapter; -import awais.instagrabber.asyncs.PostsFetcher; -import awais.instagrabber.asyncs.i.iLikedFetcher; +import awais.instagrabber.adapters.FeedAdapterV2; +import awais.instagrabber.asyncs.SavedPostFetchService; 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.FragmentSavedBinding; +import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; import awais.instagrabber.fragments.main.ProfileFragmentDirections; -import awais.instagrabber.interfaces.FetchListener; +import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.PostModel; -import awais.instagrabber.models.enums.DownloadMethod; +import awais.instagrabber.models.PostsLayoutPreferences; import awais.instagrabber.models.enums.PostItemType; +import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.DownloadUtils; -import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; -import awais.instagrabber.viewmodels.PostsViewModel; -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 final class SavedViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { - private static AsyncTask currentlyExecuting; - private PostsAdapter postsAdapter; - private boolean hasNextPage; + private static final int STORAGE_PERM_REQUEST_CODE = 8020; + private FragmentSavedBinding binding; private String username; - private String endCursor; - private RecyclerLazyLoader lazyLoader; - private ArrayList selectedItems = new ArrayList<>(); private ActionMode actionMode; - private PostsViewModel postsViewModel; - private LinearLayout root; + private SwipeRefreshLayout root; private AppCompatActivity fragmentActivity; private boolean shouldRefresh = true; private PostItemType type; private String profileId; + private final ArrayList selectedItems = new ArrayList<>(); private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { setEnabled(false); remove(); - if (postsAdapter == null) return; - postsAdapter.clearSelection(); + // if (postsAdapter == null) return; + // postsAdapter.clearSelection(); } }; private final PrimaryActionModeCallback multiSelectAction = new PrimaryActionModeCallback( @@ -90,64 +78,112 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL @Override public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) { if (item.getItemId() == R.id.action_download) { - if (postsAdapter == null || username == null) { - return false; - } - final Context context = getContext(); - if (context == null) return false; - DownloadUtils.batchDownload(context, - username, - DownloadMethod.DOWNLOAD_SAVED, - postsAdapter.getSelectedModels()); - checkAndResetAction(); + // if (postsAdapter == null || username == null) { + // return false; + // } + // final Context context = getContext(); + // if (context == null) return false; + // DownloadUtils.batchDownload(context, + // username, + // DownloadMethod.DOWNLOAD_SAVED, + // postsAdapter.getSelectedModels()); + // checkAndResetAction(); return true; } return false; } }); - private final FetchListener> postsFetchListener = new FetchListener>() { + private final FeedAdapterV2.FeedItemCallback feedItemCallback = new FeedAdapterV2.FeedItemCallback() { @Override - public void onResult(final List result) { - final List current = postsViewModel.getList().getValue(); - if (result != null && !result.isEmpty()) { - if (current == null) { - postsViewModel.getList().postValue(result); - } else { - final List currentCopy = new ArrayList<>(current); - currentCopy.addAll(result); - postsViewModel.getList().postValue(currentCopy); - } - binding.mainPosts.post(() -> { - binding.mainPosts.setNestedScrollingEnabled(true); - binding.mainPosts.setVisibility(View.VISIBLE); - }); + public void onPostClick(final FeedModel feedModel, final View profilePicView, final View mainPostImage) { + openPostDialog(feedModel, profilePicView, mainPostImage, -1); + } - final PostModel model = !result.isEmpty() ? result.get(result.size() - 1) : null; - if (model != null) { - endCursor = model.getEndCursor(); - hasNextPage = model.hasNextPage(); - if (hasNextPage) { - fetchPosts(); - } else { - binding.swipeRefreshLayout.setRefreshing(false); - } - model.setPageCursor(false, null); - } - } else if (current == null) { - final Context context = getContext(); - if (context == null) return; - Toast.makeText(context, R.string.empty_list, Toast.LENGTH_SHORT).show(); - NavHostFragment.findNavController(SavedViewerFragment.this).popBackStack(); + @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 = ProfileFragmentDirections.actionGlobalCommentsViewerFragment( + feedModel.getShortCode(), + feedModel.getPostId(), + feedModel.getProfileModel().getId() + ); + NavHostFragment.findNavController(SavedViewerFragment.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; } - binding.swipeRefreshLayout.setRefreshing(false); + requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); + } + + @Override + public void onHashtagClick(final String hashtag) { + final NavDirections action = ProfileFragmentDirections.actionGlobalHashTagFragment(hashtag); + NavHostFragment.findNavController(SavedViewerFragment.this).navigate(action); + } + + @Override + public void onLocationClick(final FeedModel feedModel) { + final NavDirections action = ProfileFragmentDirections.actionGlobalLocationFragment(feedModel.getLocationId()); + NavHostFragment.findNavController(SavedViewerFragment.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 Observer> listObserver; @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); fragmentActivity = (AppCompatActivity) getActivity(); + setHasOptionsMenu(true); } @Override @@ -164,22 +200,34 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL @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.saved_viewer_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 onResume() { super.onResume(); setTitle(); - observeData(); } - private void observeData() { - postsViewModel = new ViewModelProvider(this).get(PostsViewModel.class); - postsViewModel.getList().removeObserver(listObserver); - if (postsAdapter != null) { - postsViewModel.getList().observe(getViewLifecycleOwner(), listObserver); - } + @Override + public void onRefresh() { + binding.posts.refresh(); } private void init() { @@ -189,122 +237,79 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL username = fragmentArgs.getUsername(); profileId = fragmentArgs.getProfileId(); type = fragmentArgs.getType(); - setTitle(); - binding.swipeRefreshLayout.setOnRefreshListener(this); - // autoloadPosts = Utils.settingsHelper.getBoolean(AUTOLOAD_POSTS); - binding.mainPosts.setNestedScrollingEnabled(false); - 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 = postModels.get(0).getPostId(); - final boolean isId = postId != null; - final String[] idsOrShortCodes = new String[postModels.size()]; - for (int i = 0; i < postModels.size(); i++) { - final PostModel tempPostModel = postModels.get(i); - final String tempId = tempPostModel.getPostId(); - final String finalPostId = type == PostItemType.LIKED ? tempId.substring(0, tempId.indexOf("_")) : tempId; - idsOrShortCodes[i] = isId ? finalPostId - : tempPostModel.getShortCode(); - } - final NavDirections action = ProfileFragmentDirections.actionGlobalPostViewFragment( - position, - idsOrShortCodes, - isId); - NavHostFragment.findNavController(this).navigate(action); - }, (model, position) -> { - if (!postsAdapter.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.mainPosts.setAdapter(postsAdapter); - listObserver = list -> postsAdapter.submitList(list); - observeData(); - binding.swipeRefreshLayout.setRefreshing(true); - - lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { - if (hasNextPage) { - binding.swipeRefreshLayout.setRefreshing(true); - fetchPosts(); - endCursor = null; - } - }); - binding.mainPosts.addOnScrollListener(lazyLoader); - fetchPosts(); + setupPosts(); + // 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 = postModels.get(0).getPostId(); + // final boolean isId = postId != null; + // final String[] idsOrShortCodes = new String[postModels.size()]; + // for (int i = 0; i < postModels.size(); i++) { + // final PostModel tempPostModel = postModels.get(i); + // final String tempId = tempPostModel.getPostId(); + // final String finalPostId = type == PostItemType.LIKED ? tempId.substring(0, tempId.indexOf("_")) : tempId; + // idsOrShortCodes[i] = isId ? finalPostId + // : tempPostModel.getShortCode(); + // } + // final NavDirections action = ProfileFragmentDirections.actionGlobalPostViewFragment( + // position, + // idsOrShortCodes, + // isId); + // NavHostFragment.findNavController(this).navigate(action); + // }, (model, position) -> { + // if (!postsAdapter.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; + // }); } - private void fetchPosts() { - stopCurrentExecutor(); - final AsyncTask> asyncTask; + private void setupPosts() { + binding.posts.setViewModelStoreOwner(this) + .setLifeCycleOwner(this) + .setPostFetchService(new SavedPostFetchService(profileId, type)) + .setLayoutPreferences(PostsLayoutPreferences.fromJson(settingsHelper.getString(getPostsLayoutPreferenceKey()))) + .addFetchStatusChangeListener(fetching -> updateSwipeRefreshState()) + .setFeedItemCallback(feedItemCallback) + .init(); + binding.swipeRefreshLayout.setRefreshing(true); + } + + @NonNull + private String getPostsLayoutPreferenceKey() { switch (type) { case LIKED: - asyncTask = new iLikedFetcher(endCursor, postsFetchListener); - break; - case SAVED: + return Constants.PREF_LIKED_POSTS_LAYOUT; case TAGGED: - if (TextUtils.isEmpty(profileId)) return; - asyncTask = new PostsFetcher(profileId, type, endCursor, postsFetchListener); - break; + return Constants.PREF_TAGGED_POSTS_LAYOUT; + case SAVED: default: - return; + return Constants.PREF_SAVED_POSTS_LAYOUT; } - currentlyExecuting = asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - @Override - public void onRefresh() { - if (lazyLoader != null) lazyLoader.resetState(); - stopCurrentExecutor(); - endCursor = null; - postsViewModel.getList().postValue(Collections.emptyList()); - selectedItems.clear(); - if (postsAdapter != null) { - // postsAdapter.isSelecting = false; - postsAdapter.notifyDataSetChanged(); - } - binding.swipeRefreshLayout.setRefreshing(true); - fetchPosts(); } @Override public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (requestCode == 8020 && grantResults[0] == PackageManager.PERMISSION_GRANTED && selectedItems.size() > 0) { - final Context context = getContext(); - if (context == null) return; - DownloadUtils.batchDownload(context, null, DownloadMethod.DOWNLOAD_SAVED, selectedItems); - } - } - - public static void stopCurrentExecutor() { - if (currentlyExecuting != null) { - try { - currentlyExecuting.cancel(true); - } catch (final Exception e) { - if (logCollector != null) - logCollector.appendException(e, LogCollector.LogFile.MAIN_HELPER, "stopCurrentExecutor"); - if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); - } + if (requestCode == 8020 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // final Context context = getContext(); + // if (context == null) return; + // DownloadUtils.batchDownload(context, null, DownloadMethod.DOWNLOAD_SAVED, selectedItems); } } @@ -313,9 +318,6 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL if (actionBar == null) return; final int titleRes; switch (type) { - case SAVED: - titleRes = R.string.saved; - break; case LIKED: titleRes = R.string.liked; break; @@ -323,12 +325,74 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL titleRes = R.string.tagged; break; default: - return; // no other types supported in this view + case SAVED: + titleRes = R.string.saved; + break; } actionBar.setTitle(titleRes); actionBar.setSubtitle(username); } + 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( + getPostsLayoutPreferenceKey(), + preferences -> new Handler().postDelayed(() -> binding.posts.setLayoutPreferences(preferences), 200)); + fragment.show(getChildFragmentManager(), "posts_layout_preferences"); + } + private boolean checkAndResetAction() { if (!onBackPressedCallback.isEnabled() && actionMode == null) { return false; diff --git a/app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java b/app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java index 53ae0470..8643b682 100644 --- a/app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java @@ -14,4 +14,13 @@ public interface ProfileRepository { @GET("/graphql/query/") Call fetch(@QueryMap Map queryMap); + + @GET("/api/v1/feed/saved/") + Call fetchSaved(@QueryMap Map queryParams); + + @GET("/api/v1/feed/liked/") + Call fetchLiked(@QueryMap Map queryParams); + + @GET("/api/v1/usertags/{profileId}/feed/") + Call fetchTagged(@Path("profileId") final String profileId, @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 2122f7f6..9dfadddd 100644 --- a/app/src/main/java/awais/instagrabber/utils/Constants.java +++ b/app/src/main/java/awais/instagrabber/utils/Constants.java @@ -92,4 +92,7 @@ public final class Constants { public static final String PREF_TOPIC_POSTS_LAYOUT = "topic_posts_layout"; public static final String PREF_HASHTAG_POSTS_LAYOUT = "hashtag_posts_layout"; public static final String PREF_LOCATION_POSTS_LAYOUT = "location_posts_layout"; + public static final String PREF_LIKED_POSTS_LAYOUT = "liked_posts_layout"; + public static final String PREF_TAGGED_POSTS_LAYOUT = "tagged_posts_layout"; + public static final String PREF_SAVED_POSTS_LAYOUT = "saved_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 df239ed8..e4ec5627 100755 --- a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java +++ b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java @@ -31,9 +31,12 @@ import static awais.instagrabber.utils.Constants.MUTED_VIDEOS; import static awais.instagrabber.utils.Constants.PREF_DARK_THEME; import static awais.instagrabber.utils.Constants.PREF_HASHTAG_POSTS_LAYOUT; import static awais.instagrabber.utils.Constants.PREF_LIGHT_THEME; +import static awais.instagrabber.utils.Constants.PREF_LIKED_POSTS_LAYOUT; import static awais.instagrabber.utils.Constants.PREF_LOCATION_POSTS_LAYOUT; 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_SAVED_POSTS_LAYOUT; +import static awais.instagrabber.utils.Constants.PREF_TAGGED_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; @@ -119,7 +122,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, - PREF_TOPIC_POSTS_LAYOUT, PREF_HASHTAG_POSTS_LAYOUT, PREF_LOCATION_POSTS_LAYOUT}) + PREF_TOPIC_POSTS_LAYOUT, PREF_HASHTAG_POSTS_LAYOUT, PREF_LOCATION_POSTS_LAYOUT, PREF_LIKED_POSTS_LAYOUT, PREF_TAGGED_POSTS_LAYOUT, + PREF_SAVED_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/DiscoverService.java b/app/src/main/java/awais/instagrabber/webservices/DiscoverService.java index 1e1a9f4a..1f542694 100644 --- a/app/src/main/java/awais/instagrabber/webservices/DiscoverService.java +++ b/app/src/main/java/awais/instagrabber/webservices/DiscoverService.java @@ -118,7 +118,7 @@ public class DiscoverService extends BaseService { final JSONObject clusterJson = clustersJson.getJSONObject(i); final String id = clusterJson.optString("id"); final String title = clusterJson.optString("title"); - if (id == null || title == null) { + if (TextUtils.isEmpty(id) || TextUtils.isEmpty(title)) { continue; } final String type = clusterJson.optString("type"); diff --git a/app/src/main/java/awais/instagrabber/webservices/ProfileService.java b/app/src/main/java/awais/instagrabber/webservices/ProfileService.java index 38a4ce22..34567d58 100644 --- a/app/src/main/java/awais/instagrabber/webservices/ProfileService.java +++ b/app/src/main/java/awais/instagrabber/webservices/ProfileService.java @@ -4,6 +4,8 @@ import android.util.Log; import androidx.annotation.NonNull; +import com.google.common.collect.ImmutableMap; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -13,6 +15,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.PostChild; @@ -282,4 +285,242 @@ public class ProfileService extends BaseService { } return sliderItems; } + + public void fetchSaved(final String maxId, + final ServiceCallback callback) { + final ImmutableMap.Builder builder = ImmutableMap.builder(); + if (!TextUtils.isEmpty(maxId)) { + builder.put("max_id", maxId); + } + final Call request = repository.fetchSaved(builder.build()); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + try { + if (callback == null) { + return; + } + final String body = response.body(); + if (TextUtils.isEmpty(body)) { + callback.onSuccess(null); + return; + } + final SavedPostsFetchResponse savedPostsFetchResponse = parseSavedPostsResponse(body, true); + callback.onSuccess(savedPostsFetchResponse); + } catch (JSONException e) { + Log.e(TAG, "onResponse", e); + callback.onFailure(e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + } + + public void fetchLiked(final String maxId, + final ServiceCallback callback) { + final ImmutableMap.Builder builder = ImmutableMap.builder(); + if (!TextUtils.isEmpty(maxId)) { + builder.put("max_id", maxId); + } + final Call request = repository.fetchLiked(builder.build()); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + try { + if (callback == null) { + return; + } + final String body = response.body(); + if (TextUtils.isEmpty(body)) { + callback.onSuccess(null); + return; + } + final SavedPostsFetchResponse savedPostsFetchResponse = parseSavedPostsResponse(body, false); + callback.onSuccess(savedPostsFetchResponse); + } catch (JSONException e) { + Log.e(TAG, "onResponse", e); + callback.onFailure(e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + } + + public void fetchTagged(final String profileId, + final String maxId, + final ServiceCallback callback) { + final ImmutableMap.Builder builder = ImmutableMap.builder(); + if (!TextUtils.isEmpty(maxId)) { + builder.put("max_id", maxId); + } + final Call request = repository.fetchTagged(profileId, builder.build()); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + try { + if (callback == null) { + return; + } + final String body = response.body(); + if (TextUtils.isEmpty(body)) { + callback.onSuccess(null); + return; + } + final SavedPostsFetchResponse savedPostsFetchResponse = parseSavedPostsResponse(body, false); + callback.onSuccess(savedPostsFetchResponse); + } catch (JSONException e) { + Log.e(TAG, "onResponse", e); + callback.onFailure(e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + } + + private SavedPostsFetchResponse parseSavedPostsResponse(final String body, final boolean isInMedia) throws JSONException { + final JSONObject root = new JSONObject(body); + final boolean moreAvailable = root.optBoolean("more_available"); + final String nextMaxId = root.optString("next_max_id"); + final int numResults = root.optInt("num_results"); + final String status = root.optString("status"); + final JSONArray itemsJson = root.optJSONArray("items"); + final List items = parseItems(itemsJson, isInMedia); + return new SavedPostsFetchResponse( + moreAvailable, + nextMaxId, + numResults, + status, + items + ); + } + + private List parseItems(final JSONArray items, final boolean isInMedia) 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 FeedModel feedModel = ResponseBodyUtils.parseItem(isInMedia ? itemJson.optJSONObject("media") : itemJson); + if (feedModel != null) { + feedModels.add(feedModel); + } + } + return feedModels; + } + + public static class SavedPostsFetchResponse { + private boolean moreAvailable; + private String nextMaxId; + private int numResults; + private String status; + private List items; + + public SavedPostsFetchResponse(final boolean moreAvailable, + final String nextMaxId, + final int numResults, + final String status, + final List items) { + this.moreAvailable = moreAvailable; + this.nextMaxId = nextMaxId; + this.numResults = numResults; + this.status = status; + this.items = items; + } + + public boolean isMoreAvailable() { + return moreAvailable; + } + + public SavedPostsFetchResponse setMoreAvailable(final boolean moreAvailable) { + this.moreAvailable = moreAvailable; + return this; + } + + public String getNextMaxId() { + return nextMaxId; + } + + public SavedPostsFetchResponse setNextMaxId(final String nextMaxId) { + this.nextMaxId = nextMaxId; + return this; + } + + public int getNumResults() { + return numResults; + } + + public SavedPostsFetchResponse setNumResults(final int numResults) { + this.numResults = numResults; + return this; + } + + public String getStatus() { + return status; + } + + public SavedPostsFetchResponse setStatus(final String status) { + this.status = status; + return this; + } + + public List getItems() { + return items; + } + + public SavedPostsFetchResponse 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 SavedPostsFetchResponse that = (SavedPostsFetchResponse) o; + return moreAvailable == that.moreAvailable && + numResults == that.numResults && + Objects.equals(nextMaxId, that.nextMaxId) && + Objects.equals(status, that.status) && + Objects.equals(items, that.items); + } + + @Override + public int hashCode() { + return Objects.hash(moreAvailable, nextMaxId, numResults, status, items); + } + + @NonNull + @Override + public String toString() { + return "SavedPostsFetchResponse{" + + "moreAvailable=" + moreAvailable + + ", nextMaxId='" + nextMaxId + '\'' + + ", numResults=" + numResults + + ", status='" + status + '\'' + + ", items=" + items + + '}'; + } + } } diff --git a/app/src/main/java/awais/instagrabber/webservices/TagsService.java b/app/src/main/java/awais/instagrabber/webservices/TagsService.java index 8a7eea6d..0ef6862e 100644 --- a/app/src/main/java/awais/instagrabber/webservices/TagsService.java +++ b/app/src/main/java/awais/instagrabber/webservices/TagsService.java @@ -267,6 +267,7 @@ public class TagsService extends BaseService { return Objects.hash(moreAvailable, nextMaxId, numResults, status, items); } + @NonNull @Override public String toString() { return "TagPostsFetchResponse{" + diff --git a/app/src/main/res/layout/fragment_saved.xml b/app/src/main/res/layout/fragment_saved.xml index 9796ff87..c5778455 100644 --- a/app/src/main/res/layout/fragment_saved.xml +++ b/app/src/main/res/layout/fragment_saved.xml @@ -1,28 +1,16 @@ - - - - - - - - - - + android:clipToPadding="false" /> + diff --git a/app/src/main/res/layout/layout_feed_view.xml b/app/src/main/res/layout/layout_feed_view.xml deleted file mode 100644 index adcf4f50..00000000 --- a/app/src/main/res/layout/layout_feed_view.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/layout_profile_view.xml b/app/src/main/res/layout/layout_profile_view.xml deleted file mode 100644 index 1aefe2b5..00000000 --- a/app/src/main/res/layout/layout_profile_view.xml +++ /dev/null @@ -1,463 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/saved_viewer_menu.xml b/app/src/main/res/menu/saved_viewer_menu.xml new file mode 100644 index 00000000..1ced72b6 --- /dev/null +++ b/app/src/main/res/menu/saved_viewer_menu.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/profile_nav_graph.xml b/app/src/main/res/navigation/profile_nav_graph.xml index fbf21f1c..ae505ede 100644 --- a/app/src/main/res/navigation/profile_nav_graph.xml +++ b/app/src/main/res/navigation/profile_nav_graph.xml @@ -5,6 +5,25 @@ android:id="@+id/profile_nav_graph" app:startDestination="@id/profileFragment"> + + + + + + + +