diff --git a/app/src/main/java/awais/instagrabber/adapters/SavedCollectionsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/SavedCollectionsAdapter.java index f708a6d5..8d9ff290 100644 --- a/app/src/main/java/awais/instagrabber/adapters/SavedCollectionsAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/SavedCollectionsAdapter.java @@ -22,10 +22,13 @@ public class SavedCollectionsAdapter extends ListAdapter items = result.getItems(); + final List items = result.getItems(); final List posts; if (items == null) { posts = Collections.emptyList(); } else { posts = items.stream() - .map(TopicalExploreItem::getMedia) + .map(WrappedMedia::getMedia) .filter(Objects::nonNull) .collect(Collectors.toList()); } diff --git a/app/src/main/java/awais/instagrabber/fragments/CollectionPostsFragment.java b/app/src/main/java/awais/instagrabber/fragments/CollectionPostsFragment.java index 82487b3c..ee154b63 100644 --- a/app/src/main/java/awais/instagrabber/fragments/CollectionPostsFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/CollectionPostsFragment.java @@ -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 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() { + @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() { + @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() diff --git a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java index 1a718e6b..74bee61d 100644 --- a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java @@ -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 backStackSavedStateResultLiveData; + private final Observer 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; }); } diff --git a/app/src/main/java/awais/instagrabber/fragments/SavedCollectionsFragment.java b/app/src/main/java/awais/instagrabber/fragments/SavedCollectionsFragment.java index cf3de76e..b7b0ed5c 100644 --- a/app/src/main/java/awais/instagrabber/fragments/SavedCollectionsFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/SavedCollectionsFragment.java @@ -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); + } } diff --git a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java index 0752cb29..b6a80087 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -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 -> { diff --git a/app/src/main/java/awais/instagrabber/repositories/CollectionRepository.java b/app/src/main/java/awais/instagrabber/repositories/CollectionRepository.java new file mode 100644 index 00000000..3afca7e0 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/CollectionRepository.java @@ -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 changeCollection(@Path("id") String id, + @Path("action") String action, + @FieldMap Map signedForm); +} diff --git a/app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java b/app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java index 57071f88..38fdffc7 100644 --- a/app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java @@ -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 fetch(@Path("uid") final long uid, @QueryMap Map queryParams); @GET("/api/v1/feed/saved/") - Call fetchSaved(@QueryMap Map queryParams); + Call fetchSaved(@QueryMap Map queryParams); @GET("/api/v1/feed/collection/{collectionId}/") - Call fetchSavedCollection(@Path("collectionId") final String collectionId, - @QueryMap Map queryParams); + Call fetchSavedCollection(@Path("collectionId") final String collectionId, + @QueryMap Map queryParams); @GET("/api/v1/feed/liked/") Call fetchLiked(@QueryMap Map queryParams); diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/WrappedFeedResponse.java b/app/src/main/java/awais/instagrabber/repositories/responses/WrappedFeedResponse.java new file mode 100644 index 00000000..e40b584d --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/WrappedFeedResponse.java @@ -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 items; + + public WrappedFeedResponse(final int numResults, + final String nextMaxId, + final boolean moreAvailable, + final String status, + final List 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 getItems() { + return items; + } +} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/WrappedMedia.java b/app/src/main/java/awais/instagrabber/repositories/responses/WrappedMedia.java new file mode 100644 index 00000000..2bfb1fa1 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/WrappedMedia.java @@ -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; + } +} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/discover/TopicalExploreFeedResponse.java b/app/src/main/java/awais/instagrabber/repositories/responses/discover/TopicalExploreFeedResponse.java index 26aa9aa9..f3e7b60f 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/discover/TopicalExploreFeedResponse.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/discover/TopicalExploreFeedResponse.java @@ -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 clusters; - private final List items; + private final List items; public TopicalExploreFeedResponse(final boolean moreAvailable, final String nextMaxId, @@ -17,7 +18,7 @@ public class TopicalExploreFeedResponse { final String status, final int numResults, final List clusters, - final List items) { + final List items) { this.moreAvailable = moreAvailable; this.nextMaxId = nextMaxId; this.maxId = maxId; @@ -51,7 +52,7 @@ public class TopicalExploreFeedResponse { return clusters; } - public List getItems() { + public List getItems() { return items; } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/discover/TopicalExploreItem.java b/app/src/main/java/awais/instagrabber/repositories/responses/discover/TopicalExploreItem.java deleted file mode 100644 index 242790a3..00000000 --- a/app/src/main/java/awais/instagrabber/repositories/responses/discover/TopicalExploreItem.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/saved/SavedCollection.java b/app/src/main/java/awais/instagrabber/repositories/responses/saved/SavedCollection.java index 9e1c264f..2bedfb77 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/saved/SavedCollection.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/saved/SavedCollection.java @@ -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 coverMediaList; public SavedCollection(final String collectionId, final String collectionName, final String collectionType, final int collectionMediacount, + final Media coverMedia, final List 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 getCoverMedias() { return coverMediaList; } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.java index b17a8d75..41fd7930 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/PostViewV2ViewModel.java @@ -188,27 +188,33 @@ public class PostViewV2ViewModel extends ViewModel { @NonNull public LiveData> toggleSave() { if (!media.hasViewerSaved()) { - return save(); + return save(null, false); } return unsave(); } - public LiveData> save() { + @NonNull + public LiveData> toggleSave(final String collection, final boolean ignoreSaveState) { + return save(collection, ignoreSaveState); + } + + public LiveData> save(final String collection, final boolean ignoreSaveState) { final MutableLiveData> 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> unsave() { final MutableLiveData> 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 getSaveUnsaveCallback(final MutableLiveData> data) { + private ServiceCallback getSaveUnsaveCallback(final MutableLiveData> data, + final boolean ignoreSaveState) { return new ServiceCallback() { @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()); } diff --git a/app/src/main/java/awais/instagrabber/webservices/CollectionService.java b/app/src/main/java/awais/instagrabber/webservices/CollectionService.java new file mode 100644 index 00000000..faafb6d0 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/webservices/CollectionService.java @@ -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 posts, + final ServiceCallback callback) { + final Map form = new HashMap<>(2); + form.put("module_name", "feed_saved_add_to_collection"); + final List 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 callback) { + final Map form = new HashMap<>(1); + form.put("name", name); + changeCollection(collectionId, "edit", form, callback); + } + + public void deleteCollection(final String collectionId, + final ServiceCallback callback) { + changeCollection(collectionId, "delete", null, callback); + } + + public void changeCollection(final String collectionId, + final String action, + final Map options, + final ServiceCallback callback) { + final Map form = new HashMap<>(); + form.put("_csrftoken", csrfToken); + form.put("_uuid", deviceUuid); + form.put("_uid", userId); + if (options != null) form.putAll(options); + final Map signedForm = Utils.sign(form); + final Call request = repository.changeCollection(collectionId, action, signedForm); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response 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 call, @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + } +} diff --git a/app/src/main/java/awais/instagrabber/webservices/MediaService.java b/app/src/main/java/awais/instagrabber/webservices/MediaService.java index 1c24f95d..210c5b32 100644 --- a/app/src/main/java/awais/instagrabber/webservices/MediaService.java +++ b/app/src/main/java/awais/instagrabber/webservices/MediaService.java @@ -102,33 +102,37 @@ public class MediaService extends BaseService { public void like(final String mediaId, final ServiceCallback callback) { - action(mediaId, "like", callback); + action(mediaId, "like", null, callback); } public void unlike(final String mediaId, final ServiceCallback callback) { - action(mediaId, "unlike", callback); + action(mediaId, "unlike", null, callback); } public void save(final String mediaId, + final String collection, final ServiceCallback callback) { - action(mediaId, "save", callback); + action(mediaId, "save", collection, callback); } public void unsave(final String mediaId, final ServiceCallback callback) { - action(mediaId, "unsave", callback); + action(mediaId, "unsave", null, callback); } private void action(final String mediaId, final String action, + final String collection, final ServiceCallback callback) { - final Map form = new HashMap<>(4); + final Map 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 signedForm = Utils.sign(form); final Call request = repository.action(action, mediaId, signedForm); request.enqueue(new Callback() { diff --git a/app/src/main/java/awais/instagrabber/webservices/ProfileService.java b/app/src/main/java/awais/instagrabber/webservices/ProfileService.java index 4480223a..65f787a6 100644 --- a/app/src/main/java/awais/instagrabber/webservices/ProfileService.java +++ b/app/src/main/java/awais/instagrabber/webservices/ProfileService.java @@ -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 callback) { final ImmutableMap.Builder builder = ImmutableMap.builder(); - Call request = null; + Call 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() { + request.enqueue(new Callback() { @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { + public void onResponse(@NonNull final Call call, @NonNull final Response response) { if (callback == null) return; - final UserFeedResponse userFeedResponse = response.body(); + final WrappedFeedResponse userFeedResponse = response.body(); if (userFeedResponse == null) { callback.onSuccess(null); return; } + final List items = userFeedResponse.getItems(); + final List 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 call, @NonNull final Throwable t) { + public void onFailure(@NonNull final Call 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 callback) { final ImmutableMap.Builder builder = ImmutableMap.builder(); diff --git a/app/src/main/res/menu/collection_posts_menu.xml b/app/src/main/res/menu/collection_posts_menu.xml new file mode 100644 index 00000000..5759b068 --- /dev/null +++ b/app/src/main/res/menu/collection_posts_menu.xml @@ -0,0 +1,21 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/saved_collection_select_menu.xml b/app/src/main/res/menu/saved_collection_select_menu.xml new file mode 100644 index 00000000..466f28f5 --- /dev/null +++ b/app/src/main/res/menu/saved_collection_select_menu.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/direct_messages_nav_graph.xml b/app/src/main/res/navigation/direct_messages_nav_graph.xml index 819a428c..470ccdf8 100644 --- a/app/src/main/res/navigation/direct_messages_nav_graph.xml +++ b/app/src/main/res/navigation/direct_messages_nav_graph.xml @@ -76,6 +76,16 @@ app:nullable="false" /> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/navigation/location_nav_graph.xml b/app/src/main/res/navigation/location_nav_graph.xml index 5d52d904..acc33413 100644 --- a/app/src/main/res/navigation/location_nav_graph.xml +++ b/app/src/main/res/navigation/location_nav_graph.xml @@ -38,6 +38,16 @@ app:nullable="false" /> + + + + + + diff --git a/app/src/main/res/navigation/notification_viewer_nav_graph.xml b/app/src/main/res/navigation/notification_viewer_nav_graph.xml index dffb4a38..af9764f6 100644 --- a/app/src/main/res/navigation/notification_viewer_nav_graph.xml +++ b/app/src/main/res/navigation/notification_viewer_nav_graph.xml @@ -61,6 +61,16 @@ app:nullable="false" /> + + + + + + + app:destination="@id/saved_nav_graph"> + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e8edf4c..876e398d 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -99,6 +99,12 @@ 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? Date format Create new collection + Edit collection name + Delete collection + Are you sure you want to delete this collection? + All contained media will remain in other collections. + Add to collection... + Remove from collection Liked Saved Tagged