more collection features

This commit is contained in:
Austin Huang 2021-01-22 23:33:36 -05:00
parent 6aee7ea863
commit 89441a3562
No known key found for this signature in database
GPG Key ID: 84C23AA04587A91F
29 changed files with 517 additions and 58 deletions

View File

@ -22,10 +22,13 @@ public class SavedCollectionsAdapter extends ListAdapter<SavedCollection, TopicC
@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;
if (oldItem.getCoverMedias() != null && newItem.getCoverMedias() != null
&& oldItem.getCoverMedias().size() == newItem.getCoverMedias().size()) {
return oldItem.getCoverMedias().get(0).getId().equals(newItem.getCoverMedias().get(0).getId());
}
else if (oldItem.getCoverMedia() != null && newItem.getCoverMedia() != null) {
return oldItem.getCoverMedia().getId().equals(newItem.getCoverMedia().getId());
}
return false;
}
};

View File

@ -127,7 +127,7 @@ public class TopicClusterViewHolder extends RecyclerView.ViewHolder {
// binding.title.setTransitionName("title-" + topicCluster.getId());
binding.cover.setTransitionName("cover-" + topicCluster.getId());
final String thumbUrl = ResponseBodyUtils.getThumbUrl(topicCluster.getCoverMedias() == null
? null
? topicCluster.getCoverMedia()
: topicCluster.getCoverMedias().get(0));
if (thumbUrl == null) {
binding.cover.setImageURI((String) null);

View File

@ -9,7 +9,7 @@ import awais.instagrabber.customviews.helpers.PostFetcher;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.discover.TopicalExploreFeedResponse;
import awais.instagrabber.repositories.responses.discover.TopicalExploreItem;
import awais.instagrabber.repositories.responses.WrappedMedia;
import awais.instagrabber.webservices.DiscoverService;
import awais.instagrabber.webservices.ServiceCallback;
@ -35,13 +35,13 @@ public class DiscoverPostFetchService implements PostFetcher.PostFetchService {
}
moreAvailable = result.isMoreAvailable();
topicalExploreRequest.setMaxId(result.getNextMaxId());
final List<TopicalExploreItem> items = result.getItems();
final List<WrappedMedia> items = result.getItems();
final List<Media> posts;
if (items == null) {
posts = Collections.emptyList();
} else {
posts = items.stream()
.map(TopicalExploreItem::getMedia)
.map(WrappedMedia::getMedia)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}

View File

@ -10,6 +10,7 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
@ -17,11 +18,14 @@ 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.activity.OnBackPressedCallback;
import androidx.activity.OnBackPressedDispatcher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.PermissionChecker;
import androidx.core.graphics.ColorUtils;
@ -49,20 +53,23 @@ 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.CookieUtils;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.webservices.CollectionService;
import awais.instagrabber.webservices.ServiceCallback;
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 String TAG = "CollectionPostsFragment";
private static final int STORAGE_PERM_REQUEST_CODE = 8020;
private static final int STORAGE_PERM_REQUEST_CODE_FOR_SELECTION = 8030;
@ -75,6 +82,7 @@ public class CollectionPostsFragment extends Fragment implements SwipeRefreshLay
private Set<Media> selectedFeedModels;
private Media downloadFeedModel;
private int downloadChildPosition = -1;
private CollectionService collectionService;
private PostsLayoutPreferences layoutPreferences = Utils.getPostsLayoutPreferences(Constants.PREF_SAVED_POSTS_LAYOUT);
private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
@ -84,7 +92,7 @@ public class CollectionPostsFragment extends Fragment implements SwipeRefreshLay
}
};
private final PrimaryActionModeCallback multiSelectAction = new PrimaryActionModeCallback(
R.menu.multi_select_download_menu, new PrimaryActionModeCallback.CallbacksHelper() {
R.menu.saved_collection_select_menu, new PrimaryActionModeCallback.CallbacksHelper() {
@Override
public void onDestroy(final ActionMode mode) {
binding.posts.endSelection();
@ -241,6 +249,11 @@ public class CollectionPostsFragment extends Fragment implements SwipeRefreshLay
setSharedElementEnterTransition(transitionSet);
postponeEnterTransition();
setHasOptionsMenu(true);
final String cookie = Utils.settingsHelper.getString(Constants.COOKIE);
final long userId = CookieUtils.getUserIdFromCookie(cookie);
final String deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID);
final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
collectionService = CollectionService.getInstance(deviceUuid, csrfToken, userId);
}
@Nullable
@ -267,7 +280,7 @@ public class CollectionPostsFragment extends Fragment implements SwipeRefreshLay
@Override
public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) {
inflater.inflate(R.menu.topic_posts_menu, menu);
inflater.inflate(R.menu.collection_posts_menu, menu);
}
@Override
@ -276,6 +289,58 @@ public class CollectionPostsFragment extends Fragment implements SwipeRefreshLay
showPostsLayoutPreferences();
return true;
}
else if (item.getItemId() == R.id.delete) {
final Context context = getContext();
new AlertDialog.Builder(context)
.setTitle(R.string.edit_collection)
.setMessage(R.string.delete_collection_note)
.setPositiveButton(R.string.confirm, (d, w) -> {
collectionService.deleteCollection(
savedCollection.getId(),
new ServiceCallback<String>() {
@Override
public void onSuccess(final String result) {
SavedCollectionsFragment.pleaseRefresh = true;
NavHostFragment.findNavController(CollectionPostsFragment.this).navigateUp();
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error deleting collection", t);
Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
})
.setNegativeButton(R.string.cancel, null)
.show();
}
else if (item.getItemId() == R.id.edit) {
final Context context = getContext();
final EditText input = new EditText(context);
new AlertDialog.Builder(context)
.setTitle(R.string.edit_collection)
.setView(input)
.setPositiveButton(R.string.confirm, (d, w) -> {
collectionService.editCollectionName(
savedCollection.getId(),
input.getText().toString(),
new ServiceCallback<String>() {
@Override
public void onSuccess(final String result) {
binding.collapsingToolbarLayout.setTitle(input.getText().toString());
SavedCollectionsFragment.pleaseRefresh = true;
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error editing collection", t);
Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
})
.setNegativeButton(R.string.cancel, null)
.show();
}
return super.onOptionsItemSelected(item);
}
@ -372,7 +437,7 @@ public class CollectionPostsFragment extends Fragment implements SwipeRefreshLay
private void setupCover() {
final String coverUrl = ResponseBodyUtils.getImageUrl(savedCollection.getCoverMedias() == null
? null
? savedCollection.getCoverMedia()
: savedCollection.getCoverMedias().get(0));
final DraweeController controller = Fresco
.newDraweeControllerBuilder()

View File

@ -49,7 +49,10 @@ import androidx.core.view.ViewCompat;
import androidx.core.widget.NestedScrollView;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.RecyclerView;
@ -130,6 +133,17 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment im
private PopupMenu optionsPopup;
private EditTextDialogFragment editTextDialogFragment;
private MutableLiveData<Object> backStackSavedStateResultLiveData;
private final Observer<Object> backStackSavedStateObserver = result -> {
if (result == null) return;
if (result instanceof String) {
final String collection = (String) result;
handleSaveUnsaveResourceLiveData(viewModel.toggleSave(collection, viewModel.getMedia().hasViewerSaved()));
}
// clear result
backStackSavedStateResultLiveData.postValue(null);
};
// private final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener = new VerticalDragHelper.OnVerticalDragListener() {
//
// @Override
@ -305,6 +319,17 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment im
}
}
@Override
public void onResume() {
super.onResume();
final NavController navController = NavHostFragment.findNavController(this);
final NavBackStackEntry backStackEntry = navController.getCurrentBackStackEntry();
if (backStackEntry != null) {
backStackSavedStateResultLiveData = backStackEntry.getSavedStateHandle().getLiveData("collection");
backStackSavedStateResultLiveData.observe(getViewLifecycleOwner(), backStackSavedStateObserver);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
@ -666,7 +691,10 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment im
handleSaveUnsaveResourceLiveData(viewModel.toggleSave());
});
binding.save.setOnLongClickListener(v -> {
Utils.displayToastAboveView(context, v, getString(R.string.save));
final NavController navController = NavHostFragment.findNavController(this);
final Bundle bundle = new Bundle();
bundle.putBoolean("isSaving", true);
navController.navigate(R.id.action_global_savedCollectionsFragment, bundle);
return true;
});
}

View File

@ -17,7 +17,10 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.SavedStateHandle;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.fragment.FragmentNavigator;
import androidx.navigation.fragment.NavHostFragment;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
@ -27,7 +30,6 @@ 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;
@ -40,12 +42,14 @@ import static awais.instagrabber.utils.Utils.settingsHelper;
public class SavedCollectionsFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = "SavedCollectionsFragment";
public static boolean pleaseRefresh = false;
private MainActivity fragmentActivity;
private CoordinatorLayout root;
private FragmentSavedCollectionsBinding binding;
private SavedCollectionsViewModel savedCollectionsViewModel;
private boolean shouldRefresh = true;
private boolean isSaving;
private ProfileService profileService;
@Override
@ -82,6 +86,12 @@ public class SavedCollectionsFragment extends Fragment implements SwipeRefreshLa
inflater.inflate(R.menu.saved_collection_menu, menu);
}
@Override
public void onResume() {
super.onResume();
if (pleaseRefresh) onRefresh();
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
if (item.getItemId() == R.id.add) {
@ -120,6 +130,8 @@ public class SavedCollectionsFragment extends Fragment implements SwipeRefreshLa
private void init() {
setupTopics();
fetchTopics(null);
final SavedCollectionsFragmentArgs fragmentArgs = SavedCollectionsFragmentArgs.fromBundle(getArguments());
isSaving = fragmentArgs.getIsSaving();
}
@Override
@ -131,11 +143,18 @@ public class SavedCollectionsFragment extends Fragment implements SwipeRefreshLa
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());
final NavController navController = NavHostFragment.findNavController(this);
if (isSaving) {
setNavControllerResult(navController, topicCluster.getId());
navController.navigateUp();
}
else {
final FragmentNavigator.Extras.Builder builder = new FragmentNavigator.Extras.Builder()
.addSharedElement(cover, "collection-" + topicCluster.getId());
final SavedCollectionsFragmentDirections.ActionSavedCollectionsFragmentToCollectionPostsFragment action = SavedCollectionsFragmentDirections
.actionSavedCollectionsFragmentToCollectionPostsFragment(topicCluster, titleColor, backgroundColor);
navController.navigate(action, builder.build());
}
});
binding.topicsRecyclerView.setAdapter(adapter);
savedCollectionsViewModel.getList().observe(getViewLifecycleOwner(), adapter::submitList);
@ -158,4 +177,11 @@ public class SavedCollectionsFragment extends Fragment implements SwipeRefreshLa
}
});
}
private void setNavControllerResult(@NonNull final NavController navController, final String result) {
final NavBackStackEntry navBackStackEntry = navController.getPreviousBackStackEntry();
if (navBackStackEntry == null) return;
final SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle();
savedStateHandle.set("collection", result);
}
}

View File

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

View File

@ -0,0 +1,23 @@
package awais.instagrabber.repositories;
import java.util.Map;
import awais.instagrabber.repositories.responses.UserFeedResponse;
import awais.instagrabber.repositories.responses.WrappedFeedResponse;
import awais.instagrabber.repositories.responses.saved.CollectionsListResponse;
import retrofit2.Call;
import retrofit2.http.FieldMap;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;
import retrofit2.http.QueryMap;
public interface CollectionRepository {
@FormUrlEncoded
@POST("/api/v1/collections/{id}/{action}/")
Call<String> changeCollection(@Path("id") String id,
@Path("action") String action,
@FieldMap Map<String, String> signedForm);
}

View File

@ -2,6 +2,7 @@ package awais.instagrabber.repositories;
import java.util.Map;
import awais.instagrabber.repositories.responses.WrappedFeedResponse;
import awais.instagrabber.repositories.responses.saved.CollectionsListResponse;
import awais.instagrabber.repositories.responses.UserFeedResponse;
import retrofit2.Call;
@ -18,11 +19,11 @@ public interface ProfileRepository {
Call<UserFeedResponse> fetch(@Path("uid") final long uid, @QueryMap Map<String, String> queryParams);
@GET("/api/v1/feed/saved/")
Call<UserFeedResponse> fetchSaved(@QueryMap Map<String, String> queryParams);
Call<WrappedFeedResponse> 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);
Call<WrappedFeedResponse> fetchSavedCollection(@Path("collectionId") final String collectionId,
@QueryMap Map<String, String> queryParams);
@GET("/api/v1/feed/liked/")
Call<UserFeedResponse> fetchLiked(@QueryMap Map<String, String> queryParams);

View File

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

View File

@ -0,0 +1,13 @@
package awais.instagrabber.repositories.responses;
public class WrappedMedia {
private final Media media;
public WrappedMedia(final Media media) {
this.media = media;
}
public Media getMedia() {
return media;
}
}

View File

@ -1,6 +1,7 @@
package awais.instagrabber.repositories.responses.discover;
import java.util.List;
import awais.instagrabber.repositories.responses.WrappedMedia;
public class TopicalExploreFeedResponse {
private final boolean moreAvailable;
@ -9,7 +10,7 @@ public class TopicalExploreFeedResponse {
private final String status;
private final int numResults;
private final List<TopicCluster> clusters;
private final List<TopicalExploreItem> items;
private final List<WrappedMedia> items;
public TopicalExploreFeedResponse(final boolean moreAvailable,
final String nextMaxId,
@ -17,7 +18,7 @@ public class TopicalExploreFeedResponse {
final String status,
final int numResults,
final List<TopicCluster> clusters,
final List<TopicalExploreItem> items) {
final List<WrappedMedia> items) {
this.moreAvailable = moreAvailable;
this.nextMaxId = nextMaxId;
this.maxId = maxId;
@ -51,7 +52,7 @@ public class TopicalExploreFeedResponse {
return clusters;
}
public List<TopicalExploreItem> getItems() {
public List<WrappedMedia> getItems() {
return items;
}
}

View File

@ -1,15 +0,0 @@
package awais.instagrabber.repositories.responses.discover;
import awais.instagrabber.repositories.responses.Media;
public class TopicalExploreItem {
private final Media media;
public TopicalExploreItem(final Media media) {
this.media = media;
}
public Media getMedia() {
return media;
}
}

View File

@ -10,17 +10,20 @@ public class SavedCollection implements Serializable {
private final String collectionName;
private final String collectionType;
private final int collectionMediacount;
private final Media coverMedia;
private final List<Media> coverMediaList;
public SavedCollection(final String collectionId,
final String collectionName,
final String collectionType,
final int collectionMediacount,
final Media coverMedia,
final List<Media> coverMediaList) {
this.collectionId = collectionId;
this.collectionName = collectionName;
this.collectionType = collectionType;
this.collectionMediacount = collectionMediacount;
this.coverMedia = coverMedia;
this.coverMediaList = coverMediaList;
}
@ -40,6 +43,11 @@ public class SavedCollection implements Serializable {
return collectionMediacount;
}
// check the list first, then the single
// i have no idea what condition is required
public Media getCoverMedia() { return coverMedia; }
public List<Media> getCoverMedias() {
return coverMediaList;
}

View File

@ -188,27 +188,33 @@ public class PostViewV2ViewModel extends ViewModel {
@NonNull
public LiveData<Resource<Object>> toggleSave() {
if (!media.hasViewerSaved()) {
return save();
return save(null, false);
}
return unsave();
}
public LiveData<Resource<Object>> save() {
@NonNull
public LiveData<Resource<Object>> toggleSave(final String collection, final boolean ignoreSaveState) {
return save(collection, ignoreSaveState);
}
public LiveData<Resource<Object>> save(final String collection, final boolean ignoreSaveState) {
final MutableLiveData<Resource<Object>> data = new MutableLiveData<>();
data.postValue(Resource.loading(null));
mediaService.save(media.getPk(), getSaveUnsaveCallback(data));
mediaService.save(media.getPk(), collection, getSaveUnsaveCallback(data, ignoreSaveState));
return data;
}
public LiveData<Resource<Object>> unsave() {
final MutableLiveData<Resource<Object>> data = new MutableLiveData<>();
data.postValue(Resource.loading(null));
mediaService.unsave(media.getPk(), getSaveUnsaveCallback(data));
mediaService.unsave(media.getPk(), getSaveUnsaveCallback(data, false));
return data;
}
@NonNull
private ServiceCallback<Boolean> getSaveUnsaveCallback(final MutableLiveData<Resource<Object>> data) {
private ServiceCallback<Boolean> getSaveUnsaveCallback(final MutableLiveData<Resource<Object>> data,
final boolean ignoreSaveState) {
return new ServiceCallback<Boolean>() {
@Override
public void onSuccess(final Boolean result) {
@ -217,7 +223,7 @@ public class PostViewV2ViewModel extends ViewModel {
return;
}
data.postValue(Resource.success(true));
media.setHasViewerSaved(!media.hasViewerSaved());
if (!ignoreSaveState) media.setHasViewerSaved(!media.hasViewerSaved());
saved.postValue(media.hasViewerSaved());
}

View File

@ -0,0 +1,120 @@
package awais.instagrabber.webservices;
import androidx.annotation.NonNull;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import awais.instagrabber.repositories.CollectionRepository;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.Utils;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
public class CollectionService extends BaseService {
private static final String TAG = "ProfileService";
private final CollectionRepository repository;
private final String deviceUuid, csrfToken;
private final long userId;
private static CollectionService instance;
private CollectionService(final String deviceUuid,
final String csrfToken,
final long userId) {
this.deviceUuid = deviceUuid;
this.csrfToken = csrfToken;
this.userId = userId;
final Retrofit retrofit = getRetrofitBuilder()
.baseUrl("https://i.instagram.com")
.build();
repository = retrofit.create(CollectionRepository.class);
}
public String getCsrfToken() {
return csrfToken;
}
public String getDeviceUuid() {
return deviceUuid;
}
public long getUserId() {
return userId;
}
public static CollectionService getInstance(final String deviceUuid, final String csrfToken, final long userId) {
if (instance == null
|| !Objects.equals(instance.getCsrfToken(), csrfToken)
|| !Objects.equals(instance.getDeviceUuid(), deviceUuid)
|| !Objects.equals(instance.getUserId(), userId)) {
instance = new CollectionService(deviceUuid, csrfToken, userId);
}
return instance;
}
public void addPostsToCollection(final String collectionId,
final List<Media> posts,
final ServiceCallback<String> callback) {
final Map<String, Object> form = new HashMap<>(2);
form.put("module_name", "feed_saved_add_to_collection");
final List<String> ids;
ids = posts.stream()
.map(Media::getPk)
.filter(Objects::nonNull)
.collect(Collectors.toList());
form.put("added_media_ids", "[" + String.join(",", ids) + "]");
changeCollection(collectionId, "edit", form, callback);
}
public void editCollectionName(final String collectionId,
final String name,
final ServiceCallback<String> callback) {
final Map<String, Object> form = new HashMap<>(1);
form.put("name", name);
changeCollection(collectionId, "edit", form, callback);
}
public void deleteCollection(final String collectionId,
final ServiceCallback<String> callback) {
changeCollection(collectionId, "delete", null, callback);
}
public void changeCollection(final String collectionId,
final String action,
final Map<String, Object> options,
final ServiceCallback<String> callback) {
final Map<String, Object> form = new HashMap<>();
form.put("_csrftoken", csrfToken);
form.put("_uuid", deviceUuid);
form.put("_uid", userId);
if (options != null) form.putAll(options);
final Map<String, String> signedForm = Utils.sign(form);
final Call<String> request = repository.changeCollection(collectionId, action, 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);
}
}
});
}
}

View File

@ -102,33 +102,37 @@ public class MediaService extends BaseService {
public void like(final String mediaId,
final ServiceCallback<Boolean> callback) {
action(mediaId, "like", callback);
action(mediaId, "like", null, callback);
}
public void unlike(final String mediaId,
final ServiceCallback<Boolean> callback) {
action(mediaId, "unlike", callback);
action(mediaId, "unlike", null, callback);
}
public void save(final String mediaId,
final String collection,
final ServiceCallback<Boolean> callback) {
action(mediaId, "save", callback);
action(mediaId, "save", collection, callback);
}
public void unsave(final String mediaId,
final ServiceCallback<Boolean> callback) {
action(mediaId, "unsave", callback);
action(mediaId, "unsave", null, callback);
}
private void action(final String mediaId,
final String action,
final String collection,
final ServiceCallback<Boolean> callback) {
final Map<String, Object> form = new HashMap<>(4);
final Map<String, Object> form = new HashMap<>();
form.put("media_id", mediaId);
form.put("_csrftoken", csrfToken);
form.put("_uid", userId);
form.put("_uuid", deviceUuid);
// form.put("radio_type", "wifi-none");
if (action.equals("save") && !TextUtils.isEmpty(collection)) form.put("added_collection_ids", "[" + collection + "]");
// there also exists "removed_collection_ids" which can be used with "save" and "unsave"
final Map<String, String> signedForm = Utils.sign(form);
final Call<String> request = repository.action(action, mediaId, signedForm);
request.enqueue(new Callback<String>() {

View File

@ -4,12 +4,19 @@ import androidx.annotation.NonNull;
import com.google.common.collect.ImmutableMap;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import awais.instagrabber.repositories.ProfileRepository;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.repositories.responses.UserFeedResponse;
import awais.instagrabber.repositories.responses.WrappedFeedResponse;
import awais.instagrabber.repositories.responses.WrappedMedia;
import awais.instagrabber.repositories.responses.saved.CollectionsListResponse;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
@ -76,30 +83,40 @@ public class ProfileService extends BaseService {
final String collectionId,
final ServiceCallback<PostsFetchResponse> callback) {
final ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
Call<UserFeedResponse> request = null;
Call<WrappedFeedResponse> request = null;
if (!TextUtils.isEmpty(maxId)) {
builder.put("max_id", maxId);
}
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>() {
request.enqueue(new Callback<WrappedFeedResponse>() {
@Override
public void onResponse(@NonNull final Call<UserFeedResponse> call, @NonNull final Response<UserFeedResponse> response) {
public void onResponse(@NonNull final Call<WrappedFeedResponse> call, @NonNull final Response<WrappedFeedResponse> response) {
if (callback == null) return;
final UserFeedResponse userFeedResponse = response.body();
final WrappedFeedResponse userFeedResponse = response.body();
if (userFeedResponse == null) {
callback.onSuccess(null);
return;
}
final List<WrappedMedia> items = userFeedResponse.getItems();
final List<Media> posts;
if (items == null) {
posts = Collections.emptyList();
} else {
posts = items.stream()
.map(WrappedMedia::getMedia)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
callback.onSuccess(new PostsFetchResponse(
userFeedResponse.getItems(),
posts,
userFeedResponse.isMoreAvailable(),
userFeedResponse.getNextMaxId()
));
}
@Override
public void onFailure(@NonNull final Call<UserFeedResponse> call, @NonNull final Throwable t) {
public void onFailure(@NonNull final Call<WrappedFeedResponse> call, @NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
@ -171,7 +188,6 @@ public class ProfileService extends BaseService {
});
}
public void fetchLiked(final String maxId,
final ServiceCallback<PostsFetchResponse> callback) {
final ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();

View File

@ -0,0 +1,21 @@
<?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/edit"
android:icon="@android:drawable/ic_menu_edit"
android:title="@string/edit_collection"
app:showAsAction="always" />
<item
android:id="@+id/delete"
android:icon="@android:drawable/ic_menu_delete"
android:title="@string/delete_collection"
app:showAsAction="always" />
<item
android:id="@+id/layout"
android:title="@string/layout"
app:showAsAction="never" />
</menu>

View File

@ -0,0 +1,22 @@
<?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/action_add"
android:icon="@drawable/ic_add"
android:title="@string/add_to_collection"
android:titleCondensed="@string/action_download"
app:showAsAction="always" />
<item
android:id="@+id/action_delete"
android:icon="@drawable/ic_cancel"
android:title="@string/remove_from_collection"
android:titleCondensed="@string/action_download"
app:showAsAction="always" />
<item
android:id="@+id/action_download"
android:icon="@drawable/ic_download"
android:title="@string/action_download"
android:titleCondensed="@string/action_download"
app:showAsAction="always" />
</menu>

View File

@ -76,6 +76,16 @@
app:nullable="false" />
</action>
<include app:graph="@navigation/saved_nav_graph" />
<action
android:id="@+id/action_global_savedCollectionsFragment"
app:destination="@id/saved_nav_graph">
<argument
android:name="isSaving"
app:argType="boolean" />
</action>
<include app:graph="@navigation/user_search_nav_graph" />
<action

View File

@ -70,6 +70,16 @@
app:nullable="false" />
</action>
<include app:graph="@navigation/saved_nav_graph" />
<action
android:id="@+id/action_global_savedCollectionsFragment"
app:destination="@id/saved_nav_graph">
<argument
android:name="isSaving"
app:argType="boolean" />
</action>
<include app:graph="@navigation/notification_viewer_nav_graph" />
<action

View File

@ -70,6 +70,16 @@
app:nullable="false" />
</action>
<include app:graph="@navigation/saved_nav_graph" />
<action
android:id="@+id/action_global_savedCollectionsFragment"
app:destination="@id/saved_nav_graph">
<argument
android:name="isSaving"
app:argType="boolean" />
</action>
<include app:graph="@navigation/notification_viewer_nav_graph" />
<action

View File

@ -38,6 +38,16 @@
app:nullable="false" />
</action>
<include app:graph="@navigation/saved_nav_graph" />
<action
android:id="@+id/action_global_savedCollectionsFragment"
app:destination="@id/saved_nav_graph">
<argument
android:name="isSaving"
app:argType="boolean" />
</action>
<action
android:id="@+id/action_global_profileFragment"
app:destination="@id/profile_nav_graph">

View File

@ -38,6 +38,16 @@
app:nullable="false" />
</action>
<include app:graph="@navigation/saved_nav_graph" />
<action
android:id="@+id/action_global_savedCollectionsFragment"
app:destination="@id/saved_nav_graph">
<argument
android:name="isSaving"
app:argType="boolean" />
</action>
<action
android:id="@+id/action_global_profileFragment"
app:destination="@id/profile_nav_graph">

View File

@ -61,6 +61,16 @@
app:nullable="false" />
</action>
<include app:graph="@navigation/saved_nav_graph" />
<action
android:id="@+id/action_global_savedCollectionsFragment"
app:destination="@id/saved_nav_graph">
<argument
android:name="isSaving"
app:argType="boolean" />
</action>
<fragment
android:id="@+id/storyViewerFragment"
android:name="awais.instagrabber.fragments.StoryViewerFragment"

View File

@ -86,7 +86,11 @@
<action
android:id="@+id/action_global_savedCollectionsFragment"
app:destination="@id/saved_nav_graph" />
app:destination="@id/saved_nav_graph">
<argument
android:name="isSaving"
app:argType="boolean" />
</action>
<fragment
android:id="@+id/profileFragment"

View File

@ -78,6 +78,10 @@
android:name="awais.instagrabber.fragments.SavedCollectionsFragment"
android:label="@string/saved"
tools:layout="@layout/fragment_saved_collections" >
<argument
android:name="isSaving"
app:argType="boolean"
android:defaultValue="false" />
<action
android:id="@+id/action_savedCollectionsFragment_to_collectionPostsFragment"
app:destination="@id/collectionPostsFragment" />

View File

@ -99,6 +99,12 @@
<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="edit_collection">Edit collection name</string>
<string name="delete_collection">Delete collection</string>
<string name="delete_collection_confirm">Are you sure you want to delete this collection?</string>
<string name="delete_collection_note">All contained media will remain in other collections.</string>
<string name="add_to_collection">Add to collection...</string>
<string name="remove_from_collection">Remove from collection</string>
<string name="liked">Liked</string>
<string name="saved">Saved</string>
<string name="tagged">Tagged</string>