mirror of
				https://github.com/KokaKiwi/BarInsta
				synced 2025-10-31 03:25:34 +00:00 
			
		
		
		
	Merge remote-tracking branch 'origin/dm-notifications-enhancements' into dm-notifications-enhancements
This commit is contained in:
		
						commit
						ea7236dcc1
					
				| @ -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()); | ||||
|                 } | ||||
|  | ||||
| @ -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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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() | ||||
|  | ||||
| @ -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) {} | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|  | ||||
| @ -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); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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 -> { | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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); | ||||
| } | ||||
| @ -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); | ||||
| } | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
| } | ||||
| @ -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; | ||||
|     } | ||||
| } | ||||
| @ -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; | ||||
|     } | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
|  | ||||
| @ -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()); | ||||
|             } | ||||
| 
 | ||||
|  | ||||
| @ -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()) | ||||
|  | ||||
| @ -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); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| @ -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); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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(); | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
							
								
								
									
										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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user