Ammar Githam
4 years ago
28 changed files with 1657 additions and 285 deletions
-
5app/build.gradle
-
21app/src/main/java/awais/instagrabber/activities/MainActivity.java
-
53app/src/main/java/awais/instagrabber/adapters/DiscoverTopicsAdapter.java
-
99app/src/main/java/awais/instagrabber/adapters/viewholder/TopicClusterViewHolder.java
-
56app/src/main/java/awais/instagrabber/asyncs/DiscoverPostFetchService.java
-
8app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java
-
8app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java
-
3app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java
-
18app/src/main/java/awais/instagrabber/customviews/helpers/PostFetcher.java
-
2app/src/main/java/awais/instagrabber/customviews/helpers/RecyclerLazyLoaderAtBottom.java
-
68app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java
-
4app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java
-
363app/src/main/java/awais/instagrabber/fragments/TopicPostsFragment.java
-
354app/src/main/java/awais/instagrabber/fragments/main/DiscoverFragment.java
-
16app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java
-
57app/src/main/java/awais/instagrabber/models/TopicCluster.java
-
12app/src/main/java/awais/instagrabber/repositories/DiscoverRepository.java
-
1app/src/main/java/awais/instagrabber/utils/Constants.java
-
24app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java
-
4app/src/main/java/awais/instagrabber/utils/SettingsHelper.java
-
19app/src/main/java/awais/instagrabber/viewmodels/TopicClusterViewModel.java
-
501app/src/main/java/awais/instagrabber/webservices/DiscoverService.java
-
44app/src/main/res/layout/fragment_discover.xml
-
14app/src/main/res/layout/fragment_profile.xml
-
58app/src/main/res/layout/fragment_topic_posts.xml
-
45app/src/main/res/layout/item_discover_topic.xml
-
9app/src/main/res/menu/topic_posts_menu.xml
-
76app/src/main/res/navigation/discover_nav_graph.xml
@ -0,0 +1,53 @@ |
|||
package awais.instagrabber.adapters; |
|||
|
|||
import android.view.LayoutInflater; |
|||
import android.view.View; |
|||
import android.view.ViewGroup; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.recyclerview.widget.DiffUtil; |
|||
import androidx.recyclerview.widget.ListAdapter; |
|||
|
|||
import awais.instagrabber.adapters.viewholder.TopicClusterViewHolder; |
|||
import awais.instagrabber.databinding.ItemDiscoverTopicBinding; |
|||
import awais.instagrabber.models.TopicCluster; |
|||
|
|||
public class DiscoverTopicsAdapter extends ListAdapter<TopicCluster, TopicClusterViewHolder> { |
|||
private static final DiffUtil.ItemCallback<TopicCluster> DIFF_CALLBACK = new DiffUtil.ItemCallback<TopicCluster>() { |
|||
@Override |
|||
public boolean areItemsTheSame(@NonNull final TopicCluster oldItem, @NonNull final TopicCluster newItem) { |
|||
return oldItem.getId().equals(newItem.getId()); |
|||
} |
|||
|
|||
@Override |
|||
public boolean areContentsTheSame(@NonNull final TopicCluster oldItem, @NonNull final TopicCluster newItem) { |
|||
return oldItem.getCoverMedia().getDisplayUrl().equals(newItem.getCoverMedia().getDisplayUrl()) |
|||
&& oldItem.getTitle().equals(newItem.getTitle()); |
|||
} |
|||
}; |
|||
|
|||
private final OnTopicClickListener onTopicClickListener; |
|||
|
|||
public DiscoverTopicsAdapter(final OnTopicClickListener onTopicClickListener) { |
|||
super(DIFF_CALLBACK); |
|||
this.onTopicClickListener = onTopicClickListener; |
|||
} |
|||
|
|||
@NonNull |
|||
@Override |
|||
public TopicClusterViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { |
|||
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); |
|||
final ItemDiscoverTopicBinding binding = ItemDiscoverTopicBinding.inflate(layoutInflater, parent, false); |
|||
return new TopicClusterViewHolder(binding, onTopicClickListener); |
|||
} |
|||
|
|||
@Override |
|||
public void onBindViewHolder(@NonNull final TopicClusterViewHolder holder, final int position) { |
|||
final TopicCluster topicCluster = getItem(position); |
|||
holder.bind(topicCluster); |
|||
} |
|||
|
|||
public interface OnTopicClickListener { |
|||
void onTopicClick(TopicCluster topicCluster, View root, View cover, View title, int titleColor, int backgroundColor); |
|||
} |
|||
} |
@ -0,0 +1,99 @@ |
|||
package awais.instagrabber.adapters.viewholder; |
|||
|
|||
import android.content.res.Resources; |
|||
import android.graphics.Bitmap; |
|||
import android.graphics.Color; |
|||
import android.graphics.drawable.GradientDrawable; |
|||
import android.net.Uri; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.annotation.Nullable; |
|||
import androidx.palette.graphics.Palette; |
|||
import androidx.recyclerview.widget.RecyclerView; |
|||
|
|||
import com.facebook.common.executors.CallerThreadExecutor; |
|||
import com.facebook.common.references.CloseableReference; |
|||
import com.facebook.datasource.DataSource; |
|||
import com.facebook.drawee.backends.pipeline.Fresco; |
|||
import com.facebook.imagepipeline.core.ImagePipeline; |
|||
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber; |
|||
import com.facebook.imagepipeline.image.CloseableImage; |
|||
import com.facebook.imagepipeline.request.ImageRequest; |
|||
import com.facebook.imagepipeline.request.ImageRequestBuilder; |
|||
|
|||
import java.util.concurrent.atomic.AtomicInteger; |
|||
|
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.adapters.DiscoverTopicsAdapter; |
|||
import awais.instagrabber.databinding.ItemDiscoverTopicBinding; |
|||
import awais.instagrabber.models.TopicCluster; |
|||
|
|||
public class TopicClusterViewHolder extends RecyclerView.ViewHolder { |
|||
private final ItemDiscoverTopicBinding binding; |
|||
private final DiscoverTopicsAdapter.OnTopicClickListener onTopicClickListener; |
|||
|
|||
public TopicClusterViewHolder(@NonNull final ItemDiscoverTopicBinding binding, |
|||
final DiscoverTopicsAdapter.OnTopicClickListener onTopicClickListener) { |
|||
super(binding.getRoot()); |
|||
this.binding = binding; |
|||
this.onTopicClickListener = onTopicClickListener; |
|||
} |
|||
|
|||
public void bind(final TopicCluster topicCluster) { |
|||
if (topicCluster == null) { |
|||
return; |
|||
} |
|||
final AtomicInteger titleColor = new AtomicInteger(-1); |
|||
final AtomicInteger backgroundColor = new AtomicInteger(-1); |
|||
if (onTopicClickListener != null) { |
|||
itemView.setOnClickListener(v -> onTopicClickListener.onTopicClick( |
|||
topicCluster, |
|||
binding.getRoot(), |
|||
binding.cover, |
|||
binding.title, |
|||
titleColor.get(), |
|||
backgroundColor.get() |
|||
)); |
|||
} |
|||
// binding.title.setTransitionName("title-" + topicCluster.getId()); |
|||
binding.cover.setTransitionName("cover-" + topicCluster.getId()); |
|||
final ImageRequest imageRequest = ImageRequestBuilder |
|||
.newBuilderWithSource(Uri.parse(topicCluster.getCoverMedia().getDisplayUrl())) |
|||
.build(); |
|||
final ImagePipeline imagePipeline = Fresco.getImagePipeline(); |
|||
final DataSource<CloseableReference<CloseableImage>> dataSource = imagePipeline |
|||
.fetchDecodedImage(imageRequest, CallerThreadExecutor.getInstance()); |
|||
dataSource.subscribe(new BaseBitmapDataSubscriber() { |
|||
@Override |
|||
public void onNewResultImpl(@Nullable Bitmap bitmap) { |
|||
if (dataSource.isFinished()) { |
|||
dataSource.close(); |
|||
} |
|||
if (bitmap != null) { |
|||
Palette.from(bitmap).generate(p -> { |
|||
final Palette.Swatch swatch = p.getDominantSwatch(); |
|||
final Resources resources = itemView.getResources(); |
|||
int titleTextColor = resources.getColor(R.color.white); |
|||
if (swatch != null) { |
|||
backgroundColor.set(swatch.getRgb()); |
|||
GradientDrawable gd = new GradientDrawable( |
|||
GradientDrawable.Orientation.TOP_BOTTOM, |
|||
new int[]{Color.TRANSPARENT, backgroundColor.get()}); |
|||
titleTextColor = swatch.getTitleTextColor(); |
|||
binding.background.setBackground(gd); |
|||
} |
|||
titleColor.set(titleTextColor); |
|||
binding.title.setTextColor(titleTextColor); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onFailureImpl(@NonNull DataSource dataSource) { |
|||
dataSource.close(); |
|||
} |
|||
}, CallerThreadExecutor.getInstance()); |
|||
binding.cover.setImageRequest(imageRequest); |
|||
binding.title.setText(topicCluster.getTitle()); |
|||
} |
|||
} |
@ -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.webservices.DiscoverService; |
|||
import awais.instagrabber.webservices.ServiceCallback; |
|||
|
|||
public class DiscoverPostFetchService implements PostFetcher.PostFetchService { |
|||
private static final String TAG = "DiscoverPostFetchService"; |
|||
private final DiscoverService discoverService; |
|||
private final DiscoverService.TopicalExploreRequest topicalExploreRequest; |
|||
private boolean moreAvailable = false; |
|||
|
|||
public DiscoverPostFetchService(final DiscoverService.TopicalExploreRequest topicalExploreRequest) { |
|||
this.topicalExploreRequest = topicalExploreRequest; |
|||
discoverService = DiscoverService.getInstance(); |
|||
} |
|||
|
|||
@Override |
|||
public void fetch(final FetchListener<List<FeedModel>> fetchListener) { |
|||
discoverService.topicalExplore(topicalExploreRequest, new ServiceCallback<DiscoverService.TopicalExploreResponse>() { |
|||
@Override |
|||
public void onSuccess(final DiscoverService.TopicalExploreResponse result) { |
|||
if (result == null) { |
|||
onFailure(new RuntimeException("result is null")); |
|||
return; |
|||
} |
|||
moreAvailable = result.isMoreAvailable(); |
|||
topicalExploreRequest.setMaxId(result.getNextMaxId()); |
|||
if (fetchListener != null) { |
|||
fetchListener.onResult(result.getItems()); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(final Throwable t) { |
|||
if (fetchListener != null) { |
|||
fetchListener.onFailure(t); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
@Override |
|||
public void reset() { |
|||
topicalExploreRequest.setMaxId(-1); |
|||
} |
|||
|
|||
@Override |
|||
public boolean hasNextPage() { |
|||
return moreAvailable; |
|||
} |
|||
} |
@ -0,0 +1,363 @@ |
|||
package awais.instagrabber.fragments; |
|||
|
|||
import android.animation.ArgbEvaluator; |
|||
import android.content.Context; |
|||
import android.graphics.Color; |
|||
import android.graphics.PorterDuff; |
|||
import android.graphics.drawable.Animatable; |
|||
import android.graphics.drawable.Drawable; |
|||
import android.graphics.drawable.GradientDrawable; |
|||
import android.os.Bundle; |
|||
import android.os.Handler; |
|||
import android.view.LayoutInflater; |
|||
import android.view.Menu; |
|||
import android.view.MenuInflater; |
|||
import android.view.MenuItem; |
|||
import android.view.View; |
|||
import android.view.ViewGroup; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.annotation.Nullable; |
|||
import androidx.core.content.PermissionChecker; |
|||
import androidx.core.graphics.ColorUtils; |
|||
import androidx.fragment.app.Fragment; |
|||
import androidx.navigation.NavController; |
|||
import androidx.navigation.NavDirections; |
|||
import androidx.navigation.fragment.NavHostFragment; |
|||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; |
|||
import androidx.transition.ChangeBounds; |
|||
import androidx.transition.TransitionInflater; |
|||
import androidx.transition.TransitionSet; |
|||
|
|||
import com.facebook.drawee.backends.pipeline.Fresco; |
|||
import com.facebook.drawee.controller.BaseControllerListener; |
|||
import com.facebook.drawee.interfaces.DraweeController; |
|||
import com.facebook.imagepipeline.image.ImageInfo; |
|||
|
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.activities.MainActivity; |
|||
import awais.instagrabber.adapters.FeedAdapterV2; |
|||
import awais.instagrabber.asyncs.DiscoverPostFetchService; |
|||
import awais.instagrabber.customviews.helpers.NestedCoordinatorLayout; |
|||
import awais.instagrabber.databinding.FragmentTopicPostsBinding; |
|||
import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; |
|||
import awais.instagrabber.fragments.main.DiscoverFragmentDirections; |
|||
import awais.instagrabber.models.FeedModel; |
|||
import awais.instagrabber.models.PostsLayoutPreferences; |
|||
import awais.instagrabber.models.TopicCluster; |
|||
import awais.instagrabber.utils.Constants; |
|||
import awais.instagrabber.utils.DownloadUtils; |
|||
import awais.instagrabber.utils.Utils; |
|||
import awais.instagrabber.webservices.DiscoverService; |
|||
|
|||
import static androidx.core.content.PermissionChecker.checkSelfPermission; |
|||
import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; |
|||
import static awais.instagrabber.utils.Utils.settingsHelper; |
|||
|
|||
public class TopicPostsFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { |
|||
private static final int STORAGE_PERM_REQUEST_CODE = 8020; |
|||
private MainActivity fragmentActivity; |
|||
private FragmentTopicPostsBinding binding; |
|||
private NestedCoordinatorLayout root; |
|||
private boolean shouldRefresh = true; |
|||
private TopicCluster topicCluster; |
|||
|
|||
private final FeedAdapterV2.FeedItemCallback feedItemCallback = new FeedAdapterV2.FeedItemCallback() { |
|||
@Override |
|||
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 = DiscoverFragmentDirections.actionGlobalCommentsViewerFragment( |
|||
feedModel.getShortCode(), |
|||
feedModel.getPostId(), |
|||
feedModel.getProfileModel().getId() |
|||
); |
|||
NavHostFragment.findNavController(TopicPostsFragment.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; |
|||
} |
|||
requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); |
|||
} |
|||
|
|||
@Override |
|||
public void onHashtagClick(final String hashtag) { |
|||
final NavDirections action = DiscoverFragmentDirections.actionGlobalHashTagFragment(hashtag); |
|||
NavHostFragment.findNavController(TopicPostsFragment.this).navigate(action); |
|||
} |
|||
|
|||
@Override |
|||
public void onLocationClick(final FeedModel feedModel) { |
|||
final NavDirections action = DiscoverFragmentDirections.actionGlobalLocationFragment(feedModel.getLocationId()); |
|||
NavHostFragment.findNavController(TopicPostsFragment.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); |
|||
} |
|||
final PostViewV2Fragment fragment = builder |
|||
.setSharedProfilePicElement(profilePicView) |
|||
.setSharedMainPostElement(mainPostImage) |
|||
.build(); |
|||
fragment.show(getChildFragmentManager(), "post_view"); |
|||
} |
|||
}; |
|||
|
|||
@Override |
|||
public void onCreate(@Nullable final Bundle savedInstanceState) { |
|||
super.onCreate(savedInstanceState); |
|||
fragmentActivity = (MainActivity) requireActivity(); |
|||
final TransitionSet transitionSet = new TransitionSet(); |
|||
transitionSet.addTransition(new ChangeBounds()) |
|||
.addTransition(TransitionInflater.from(getContext()).inflateTransition(android.R.transition.move)) |
|||
.setDuration(200); |
|||
setSharedElementEnterTransition(transitionSet); |
|||
postponeEnterTransition(); |
|||
setHasOptionsMenu(true); |
|||
} |
|||
|
|||
@Nullable |
|||
@Override |
|||
public View onCreateView(@NonNull final LayoutInflater inflater, |
|||
@Nullable final ViewGroup container, |
|||
@Nullable final Bundle savedInstanceState) { |
|||
if (root != null) { |
|||
shouldRefresh = false; |
|||
return root; |
|||
} |
|||
binding = FragmentTopicPostsBinding.inflate(inflater, container, false); |
|||
root = binding.getRoot(); |
|||
return root; |
|||
} |
|||
|
|||
@Override |
|||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { |
|||
if (!shouldRefresh) return; |
|||
binding.swipeRefreshLayout.setOnRefreshListener(this); |
|||
init(); |
|||
shouldRefresh = false; |
|||
} |
|||
|
|||
@Override |
|||
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); |
|||
} |
|||
|
|||
@Override |
|||
public void onRefresh() { |
|||
binding.posts.refresh(); |
|||
} |
|||
|
|||
@Override |
|||
public void onDestroy() { |
|||
super.onDestroy(); |
|||
resetToolbar(); |
|||
} |
|||
|
|||
@Override |
|||
public void onDestroyView() { |
|||
super.onDestroyView(); |
|||
resetToolbar(); |
|||
} |
|||
|
|||
private void resetToolbar() { |
|||
fragmentActivity.resetToolbar(); |
|||
} |
|||
|
|||
private void init() { |
|||
if (getArguments() == null) return; |
|||
final TopicPostsFragmentArgs fragmentArgs = TopicPostsFragmentArgs.fromBundle(getArguments()); |
|||
topicCluster = fragmentArgs.getTopicCluster(); |
|||
setupToolbar(fragmentArgs.getTitleColor(), fragmentArgs.getBackgroundColor()); |
|||
setupPosts(); |
|||
} |
|||
|
|||
private void setupToolbar(final int titleColor, final int backgroundColor) { |
|||
if (topicCluster == null) { |
|||
return; |
|||
} |
|||
binding.cover.setTransitionName("cover-" + topicCluster.getId()); |
|||
fragmentActivity.setToolbar(binding.toolbar); |
|||
binding.collapsingToolbarLayout.setTitle(topicCluster.getTitle()); |
|||
final int collapsedTitleTextColor = ColorUtils.setAlphaComponent(titleColor, 0xFF); |
|||
final int expandedTitleTextColor = ColorUtils.setAlphaComponent(titleColor, 0x99); |
|||
binding.collapsingToolbarLayout.setExpandedTitleColor(expandedTitleTextColor); |
|||
binding.collapsingToolbarLayout.setCollapsedTitleTextColor(collapsedTitleTextColor); |
|||
binding.collapsingToolbarLayout.setContentScrimColor(backgroundColor); |
|||
final Drawable navigationIcon = binding.toolbar.getNavigationIcon(); |
|||
final Drawable overflowIcon = binding.toolbar.getOverflowIcon(); |
|||
if (navigationIcon != null && overflowIcon != null) { |
|||
final Drawable navDrawable = navigationIcon.mutate(); |
|||
final Drawable overflowDrawable = overflowIcon.mutate(); |
|||
navDrawable.setAlpha(0xFF); |
|||
overflowDrawable.setAlpha(0xFF); |
|||
final ArgbEvaluator argbEvaluator = new ArgbEvaluator(); |
|||
binding.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { |
|||
final int totalScrollRange = appBarLayout.getTotalScrollRange(); |
|||
final float current = totalScrollRange + verticalOffset; |
|||
final float fraction = current / totalScrollRange; |
|||
final int tempColor = (int) argbEvaluator.evaluate(fraction, collapsedTitleTextColor, expandedTitleTextColor); |
|||
navDrawable.setColorFilter(tempColor, PorterDuff.Mode.SRC_ATOP); |
|||
overflowDrawable.setColorFilter(tempColor, PorterDuff.Mode.SRC_ATOP); |
|||
|
|||
}); |
|||
} |
|||
final GradientDrawable gd = new GradientDrawable( |
|||
GradientDrawable.Orientation.TOP_BOTTOM, |
|||
new int[]{Color.TRANSPARENT, backgroundColor}); |
|||
binding.background.setBackground(gd); |
|||
setupCover(); |
|||
} |
|||
|
|||
private void setupCover() { |
|||
final String coverUrl = topicCluster.getCoverMedia().getDisplayUrl(); |
|||
final DraweeController controller = Fresco |
|||
.newDraweeControllerBuilder() |
|||
.setOldController(binding.cover.getController()) |
|||
.setUri(coverUrl) |
|||
.setControllerListener(new BaseControllerListener<ImageInfo>() { |
|||
|
|||
@Override |
|||
public void onFailure(final String id, final Throwable throwable) { |
|||
super.onFailure(id, throwable); |
|||
startPostponedEnterTransition(); |
|||
} |
|||
|
|||
@Override |
|||
public void onFinalImageSet(final String id, |
|||
@Nullable final ImageInfo imageInfo, |
|||
@Nullable final Animatable animatable) { |
|||
startPostponedEnterTransition(); |
|||
} |
|||
}) |
|||
.build(); |
|||
binding.cover.setController(controller); |
|||
} |
|||
|
|||
private void setupPosts() { |
|||
final DiscoverService.TopicalExploreRequest topicalExploreRequest = new DiscoverService.TopicalExploreRequest(); |
|||
topicalExploreRequest.setClusterId(topicCluster.getId()); |
|||
binding.posts.setViewModelStoreOwner(this) |
|||
.setLifeCycleOwner(this) |
|||
.setPostFetchService(new DiscoverPostFetchService(topicalExploreRequest)) |
|||
.setLayoutPreferences(PostsLayoutPreferences.fromJson(settingsHelper.getString(Constants.PREF_TOPIC_POSTS_LAYOUT))) |
|||
.addFetchStatusChangeListener(fetching -> updateSwipeRefreshState()) |
|||
.setFeedItemCallback(feedItemCallback) |
|||
.init(); |
|||
binding.swipeRefreshLayout.setRefreshing(true); |
|||
} |
|||
|
|||
private void updateSwipeRefreshState() { |
|||
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching()); |
|||
} |
|||
|
|||
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 void showPostsLayoutPreferences() { |
|||
final PostsLayoutPreferencesDialogFragment fragment = new PostsLayoutPreferencesDialogFragment( |
|||
Constants.PREF_TOPIC_POSTS_LAYOUT, |
|||
preferences -> new Handler().postDelayed(() -> binding.posts.setLayoutPreferences(preferences), 200)); |
|||
fragment.show(getChildFragmentManager(), "posts_layout_preferences"); |
|||
} |
|||
} |
@ -0,0 +1,57 @@ |
|||
package awais.instagrabber.models; |
|||
|
|||
import java.io.Serializable; |
|||
|
|||
public class TopicCluster implements Serializable { |
|||
private String id; |
|||
private String title; |
|||
private String type; |
|||
private boolean canMute; |
|||
private boolean isMuted; |
|||
private int rankedPosition; |
|||
private FeedModel coverMedia; |
|||
|
|||
public TopicCluster(final String id, |
|||
final String title, |
|||
final String type, |
|||
final boolean canMute, |
|||
final boolean isMuted, |
|||
final int rankedPosition, |
|||
final FeedModel coverMedia) { |
|||
this.id = id; |
|||
this.title = title; |
|||
this.type = type; |
|||
this.canMute = canMute; |
|||
this.isMuted = isMuted; |
|||
this.rankedPosition = rankedPosition; |
|||
this.coverMedia = coverMedia; |
|||
} |
|||
|
|||
public String getId() { |
|||
return id; |
|||
} |
|||
|
|||
public String getTitle() { |
|||
return title; |
|||
} |
|||
|
|||
public String getType() { |
|||
return type; |
|||
} |
|||
|
|||
public boolean isCanMute() { |
|||
return canMute; |
|||
} |
|||
|
|||
public boolean isMuted() { |
|||
return isMuted; |
|||
} |
|||
|
|||
public int getRankedPosition() { |
|||
return rankedPosition; |
|||
} |
|||
|
|||
public FeedModel getCoverMedia() { |
|||
return coverMedia; |
|||
} |
|||
} |
@ -0,0 +1,12 @@ |
|||
package awais.instagrabber.repositories; |
|||
|
|||
import java.util.Map; |
|||
|
|||
import retrofit2.Call; |
|||
import retrofit2.http.GET; |
|||
import retrofit2.http.QueryMap; |
|||
|
|||
public interface DiscoverRepository { |
|||
@GET("/api/v1/discover/topical_explore/") |
|||
Call<String> topicalExplore(@QueryMap Map<String, String> queryParams); |
|||
} |
@ -0,0 +1,19 @@ |
|||
package awais.instagrabber.viewmodels; |
|||
|
|||
import androidx.lifecycle.MutableLiveData; |
|||
import androidx.lifecycle.ViewModel; |
|||
|
|||
import java.util.List; |
|||
|
|||
import awais.instagrabber.models.TopicCluster; |
|||
|
|||
public class TopicClusterViewModel extends ViewModel { |
|||
private MutableLiveData<List<TopicCluster>> list; |
|||
|
|||
public MutableLiveData<List<TopicCluster>> getList() { |
|||
if (list == null) { |
|||
list = new MutableLiveData<>(); |
|||
} |
|||
return list; |
|||
} |
|||
} |
@ -0,0 +1,501 @@ |
|||
package awais.instagrabber.webservices; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
|
|||
import com.google.common.collect.ImmutableBiMap; |
|||
|
|||
import org.json.JSONArray; |
|||
import org.json.JSONException; |
|||
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.models.PostChild; |
|||
import awais.instagrabber.models.ProfileModel; |
|||
import awais.instagrabber.models.TopicCluster; |
|||
import awais.instagrabber.models.enums.MediaItemType; |
|||
import awais.instagrabber.repositories.DiscoverRepository; |
|||
import awais.instagrabber.utils.Constants; |
|||
import awais.instagrabber.utils.ResponseBodyUtils; |
|||
import awais.instagrabber.utils.TextUtils; |
|||
import retrofit2.Call; |
|||
import retrofit2.Callback; |
|||
import retrofit2.Response; |
|||
import retrofit2.Retrofit; |
|||
|
|||
public class DiscoverService extends BaseService { |
|||
|
|||
private static final String TAG = "DiscoverService"; |
|||
|
|||
private final DiscoverRepository repository; |
|||
|
|||
private static DiscoverService instance; |
|||
|
|||
private DiscoverService() { |
|||
final Retrofit retrofit = getRetrofitBuilder() |
|||
.baseUrl("https://i.instagram.com") |
|||
.build(); |
|||
repository = retrofit.create(DiscoverRepository.class); |
|||
} |
|||
|
|||
public static DiscoverService getInstance() { |
|||
if (instance == null) { |
|||
instance = new DiscoverService(); |
|||
} |
|||
return instance; |
|||
} |
|||
|
|||
public void topicalExplore(@NonNull final TopicalExploreRequest request, |
|||
final ServiceCallback<TopicalExploreResponse> callback) { |
|||
final ImmutableBiMap.Builder<String, String> builder = ImmutableBiMap.<String, String>builder() |
|||
.put("module", "explore_popular"); |
|||
if (!TextUtils.isEmpty(request.getModule())) { |
|||
builder.put("module", request.getModule()); |
|||
} |
|||
if (!TextUtils.isEmpty(request.getClusterId())) { |
|||
builder.put("cluster_id", request.getClusterId()); |
|||
} |
|||
if (request.getMaxId() >= 0) { |
|||
builder.put("max_id", String.valueOf(request.getMaxId())); |
|||
} |
|||
final Call<String> req = repository.topicalExplore(builder.build()); |
|||
req.enqueue(new Callback<String>() { |
|||
@Override |
|||
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { |
|||
if (callback == null) { |
|||
return; |
|||
} |
|||
final String body = response.body(); |
|||
if (TextUtils.isEmpty(body)) { |
|||
callback.onSuccess(null); |
|||
return; |
|||
} |
|||
try { |
|||
final TopicalExploreResponse topicalExploreResponse = parseTopicalExploreResponse(body); |
|||
callback.onSuccess(topicalExploreResponse); |
|||
} catch (JSONException e) { |
|||
callback.onFailure(e); |
|||
// Log.e(TAG, "Error parsing topicalExplore response", e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { |
|||
callback.onFailure(t); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private TopicalExploreResponse parseTopicalExploreResponse(@NonNull final String body) throws JSONException { |
|||
final JSONObject root = new JSONObject(body); |
|||
final boolean moreAvailable = root.optBoolean("more_available"); |
|||
final int nextMaxId = root.optInt("next_max_id", -1); |
|||
final int numResults = root.optInt("num_results"); |
|||
final String status = root.optString("status"); |
|||
final JSONArray clustersJson = root.optJSONArray("clusters"); |
|||
final List<TopicCluster> clusters = parseClusters(clustersJson); |
|||
final JSONArray itemsJson = root.optJSONArray("items"); |
|||
final List<FeedModel> items = parseItems(itemsJson); |
|||
return new TopicalExploreResponse( |
|||
moreAvailable, |
|||
nextMaxId, |
|||
numResults, |
|||
status, |
|||
clusters, |
|||
items |
|||
); |
|||
} |
|||
|
|||
private List<TopicCluster> parseClusters(final JSONArray clustersJson) throws JSONException { |
|||
if (clustersJson == null) { |
|||
return Collections.emptyList(); |
|||
} |
|||
final List<TopicCluster> clusters = new ArrayList<>(); |
|||
for (int i = 0; i < clustersJson.length(); i++) { |
|||
final JSONObject clusterJson = clustersJson.getJSONObject(i); |
|||
final String id = clusterJson.optString("id"); |
|||
final String title = clusterJson.optString("title"); |
|||
if (id == null || title == null) { |
|||
continue; |
|||
} |
|||
final String type = clusterJson.optString("type"); |
|||
final boolean canMute = clusterJson.optBoolean("can_mute"); |
|||
final boolean isMuted = clusterJson.optBoolean("is_muted"); |
|||
final JSONObject coverMediaJson = clusterJson.optJSONObject("cover_media"); |
|||
final int rankedPosition = clusterJson.optInt("ranked_position"); |
|||
final FeedModel feedModel = parseClusterCover(coverMediaJson); |
|||
final TopicCluster topicCluster = new TopicCluster( |
|||
id, |
|||
title, |
|||
type, |
|||
canMute, |
|||
isMuted, |
|||
rankedPosition, |
|||
feedModel |
|||
); |
|||
clusters.add(topicCluster); |
|||
} |
|||
return clusters; |
|||
} |
|||
|
|||
private FeedModel parseClusterCover(final JSONObject coverMediaJson) throws JSONException { |
|||
if (coverMediaJson == null) { |
|||
return null; |
|||
} |
|||
ProfileModel profileModel = null; |
|||
if (coverMediaJson.has("user")) { |
|||
final JSONObject user = coverMediaJson.getJSONObject("user"); |
|||
profileModel = new ProfileModel( |
|||
user.optBoolean("is_private"), |
|||
false, |
|||
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, |
|||
false, |
|||
false, |
|||
false, |
|||
false); |
|||
} |
|||
final String resourceUrl = ResponseBodyUtils.getHighQualityImage(coverMediaJson); |
|||
final String thumbnailUrl = ResponseBodyUtils.getLowQualityImage(coverMediaJson); |
|||
final int width = coverMediaJson.optInt("original_width"); |
|||
final int height = coverMediaJson.optInt("original_height"); |
|||
return new FeedModel.Builder() |
|||
.setProfileModel(profileModel) |
|||
.setItemType(MediaItemType.MEDIA_TYPE_IMAGE) |
|||
.setViewCount(0) |
|||
.setPostId(coverMediaJson.getString(Constants.EXTRAS_ID)) |
|||
.setDisplayUrl(resourceUrl) |
|||
.setThumbnailUrl(thumbnailUrl) |
|||
.setShortCode(coverMediaJson.getString("code")) |
|||
.setPostCaption(null) |
|||
.setCommentsCount(0) |
|||
.setTimestamp(coverMediaJson.optLong("taken_at", -1)) |
|||
.setLiked(false) |
|||
.setBookmarked(false) |
|||
.setLikesCount(0) |
|||
.setLocationName(null) |
|||
.setLocationId(null) |
|||
.setImageHeight(height) |
|||
.setImageWidth(width) |
|||
.build(); |
|||
} |
|||
|
|||
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 JSONObject mediaJson = itemJson.optJSONObject("media"); |
|||
final FeedModel feedModel = parseClusterItemMedia(mediaJson); |
|||
if (feedModel != null) { |
|||
feedModels.add(feedModel); |
|||
} |
|||
} |
|||
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 { |
|||
|
|||
private String module; |
|||
private String clusterId; |
|||
private int maxId = -1; |
|||
|
|||
public TopicalExploreRequest() {} |
|||
|
|||
public TopicalExploreRequest(final String module, final String clusterId, final int maxId) { |
|||
this.module = module; |
|||
this.clusterId = clusterId; |
|||
this.maxId = maxId; |
|||
} |
|||
|
|||
public String getModule() { |
|||
return module; |
|||
} |
|||
|
|||
public TopicalExploreRequest setModule(final String module) { |
|||
this.module = module; |
|||
return this; |
|||
} |
|||
|
|||
public String getClusterId() { |
|||
return clusterId; |
|||
} |
|||
|
|||
public TopicalExploreRequest setClusterId(final String clusterId) { |
|||
this.clusterId = clusterId; |
|||
return this; |
|||
} |
|||
|
|||
public int getMaxId() { |
|||
return maxId; |
|||
} |
|||
|
|||
public TopicalExploreRequest setMaxId(final int maxId) { |
|||
this.maxId = maxId; |
|||
return this; |
|||
} |
|||
|
|||
@Override |
|||
public boolean equals(final Object o) { |
|||
if (this == o) return true; |
|||
if (o == null || getClass() != o.getClass()) return false; |
|||
final TopicalExploreRequest that = (TopicalExploreRequest) o; |
|||
return maxId == that.maxId && |
|||
Objects.equals(module, that.module) && |
|||
Objects.equals(clusterId, that.clusterId); |
|||
} |
|||
|
|||
@Override |
|||
public int hashCode() { |
|||
return Objects.hash(module, clusterId, maxId); |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return "TopicalExploreRequest{" + |
|||
"module='" + module + '\'' + |
|||
", clusterId='" + clusterId + '\'' + |
|||
", maxId=" + maxId + |
|||
'}'; |
|||
} |
|||
} |
|||
|
|||
public static class TopicalExploreResponse { |
|||
|
|||
private boolean moreAvailable; |
|||
private int nextMaxId; |
|||
private int numResults; |
|||
private String status; |
|||
private List<TopicCluster> clusters; |
|||
private List<FeedModel> items; |
|||
|
|||
public TopicalExploreResponse() {} |
|||
|
|||
public TopicalExploreResponse(final boolean moreAvailable, |
|||
final int nextMaxId, |
|||
final int numResults, |
|||
final String status, |
|||
final List<TopicCluster> clusters, final List<FeedModel> items) { |
|||
this.moreAvailable = moreAvailable; |
|||
this.nextMaxId = nextMaxId; |
|||
this.numResults = numResults; |
|||
this.status = status; |
|||
this.clusters = clusters; |
|||
this.items = items; |
|||
} |
|||
|
|||
public boolean isMoreAvailable() { |
|||
return moreAvailable; |
|||
} |
|||
|
|||
public TopicalExploreResponse setMoreAvailable(final boolean moreAvailable) { |
|||
this.moreAvailable = moreAvailable; |
|||
return this; |
|||
} |
|||
|
|||
public int getNextMaxId() { |
|||
return nextMaxId; |
|||
} |
|||
|
|||
public TopicalExploreResponse setNextMaxId(final int nextMaxId) { |
|||
this.nextMaxId = nextMaxId; |
|||
return this; |
|||
} |
|||
|
|||
public int getNumResults() { |
|||
return numResults; |
|||
} |
|||
|
|||
public TopicalExploreResponse setNumResults(final int numResults) { |
|||
this.numResults = numResults; |
|||
return this; |
|||
} |
|||
|
|||
public String getStatus() { |
|||
return status; |
|||
} |
|||
|
|||
public TopicalExploreResponse setStatus(final String status) { |
|||
this.status = status; |
|||
return this; |
|||
} |
|||
|
|||
public List<TopicCluster> getClusters() { |
|||
return clusters; |
|||
} |
|||
|
|||
public TopicalExploreResponse setClusters(final List<TopicCluster> clusters) { |
|||
this.clusters = clusters; |
|||
return this; |
|||
} |
|||
|
|||
public List<FeedModel> getItems() { |
|||
return items; |
|||
} |
|||
|
|||
public TopicalExploreResponse 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 TopicalExploreResponse that = (TopicalExploreResponse) o; |
|||
return moreAvailable == that.moreAvailable && |
|||
nextMaxId == that.nextMaxId && |
|||
numResults == that.numResults && |
|||
Objects.equals(status, that.status) && |
|||
Objects.equals(clusters, that.clusters) && |
|||
Objects.equals(items, that.items); |
|||
} |
|||
|
|||
@Override |
|||
public int hashCode() { |
|||
return Objects.hash(moreAvailable, nextMaxId, numResults, status, clusters, items); |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return "TopicalExploreResponse{" + |
|||
"moreAvailable=" + moreAvailable + |
|||
", nextMaxId=" + nextMaxId + |
|||
", numResults=" + numResults + |
|||
", status='" + status + '\'' + |
|||
", clusters=" + clusters + |
|||
", items=" + items + |
|||
'}'; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,58 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<awais.instagrabber.customviews.helpers.NestedCoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:app="http://schemas.android.com/apk/res-auto" |
|||
xmlns:tools="http://schemas.android.com/tools" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" |
|||
android:background="?attr/colorSurface"> |
|||
|
|||
<com.google.android.material.appbar.AppBarLayout |
|||
android:id="@+id/app_bar_layout" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content"> |
|||
|
|||
<com.google.android.material.appbar.CollapsingToolbarLayout |
|||
android:id="@+id/collapsing_toolbar_layout" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
app:layout_scrollFlags="scroll|exitUntilCollapsed"> |
|||
|
|||
<FrameLayout |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
app:layout_collapseMode="parallax"> |
|||
|
|||
<com.facebook.drawee.view.SimpleDraweeView |
|||
android:id="@+id/cover" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="200dp" |
|||
app:actualImageScaleType="centerCrop" |
|||
tools:background="@mipmap/ic_launcher" /> |
|||
|
|||
<View |
|||
android:id="@+id/background" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" /> |
|||
</FrameLayout> |
|||
|
|||
<androidx.appcompat.widget.Toolbar |
|||
android:id="@+id/toolbar" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="?attr/actionBarSize" |
|||
app:layout_collapseMode="pin" /> |
|||
</com.google.android.material.appbar.CollapsingToolbarLayout> |
|||
</com.google.android.material.appbar.AppBarLayout> |
|||
|
|||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout |
|||
android:id="@+id/swipe_refresh_layout" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" |
|||
app:layout_behavior="@string/appbar_scrolling_view_behavior"> |
|||
|
|||
<awais.instagrabber.customviews.PostsRecyclerView |
|||
android:id="@+id/posts" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" |
|||
android:clipToPadding="false" /> |
|||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> |
|||
</awais.instagrabber.customviews.helpers.NestedCoordinatorLayout> |
@ -0,0 +1,45 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:app="http://schemas.android.com/apk/res-auto" |
|||
xmlns:tools="http://schemas.android.com/tools" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content"> |
|||
|
|||
<com.facebook.drawee.view.SimpleDraweeView |
|||
android:id="@+id/cover" |
|||
android:layout_width="0dp" |
|||
android:layout_height="0dp" |
|||
app:actualImageScaleType="centerCrop" |
|||
app:layout_constraintBottom_toBottomOf="parent" |
|||
app:layout_constraintDimensionRatio="H,1:1" |
|||
app:layout_constraintLeft_toLeftOf="parent" |
|||
app:layout_constraintRight_toRightOf="parent" |
|||
app:layout_constraintTop_toTopOf="parent" |
|||
app:roundedCornerRadius="5dp" |
|||
app:viewAspectRatio="1" |
|||
tools:background="@mipmap/ic_launcher" /> |
|||
|
|||
<View |
|||
android:id="@+id/background" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="0dp" |
|||
app:layout_constraintBottom_toBottomOf="parent" |
|||
app:layout_constraintEnd_toEndOf="parent" |
|||
app:layout_constraintHeight_percent="0.5" |
|||
app:layout_constraintStart_toStartOf="parent" /> |
|||
|
|||
<androidx.appcompat.widget.AppCompatTextView |
|||
android:id="@+id/title" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="0dp" |
|||
android:gravity="bottom" |
|||
android:padding="8dp" |
|||
android:textStyle="bold" |
|||
app:autoSizeTextType="uniform" |
|||
app:layout_constraintBottom_toBottomOf="parent" |
|||
app:layout_constraintEnd_toEndOf="parent" |
|||
app:layout_constraintHeight_percent="0.3" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
tools:text="Title" /> |
|||
|
|||
</androidx.constraintlayout.widget.ConstraintLayout> |
@ -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> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue