Browse Source

Add Posts view to Hashtag fragment

renovate/org.robolectric-robolectric-4.x
Ammar Githam 5 years ago
parent
commit
2931f2d3ab
  1. 56
      app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java
  2. 11
      app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java
  3. 472
      app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java
  4. 1
      app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java
  5. 4
      app/src/main/java/awais/instagrabber/models/HashtagModel.java
  6. 13
      app/src/main/java/awais/instagrabber/models/StoryModel.java
  7. 8
      app/src/main/java/awais/instagrabber/repositories/TagsRepository.java
  8. 1
      app/src/main/java/awais/instagrabber/utils/Constants.java
  9. 123
      app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java
  10. 3
      app/src/main/java/awais/instagrabber/utils/SettingsHelper.java
  11. 119
      app/src/main/java/awais/instagrabber/webservices/DiscoverService.java
  12. 186
      app/src/main/java/awais/instagrabber/webservices/TagsService.java
  13. 66
      app/src/main/res/layout/fragment_hashtag.xml
  14. 9
      app/src/main/res/menu/hashtag_menu.xml
  15. 2
      app/src/main/res/navigation/comments_nav_graph.xml
  16. 21
      app/src/main/res/navigation/hashtag_nav_graph.xml

56
app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java

@ -0,0 +1,56 @@
package awais.instagrabber.asyncs;
import java.util.List;
import awais.instagrabber.customviews.helpers.PostFetcher;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.FeedModel;
import awais.instagrabber.models.HashtagModel;
import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.TagsService;
import awais.instagrabber.webservices.TagsService.TagPostsFetchResponse;
public class HashtagPostFetchService implements PostFetcher.PostFetchService {
private final TagsService tagsService;
private final HashtagModel hashtagModel;
private String nextMaxId;
private boolean moreAvailable;
public HashtagPostFetchService(final HashtagModel hashtagModel) {
this.hashtagModel = hashtagModel;
tagsService = TagsService.getInstance();
}
@Override
public void fetch(final FetchListener<List<FeedModel>> fetchListener) {
tagsService.fetchPosts(hashtagModel.getName().toLowerCase(), nextMaxId, new ServiceCallback<TagPostsFetchResponse>() {
@Override
public void onSuccess(final TagPostsFetchResponse result) {
if (result == null) return;
nextMaxId = result.getNextMaxId();
moreAvailable = result.isMoreAvailable();
if (fetchListener != null) {
fetchListener.onResult(result.getItems());
}
}
@Override
public void onFailure(final Throwable t) {
// Log.e(TAG, "onFailure: ", t);
if (fetchListener != null) {
fetchListener.onFailure(t);
}
}
});
}
@Override
public void reset() {
nextMaxId = null;
}
@Override
public boolean hasNextPage() {
return moreAvailable;
}
}

11
app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java

@ -44,6 +44,9 @@ public class PostsRecyclerView extends RecyclerView {
private GridSpacingItemDecoration gridSpacingItemDecoration; private GridSpacingItemDecoration gridSpacingItemDecoration;
private RecyclerLazyLoaderAtBottom lazyLoader; private RecyclerLazyLoaderAtBottom lazyLoader;
private FeedAdapterV2.FeedItemCallback feedItemCallback; private FeedAdapterV2.FeedItemCallback feedItemCallback;
private boolean shouldScrollToTop;
private final List<FetchStatusChangeListener> fetchStatusChangeListeners = new ArrayList<>();
private final FetchListener<List<FeedModel>> fetchListener = new FetchListener<List<FeedModel>>() { private final FetchListener<List<FeedModel>> fetchListener = new FetchListener<List<FeedModel>>() {
@Override @Override
@ -51,6 +54,7 @@ public class PostsRecyclerView extends RecyclerView {
final int currentPage = lazyLoader.getCurrentPage(); final int currentPage = lazyLoader.getCurrentPage();
if (currentPage == 0) { if (currentPage == 0) {
feedViewModel.getList().postValue(result); feedViewModel.getList().postValue(result);
shouldScrollToTop = true;
dispatchFetchStatus(); dispatchFetchStatus();
return; return;
} }
@ -66,7 +70,6 @@ public class PostsRecyclerView extends RecyclerView {
Log.e(TAG, "onFailure: ", t); Log.e(TAG, "onFailure: ", t);
} }
}; };
private final List<FetchStatusChangeListener> fetchStatusChangeListeners = new ArrayList<>();
public PostsRecyclerView(@NonNull final Context context) { public PostsRecyclerView(@NonNull final Context context) {
super(context); super(context);
@ -158,7 +161,11 @@ public class PostsRecyclerView extends RecyclerView {
private void initSelf() { private void initSelf() {
feedViewModel = new ViewModelProvider(viewModelStoreOwner).get(FeedViewModel.class); feedViewModel = new ViewModelProvider(viewModelStoreOwner).get(FeedViewModel.class);
feedViewModel.getList().observe(lifeCycleOwner, feedAdapter::submitList);
feedViewModel.getList().observe(lifeCycleOwner, list -> feedAdapter.submitList(list, () -> {
if (!shouldScrollToTop) return;
smoothScrollToPosition(0);
shouldScrollToTop = false;
}));
postFetcher = new PostFetcher(postFetchService, fetchListener); postFetcher = new PostFetcher(postFetchService, fetchListener);
if (layoutPreferences.getHasGap()) { if (layoutPreferences.getHasGap()) {
addItemDecoration(gridSpacingItemDecoration); addItemDecoration(gridSpacingItemDecoration);

472
app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java

@ -4,24 +4,27 @@ import android.content.Context;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.style.RelativeSizeSpan; import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.util.Log; import android.util.Log;
import android.view.ActionMode; import android.view.ActionMode;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast; import android.widget.Toast;
import androidx.activity.OnBackPressedCallback; import androidx.activity.OnBackPressedCallback;
import androidx.activity.OnBackPressedDispatcher;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.core.content.PermissionChecker;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavController;
import androidx.navigation.NavDirections; import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
@ -29,73 +32,67 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.BaseTransientBottomBar;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import awais.instagrabber.R; import awais.instagrabber.R;
import awais.instagrabber.activities.MainActivity; import awais.instagrabber.activities.MainActivity;
import awais.instagrabber.adapters.PostsAdapter;
import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.asyncs.HashtagFetcher; import awais.instagrabber.asyncs.HashtagFetcher;
import awais.instagrabber.asyncs.PostsFetcher;
import awais.instagrabber.asyncs.HashtagPostFetchService;
import awais.instagrabber.customviews.PrimaryActionModeCallback; import awais.instagrabber.customviews.PrimaryActionModeCallback;
import awais.instagrabber.customviews.helpers.GridAutofitLayoutManager;
import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration;
import awais.instagrabber.customviews.helpers.NestedCoordinatorLayout; import awais.instagrabber.customviews.helpers.NestedCoordinatorLayout;
import awais.instagrabber.customviews.helpers.RecyclerLazyLoader;
import awais.instagrabber.databinding.FragmentHashtagBinding; import awais.instagrabber.databinding.FragmentHashtagBinding;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment;
import awais.instagrabber.fragments.main.FeedFragmentDirections;
import awais.instagrabber.models.FeedModel;
import awais.instagrabber.models.HashtagModel; import awais.instagrabber.models.HashtagModel;
import awais.instagrabber.models.PostModel;
import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.models.StoryModel; import awais.instagrabber.models.StoryModel;
import awais.instagrabber.models.enums.DownloadMethod;
import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.models.enums.PostItemType;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.DataBox; import awais.instagrabber.utils.DataBox;
import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.PostsViewModel;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.StoriesService; import awais.instagrabber.webservices.StoriesService;
import awais.instagrabber.webservices.TagsService; import awais.instagrabber.webservices.TagsService;
import awaisomereport.LogCollector; import awaisomereport.LogCollector;
import static androidx.core.content.PermissionChecker.checkSelfPermission;
import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION;
import static awais.instagrabber.utils.Utils.logCollector; import static awais.instagrabber.utils.Utils.logCollector;
import static awais.instagrabber.utils.Utils.settingsHelper; import static awais.instagrabber.utils.Utils.settingsHelper;
public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = "HashTagFragment"; private static final String TAG = "HashTagFragment";
private static final int STORAGE_PERM_REQUEST_CODE = 8020;
public static final String ARG_HASHTAG = "hashtag"; public static final String ARG_HASHTAG = "hashtag";
private MainActivity fragmentActivity; private MainActivity fragmentActivity;
private FragmentHashtagBinding binding; private FragmentHashtagBinding binding;
private NestedCoordinatorLayout root; private NestedCoordinatorLayout root;
private boolean shouldRefresh = true, hasStories = false;
private boolean shouldRefresh = true;
private boolean hasStories = false;
private String hashtag; private String hashtag;
private HashtagModel hashtagModel; private HashtagModel hashtagModel;
private PostsViewModel postsViewModel;
private PostsAdapter postsAdapter;
private ActionMode actionMode; private ActionMode actionMode;
private StoriesService storiesService; private StoriesService storiesService;
private boolean hasNextPage;
private String endCursor;
private AsyncTask<?, ?, ?> currentlyExecuting; private AsyncTask<?, ?, ?> currentlyExecuting;
private boolean isLoggedIn; private boolean isLoggedIn;
private TagsService tagsService; private TagsService tagsService;
private boolean isPullToRefresh;
private boolean storiesFetching;
private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) { private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
@Override @Override
public void handleOnBackPressed() { public void handleOnBackPressed() {
setEnabled(false); setEnabled(false);
remove(); remove();
if (postsAdapter == null) return;
postsAdapter.clearSelection();
// if (postsAdapter == null) return;
// postsAdapter.clearSelection();
} }
}; };
private final PrimaryActionModeCallback multiSelectAction = new PrimaryActionModeCallback( private final PrimaryActionModeCallback multiSelectAction = new PrimaryActionModeCallback(
@ -109,47 +106,132 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
@Override @Override
public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) { public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
if (item.getItemId() == R.id.action_download) { if (item.getItemId() == R.id.action_download) {
if (postsAdapter == null || hashtag == null) {
return false;
}
final Context context = getContext();
if (context == null) return false;
DownloadUtils.batchDownload(context,
hashtag,
DownloadMethod.DOWNLOAD_MAIN,
postsAdapter.getSelectedModels());
checkAndResetAction();
// if (postsAdapter == null || hashtag == null) {
// return false;
// }
// final Context context = getContext();
// if (context == null) return false;
// DownloadUtils.batchDownload(context,
// hashtag,
// DownloadMethod.DOWNLOAD_MAIN,
// postsAdapter.getSelectedModels());
// checkAndResetAction();
return true; return true;
} }
return false; return false;
} }
}); });
private final FetchListener<List<PostModel>> postsFetchListener = new FetchListener<List<PostModel>>() {
// private final FetchListener<List<PostModel>> postsFetchListener = new FetchListener<List<PostModel>>() {
// @Override
// public void onResult(final List<PostModel> result) {
// binding.swipeRefreshLayout.setRefreshing(false);
// if (result == null) return;
// binding.mainPosts.post(() -> binding.mainPosts.setVisibility(View.VISIBLE));
// final List<PostModel> postModels = postsViewModel.getList().getValue();
// List<PostModel> finalList = postModels == null || postModels.isEmpty()
// ? new ArrayList<>()
// : new ArrayList<>(postModels);
// if (isPullToRefresh) {
// finalList = result;
// isPullToRefresh = false;
// } else {
// finalList.addAll(result);
// }
// finalList.addAll(result);
// postsViewModel.getList().postValue(finalList);
// PostModel model = null;
// if (!result.isEmpty()) {
// model = result.get(result.size() - 1);
// }
// if (model == null) return;
// endCursor = model.getEndCursor();
// hasNextPage = model.hasNextPage();
// model.setPageCursor(false, null);
// }
// };
private final FeedAdapterV2.FeedItemCallback feedItemCallback = new FeedAdapterV2.FeedItemCallback() {
@Override @Override
public void onResult(final List<PostModel> result) {
binding.swipeRefreshLayout.setRefreshing(false);
if (result == null) return;
binding.mainPosts.post(() -> binding.mainPosts.setVisibility(View.VISIBLE));
final List<PostModel> postModels = postsViewModel.getList().getValue();
List<PostModel> finalList = postModels == null || postModels.isEmpty()
? new ArrayList<>()
: new ArrayList<>(postModels);
if (isPullToRefresh) {
finalList = result;
isPullToRefresh = false;
} else {
finalList.addAll(result);
public void onPostClick(final FeedModel feedModel, final View profilePicView, final View mainPostImage) {
openPostDialog(feedModel, profilePicView, mainPostImage, -1);
}
@Override
public void onSliderClick(final FeedModel feedModel, final int position) {
openPostDialog(feedModel, null, null, position);
}
@Override
public void onCommentsClick(final FeedModel feedModel) {
final NavDirections commentsAction = HashTagFragmentDirections.actionGlobalCommentsViewerFragment(
feedModel.getShortCode(),
feedModel.getPostId(),
feedModel.getProfileModel().getId()
);
NavHostFragment.findNavController(HashTagFragment.this).navigate(commentsAction);
}
@Override
public void onDownloadClick(final FeedModel feedModel) {
final Context context = getContext();
if (context == null) return;
if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) {
showDownloadDialog(feedModel);
return;
} }
finalList.addAll(result);
postsViewModel.getList().postValue(finalList);
PostModel model = null;
if (!result.isEmpty()) {
model = result.get(result.size() - 1);
requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE);
}
@Override
public void onHashtagClick(final String hashtag) {
final NavDirections action = FeedFragmentDirections.actionGlobalHashTagFragment(hashtag);
NavHostFragment.findNavController(HashTagFragment.this).navigate(action);
}
@Override
public void onLocationClick(final FeedModel feedModel) {
final NavDirections action = FeedFragmentDirections.actionGlobalLocationFragment(feedModel.getLocationId());
NavHostFragment.findNavController(HashTagFragment.this).navigate(action);
}
@Override
public void onMentionClick(final String mention) {
navigateToProfile(mention.trim());
}
@Override
public void onNameClick(final FeedModel feedModel, final View profilePicView) {
navigateToProfile("@" + feedModel.getProfileModel().getUsername());
}
@Override
public void onProfilePicClick(final FeedModel feedModel, final View profilePicView) {
navigateToProfile("@" + feedModel.getProfileModel().getUsername());
}
@Override
public void onURLClick(final String url) {
Utils.openURL(getContext(), url);
}
@Override
public void onEmailClick(final String emailId) {
Utils.openEmailAddress(getContext(), emailId);
}
private void openPostDialog(final FeedModel feedModel,
final View profilePicView,
final View mainPostImage,
final int position) {
final PostViewV2Fragment.Builder builder = PostViewV2Fragment
.builder(feedModel);
if (position >= 0) {
builder.setPosition(position);
} }
if (model == null) return;
endCursor = model.getEndCursor();
hasNextPage = model.hasNextPage();
model.setPageCursor(false, null);
final PostViewV2Fragment fragment = builder
.setSharedProfilePicElement(profilePicView)
.setSharedMainPostElement(mainPostImage)
.build();
fragment.show(getChildFragmentManager(), "post_view");
} }
}; };
@ -159,6 +241,7 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
fragmentActivity = (MainActivity) requireActivity(); fragmentActivity = (MainActivity) requireActivity();
tagsService = TagsService.getInstance(); tagsService = TagsService.getInstance();
storiesService = StoriesService.getInstance(); storiesService = StoriesService.getInstance();
setHasOptionsMenu(true);
} }
@Nullable @Nullable
@ -183,9 +266,8 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
@Override @Override
public void onRefresh() { public void onRefresh() {
isPullToRefresh = true;
endCursor = null;
fetchHashtagModel();
binding.posts.refresh();
fetchStories();
} }
@Override @Override
@ -195,11 +277,17 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
} }
@Override @Override
public void onDestroy() {
super.onDestroy();
if (postsViewModel != null) {
postsViewModel.getList().postValue(Collections.emptyList());
public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) {
inflater.inflate(R.menu.topic_posts_menu, menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
if (item.getItemId() == R.id.layout) {
showPostsLayoutPreferences();
return true;
} }
return super.onOptionsItemSelected(item);
} }
private void init() { private void init() {
@ -208,114 +296,96 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != null; isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != null;
final HashTagFragmentArgs fragmentArgs = HashTagFragmentArgs.fromBundle(getArguments()); final HashTagFragmentArgs fragmentArgs = HashTagFragmentArgs.fromBundle(getArguments());
hashtag = fragmentArgs.getHashtag(); hashtag = fragmentArgs.getHashtag();
// setTitle();
setupPosts();
fetchHashtagModel(); fetchHashtagModel();
} }
private void setupPosts() {
postsViewModel = new ViewModelProvider(this).get(PostsViewModel.class);
final Context context = getContext();
if (context == null) return;
final GridAutofitLayoutManager layoutManager = new GridAutofitLayoutManager(context, Utils.convertDpToPx(110));
binding.mainPosts.setLayoutManager(layoutManager);
binding.mainPosts.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(4)));
postsAdapter = new PostsAdapter((postModel, position) -> {
if (postsAdapter.isSelecting()) {
if (actionMode == null) return;
final String title = getString(R.string.number_selected, postsAdapter.getSelectedModels().size());
actionMode.setTitle(title);
return;
}
if (checkAndResetAction()) return;
final List<PostModel> postModels = postsViewModel.getList().getValue();
if (postModels == null || postModels.size() == 0) return;
if (postModels.get(0) == null) return;
final String postId = postModels.get(0).getPostId();
final boolean isId = postId != null && isLoggedIn;
final String[] idsOrShortCodes = new String[postModels.size()];
for (int i = 0; i < postModels.size(); i++) {
idsOrShortCodes[i] = isId ? postModels.get(i).getPostId()
: postModels.get(i).getShortCode();
}
final NavDirections action = HashTagFragmentDirections.actionGlobalPostViewFragment(
position,
idsOrShortCodes,
isId);
NavHostFragment.findNavController(this).navigate(action);
}, (model, position) -> {
if (!postsAdapter.isSelecting()) {
checkAndResetAction();
return true;
}
if (onBackPressedCallback.isEnabled()) {
return true;
}
final OnBackPressedDispatcher onBackPressedDispatcher = fragmentActivity.getOnBackPressedDispatcher();
onBackPressedCallback.setEnabled(true);
actionMode = fragmentActivity.startActionMode(multiSelectAction);
final String title = getString(R.string.number_selected, 1);
actionMode.setTitle(title);
onBackPressedDispatcher.addCallback(getViewLifecycleOwner(), onBackPressedCallback);
return true;
});
postsViewModel.getList().observe(fragmentActivity, postsAdapter::submitList);
binding.mainPosts.setAdapter(postsAdapter);
final RecyclerLazyLoader lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> {
if (!hasNextPage || getContext() == null) return;
binding.swipeRefreshLayout.setRefreshing(true);
fetchPosts();
endCursor = null;
});
binding.mainPosts.addOnScrollListener(lazyLoader);
}
private void fetchHashtagModel() { private void fetchHashtagModel() {
stopCurrentExecutor(); stopCurrentExecutor();
binding.swipeRefreshLayout.setRefreshing(true); binding.swipeRefreshLayout.setRefreshing(true);
currentlyExecuting = new HashtagFetcher(hashtag.substring(1), result -> { currentlyExecuting = new HashtagFetcher(hashtag.substring(1), result -> {
final Context context = getContext();
if (context == null) return;
hashtagModel = result; hashtagModel = result;
binding.swipeRefreshLayout.setRefreshing(false); binding.swipeRefreshLayout.setRefreshing(false);
final Context context = getContext();
if (context == null) return;
if (hashtagModel == null) { if (hashtagModel == null) {
Toast.makeText(context, R.string.error_loading_profile, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.error_loading_profile, Toast.LENGTH_SHORT).show();
return; return;
} }
setTitle(); setTitle();
fetchPosts();
setHashtagDetails();
setupPosts();
fetchStories();
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
private void fetchPosts() {
stopCurrentExecutor();
private void setupPosts() {
binding.posts.setViewModelStoreOwner(this)
.setLifeCycleOwner(this)
.setPostFetchService(new HashtagPostFetchService(hashtagModel))
.setLayoutPreferences(PostsLayoutPreferences.fromJson(settingsHelper.getString(Constants.PREF_HASHTAG_POSTS_LAYOUT)))
.addFetchStatusChangeListener(fetching -> updateSwipeRefreshState())
.setFeedItemCallback(feedItemCallback)
.init();
binding.swipeRefreshLayout.setRefreshing(true); binding.swipeRefreshLayout.setRefreshing(true);
if (TextUtils.isEmpty(hashtag)) return;
currentlyExecuting = new PostsFetcher(hashtag.substring(1), PostItemType.HASHTAG, endCursor, postsFetchListener)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
final Context context = getContext();
if (context == null) return;
// postsViewModel = new ViewModelProvider(this).get(PostsViewModel.class);
// final Context context = getContext();
// if (context == null) return;
// final GridAutofitLayoutManager layoutManager = new GridAutofitLayoutManager(context, Utils.convertDpToPx(110));
// binding.mainPosts.setLayoutManager(layoutManager);
// binding.mainPosts.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(4)));
// postsAdapter = new PostsAdapter((postModel, position) -> {
// if (postsAdapter.isSelecting()) {
// if (actionMode == null) return;
// final String title = getString(R.string.number_selected, postsAdapter.getSelectedModels().size());
// actionMode.setTitle(title);
// return;
// }
// if (checkAndResetAction()) return;
// final List<PostModel> postModels = postsViewModel.getList().getValue();
// if (postModels == null || postModels.size() == 0) return;
// if (postModels.get(0) == null) return;
// final String postId = postModels.get(0).getPostId();
// final boolean isId = postId != null && isLoggedIn;
// final String[] idsOrShortCodes = new String[postModels.size()];
// for (int i = 0; i < postModels.size(); i++) {
// idsOrShortCodes[i] = isId ? postModels.get(i).getPostId()
// : postModels.get(i).getShortCode();
// }
// final NavDirections action = HashTagFragmentDirections.actionGlobalPostViewFragment(
// position,
// idsOrShortCodes,
// isId);
// NavHostFragment.findNavController(this).navigate(action);
//
// }, (model, position) -> {
// if (!postsAdapter.isSelecting()) {
// checkAndResetAction();
// return true;
// }
// if (onBackPressedCallback.isEnabled()) {
// return true;
// }
// final OnBackPressedDispatcher onBackPressedDispatcher = fragmentActivity.getOnBackPressedDispatcher();
// onBackPressedCallback.setEnabled(true);
// actionMode = fragmentActivity.startActionMode(multiSelectAction);
// final String title = getString(R.string.number_selected, 1);
// actionMode.setTitle(title);
// onBackPressedDispatcher.addCallback(getViewLifecycleOwner(), onBackPressedCallback);
// return true;
// });
// postsViewModel.getList().observe(fragmentActivity, postsAdapter::submitList);
// binding.mainPosts.setAdapter(postsAdapter);
// final RecyclerLazyLoader lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> {
// if (!hasNextPage || getContext() == null) return;
// binding.swipeRefreshLayout.setRefreshing(true);
// fetchPosts();
// endCursor = null;
// });
// binding.mainPosts.addOnScrollListener(lazyLoader);
}
private void setHashtagDetails() {
if (isLoggedIn) { if (isLoggedIn) {
storiesService.getUserStory(hashtagModel.getName(),
null,
false,
true,
false,
new ServiceCallback<List<StoryModel>>() {
@Override
public void onSuccess(final List<StoryModel> storyModels) {
if (storyModels != null && !storyModels.isEmpty()) {
binding.mainHashtagImage.setStoriesBorder();
hasStories = true;
}
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error", t);
}
});
binding.btnFollowTag.setVisibility(View.VISIBLE); binding.btnFollowTag.setVisibility(View.VISIBLE);
binding.btnFollowTag.setText(hashtagModel.getFollowing() ? R.string.unfollow : R.string.follow); binding.btnFollowTag.setText(hashtagModel.getFollowing() ? R.string.unfollow : R.string.follow);
binding.btnFollowTag.setChipIconResource(hashtagModel.getFollowing() binding.btnFollowTag.setChipIconResource(hashtagModel.getFollowing()
@ -422,15 +492,43 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
binding.mainTagPostCount.setText(span); binding.mainTagPostCount.setText(span);
binding.mainTagPostCount.setVisibility(View.VISIBLE); binding.mainTagPostCount.setVisibility(View.VISIBLE);
binding.mainHashtagImage.setOnClickListener(v -> { binding.mainHashtagImage.setOnClickListener(v -> {
if (hasStories) {
// show stories
final NavDirections action = HashTagFragmentDirections
.actionHashtagFragmentToStoryViewerFragment(-1, null, true, false, hashtagModel.getName(), hashtagModel.getName());
NavHostFragment.findNavController(this).navigate(action);
}
if (!hasStories) return;
// show stories
final NavDirections action = HashTagFragmentDirections
.actionHashtagFragmentToStoryViewerFragment(-1, null, true, false, hashtagModel.getName(), hashtagModel.getName());
NavHostFragment.findNavController(this).navigate(action);
}); });
} }
private void fetchStories() {
if (!isLoggedIn) return;
storiesFetching = true;
storiesService.getUserStory(
hashtagModel.getName(),
null,
false,
true,
false,
new ServiceCallback<List<StoryModel>>() {
@Override
public void onSuccess(final List<StoryModel> storyModels) {
if (storyModels != null && !storyModels.isEmpty()) {
binding.mainHashtagImage.setStoriesBorder();
hasStories = true;
} else {
hasStories = false;
}
storiesFetching = false;
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Error", t);
storiesFetching = false;
}
});
}
public void stopCurrentExecutor() { public void stopCurrentExecutor() {
if (currentlyExecuting != null) { if (currentlyExecuting != null) {
try { try {
@ -453,6 +551,59 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
} }
} }
private void updateSwipeRefreshState() {
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching() || storiesFetching);
}
private void showDownloadDialog(final FeedModel feedModel) {
final Context context = getContext();
if (context == null) return;
DownloadUtils.download(context, feedModel);
// switch (feedModel.getItemType()) {
// case MEDIA_TYPE_IMAGE:
// case MEDIA_TYPE_VIDEO:
// break;
// case MEDIA_TYPE_SLIDER:
// break;
// }
// final List<ViewerPostModel> postModelsToDownload = new ArrayList<>();
// // if (!session) {
// final DialogInterface.OnClickListener clickListener = (dialog, which) -> {
// if (which == DialogInterface.BUTTON_NEGATIVE) {
// postModelsToDownload.addAll(postModels);
// } else if (which == DialogInterface.BUTTON_POSITIVE) {
// postModelsToDownload.add(postModels.get(childPosition));
// } else {
// session = true;
// postModelsToDownload.add(postModels.get(childPosition));
// }
// if (postModelsToDownload.size() > 0) {
// DownloadUtils.batchDownload(context,
// username,
// DownloadMethod.DOWNLOAD_POST_VIEWER,
// postModelsToDownload);
// }
// };
// new AlertDialog.Builder(context)
// .setTitle(R.string.post_viewer_download_dialog_title)
// .setMessage(R.string.post_viewer_download_message)
// .setNeutralButton(R.string.post_viewer_download_session, clickListener)
// .setPositiveButton(R.string.post_viewer_download_current, clickListener)
// .setNegativeButton(R.string.post_viewer_download_album, clickListener).show();
// } else {
// DownloadUtils.batchDownload(context,
// username,
// DownloadMethod.DOWNLOAD_POST_VIEWER,
// Collections.singletonList(postModels.get(childPosition)));
}
private void navigateToProfile(final String username) {
final NavController navController = NavHostFragment.findNavController(this);
final Bundle bundle = new Bundle();
bundle.putString("username", username);
navController.navigate(R.id.action_global_profileFragment, bundle);
}
private boolean checkAndResetAction() { private boolean checkAndResetAction() {
if (!onBackPressedCallback.isEnabled() && actionMode == null) { if (!onBackPressedCallback.isEnabled() && actionMode == null) {
return false; return false;
@ -467,4 +618,11 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
} }
return true; return true;
} }
private void showPostsLayoutPreferences() {
final PostsLayoutPreferencesDialogFragment fragment = new PostsLayoutPreferencesDialogFragment(
Constants.PREF_HASHTAG_POSTS_LAYOUT,
preferences -> new Handler().postDelayed(() -> binding.posts.setLayoutPreferences(preferences), 200));
fragment.show(getChildFragmentManager(), "posts_layout_preferences");
}
} }

1
app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java

@ -232,7 +232,6 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre
.setFeedItemCallback(feedItemCallback) .setFeedItemCallback(feedItemCallback)
.init(); .init();
binding.feedSwipeRefreshLayout.setRefreshing(true); binding.feedSwipeRefreshLayout.setRefreshing(true);
// feedAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY);
// if (shouldAutoPlay) { // if (shouldAutoPlay) {
// videoAwareRecyclerScroller = new VideoAwareRecyclerScroller(); // videoAwareRecyclerScroller = new VideoAwareRecyclerScroller();
// binding.feedRecyclerView.addOnScrollListener(videoAwareRecyclerScroller); // binding.feedRecyclerView.addOnScrollListener(videoAwareRecyclerScroller);

4
app/src/main/java/awais/instagrabber/models/HashtagModel.java

@ -5,7 +5,9 @@ import java.io.Serializable;
public final class HashtagModel implements Serializable { public final class HashtagModel implements Serializable {
private final boolean following; private final boolean following;
private final long postCount; private final long postCount;
private final String id, name, sdProfilePic;
private final String id;
private final String name;
private final String sdProfilePic;
public HashtagModel(final String id, final String name, final String sdProfilePic, final long postCount, final boolean following) { public HashtagModel(final String id, final String name, final String sdProfilePic, final long postCount, final boolean following) {
this.id = id; this.id = id;

13
app/src/main/java/awais/instagrabber/models/StoryModel.java

@ -9,17 +9,24 @@ import awais.instagrabber.models.stickers.QuizModel;
import awais.instagrabber.models.stickers.SwipeUpModel; import awais.instagrabber.models.stickers.SwipeUpModel;
public final class StoryModel implements Serializable { public final class StoryModel implements Serializable {
private final String storyMediaId, storyUrl, username, userId;
private final String storyMediaId;
private final String storyUrl;
private final String username;
private final String userId;
private final MediaItemType itemType; private final MediaItemType itemType;
private final long timestamp; private final long timestamp;
private String videoUrl, tappableShortCode, tappableId, spotify;
private String videoUrl;
private String tappableShortCode;
private String tappableId;
private String spotify;
private PollModel poll; private PollModel poll;
private QuestionModel question; private QuestionModel question;
private QuizModel quiz; private QuizModel quiz;
private SwipeUpModel swipeUp; private SwipeUpModel swipeUp;
private String[] mentions; private String[] mentions;
private int position; private int position;
private boolean isCurrentSlide = false, canReply = false;
private boolean isCurrentSlide = false;
private boolean canReply = false;
public StoryModel(final String storyMediaId, final String storyUrl, final MediaItemType itemType, public StoryModel(final String storyMediaId, final String storyUrl, final MediaItemType itemType,
final long timestamp, final String username, final String userId, final boolean canReply) { final long timestamp, final String username, final String userId, final boolean canReply) {

8
app/src/main/java/awais/instagrabber/repositories/TagsRepository.java

@ -1,9 +1,13 @@
package awais.instagrabber.repositories; package awais.instagrabber.repositories;
import java.util.Map;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Header; import retrofit2.http.Header;
import retrofit2.http.POST; import retrofit2.http.POST;
import retrofit2.http.Path; import retrofit2.http.Path;
import retrofit2.http.QueryMap;
public interface TagsRepository { public interface TagsRepository {
@ -16,4 +20,8 @@ public interface TagsRepository {
Call<String> unfollow(@Header("User-Agent") String userAgent, Call<String> unfollow(@Header("User-Agent") String userAgent,
@Header("x-csrftoken") String csrfToken, @Header("x-csrftoken") String csrfToken,
@Path("tag") String tag); @Path("tag") String tag);
@GET("/api/v1/feed/tag/{tag}/")
Call<String> fetchPosts(@Path("tag") final String tag,
@QueryMap Map<String, String> queryParams);
} }

1
app/src/main/java/awais/instagrabber/utils/Constants.java

@ -90,4 +90,5 @@ public final class Constants {
public static final String PREF_POSTS_LAYOUT = "posts_layout"; public static final String PREF_POSTS_LAYOUT = "posts_layout";
public static final String PREF_PROFILE_POSTS_LAYOUT = "profile_posts_layout"; public static final String PREF_PROFILE_POSTS_LAYOUT = "profile_posts_layout";
public static final String PREF_TOPIC_POSTS_LAYOUT = "topic_posts_layout"; public static final String PREF_TOPIC_POSTS_LAYOUT = "topic_posts_layout";
public static final String PREF_HASHTAG_POSTS_LAYOUT = "hashtag_posts_layout";
} }

123
app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java

@ -7,11 +7,16 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import awais.instagrabber.BuildConfig; import awais.instagrabber.BuildConfig;
import awais.instagrabber.models.FeedModel;
import awais.instagrabber.models.PostChild;
import awais.instagrabber.models.ProfileModel; import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.models.direct_messages.DirectItemModel; import awais.instagrabber.models.direct_messages.DirectItemModel;
import awais.instagrabber.models.direct_messages.InboxThreadModel; import awais.instagrabber.models.direct_messages.InboxThreadModel;
@ -580,4 +585,122 @@ public final class ResponseBodyUtils {
//if ("raven_unknown".equals(type)) [default?] //if ("raven_unknown".equals(type)) [default?]
return RavenExpiringMediaType.RAVEN_UNKNOWN; return RavenExpiringMediaType.RAVEN_UNKNOWN;
} }
public static FeedModel parseItem(final JSONObject itemJson) throws JSONException {
if (itemJson == null) {
return null;
}
ProfileModel profileModel = null;
if (itemJson.has("user")) {
final JSONObject user = itemJson.getJSONObject("user");
final JSONObject friendshipStatus = user.optJSONObject("friendship_status");
boolean following = false;
boolean restricted = false;
boolean requested = false;
if (friendshipStatus != null) {
following = friendshipStatus.optBoolean("following");
requested = friendshipStatus.optBoolean("outgoing_request");
restricted = friendshipStatus.optBoolean("is_restricted");
}
profileModel = new ProfileModel(
user.optBoolean("is_private"),
false, // if you can see it then you def follow
user.optBoolean("is_verified"),
user.getString("pk"),
user.getString(Constants.EXTRAS_USERNAME),
user.optString("full_name"),
null,
null,
user.getString("profile_pic_url"),
null,
0,
0,
0,
following,
restricted,
false,
requested);
}
final JSONObject captionJson = itemJson.optJSONObject("caption");
final JSONObject locationJson = itemJson.optJSONObject("location");
final MediaItemType mediaType = ResponseBodyUtils.getMediaItemType(itemJson.optInt("media_type"));
if (mediaType == null) {
return null;
}
final FeedModel.Builder feedModelBuilder = new FeedModel.Builder()
.setItemType(mediaType)
.setProfileModel(profileModel)
.setPostId(itemJson.getString(Constants.EXTRAS_ID))
.setThumbnailUrl(mediaType != MediaItemType.MEDIA_TYPE_SLIDER ? ResponseBodyUtils.getLowQualityImage(itemJson) : null)
.setShortCode(itemJson.getString("code"))
.setPostCaption(captionJson != null ? captionJson.optString("text") : null)
.setCommentsCount(itemJson.optInt("comment_count"))
.setTimestamp(itemJson.optLong("taken_at", -1))
.setLiked(itemJson.optBoolean("has_liked"))
// .setBookmarked()
.setLikesCount(itemJson.optInt("like_count"))
.setLocationName(locationJson != null ? locationJson.optString("name") : null)
.setLocationId(locationJson != null ? String.valueOf(locationJson.optLong("pk")) : null)
.setImageHeight(itemJson.optInt("original_height"))
.setImageWidth(itemJson.optInt("original_width"));
switch (mediaType) {
case MEDIA_TYPE_VIDEO:
final long videoViews = itemJson.optLong("view_count", 0);
feedModelBuilder.setViewCount(videoViews)
.setDisplayUrl(ResponseBodyUtils.getVideoUrl(itemJson));
break;
case MEDIA_TYPE_IMAGE:
feedModelBuilder.setDisplayUrl(ResponseBodyUtils.getHighQualityImage(itemJson));
break;
case MEDIA_TYPE_SLIDER:
final List<PostChild> childPosts = getChildPosts(itemJson);
feedModelBuilder.setSliderItems(childPosts);
break;
}
return feedModelBuilder.build();
}
private static List<PostChild> getChildPosts(final JSONObject mediaJson) throws JSONException {
if (mediaJson == null) {
return Collections.emptyList();
}
final JSONArray carouselMedia = mediaJson.optJSONArray("carousel_media");
if (carouselMedia == null) {
return Collections.emptyList();
}
final List<PostChild> children = new ArrayList<>();
for (int i = 0; i < carouselMedia.length(); i++) {
final JSONObject childJson = carouselMedia.optJSONObject(i);
final PostChild childPost = getChildPost(childJson);
if (childPost != null) {
children.add(childPost);
}
}
return children;
}
private static PostChild getChildPost(final JSONObject childJson) throws JSONException {
if (childJson == null) {
return null;
}
final MediaItemType mediaType = ResponseBodyUtils.getMediaItemType(childJson.optInt("media_type"));
if (mediaType == null) {
return null;
}
final PostChild.Builder builder = new PostChild.Builder();
switch (mediaType) {
case MEDIA_TYPE_VIDEO:
builder.setDisplayUrl(ResponseBodyUtils.getVideoUrl(childJson));
break;
case MEDIA_TYPE_IMAGE:
builder.setDisplayUrl(ResponseBodyUtils.getHighQualityImage(childJson));
break;
}
return builder.setItemType(mediaType)
.setPostId(childJson.getString("id"))
.setThumbnailUrl(ResponseBodyUtils.getLowQualityImage(childJson))
.setHeight(childJson.optInt("original_height"))
.setWidth(childJson.optInt("original_width"))
.build();
}
} }

3
app/src/main/java/awais/instagrabber/utils/SettingsHelper.java

@ -29,6 +29,7 @@ import static awais.instagrabber.utils.Constants.INSTADP;
import static awais.instagrabber.utils.Constants.MARK_AS_SEEN; import static awais.instagrabber.utils.Constants.MARK_AS_SEEN;
import static awais.instagrabber.utils.Constants.MUTED_VIDEOS; import static awais.instagrabber.utils.Constants.MUTED_VIDEOS;
import static awais.instagrabber.utils.Constants.PREF_DARK_THEME; import static awais.instagrabber.utils.Constants.PREF_DARK_THEME;
import static awais.instagrabber.utils.Constants.PREF_HASHTAG_POSTS_LAYOUT;
import static awais.instagrabber.utils.Constants.PREF_LIGHT_THEME; import static awais.instagrabber.utils.Constants.PREF_LIGHT_THEME;
import static awais.instagrabber.utils.Constants.PREF_POSTS_LAYOUT; import static awais.instagrabber.utils.Constants.PREF_POSTS_LAYOUT;
import static awais.instagrabber.utils.Constants.PREF_PROFILE_POSTS_LAYOUT; import static awais.instagrabber.utils.Constants.PREF_PROFILE_POSTS_LAYOUT;
@ -117,7 +118,7 @@ public final class SettingsHelper {
@StringDef( @StringDef(
{APP_LANGUAGE, APP_THEME, COOKIE, FOLDER_PATH, DATE_TIME_FORMAT, DATE_TIME_SELECTION, CUSTOM_DATE_TIME_FORMAT, {APP_LANGUAGE, APP_THEME, COOKIE, FOLDER_PATH, DATE_TIME_FORMAT, DATE_TIME_SELECTION, CUSTOM_DATE_TIME_FORMAT,
DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME, PREF_POSTS_LAYOUT, PREF_PROFILE_POSTS_LAYOUT, DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME, PREF_POSTS_LAYOUT, PREF_PROFILE_POSTS_LAYOUT,
PREF_TOPIC_POSTS_LAYOUT})
PREF_TOPIC_POSTS_LAYOUT, PREF_HASHTAG_POSTS_LAYOUT})
public @interface StringSettings {} public @interface StringSettings {}
@StringDef({DOWNLOAD_USER_FOLDER, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, SHOW_QUICK_ACCESS_DIALOG, MUTED_VIDEOS, @StringDef({DOWNLOAD_USER_FOLDER, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, SHOW_QUICK_ACCESS_DIALOG, MUTED_VIDEOS,

119
app/src/main/java/awais/instagrabber/webservices/DiscoverService.java

@ -2,7 +2,7 @@ package awais.instagrabber.webservices;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableMap;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
@ -14,7 +14,6 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.FeedModel;
import awais.instagrabber.models.PostChild;
import awais.instagrabber.models.ProfileModel; import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.models.TopicCluster; import awais.instagrabber.models.TopicCluster;
import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.models.enums.MediaItemType;
@ -51,7 +50,7 @@ public class DiscoverService extends BaseService {
public void topicalExplore(@NonNull final TopicalExploreRequest request, public void topicalExplore(@NonNull final TopicalExploreRequest request,
final ServiceCallback<TopicalExploreResponse> callback) { final ServiceCallback<TopicalExploreResponse> callback) {
final ImmutableBiMap.Builder<String, String> builder = ImmutableBiMap.<String, String>builder()
final ImmutableMap.Builder<String, String> builder = ImmutableMap.<String, String>builder()
.put("module", "explore_popular"); .put("module", "explore_popular");
if (!TextUtils.isEmpty(request.getModule())) { if (!TextUtils.isEmpty(request.getModule())) {
builder.put("module", request.getModule()); builder.put("module", request.getModule());
@ -204,7 +203,7 @@ public class DiscoverService extends BaseService {
continue; continue;
} }
final JSONObject mediaJson = itemJson.optJSONObject("media"); final JSONObject mediaJson = itemJson.optJSONObject("media");
final FeedModel feedModel = parseClusterItemMedia(mediaJson);
final FeedModel feedModel = ResponseBodyUtils.parseItem(mediaJson);
if (feedModel != null) { if (feedModel != null) {
feedModels.add(feedModel); feedModels.add(feedModel);
} }
@ -212,118 +211,6 @@ public class DiscoverService extends BaseService {
return feedModels; return feedModels;
} }
private FeedModel parseClusterItemMedia(final JSONObject mediaJson) throws JSONException {
if (mediaJson == null) {
return null;
}
ProfileModel profileModel = null;
if (mediaJson.has("user")) {
final JSONObject user = mediaJson.getJSONObject("user");
final JSONObject friendshipStatus = user.optJSONObject("friendship_status");
boolean following = false;
boolean restricted = false;
boolean requested = false;
if (friendshipStatus != null) {
following = friendshipStatus.optBoolean("following");
requested = friendshipStatus.optBoolean("outgoing_request");
restricted = friendshipStatus.optBoolean("is_restricted");
}
profileModel = new ProfileModel(
user.optBoolean("is_private"),
false, // if you can see it then you def follow
user.optBoolean("is_verified"),
user.getString("pk"),
user.getString(Constants.EXTRAS_USERNAME),
user.optString("full_name"),
null,
null,
user.getString("profile_pic_url"),
null,
0,
0,
0,
following,
restricted,
false,
requested);
}
final JSONObject captionJson = mediaJson.optJSONObject("caption");
final JSONObject locationJson = mediaJson.optJSONObject("location");
final MediaItemType mediaType = ResponseBodyUtils.getMediaItemType(mediaJson.optInt("media_type"));
final FeedModel.Builder feedModelBuilder = new FeedModel.Builder()
.setItemType(mediaType)
.setProfileModel(profileModel)
.setPostId(mediaJson.getString(Constants.EXTRAS_ID))
.setThumbnailUrl(mediaType != MediaItemType.MEDIA_TYPE_SLIDER ? ResponseBodyUtils.getLowQualityImage(mediaJson) : null)
.setShortCode(mediaJson.getString("code"))
.setPostCaption(captionJson != null ? captionJson.optString("text") : null)
.setCommentsCount(mediaJson.optInt("comment_count"))
.setTimestamp(mediaJson.optLong("taken_at", -1))
.setLiked(mediaJson.optBoolean("has_liked"))
// .setBookmarked()
.setLikesCount(mediaJson.optInt("like_count"))
.setLocationName(locationJson != null ? locationJson.optString("name") : null)
.setLocationId(locationJson != null ? String.valueOf(locationJson.optInt("pk")) : null)
.setImageHeight(mediaJson.optInt("original_height"))
.setImageWidth(mediaJson.optInt("original_width"));
switch (mediaType) {
case MEDIA_TYPE_VIDEO:
final long videoViews = mediaJson.optLong("view_count", 0);
feedModelBuilder.setViewCount(videoViews)
.setDisplayUrl(ResponseBodyUtils.getVideoUrl(mediaJson));
break;
case MEDIA_TYPE_IMAGE:
feedModelBuilder.setDisplayUrl(ResponseBodyUtils.getHighQualityImage(mediaJson));
break;
case MEDIA_TYPE_SLIDER:
final List<PostChild> childPosts = getChildPosts(mediaJson);
feedModelBuilder.setSliderItems(childPosts);
break;
}
return feedModelBuilder.build();
}
private List<PostChild> getChildPosts(final JSONObject mediaJson) throws JSONException {
if (mediaJson == null) {
return Collections.emptyList();
}
final JSONArray carouselMedia = mediaJson.optJSONArray("carousel_media");
if (carouselMedia == null) {
return Collections.emptyList();
}
final List<PostChild> children = new ArrayList<>();
for (int i = 0; i < carouselMedia.length(); i++) {
final JSONObject childJson = carouselMedia.optJSONObject(i);
final PostChild childPost = getChildPost(childJson);
if (childPost != null) {
children.add(childPost);
}
}
return children;
}
private PostChild getChildPost(final JSONObject childJson) throws JSONException {
if (childJson == null) {
return null;
}
final MediaItemType mediaType = ResponseBodyUtils.getMediaItemType(childJson.optInt("media_type"));
final PostChild.Builder builder = new PostChild.Builder();
switch (mediaType) {
case MEDIA_TYPE_VIDEO:
builder.setDisplayUrl(ResponseBodyUtils.getVideoUrl(childJson));
break;
case MEDIA_TYPE_IMAGE:
builder.setDisplayUrl(ResponseBodyUtils.getHighQualityImage(childJson));
break;
}
return builder.setItemType(mediaType)
.setPostId(childJson.getString("id"))
.setThumbnailUrl(ResponseBodyUtils.getLowQualityImage(childJson))
.setHeight(childJson.optInt("original_height"))
.setWidth(childJson.optInt("original_width"))
.build();
}
public static class TopicalExploreRequest { public static class TopicalExploreRequest {
private String module; private String module;

186
app/src/main/java/awais/instagrabber/webservices/TagsService.java

@ -4,11 +4,22 @@ import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.common.collect.ImmutableMap;
import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import awais.instagrabber.models.FeedModel;
import awais.instagrabber.repositories.TagsRepository; import awais.instagrabber.repositories.TagsRepository;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
import retrofit2.Response; import retrofit2.Response;
@ -18,16 +29,20 @@ public class TagsService extends BaseService {
private static final String TAG = "TagsService"; private static final String TAG = "TagsService";
// web for www.instagram.com
private final TagsRepository webRepository;
private static TagsService instance; private static TagsService instance;
private final TagsRepository webRepository;
private final TagsRepository repository;
private TagsService() { private TagsService() {
final Retrofit webRetrofit = getRetrofitBuilder() final Retrofit webRetrofit = getRetrofitBuilder()
.baseUrl("https://www.instagram.com/") .baseUrl("https://www.instagram.com/")
.build(); .build();
webRepository = webRetrofit.create(TagsRepository.class); webRepository = webRetrofit.create(TagsRepository.class);
final Retrofit retrofit = getRetrofitBuilder()
.baseUrl("https://i.instagram.com/")
.build();
repository = retrofit.create(TagsRepository.class);
} }
public static TagsService getInstance() { public static TagsService getInstance() {
@ -98,4 +113,169 @@ public class TagsService extends BaseService {
} }
}); });
} }
public void fetchPosts(@NonNull final String tag,
final String maxId,
final ServiceCallback<TagPostsFetchResponse> callback) {
final ImmutableMap.Builder<String, String> builder = ImmutableMap.<String, String>builder();
if (!TextUtils.isEmpty(maxId)) {
builder.put("max_id", maxId);
}
final Call<String> request = repository.fetchPosts(tag, builder.build());
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {
try {
if (callback == null) {
return;
}
final String body = response.body();
if (TextUtils.isEmpty(body)) {
callback.onSuccess(null);
return;
}
final TagPostsFetchResponse tagPostsFetchResponse = parseResponse(body);
callback.onSuccess(tagPostsFetchResponse);
} catch (JSONException e) {
Log.e(TAG, "onResponse", e);
callback.onFailure(e);
}
}
@Override
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
private TagPostsFetchResponse parseResponse(@NonNull final String body) throws JSONException {
final JSONObject root = new JSONObject(body);
final boolean moreAvailable = root.optBoolean("more_available");
final String nextMaxId = root.optString("next_max_id");
final int numResults = root.optInt("num_results");
final String status = root.optString("status");
final JSONArray itemsJson = root.optJSONArray("items");
final List<FeedModel> items = parseItems(itemsJson);
return new TagPostsFetchResponse(
moreAvailable,
nextMaxId,
numResults,
status,
items
);
}
private List<FeedModel> parseItems(final JSONArray items) throws JSONException {
if (items == null) {
return Collections.emptyList();
}
final List<FeedModel> feedModels = new ArrayList<>();
for (int i = 0; i < items.length(); i++) {
final JSONObject itemJson = items.optJSONObject(i);
if (itemJson == null) {
continue;
}
final FeedModel feedModel = ResponseBodyUtils.parseItem(itemJson);
if (feedModel != null) {
feedModels.add(feedModel);
}
}
return feedModels;
}
public static class TagPostsFetchResponse {
private boolean moreAvailable;
private String nextMaxId;
private int numResults;
private String status;
private List<FeedModel> items;
public TagPostsFetchResponse(final boolean moreAvailable,
final String nextMaxId,
final int numResults,
final String status,
final List<FeedModel> items) {
this.moreAvailable = moreAvailable;
this.nextMaxId = nextMaxId;
this.numResults = numResults;
this.status = status;
this.items = items;
}
public boolean isMoreAvailable() {
return moreAvailable;
}
public TagPostsFetchResponse setMoreAvailable(final boolean moreAvailable) {
this.moreAvailable = moreAvailable;
return this;
}
public String getNextMaxId() {
return nextMaxId;
}
public TagPostsFetchResponse setNextMaxId(final String nextMaxId) {
this.nextMaxId = nextMaxId;
return this;
}
public int getNumResults() {
return numResults;
}
public TagPostsFetchResponse setNumResults(final int numResults) {
this.numResults = numResults;
return this;
}
public String getStatus() {
return status;
}
public TagPostsFetchResponse setStatus(final String status) {
this.status = status;
return this;
}
public List<FeedModel> getItems() {
return items;
}
public TagPostsFetchResponse setItems(final List<FeedModel> items) {
this.items = items;
return this;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final TagPostsFetchResponse that = (TagPostsFetchResponse) o;
return moreAvailable == that.moreAvailable &&
numResults == that.numResults &&
Objects.equals(nextMaxId, that.nextMaxId) &&
Objects.equals(status, that.status) &&
Objects.equals(items, that.items);
}
@Override
public int hashCode() {
return Objects.hash(moreAvailable, nextMaxId, numResults, status, items);
}
@Override
public String toString() {
return "TagPostsFetchResponse{" +
"moreAvailable=" + moreAvailable +
", nextMaxId='" + nextMaxId + '\'' +
", numResults=" + numResults +
", status='" + status + '\'' +
", items=" + items +
'}';
}
}
} }

66
app/src/main/res/layout/fragment_hashtag.xml

@ -51,7 +51,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:text="@string/follow" android:text="@string/follow"
android:visibility="gone" android:visibility="gone"
app:chipBackgroundColor="@null" app:chipBackgroundColor="@null"
@ -67,7 +66,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:text="@string/add_to_favorites" android:text="@string/add_to_favorites"
android:visibility="gone" android:visibility="gone"
app:chipBackgroundColor="@null" app:chipBackgroundColor="@null"
@ -77,74 +75,20 @@
app:layout_constraintStart_toEndOf="@id/btnFollowTag" app:layout_constraintStart_toEndOf="@id/btnFollowTag"
app:layout_constraintTop_toBottomOf="@id/mainTagPostCount" app:layout_constraintTop_toBottomOf="@id/mainTagPostCount"
app:rippleColor="@color/yellow_400" /> app:rippleColor="@color/yellow_400" />
<!--<com.google.android.material.chip.Chip-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- app:layout_constraintBottom_toBottomOf="@id/mainHashtagImage"-->
<!-- app:layout_constraintStart_toEndOf="@id/mainHashtagImage"-->
<!-- app:layout_constraintTop_toBottomOf="@id/mainTagPostCount" />-->
<!--<com.google.android.material.button.MaterialButton-->
<!-- android:id="@+id/btnFollowTag"-->
<!-- style="@style/Widget.MaterialComponents.Button.TextButton"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="0dp"-->
<!-- android:text="@string/follow"-->
<!-- android:textColor="@color/deep_purple_200"-->
<!-- android:visibility="gone"-->
<!-- app:icon="@drawable/ic_outline_person_add_24"-->
<!-- app:iconGravity="top"-->
<!-- app:iconTint="@color/deep_purple_200"-->
<!-- app:layout_constraintBottom_toTopOf="@id/fav_cb"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toEndOf="@id/mainTagPostCount"-->
<!-- app:layout_constraintTop_toTopOf="@id/mainHashtagImage"-->
<!-- tools:visibility="visible" />-->
<!--<CheckBox-->
<!-- android:id="@+id/fav_cb"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:button="@drawable/sl_favourite_24"-->
<!-- android:paddingStart="8dp"-->
<!-- android:paddingEnd="8dp"-->
<!-- android:text="Add to favorites"-->
<!-- android:visibility="gone"-->
<!-- app:buttonTint="@color/yellow_800"-->
<!-- app:layout_constraintBottom_toBottomOf="@id/mainHashtagImage"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toEndOf="@id/mainHashtagImage"-->
<!-- app:layout_constraintTop_toBottomOf="@id/btnFollowTag"-->
<!-- tools:visibility="gone" />-->
<!--<ProgressBar-->
<!-- android:id="@+id/fav_progress"-->
<!-- style="@style/Widget.MaterialComponents.ProgressIndicator.Circular.Indeterminate"-->
<!-- android:layout_width="24dp"-->
<!-- android:layout_height="24dp"-->
<!-- android:paddingStart="8dp"-->
<!-- android:paddingEnd="8dp"-->
<!-- android:visibility="gone"-->
<!-- app:layout_constraintBottom_toBottomOf="@id/mainHashtagImage"-->
<!-- app:layout_constraintStart_toStartOf="@id/fav_cb"-->
<!-- app:layout_constraintTop_toBottomOf="@id/mainTagPostCount"-->
<!-- tools:visibility="gone" />-->
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mainPosts"
<awais.instagrabber.customviews.PostsRecyclerView
android:id="@+id/posts"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false"
tools:listitem="@layout/item_post" />
android:clipToPadding="false" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</awais.instagrabber.customviews.helpers.NestedCoordinatorLayout> </awais.instagrabber.customviews.helpers.NestedCoordinatorLayout>

9
app/src/main/res/menu/hashtag_menu.xml

@ -0,0 +1,9 @@
<?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/layout"
android:title="@string/layout"
app:showAsAction="never" />
</menu>

2
app/src/main/res/navigation/comments_nav_graph.xml

@ -5,7 +5,7 @@
android:id="@+id/comments_nav_graph" android:id="@+id/comments_nav_graph"
app:startDestination="@id/commentsViewerFragment"> app:startDestination="@id/commentsViewerFragment">
<include app:graph="@navigation/hashtag_nav_graph" />
<!--<include app:graph="@navigation/hashtag_nav_graph" />-->
<action <action
android:id="@+id/action_global_hashTagFragment" android:id="@+id/action_global_hashTagFragment"

21
app/src/main/res/navigation/hashtag_nav_graph.xml

@ -19,13 +19,32 @@
app:argType="boolean" /> app:argType="boolean" />
</action> </action>
<include app:graph="@navigation/comments_nav_graph" />
<action
android:id="@+id/action_global_commentsViewerFragment"
app:destination="@id/comments_nav_graph">
<argument
android:name="shortCode"
app:argType="string"
app:nullable="false" />
<argument
android:name="postId"
app:argType="string"
app:nullable="false" />
<argument
android:name="postUserId"
app:argType="string"
app:nullable="false" />
</action>
<action <action
android:id="@+id/action_global_profileFragment" android:id="@+id/action_global_profileFragment"
app:destination="@id/profile_nav_graph"> app:destination="@id/profile_nav_graph">
<argument <argument
android:name="username" android:name="username"
app:argType="string" app:argType="string"
app:nullable="false" />
app:nullable="true" />
</action> </action>
<fragment <fragment

Loading…
Cancel
Save