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