Merge remote-tracking branch 'origin/dm-notifications-enhancements' into dm-notifications-enhancements

This commit is contained in:
Ammar Githam 2021-01-24 23:40:56 +09:00
commit ea7236dcc1
44 changed files with 782 additions and 224 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

@ -5,131 +5,81 @@ import android.util.Log;
import androidx.annotation.Nullable;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import java.net.HttpURLConnection;
import java.net.URL;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.repositories.responses.FriendshipStatus;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.NetworkUtils;
import awais.instagrabber.utils.TextUtils;
import awaisomereport.LogCollector;
import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.UserService;
import static awais.instagrabber.utils.Utils.logCollector;
import static awais.instagrabber.utils.Utils.settingsHelper;
public final class ProfileFetcher extends AsyncTask<Void, Void, User> {
public final class ProfileFetcher extends AsyncTask<Void, Void, String> {
private static final String TAG = ProfileFetcher.class.getSimpleName();
private final UserService userService;
private final GraphQLService graphQLService;
private final FetchListener<User> fetchListener;
private final boolean isLoggedIn;
private final String userName;
public ProfileFetcher(String userName, FetchListener<User> fetchListener) {
public ProfileFetcher(final String userName,
final boolean isLoggedIn,
final FetchListener<User> fetchListener) {
this.userName = userName;
this.isLoggedIn = isLoggedIn;
this.fetchListener = fetchListener;
userService = isLoggedIn ? UserService.getInstance() : null;
graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
}
@Nullable
@Override
protected User doInBackground(final Void... voids) {
User result = null;
protected String doInBackground(final Void... voids) {
if (isLoggedIn) {
userService.getUsernameInfo(userName, new ServiceCallback<User>() {
@Override
public void onSuccess(final User user) {
Log.d("austin_debug", user.getUsername() + " " + userName);
userService.getUserFriendship(user.getPk(), new ServiceCallback<FriendshipStatus>() {
@Override
public void onSuccess(final FriendshipStatus status) {
user.setFriendshipStatus(status);
fetchListener.onResult(user);
}
try {
final HttpURLConnection conn = (HttpURLConnection) new URL("https://www.instagram.com/" + userName + "/?__a=1").openConnection();
conn.setUseCaches(true);
conn.connect();
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error", t);
}
});
}
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
final String json = NetworkUtils.readFromConnection(conn);
// Log.d(TAG, "doInBackground: " + json);
final JSONObject userJson = new JSONObject(json).getJSONObject("graphql")
.getJSONObject(Constants.EXTRAS_USER);
final String cookie = settingsHelper.getString(Constants.COOKIE);
boolean isPrivate = userJson.getBoolean("is_private");
final long id = userJson.optLong(Constants.EXTRAS_ID, 0);
final long uid = CookieUtils.getUserIdFromCookie(cookie);
final JSONObject timelineMedia = userJson.getJSONObject("edge_owner_to_timeline_media");
// if (timelineMedia.has("edges")) {
// final JSONArray edges = timelineMedia.getJSONArray("edges");
// }
String url = userJson.optString("external_url");
if (TextUtils.isEmpty(url)) url = null;
return new User(
id,
userName,
userJson.getString("full_name"),
isPrivate,
userJson.getString("profile_pic_url_hd"),
null,
new FriendshipStatus(
userJson.optBoolean("followed_by_viewer"),
userJson.optBoolean("follows_viewer"),
userJson.optBoolean("blocked_by_viewer"),
false,
isPrivate,
userJson.optBoolean("has_requested_viewer"),
userJson.optBoolean("requested_by_viewer"),
false,
userJson.optBoolean("restricted_by_viewer"),
false
),
userJson.getBoolean("is_verified"),
false,
false,
false,
false,
null,
null,
timelineMedia.getLong("count"),
userJson.getJSONObject("edge_followed_by").getLong("count"),
userJson.getJSONObject("edge_follow").getLong("count"),
0,
userJson.getString("biography"),
url,
0,
null);
// result = new ProfileModel(isPrivate,
// !user.optBoolean("followed_by_viewer") && (id != uid && isPrivate),
// user.getBoolean("is_verified"),
// id,
// userName,
// user.getString("full_name"),
// user.getString("biography"),
// url,
// user.getString("profile_pic_url"),
// user.getString("profile_pic_url_hd"),
// timelineMedia.getLong("count"),
// user.getJSONObject("edge_followed_by").getLong("count"),
// user.getJSONObject("edge_follow").getLong("count"),
// user.optBoolean("followed_by_viewer"),
// user.optBoolean("follows_viewer"),
// user.optBoolean("restricted_by_viewer"),
// user.optBoolean("blocked_by_viewer"),
// user.optBoolean("requested_by_viewer"));
}
conn.disconnect();
} catch (final Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogCollector.LogFile.ASYNC_PROFILE_FETCHER, "doInBackground");
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error", t);
}
});
}
else {
graphQLService.fetchUser(userName, new ServiceCallback<User>() {
@Override
public void onSuccess(final User user) {
fetchListener.onResult(user);
}
return result;
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error", t);
}
});
}
return "yeah";
}
@Override
protected void onPostExecute(final User result) {
if (fetchListener != null) fetchListener.onResult(result);
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
}

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

@ -99,9 +99,12 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl
@Override
public void onFailure(Throwable t) {
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
binding.swipeRefreshLayout.setRefreshing(false);
stopCurrentExecutor();
try {
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
binding.swipeRefreshLayout.setRefreshing(false);
stopCurrentExecutor();
}
catch(Throwable e) {}
}
};

View File

@ -83,8 +83,11 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
@Override
public void onFailure(final Throwable t) {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
try {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
}
catch(Throwable e) {}
Log.e(TAG, "Error fetching list (double, following)", t);
}
};
@ -109,8 +112,11 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
@Override
public void onFailure(final Throwable t) {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
try {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
}
catch(Throwable e) {}
Log.e(TAG, "Error fetching list (double, follower)", t);
}
};
@ -221,8 +227,11 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh
@Override
public void onFailure(final Throwable t) {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
try {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
}
catch(Throwable e) {}
Log.e(TAG, "Error fetching list (single)", t);
}
};

View File

@ -271,8 +271,11 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
@Override
public void onFailure(Throwable t) {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
try {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
}
catch(Throwable e) {}
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
break;
@ -287,8 +290,11 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
@Override
public void onFailure(final Throwable t) {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
try {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
}
catch(Throwable e) {}
}
});
break;

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

@ -236,8 +236,11 @@ public class StoryViewerFragment extends Fragment {
@Override
public void onFailure(@NonNull final Call<DirectThreadBroadcastResponse> call, @NonNull final Throwable t) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
Log.e(TAG, "onFailure: ", t);
try {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
Log.e(TAG, "onFailure: ", t);
}
catch(Throwable e) {}
}
});
} catch (UnsupportedEncodingException e) {

View File

@ -538,7 +538,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
private void fetchProfileDetails() {
if (TextUtils.isEmpty(username)) return;
new ProfileFetcher(username.trim().substring(1), profileModel -> {
new ProfileFetcher(username.trim().substring(1), isLoggedIn, profileModel -> {
if (getContext() == null) return;
this.profileModel = profileModel;
setProfileDetails();
@ -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

@ -26,7 +26,9 @@ import awais.instagrabber.dialogs.TimeSettingsDialog;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.DirectoryChooser;
import awais.instagrabber.utils.LocaleUtils;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.UserAgentUtils;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.utils.Constants.FOLDER_PATH;
@ -111,6 +113,9 @@ public class SettingsPreferencesFragment extends BasePreferencesFragment {
preference.setEntryValues(values);
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
shouldRecreate();
final int appUaCode = settingsHelper.getInteger(Constants.APP_UA_CODE);
final String appUa = UserAgentUtils.generateAppUA(appUaCode, LocaleUtils.getCurrentLocale().getLanguage());
settingsHelper.putString(Constants.APP_UA, appUa);
return true;
});
return preference;

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

@ -4,9 +4,13 @@ import java.util.Map;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.QueryMap;
public interface GraphQLRepository {
@GET("/graphql/query/")
Call<String> fetch(@QueryMap(encoded = true) Map<String, String> queryParams);
@GET("/{username}/?__a=1")
Call<String> getUser(@Path("username") String username);
}

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

@ -1,7 +1,8 @@
package awais.instagrabber.repositories;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.FriendshipStatus;
import awais.instagrabber.repositories.responses.UserSearchResponse;
import awais.instagrabber.repositories.responses.WrappedUser;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
@ -10,7 +11,13 @@ import retrofit2.http.Query;
public interface UserRepository {
@GET("/api/v1/users/{uid}/info/")
Call<User> getUserInfo(@Path("uid") final long uid);
Call<WrappedUser> getUserInfo(@Path("uid") final long uid);
@GET("/api/v1/users/{username}/usernameinfo/")
Call<WrappedUser> getUsernameInfo(@Path("username") final String username);
@GET("/api/v1/friendships/show/{uid}/")
Call<FriendshipStatus> getUserFriendship(@Path("uid") final long uid);
@GET("/api/v1/users/search/")
Call<UserSearchResponse> search(@Query("timezone_offset") float timezoneOffset,

View File

@ -10,7 +10,7 @@ public class User implements Serializable {
private final boolean isPrivate;
private final String profilePicUrl;
private final String profilePicId;
private final FriendshipStatus friendshipStatus;
private FriendshipStatus friendshipStatus;
private final boolean isVerified;
private final boolean hasAnonymousProfilePicture;
private final boolean isUnpublished;
@ -102,6 +102,10 @@ public class User implements Serializable {
return friendshipStatus;
}
public void setFriendshipStatus(final FriendshipStatus friendshipStatus) {
this.friendshipStatus = friendshipStatus;
}
public boolean isVerified() {
return isVerified;
}

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

@ -0,0 +1,13 @@
package awais.instagrabber.repositories.responses;
public class WrappedUser {
private final User user;
public WrappedUser(final User user) {
this.user = user;
}
public User getUser() {
return user;
}
}

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

@ -20,49 +20,21 @@ public class UserAgentUtils {
// use APKpure, assume x86
private static final String igVersion = "169.3.0.30.135";
private static final String igVersionCode = "264009054";
// https://github.com/dilame/instagram-private-api/blob/master/src/samples/devices.json
// presumed constant, so no need to update
// only pick the ones that has width 1440 for maximum download quality
public static final String[] devices = {
"25/7.1.1; 440dpi; 1080x1920; Xiaomi; Mi Note 3; jason; qcom",
"23/6.0.1; 480dpi; 1080x1920; Xiaomi; Redmi Note 3; kenzo; qcom",
"23/6.0; 480dpi; 1080x1920; Xiaomi; Redmi Note 4; nikel; mt6797",
"24/7.0; 480dpi; 1080x1920; Xiaomi/xiaomi; Redmi Note 4; mido; qcom",
"23/6.0; 480dpi; 1080x1920; Xiaomi; Redmi Note 4X; nikel; mt6797",
"27/8.1.0; 440dpi; 1080x2030; Xiaomi/xiaomi; Redmi Note 5; whyred; qcom",
"23/6.0.1; 480dpi; 1080x1920; Xiaomi; Redmi 4; markw; qcom",
"27/8.1.0; 440dpi; 1080x2030; Xiaomi/xiaomi; Redmi 5 Plus; vince; qcom",
"25/7.1.2; 440dpi; 1080x2030; Xiaomi/xiaomi; Redmi 5 Plus; vince; qcom",
"26/8.0.0; 480dpi; 1080x1920; Xiaomi; MI 5; gemini; qcom",
"27/8.1.0; 480dpi; 1080x1920; Xiaomi/xiaomi; Mi A1; tissot_sprout; qcom",
"26/8.0.0; 480dpi; 1080x1920; Xiaomi; MI 6; sagit; qcom",
"25/7.1.1; 440dpi; 1080x1920; Xiaomi; MI MAX 2; oxygen; qcom",
"24/7.0; 480dpi; 1080x1920; Xiaomi; MI 5s; capricorn; qcom",
"26/8.0.0; 480dpi; 1080x1920; samsung; SM-A520F; a5y17lte; samsungexynos7880",
"26/8.0.0; 480dpi; 1080x2076; samsung; SM-G950F; dreamlte; samsungexynos8895",
// https://github.com/dilame/instagram-private-api/blob/master/src/samples/devices.json
"26/8.0.0; 640dpi; 1440x2768; samsung; SM-G950F; dreamlte; samsungexynos8895",
"26/8.0.0; 420dpi; 1080x2094; samsung; SM-G955F; dream2lte; samsungexynos8895",
"26/8.0.0; 560dpi; 1440x2792; samsung; SM-G955F; dream2lte; samsungexynos8895",
"24/7.0; 480dpi; 1080x1920; samsung; SM-A510F; a5xelte; samsungexynos7580",
"26/8.0.0; 480dpi; 1080x1920; samsung; SM-G930F; herolte; samsungexynos8890",
"26/8.0.0; 480dpi; 1080x1920; samsung; SM-G935F; hero2lte; samsungexynos8890",
"26/8.0.0; 420dpi; 1080x2094; samsung; SM-G965F; star2lte; samsungexynos9810",
"26/8.0.0; 480dpi; 1080x2076; samsung; SM-A530F; jackpotlte; samsungexynos7885",
"24/7.0; 640dpi; 1440x2560; samsung; SM-G925F; zerolte; samsungexynos7420",
"26/8.0.0; 420dpi; 1080x1920; samsung; SM-A720F; a7y17lte; samsungexynos7880",
"24/7.0; 640dpi; 1440x2560; samsung; SM-G920F; zeroflte; samsungexynos7420",
"24/7.0; 420dpi; 1080x1920; samsung; SM-J730FM; j7y17lte; samsungexynos7870",
"26/8.0.0; 480dpi; 1080x2076; samsung; SM-G960F; starlte; samsungexynos9810",
"26/8.0.0; 420dpi; 1080x2094; samsung; SM-N950F; greatlte; samsungexynos8895",
"26/8.0.0; 420dpi; 1080x2094; samsung; SM-A730F; jackpot2lte; samsungexynos7885",
"26/8.0.0; 420dpi; 1080x2094; samsung; SM-A605FN; a6plte; qcom",
"26/8.0.0; 480dpi; 1080x1920; HUAWEI/HONOR; STF-L09; HWSTF; hi3660",
"27/8.1.0; 480dpi; 1080x2280; HUAWEI/HONOR; COL-L29; HWCOL; kirin970",
"26/8.0.0; 480dpi; 1080x2032; HUAWEI/HONOR; LLD-L31; HWLLD-H; hi6250",
"26/8.0.0; 480dpi; 1080x2150; HUAWEI; ANE-LX1; HWANE; hi6250",
"26/8.0.0; 480dpi; 1080x2032; HUAWEI; FIG-LX1; HWFIG-H; hi6250",
"27/8.1.0; 480dpi; 1080x2150; HUAWEI/HONOR; COL-L29; HWCOL; kirin970",
"26/8.0.0; 480dpi; 1080x2038; HUAWEI/HONOR; BND-L21; HWBND-H; hi6250",
"23/6.0.1; 420dpi; 1080x1920; LeMobile/LeEco; Le X527; le_s2_ww; qcom"
// https://github.com/mimmi20/BrowserDetector/tree/master
"28/9; 560dpi; 1440x2792; samsung; SM-N960F; crownlte; samsungexynos9810",
// mgp25
"23/6.0.1; 640dpi; 1440x2392; LGE/lge; RS988; h1; h1",
"24/7.0; 640dpi; 1440x2560; HUAWEI; LON-L29; HWLON; hi3660",
"23/6.0.1; 640dpi; 1440x2560; ZTE; ZTE A2017U; ailsa_ii; qcom",
"23/6.0.1; 640dpi; 1440x2560; samsung; SM-G935F; hero2lte; samsungexynos8890",
"23/6.0.1; 640dpi; 1440x2560; samsung; SM-G930F; herolte; samsungexynos8890"
};
@NonNull

View File

@ -83,6 +83,7 @@ public class AppStateViewModel extends AndroidViewModel {
if (TextUtils.isEmpty(username)) return;
new ProfileFetcher(
username.trim().substring(1),
true,
currentUser::postValue
).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}

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

@ -35,6 +35,7 @@ public abstract class BaseService {
final Gson gson = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(Caption.class, new Caption.CaptionDeserializer())
.setLenient()
.create();
builder = new Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())

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

@ -271,4 +271,82 @@ public class GraphQLService extends BaseService {
}
});
}
public void fetchUser(final String username,
final ServiceCallback<User> callback) {
final Call<String> request = repository.getUser(username);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {
final String rawBody = response.body();
if (rawBody == null) {
Log.e(TAG, "Error occurred while fetching gql user of " + username);
callback.onSuccess(null);
return;
}
try {
final JSONObject body = new JSONObject(rawBody);
final JSONObject userJson = body.getJSONObject("graphql")
.getJSONObject(Constants.EXTRAS_USER);
boolean isPrivate = userJson.getBoolean("is_private");
final long id = userJson.optLong(Constants.EXTRAS_ID, 0);
final JSONObject timelineMedia = userJson.getJSONObject("edge_owner_to_timeline_media");
// if (timelineMedia.has("edges")) {
// final JSONArray edges = timelineMedia.getJSONArray("edges");
// }
String url = userJson.optString("external_url");
if (TextUtils.isEmpty(url)) url = null;
callback.onSuccess(new User(
id,
username,
userJson.getString("full_name"),
isPrivate,
userJson.getString("profile_pic_url_hd"),
null,
new FriendshipStatus(
userJson.optBoolean("followed_by_viewer"),
userJson.optBoolean("follows_viewer"),
userJson.optBoolean("blocked_by_viewer"),
false,
isPrivate,
userJson.optBoolean("has_requested_viewer"),
userJson.optBoolean("requested_by_viewer"),
false,
userJson.optBoolean("restricted_by_viewer"),
false
),
userJson.getBoolean("is_verified"),
false,
false,
false,
false,
null,
null,
timelineMedia.getLong("count"),
userJson.getJSONObject("edge_followed_by").getLong("count"),
userJson.getJSONObject("edge_follow").getLong("count"),
0,
userJson.getString("biography"),
url,
0,
null));
} catch (JSONException e) {
Log.e(TAG, "onResponse", e);
if (callback != null) {
callback.onFailure(e);
}
}
}
@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

@ -5,8 +5,10 @@ import androidx.annotation.NonNull;
import java.util.TimeZone;
import awais.instagrabber.repositories.UserRepository;
import awais.instagrabber.repositories.responses.FriendshipStatus;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.UserSearchResponse;
import awais.instagrabber.repositories.responses.WrappedUser;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@ -34,25 +36,66 @@ public class UserService extends BaseService {
}
public void getUserInfo(final long uid, final ServiceCallback<User> callback) {
final Call<User> request = repository.getUserInfo(uid);
request.enqueue(new Callback<User>() {
final Call<WrappedUser> request = repository.getUserInfo(uid);
request.enqueue(new Callback<WrappedUser>() {
@Override
public void onResponse(@NonNull final Call<User> call, @NonNull final Response<User> response) {
final User user = response.body();
public void onResponse(@NonNull final Call<WrappedUser> call, @NonNull final Response<WrappedUser> response) {
final WrappedUser user = response.body();
if (user == null) {
callback.onSuccess(null);
return;
}
callback.onSuccess(user);
callback.onSuccess(user.getUser());
}
@Override
public void onFailure(@NonNull final Call<User> call, @NonNull final Throwable t) {
public void onFailure(@NonNull final Call<WrappedUser> call, @NonNull final Throwable t) {
callback.onFailure(t);
}
});
}
public void getUsernameInfo(final String username, final ServiceCallback<User> callback) {
final Call<WrappedUser> request = repository.getUsernameInfo(username);
request.enqueue(new Callback<WrappedUser>() {
@Override
public void onResponse(@NonNull final Call<WrappedUser> call, @NonNull final Response<WrappedUser> response) {
final WrappedUser user = response.body();
if (user == null) {
callback.onSuccess(null);
return;
}
callback.onSuccess(user.getUser());
}
@Override
public void onFailure(@NonNull final Call<WrappedUser> call, @NonNull final Throwable t) {
callback.onFailure(t);
}
});
}
public void getUserFriendship(final long uid, final ServiceCallback<FriendshipStatus> callback) {
final Call<FriendshipStatus> request = repository.getUserFriendship(uid);
request.enqueue(new Callback<FriendshipStatus>() {
@Override
public void onResponse(@NonNull final Call<FriendshipStatus> call, @NonNull final Response<FriendshipStatus> response) {
final FriendshipStatus status = response.body();
if (status == null) {
callback.onSuccess(null);
return;
}
callback.onSuccess(status);
}
@Override
public void onFailure(@NonNull final Call<FriendshipStatus> call, @NonNull final Throwable t) {
callback.onFailure(t);
}
});
}
public Call<UserSearchResponse> search(final String query) {
final float timezoneOffset = (float) TimeZone.getDefault().getRawOffset() / 1000;
return repository.search(timezoneOffset, query);

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>