mirror of
https://github.com/KokaKiwi/BarInsta
synced 2024-11-22 22:57:29 +00:00
more collection features
This commit is contained in:
parent
6aee7ea863
commit
89441a3562
@ -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;
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 -> {
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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>() {
|
||||
|
@ -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();
|
||||
|
21
app/src/main/res/menu/collection_posts_menu.xml
Normal file
21
app/src/main/res/menu/collection_posts_menu.xml
Normal 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>
|
22
app/src/main/res/menu/saved_collection_select_menu.xml
Normal file
22
app/src/main/res/menu/saved_collection_select_menu.xml
Normal 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>
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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" />
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user