viewing saved collections, so half of #545

This commit is contained in:
Austin Huang 2021-01-17 14:11:15 -05:00
parent 6aacf1945f
commit 117b1c1629
No known key found for this signature in database
GPG Key ID: 84C23AA04587A91F
20 changed files with 1136 additions and 10 deletions

View File

@ -40,7 +40,7 @@ public class DiscoverTopicsAdapter extends ListAdapter<TopicCluster, TopicCluste
public TopicClusterViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemDiscoverTopicBinding binding = ItemDiscoverTopicBinding.inflate(layoutInflater, parent, false);
return new TopicClusterViewHolder(binding, onTopicClickListener);
return new TopicClusterViewHolder(binding, onTopicClickListener, null);
}
@Override

View File

@ -0,0 +1,57 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import awais.instagrabber.adapters.viewholder.TopicClusterViewHolder;
import awais.instagrabber.databinding.ItemDiscoverTopicBinding;
import awais.instagrabber.repositories.responses.saved.SavedCollection;
import awais.instagrabber.utils.ResponseBodyUtils;
public class SavedCollectionsAdapter extends ListAdapter<SavedCollection, TopicClusterViewHolder> {
private static final DiffUtil.ItemCallback<SavedCollection> DIFF_CALLBACK = new DiffUtil.ItemCallback<SavedCollection>() {
@Override
public boolean areItemsTheSame(@NonNull final SavedCollection oldItem, @NonNull final SavedCollection newItem) {
return oldItem.getId().equals(newItem.getId());
}
@Override
public boolean areContentsTheSame(@NonNull final SavedCollection oldItem, @NonNull final SavedCollection newItem) {
if (oldItem.getCoverMedias().size() == newItem.getCoverMedias().size()) {
if (oldItem.getCoverMedias().size() == 0) return true;
return oldItem.getCoverMedias().get(0).getId().equals(newItem.getCoverMedias().get(0).getId());
}
return false;
}
};
private final OnCollectionClickListener onCollectionClickListener;
public SavedCollectionsAdapter(final OnCollectionClickListener onCollectionClickListener) {
super(DIFF_CALLBACK);
this.onCollectionClickListener = onCollectionClickListener;
}
@NonNull
@Override
public TopicClusterViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemDiscoverTopicBinding binding = ItemDiscoverTopicBinding.inflate(layoutInflater, parent, false);
return new TopicClusterViewHolder(binding, null, onCollectionClickListener);
}
@Override
public void onBindViewHolder(@NonNull final TopicClusterViewHolder holder, final int position) {
final SavedCollection topicCluster = getItem(position);
holder.bind(topicCluster);
}
public interface OnCollectionClickListener {
void onCollectionClick(SavedCollection savedCollection, View root, View cover, View title, int titleColor, int backgroundColor);
}
}

View File

@ -25,19 +25,24 @@ import java.util.concurrent.atomic.AtomicInteger;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DiscoverTopicsAdapter;
import awais.instagrabber.adapters.SavedCollectionsAdapter;
import awais.instagrabber.databinding.ItemDiscoverTopicBinding;
import awais.instagrabber.repositories.responses.discover.TopicCluster;
import awais.instagrabber.repositories.responses.saved.SavedCollection;
import awais.instagrabber.utils.ResponseBodyUtils;
public class TopicClusterViewHolder extends RecyclerView.ViewHolder {
private final ItemDiscoverTopicBinding binding;
private final DiscoverTopicsAdapter.OnTopicClickListener onTopicClickListener;
private final SavedCollectionsAdapter.OnCollectionClickListener onCollectionClickListener;
public TopicClusterViewHolder(@NonNull final ItemDiscoverTopicBinding binding,
final DiscoverTopicsAdapter.OnTopicClickListener onTopicClickListener) {
final DiscoverTopicsAdapter.OnTopicClickListener onTopicClickListener,
final SavedCollectionsAdapter.OnCollectionClickListener onCollectionClickListener) {
super(binding.getRoot());
this.binding = binding;
this.onTopicClickListener = onTopicClickListener;
this.onCollectionClickListener = onCollectionClickListener;
}
public void bind(final TopicCluster topicCluster) {
@ -102,4 +107,69 @@ public class TopicClusterViewHolder extends RecyclerView.ViewHolder {
}
binding.title.setText(topicCluster.getTitle());
}
public void bind(final SavedCollection topicCluster) {
if (topicCluster == null) {
return;
}
final AtomicInteger titleColor = new AtomicInteger(-1);
final AtomicInteger backgroundColor = new AtomicInteger(-1);
if (onCollectionClickListener != null) {
itemView.setOnClickListener(v -> onCollectionClickListener.onCollectionClick(
topicCluster,
binding.getRoot(),
binding.cover,
binding.title,
titleColor.get(),
backgroundColor.get()
));
}
// binding.title.setTransitionName("title-" + topicCluster.getId());
binding.cover.setTransitionName("cover-" + topicCluster.getId());
final String thumbUrl = ResponseBodyUtils.getThumbUrl(topicCluster.getCoverMedias() == null
? null
: topicCluster.getCoverMedias().get(0));
if (thumbUrl == null) {
binding.cover.setImageURI((String) null);
} else {
final ImageRequest imageRequest = ImageRequestBuilder
.newBuilderWithSource(Uri.parse(thumbUrl))
.build();
final ImagePipeline imagePipeline = Fresco.getImagePipeline();
final DataSource<CloseableReference<CloseableImage>> dataSource = imagePipeline
.fetchDecodedImage(imageRequest, CallerThreadExecutor.getInstance());
dataSource.subscribe(new BaseBitmapDataSubscriber() {
@Override
public void onNewResultImpl(@Nullable Bitmap bitmap) {
if (dataSource.isFinished()) {
dataSource.close();
}
if (bitmap != null) {
Palette.from(bitmap).generate(p -> {
final Palette.Swatch swatch = p.getDominantSwatch();
final Resources resources = itemView.getResources();
int titleTextColor = resources.getColor(R.color.white);
if (swatch != null) {
backgroundColor.set(swatch.getRgb());
GradientDrawable gd = new GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
new int[]{Color.TRANSPARENT, backgroundColor.get()});
titleTextColor = swatch.getTitleTextColor();
binding.background.setBackground(gd);
}
titleColor.set(titleTextColor);
binding.title.setTextColor(titleTextColor);
});
}
}
@Override
public void onFailureImpl(@NonNull DataSource dataSource) {
dataSource.close();
}
}, CallerThreadExecutor.getInstance());
binding.cover.setImageRequest(imageRequest);
}
binding.title.setText(topicCluster.getTitle());
}
}

View File

@ -19,12 +19,14 @@ public class SavedPostFetchService implements PostFetcher.PostFetchService {
private final boolean isLoggedIn;
private String nextMaxId;
private final String collectionId;
private boolean moreAvailable;
public SavedPostFetchService(final long profileId, final PostItemType type, final boolean isLoggedIn) {
public SavedPostFetchService(final long profileId, final PostItemType type, final boolean isLoggedIn, final String collectionId) {
this.profileId = profileId;
this.type = type;
this.isLoggedIn = isLoggedIn;
this.collectionId = collectionId;
graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
profileService = isLoggedIn ? ProfileService.getInstance() : null;
}
@ -58,10 +60,12 @@ public class SavedPostFetchService implements PostFetcher.PostFetchService {
if (isLoggedIn) profileService.fetchTagged(profileId, nextMaxId, callback);
else graphQLService.fetchTaggedPosts(profileId, 30, nextMaxId, callback);
break;
case COLLECTION:
case SAVED:
default:
profileService.fetchSaved(nextMaxId, callback);
profileService.fetchSaved(nextMaxId, collectionId, callback);
break;
default:
callback.onFailure(null);
}
}

View File

@ -0,0 +1,432 @@
package awais.instagrabber.fragments;
import android.animation.ArgbEvaluator;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.view.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 androidx.activity.OnBackPressedCallback;
import androidx.activity.OnBackPressedDispatcher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.PermissionChecker;
import androidx.core.graphics.ColorUtils;
import androidx.fragment.app.Fragment;
import androidx.navigation.NavController;
import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.transition.ChangeBounds;
import androidx.transition.TransitionInflater;
import androidx.transition.TransitionSet;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.imagepipeline.image.ImageInfo;
import com.google.common.collect.ImmutableList;
import java.util.Set;
import awais.instagrabber.R;
import awais.instagrabber.activities.MainActivity;
import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.asyncs.SavedPostFetchService;
import awais.instagrabber.customviews.PrimaryActionModeCallback;
import awais.instagrabber.databinding.FragmentCollectionPostsBinding;
import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment;
import awais.instagrabber.fragments.CollectionPostsFragmentDirections;
import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.models.enums.PostItemType;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.saved.SavedCollection;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils;
import static androidx.core.content.PermissionChecker.checkSelfPermission;
import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION;
public class CollectionPostsFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
private static final int STORAGE_PERM_REQUEST_CODE = 8020;
private static final int STORAGE_PERM_REQUEST_CODE_FOR_SELECTION = 8030;
private MainActivity fragmentActivity;
private FragmentCollectionPostsBinding binding;
private CoordinatorLayout root;
private boolean shouldRefresh = true;
private SavedCollection savedCollection;
private ActionMode actionMode;
private Set<Media> selectedFeedModels;
private Media downloadFeedModel;
private int downloadChildPosition = -1;
private PostsLayoutPreferences layoutPreferences = Utils.getPostsLayoutPreferences(Constants.PREF_SAVED_POSTS_LAYOUT);
private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
@Override
public void handleOnBackPressed() {
binding.posts.endSelection();
}
};
private final PrimaryActionModeCallback multiSelectAction = new PrimaryActionModeCallback(
R.menu.multi_select_download_menu, new PrimaryActionModeCallback.CallbacksHelper() {
@Override
public void onDestroy(final ActionMode mode) {
binding.posts.endSelection();
}
@Override
public boolean onActionItemClicked(final ActionMode mode,
final MenuItem item) {
if (item.getItemId() == R.id.action_download) {
if (CollectionPostsFragment.this.selectedFeedModels == null) return false;
final Context context = getContext();
if (context == null) return false;
if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) {
DownloadUtils.download(context, ImmutableList.copyOf(CollectionPostsFragment.this.selectedFeedModels));
binding.posts.endSelection();
return true;
}
requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE_FOR_SELECTION);
}
return false;
}
});
private final FeedAdapterV2.FeedItemCallback feedItemCallback = new FeedAdapterV2.FeedItemCallback() {
@Override
public void onPostClick(final Media feedModel, final View profilePicView, final View mainPostImage) {
openPostDialog(feedModel, profilePicView, mainPostImage, -1);
}
@Override
public void onSliderClick(final Media feedModel, final int position) {
openPostDialog(feedModel, null, null, position);
}
@Override
public void onCommentsClick(final Media feedModel) {
final NavDirections commentsAction = CollectionPostsFragmentDirections.actionGlobalCommentsViewerFragment(
feedModel.getCode(),
feedModel.getPk(),
feedModel.getUser().getPk()
);
NavHostFragment.findNavController(CollectionPostsFragment.this).navigate(commentsAction);
}
@Override
public void onDownloadClick(final Media feedModel, final int childPosition) {
final Context context = getContext();
if (context == null) return;
if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) {
DownloadUtils.showDownloadDialog(context, feedModel, childPosition);
return;
}
downloadFeedModel = feedModel;
downloadChildPosition = -1;
requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE);
}
@Override
public void onHashtagClick(final String hashtag) {
final NavDirections action = CollectionPostsFragmentDirections.actionGlobalHashTagFragment(hashtag);
NavHostFragment.findNavController(CollectionPostsFragment.this).navigate(action);
}
@Override
public void onLocationClick(final Media feedModel) {
final NavDirections action = CollectionPostsFragmentDirections.actionGlobalLocationFragment(feedModel.getLocation().getPk());
NavHostFragment.findNavController(CollectionPostsFragment.this).navigate(action);
}
@Override
public void onMentionClick(final String mention) {
navigateToProfile(mention.trim());
}
@Override
public void onNameClick(final Media feedModel, final View profilePicView) {
navigateToProfile("@" + feedModel.getUser().getUsername());
}
@Override
public void onProfilePicClick(final Media feedModel, final View profilePicView) {
navigateToProfile("@" + feedModel.getUser().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 Media feedModel,
final View profilePicView,
final View mainPostImage,
final int position) {
final PostViewV2Fragment.Builder builder = PostViewV2Fragment
.builder(feedModel);
if (position >= 0) {
builder.setPosition(position);
}
if (!layoutPreferences.isAnimationDisabled()) {
builder.setSharedProfilePicElement(profilePicView)
.setSharedMainPostElement(mainPostImage);
}
builder.build().show(getChildFragmentManager(), "post_view");
}
};
private final FeedAdapterV2.SelectionModeCallback selectionModeCallback = new FeedAdapterV2.SelectionModeCallback() {
@Override
public void onSelectionStart() {
if (!onBackPressedCallback.isEnabled()) {
final OnBackPressedDispatcher onBackPressedDispatcher = fragmentActivity.getOnBackPressedDispatcher();
onBackPressedCallback.setEnabled(true);
onBackPressedDispatcher.addCallback(getViewLifecycleOwner(), onBackPressedCallback);
}
if (actionMode == null) {
actionMode = fragmentActivity.startActionMode(multiSelectAction);
}
}
@Override
public void onSelectionChange(final Set<Media> selectedFeedModels) {
final String title = getString(R.string.number_selected, selectedFeedModels.size());
if (actionMode != null) {
actionMode.setTitle(title);
}
CollectionPostsFragment.this.selectedFeedModels = selectedFeedModels;
}
@Override
public void onSelectionEnd() {
if (onBackPressedCallback.isEnabled()) {
onBackPressedCallback.setEnabled(false);
onBackPressedCallback.remove();
}
if (actionMode != null) {
actionMode.finish();
actionMode = null;
}
}
};
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
fragmentActivity = (MainActivity) requireActivity();
final TransitionSet transitionSet = new TransitionSet();
transitionSet.addTransition(new ChangeBounds())
.addTransition(TransitionInflater.from(getContext()).inflateTransition(android.R.transition.move))
.setDuration(200);
setSharedElementEnterTransition(transitionSet);
postponeEnterTransition();
setHasOptionsMenu(true);
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
if (root != null) {
shouldRefresh = false;
return root;
}
binding = FragmentCollectionPostsBinding.inflate(inflater, container, false);
root = binding.getRoot();
return root;
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
if (!shouldRefresh) return;
binding.swipeRefreshLayout.setOnRefreshListener(this);
init();
shouldRefresh = false;
}
@Override
public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) {
inflater.inflate(R.menu.topic_posts_menu, menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
if (item.getItemId() == R.id.layout) {
showPostsLayoutPreferences();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onResume() {
super.onResume();
fragmentActivity.setToolbar(binding.toolbar);
}
@Override
public void onRefresh() {
binding.posts.refresh();
}
@Override
public void onDestroy() {
super.onDestroy();
resetToolbar();
}
@Override
public void onDestroyView() {
super.onDestroyView();
resetToolbar();
}
@Override
public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
final boolean granted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
final Context context = getContext();
if (context == null) return;
if (requestCode == STORAGE_PERM_REQUEST_CODE && granted) {
if (downloadFeedModel == null) return;
DownloadUtils.showDownloadDialog(context, downloadFeedModel, downloadChildPosition);
downloadFeedModel = null;
downloadChildPosition = -1;
return;
}
if (requestCode == STORAGE_PERM_REQUEST_CODE_FOR_SELECTION && granted) {
DownloadUtils.download(context, ImmutableList.copyOf(selectedFeedModels));
binding.posts.endSelection();
}
}
private void resetToolbar() {
fragmentActivity.resetToolbar();
}
private void init() {
if (getArguments() == null) return;
final CollectionPostsFragmentArgs fragmentArgs = CollectionPostsFragmentArgs.fromBundle(getArguments());
savedCollection = fragmentArgs.getSavedCollection();
setupToolbar(fragmentArgs.getTitleColor(), fragmentArgs.getBackgroundColor());
setupPosts();
}
private void setupToolbar(final int titleColor, final int backgroundColor) {
if (savedCollection == null) {
return;
}
binding.cover.setTransitionName("collection-" + savedCollection.getId());
fragmentActivity.setToolbar(binding.toolbar);
binding.collapsingToolbarLayout.setTitle(savedCollection.getTitle());
final int collapsedTitleTextColor = ColorUtils.setAlphaComponent(titleColor, 0xFF);
final int expandedTitleTextColor = ColorUtils.setAlphaComponent(titleColor, 0x99);
binding.collapsingToolbarLayout.setExpandedTitleColor(expandedTitleTextColor);
binding.collapsingToolbarLayout.setCollapsedTitleTextColor(collapsedTitleTextColor);
binding.collapsingToolbarLayout.setContentScrimColor(backgroundColor);
final Drawable navigationIcon = binding.toolbar.getNavigationIcon();
final Drawable overflowIcon = binding.toolbar.getOverflowIcon();
if (navigationIcon != null && overflowIcon != null) {
final Drawable navDrawable = navigationIcon.mutate();
final Drawable overflowDrawable = overflowIcon.mutate();
navDrawable.setAlpha(0xFF);
overflowDrawable.setAlpha(0xFF);
final ArgbEvaluator argbEvaluator = new ArgbEvaluator();
binding.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> {
final int totalScrollRange = appBarLayout.getTotalScrollRange();
final float current = totalScrollRange + verticalOffset;
final float fraction = current / totalScrollRange;
final int tempColor = (int) argbEvaluator.evaluate(fraction, collapsedTitleTextColor, expandedTitleTextColor);
navDrawable.setColorFilter(tempColor, PorterDuff.Mode.SRC_ATOP);
overflowDrawable.setColorFilter(tempColor, PorterDuff.Mode.SRC_ATOP);
});
}
final GradientDrawable gd = new GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
new int[]{Color.TRANSPARENT, backgroundColor});
binding.background.setBackground(gd);
setupCover();
}
private void setupCover() {
final String coverUrl = ResponseBodyUtils.getImageUrl(savedCollection.getCoverMedias() == null
? null
: savedCollection.getCoverMedias().get(0));
final DraweeController controller = Fresco
.newDraweeControllerBuilder()
.setOldController(binding.cover.getController())
.setUri(coverUrl)
.setControllerListener(new BaseControllerListener<ImageInfo>() {
@Override
public void onFailure(final String id, final Throwable throwable) {
super.onFailure(id, throwable);
startPostponedEnterTransition();
}
@Override
public void onFinalImageSet(final String id,
@Nullable final ImageInfo imageInfo,
@Nullable final Animatable animatable) {
startPostponedEnterTransition();
}
})
.build();
binding.cover.setController(controller);
}
private void setupPosts() {
binding.posts.setViewModelStoreOwner(this)
.setLifeCycleOwner(this)
.setPostFetchService(new SavedPostFetchService(0, PostItemType.COLLECTION, true, savedCollection.getId()))
.setLayoutPreferences(layoutPreferences)
.addFetchStatusChangeListener(fetching -> updateSwipeRefreshState())
.setFeedItemCallback(feedItemCallback)
.setSelectionModeCallback(selectionModeCallback)
.init();
binding.swipeRefreshLayout.setRefreshing(true);
}
private void updateSwipeRefreshState() {
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching());
}
private void navigateToProfile(final String username) {
final NavController navController = NavHostFragment.findNavController(this);
final Bundle bundle = new Bundle();
bundle.putString("username", username);
navController.navigate(R.id.action_global_profileFragment, bundle);
}
private void showPostsLayoutPreferences() {
final PostsLayoutPreferencesDialogFragment fragment = new PostsLayoutPreferencesDialogFragment(
Constants.PREF_TOPIC_POSTS_LAYOUT,
preferences -> {
layoutPreferences = preferences;
new Handler().postDelayed(() -> binding.posts.setLayoutPreferences(preferences), 200);
});
fragment.show(getChildFragmentManager(), "posts_layout_preferences");
}
}

View File

@ -0,0 +1,161 @@
package awais.instagrabber.fragments;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.FragmentNavigator;
import androidx.navigation.fragment.NavHostFragment;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import awais.instagrabber.R;
import awais.instagrabber.activities.MainActivity;
import awais.instagrabber.adapters.SavedCollectionsAdapter;
import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration;
import awais.instagrabber.databinding.FragmentSavedCollectionsBinding;
import awais.instagrabber.repositories.responses.StoryStickerResponse;
import awais.instagrabber.repositories.responses.saved.CollectionsListResponse;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.SavedCollectionsViewModel;
import awais.instagrabber.webservices.ProfileService;
import awais.instagrabber.webservices.ServiceCallback;
import static awais.instagrabber.utils.Utils.settingsHelper;
public class SavedCollectionsFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = "SavedCollectionsFragment";
private MainActivity fragmentActivity;
private CoordinatorLayout root;
private FragmentSavedCollectionsBinding binding;
private SavedCollectionsViewModel savedCollectionsViewModel;
private boolean shouldRefresh = true;
private ProfileService profileService;
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
fragmentActivity = (MainActivity) requireActivity();
profileService = ProfileService.getInstance();
setHasOptionsMenu(true);
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
final ViewGroup container,
final Bundle savedInstanceState) {
if (root != null) {
shouldRefresh = false;
return root;
}
binding = FragmentSavedCollectionsBinding.inflate(inflater, container, false);
root = binding.getRoot();
return root;
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
if (!shouldRefresh) return;
binding.swipeRefreshLayout.setOnRefreshListener(this);
init();
shouldRefresh = false;
}
@Override
public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) {
inflater.inflate(R.menu.saved_collection_menu, menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
if (item.getItemId() == R.id.add) {
final Context context = getContext();
final EditText input = new EditText(context);
new AlertDialog.Builder(context)
.setTitle(R.string.saved_create_collection)
.setView(input)
.setPositiveButton(R.string.confirm, (d, w) -> {
final String cookie = settingsHelper.getString(Constants.COOKIE);
profileService.createCollection(
input.getText().toString(),
settingsHelper.getString(Constants.DEVICE_UUID),
CookieUtils.getUserIdFromCookie(cookie),
CookieUtils.getCsrfTokenFromCookie(cookie),
new ServiceCallback<String>() {
@Override
public void onSuccess(final String result) {
onRefresh();
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error creating collection", t);
Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
})
.setNegativeButton(R.string.cancel, null)
.show();
return true;
}
return false;
}
private void init() {
setupTopics();
fetchTopics(null);
}
@Override
public void onRefresh() {
fetchTopics(null);
}
public void setupTopics() {
savedCollectionsViewModel = new ViewModelProvider(fragmentActivity).get(SavedCollectionsViewModel.class);
binding.topicsRecyclerView.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(2)));
final SavedCollectionsAdapter adapter = new SavedCollectionsAdapter((topicCluster, root, cover, title, titleColor, backgroundColor) -> {
final FragmentNavigator.Extras.Builder builder = new FragmentNavigator.Extras.Builder()
.addSharedElement(cover, "collection-" + topicCluster.getId());
final SavedCollectionsFragmentDirections.ActionSavedCollectionsFragmentToCollectionPostsFragment action = SavedCollectionsFragmentDirections
.actionSavedCollectionsFragmentToCollectionPostsFragment(topicCluster, titleColor, backgroundColor);
NavHostFragment.findNavController(this).navigate(action, builder.build());
});
binding.topicsRecyclerView.setAdapter(adapter);
savedCollectionsViewModel.getList().observe(getViewLifecycleOwner(), adapter::submitList);
}
private void fetchTopics(final String maxId) {
binding.swipeRefreshLayout.setRefreshing(true);
profileService.fetchCollections(maxId, new ServiceCallback<CollectionsListResponse>() {
@Override
public void onSuccess(final CollectionsListResponse result) {
if (result == null) return;
savedCollectionsViewModel.getList().postValue(result.getItems());
binding.swipeRefreshLayout.setRefreshing(false);
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "onFailure", t);
binding.swipeRefreshLayout.setRefreshing(false);
}
});
}
}

View File

@ -286,7 +286,7 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL
private void setupPosts() {
binding.posts.setViewModelStoreOwner(this)
.setLifeCycleOwner(this)
.setPostFetchService(new SavedPostFetchService(profileId, type, isLoggedIn))
.setPostFetchService(new SavedPostFetchService(profileId, type, isLoggedIn, null))
.setLayoutPreferences(layoutPreferences)
.addFetchStatusChangeListener(fetching -> updateSwipeRefreshState())
.setFeedItemCallback(feedItemCallback)

View File

@ -946,9 +946,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
}
});
profileDetailsBinding.btnSaved.setOnClickListener(v -> {
final NavDirections action = ProfileFragmentDirections.actionProfileFragmentToSavedViewerFragment(profileModel.getUsername(),
profileModel.getPk(),
PostItemType.SAVED);
final NavDirections action = ProfileFragmentDirections.actionGlobalSavedCollectionsFragment();
NavHostFragment.findNavController(this).navigate(action);
});
profileDetailsBinding.btnLiked.setOnClickListener(v -> {

View File

@ -7,6 +7,7 @@ public enum PostItemType implements Serializable {
DISCOVER,
FEED,
SAVED,
COLLECTION,
LIKED,
TAGGED,
HASHTAG,

View File

@ -2,10 +2,14 @@ package awais.instagrabber.repositories;
import java.util.Map;
import awais.instagrabber.repositories.responses.saved.CollectionsListResponse;
import awais.instagrabber.repositories.responses.UserFeedResponse;
import retrofit2.Call;
import retrofit2.http.FieldMap;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.POST;
import retrofit2.http.QueryMap;
public interface ProfileRepository {
@ -16,9 +20,20 @@ public interface ProfileRepository {
@GET("/api/v1/feed/saved/")
Call<UserFeedResponse> fetchSaved(@QueryMap Map<String, String> queryParams);
@GET("/api/v1/feed/collection/{collectionId}/")
Call<UserFeedResponse> fetchSavedCollection(@Path("collectionId") final String collectionId,
@QueryMap Map<String, String> queryParams);
@GET("/api/v1/feed/liked/")
Call<UserFeedResponse> fetchLiked(@QueryMap Map<String, String> queryParams);
@GET("/api/v1/usertags/{profileId}/feed/")
Call<UserFeedResponse> fetchTagged(@Path("profileId") final long profileId, @QueryMap Map<String, String> queryParams);
@GET("/api/v1/collections/list/")
Call<CollectionsListResponse> fetchCollections(@QueryMap Map<String, String> queryParams);
@FormUrlEncoded
@POST("/api/v1/collections/create/")
Call<String> createCollection(@FieldMap Map<String, String> signedForm);
}

View File

@ -0,0 +1,50 @@
package awais.instagrabber.repositories.responses.saved;
import java.util.List;
public class CollectionsListResponse {
private final boolean moreAvailable;
private final String nextMaxId;
private final String maxId;
private final String status;
// private final int numResults;
private final List<SavedCollection> items;
public CollectionsListResponse(final boolean moreAvailable,
final String nextMaxId,
final String maxId,
final String status,
// final int numResults,
final List<SavedCollection> items) {
this.moreAvailable = moreAvailable;
this.nextMaxId = nextMaxId;
this.maxId = maxId;
this.status = status;
// this.numResults = numResults;
this.items = items;
}
public boolean isMoreAvailable() {
return moreAvailable;
}
public String getNextMaxId() {
return nextMaxId;
}
public String getMaxId() {
return maxId;
}
public String getStatus() {
return status;
}
// public int getNumResults() {
// return numResults;
// }
public List<SavedCollection> getItems() {
return items;
}
}

View File

@ -0,0 +1,46 @@
package awais.instagrabber.repositories.responses.saved;
import java.io.Serializable;
import java.util.List;
import awais.instagrabber.repositories.responses.Media;
public class SavedCollection implements Serializable {
private final String collectionId;
private final String collectionName;
private final String collectionType;
private final int collectionMediacount;
private final List<Media> coverMediaList;
public SavedCollection(final String collectionId,
final String collectionName,
final String collectionType,
final int collectionMediacount,
final List<Media> coverMediaList) {
this.collectionId = collectionId;
this.collectionName = collectionName;
this.collectionType = collectionType;
this.collectionMediacount = collectionMediacount;
this.coverMediaList = coverMediaList;
}
public String getId() {
return collectionId;
}
public String getTitle() {
return collectionName;
}
public String getType() {
return collectionType;
}
public int getMediaCount() {
return collectionMediacount;
}
public List<Media> getCoverMedias() {
return coverMediaList;
}
}

View File

@ -0,0 +1,19 @@
package awais.instagrabber.viewmodels;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import java.util.List;
import awais.instagrabber.repositories.responses.saved.SavedCollection;
public class SavedCollectionsViewModel extends ViewModel {
private MutableLiveData<List<SavedCollection>> list;
public MutableLiveData<List<SavedCollection>> getList() {
if (list == null) {
list = new MutableLiveData<>();
}
return list;
}
}

View File

@ -4,10 +4,15 @@ import androidx.annotation.NonNull;
import com.google.common.collect.ImmutableMap;
import java.util.HashMap;
import java.util.Map;
import awais.instagrabber.repositories.ProfileRepository;
import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.repositories.responses.UserFeedResponse;
import awais.instagrabber.repositories.responses.saved.CollectionsListResponse;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@ -68,12 +73,15 @@ public class ProfileService extends BaseService {
}
public void fetchSaved(final String maxId,
final String collectionId,
final ServiceCallback<PostsFetchResponse> callback) {
final ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
Call<UserFeedResponse> request = null;
if (!TextUtils.isEmpty(maxId)) {
builder.put("max_id", maxId);
}
final Call<UserFeedResponse> request = repository.fetchSaved(builder.build());
if (TextUtils.isEmpty(collectionId) || collectionId.equals("ALL_MEDIA_AUTO_COLLECTION")) request = repository.fetchSaved(builder.build());
else request = repository.fetchSavedCollection(collectionId, builder.build());
request.enqueue(new Callback<UserFeedResponse>() {
@Override
public void onResponse(@NonNull final Call<UserFeedResponse> call, @NonNull final Response<UserFeedResponse> response) {
@ -99,6 +107,71 @@ public class ProfileService extends BaseService {
});
}
public void fetchCollections(final String maxId,
final ServiceCallback<CollectionsListResponse> callback) {
final ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
if (!TextUtils.isEmpty(maxId)) {
builder.put("max_id", maxId);
}
builder.put("collection_types", "[\"ALL_MEDIA_AUTO_COLLECTION\",\"MEDIA\",\"PRODUCT_AUTO_COLLECTION\"]");
final Call<CollectionsListResponse> request = repository.fetchCollections(builder.build());
request.enqueue(new Callback<CollectionsListResponse>() {
@Override
public void onResponse(@NonNull final Call<CollectionsListResponse> call, @NonNull final Response<CollectionsListResponse> response) {
if (callback == null) return;
final CollectionsListResponse collectionsListResponse = response.body();
if (collectionsListResponse == null) {
callback.onSuccess(null);
return;
}
callback.onSuccess(collectionsListResponse);
}
@Override
public void onFailure(@NonNull final Call<CollectionsListResponse> call, @NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
public void createCollection(final String name,
final String deviceUuid,
final long userId,
final String csrfToken,
final ServiceCallback<String> callback) {
final Map<String, Object> form = new HashMap<>(6);
form.put("_csrftoken", csrfToken);
form.put("_uuid", deviceUuid);
form.put("_uid", userId);
form.put("collection_visibility", "0"); // 1 for public, planned for future but currently inexistant
form.put("module_name", "collection_create");
form.put("name", name);
final Map<String, String> signedForm = Utils.sign(form);
final Call<String> request = repository.createCollection(signedForm);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {
if (callback == null) return;
final String collectionsListResponse = response.body();
if (collectionsListResponse == null) {
callback.onSuccess(null);
return;
}
callback.onSuccess(collectionsListResponse);
}
@Override
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
public void fetchLiked(final String maxId,
final ServiceCallback<PostsFetchResponse> callback) {
final ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_collapseMode="parallax">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/cover"
android:layout_width="match_parent"
android:layout_height="200dp"
app:actualImageScaleType="centerCrop"
tools:background="@mipmap/ic_launcher" />
<View
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<awais.instagrabber.customviews.PostsRecyclerView
android:id="@+id/posts"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/topics_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="10"
tools:listitem="@layout/item_discover_topic" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/add"
android:icon="@drawable/ic_add"
android:title="@string/saved_create_collection"
app:showAsAction="always" />
</menu>

View File

@ -82,6 +82,12 @@
app:nullable="false" />
</action>
<include app:graph="@navigation/saved_nav_graph" />
<action
android:id="@+id/action_global_savedCollectionsFragment"
app:destination="@id/saved_nav_graph" />
<fragment
android:id="@+id/profileFragment"
android:name="awais.instagrabber.fragments.main.ProfileFragment"

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/saved_nav_graph"
app:startDestination="@id/savedCollectionsFragment">
<action
android:id="@+id/action_global_hashTagFragment"
app:destination="@id/hashtag_nav_graph">
<argument
android:name="hashtag"
app:argType="string"
app:nullable="false" />
</action>
<action
android:id="@+id/action_global_profileFragment"
app:destination="@id/profile_nav_graph">
<argument
android:name="username"
app:argType="string"
app:nullable="true" />
</action>
<action
android:id="@+id/action_global_locationFragment"
app:destination="@id/location_nav_graph">
<argument
android:name="locationId"
app:argType="long" />
</action>
<include app:graph="@navigation/comments_nav_graph" />
<action
android:id="@+id/action_global_commentsViewerFragment"
app:destination="@id/comments_nav_graph">
<argument
android:name="shortCode"
app:argType="string"
app:nullable="false" />
<argument
android:name="postId"
app:argType="string"
app:nullable="false" />
<argument
android:name="postUserId"
app:argType="long" />
</action>
<include app:graph="@navigation/likes_nav_graph" />
<action
android:id="@+id/action_global_likesViewerFragment"
app:destination="@id/likes_nav_graph">
<argument
android:name="postId"
app:argType="string"
app:nullable="false" />
<argument
android:name="isComment"
app:argType="boolean"
app:nullable="false" />
</action>
<action
android:id="@+id/action_global_notificationsViewerFragment"
app:destination="@id/notification_viewer_nav_graph">
<argument
android:name="type"
app:argType="string"
app:nullable="false" />
</action>
<fragment
android:id="@+id/savedCollectionsFragment"
android:name="awais.instagrabber.fragments.SavedCollectionsFragment"
android:label="@string/saved"
tools:layout="@layout/fragment_saved_collections" >
<action
android:id="@+id/action_savedCollectionsFragment_to_collectionPostsFragment"
app:destination="@id/collectionPostsFragment" />
</fragment>
<fragment
android:id="@+id/collectionPostsFragment"
android:name="awais.instagrabber.fragments.CollectionPostsFragment"
tools:layout="@layout/fragment_collection_posts">
<argument
android:name="savedCollection"
app:argType="awais.instagrabber.repositories.responses.saved.SavedCollection" />
<argument
android:name="titleColor"
app:argType="integer" />
<argument
android:name="backgroundColor"
app:argType="integer" />
</fragment>
</navigation>

View File

@ -97,6 +97,7 @@
<string name="remove_all_acc">Remove all accounts</string>
<string name="remove_all_acc_warning">This will remove all added accounts from the app!\nTo remove just one account, long tap the account from the account switcher dialog.\nDo you want to continue?</string>
<string name="time_settings">Date format</string>
<string name="saved_create_collection">Create new collection</string>
<string name="liked">Liked</string>
<string name="saved">Saved</string>
<string name="tagged">Tagged</string>