Add Posts view to Hashtag fragment

This commit is contained in:
Ammar Githam 2020-11-01 20:34:42 +09:00
parent 6d9dadc0cd
commit 2931f2d3ab
16 changed files with 748 additions and 347 deletions

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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; // if (postsAdapter == null) return;
postsAdapter.clearSelection(); // 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) { // if (postsAdapter == null || hashtag == null) {
return false; // return false;
} // }
final Context context = getContext(); // final Context context = getContext();
if (context == null) return false; // if (context == null) return false;
DownloadUtils.batchDownload(context, // DownloadUtils.batchDownload(context,
hashtag, // hashtag,
DownloadMethod.DOWNLOAD_MAIN, // DownloadMethod.DOWNLOAD_MAIN,
postsAdapter.getSelectedModels()); // postsAdapter.getSelectedModels());
checkAndResetAction(); // 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) { public void onPostClick(final FeedModel feedModel, final View profilePicView, final View mainPostImage) {
binding.swipeRefreshLayout.setRefreshing(false); openPostDialog(feedModel, profilePicView, mainPostImage, -1);
if (result == null) return; }
binding.mainPosts.post(() -> binding.mainPosts.setVisibility(View.VISIBLE));
final List<PostModel> postModels = postsViewModel.getList().getValue(); @Override
List<PostModel> finalList = postModels == null || postModels.isEmpty() public void onSliderClick(final FeedModel feedModel, final int position) {
? new ArrayList<>() openPostDialog(feedModel, null, null, position);
: new ArrayList<>(postModels); }
if (isPullToRefresh) {
finalList = result; @Override
isPullToRefresh = false; public void onCommentsClick(final FeedModel feedModel) {
} else { final NavDirections commentsAction = HashTagFragmentDirections.actionGlobalCommentsViewerFragment(
finalList.addAll(result); 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); requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE);
postsViewModel.getList().postValue(finalList); }
PostModel model = null;
if (!result.isEmpty()) { @Override
model = result.get(result.size() - 1); 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; final PostViewV2Fragment fragment = builder
endCursor = model.getEndCursor(); .setSharedProfilePicElement(profilePicView)
hasNextPage = model.hasNextPage(); .setSharedMainPostElement(mainPostImage)
model.setPageCursor(false, null); .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; binding.posts.refresh();
endCursor = null; fetchStories();
fetchHashtagModel();
} }
@Override @Override
@ -195,11 +277,17 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
} }
@Override @Override
public void onDestroy() { public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) {
super.onDestroy(); inflater.inflate(R.menu.topic_posts_menu, menu);
if (postsViewModel != null) { }
postsViewModel.getList().postValue(Collections.emptyList());
@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() { private void setupPosts() {
stopCurrentExecutor(); 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; // postsViewModel = new ViewModelProvider(this).get(PostsViewModel.class);
currentlyExecuting = new PostsFetcher(hashtag.substring(1), PostItemType.HASHTAG, endCursor, postsFetchListener) // final Context context = getContext();
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); // if (context == null) return;
final Context context = getContext(); // final GridAutofitLayoutManager layoutManager = new GridAutofitLayoutManager(context, Utils.convertDpToPx(110));
if (context == null) return; // binding.mainPosts.setLayoutManager(layoutManager);
if (isLoggedIn) { // binding.mainPosts.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(4)));
storiesService.getUserStory(hashtagModel.getName(), // postsAdapter = new PostsAdapter((postModel, position) -> {
null, // if (postsAdapter.isSelecting()) {
false, // if (actionMode == null) return;
true, // final String title = getString(R.string.number_selected, postsAdapter.getSelectedModels().size());
false, // actionMode.setTitle(title);
new ServiceCallback<List<StoryModel>>() { // return;
@Override // }
public void onSuccess(final List<StoryModel> storyModels) { // if (checkAndResetAction()) return;
if (storyModels != null && !storyModels.isEmpty()) { // final List<PostModel> postModels = postsViewModel.getList().getValue();
binding.mainHashtagImage.setStoriesBorder(); // if (postModels == null || postModels.size() == 0) return;
hasStories = true; // 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);
}
@Override private void setHashtagDetails() {
public void onFailure(final Throwable t) { if (isLoggedIn) {
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) { if (!hasStories) return;
// show stories // show stories
final NavDirections action = HashTagFragmentDirections final NavDirections action = HashTagFragmentDirections
.actionHashtagFragmentToStoryViewerFragment(-1, null, true, false, hashtagModel.getName(), hashtagModel.getName()); .actionHashtagFragmentToStoryViewerFragment(-1, null, true, false, hashtagModel.getName(), hashtagModel.getName());
NavHostFragment.findNavController(this).navigate(action); 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");
}
} }

View File

@ -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);

View File

@ -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;

View File

@ -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) {

View File

@ -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);
} }

View File

@ -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";
} }

View File

@ -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();
}
} }

View File

@ -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,

View File

@ -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;

View File

@ -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 +
'}';
}
}
} }

View File

@ -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 <awais.instagrabber.customviews.PostsRecyclerView
android:id="@+id/mainPosts" 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" android:clipToPadding="false" />
tools:listitem="@layout/item_post" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</awais.instagrabber.customviews.helpers.NestedCoordinatorLayout> </awais.instagrabber.customviews.helpers.NestedCoordinatorLayout>

View File

@ -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>

View File

@ -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"

View File

@ -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