1
0
Fork 0
mirror of https://github.com/KokaKiwi/BarInsta synced 2026-03-14 00:11:40 +00:00

Merge branch 'master' into pr/1542

This commit is contained in:
Austin Huang 2021-07-10 17:27:10 -04:00
commit 3dcb8c5c7d
No known key found for this signature in database
GPG key ID: 84C23AA04587A91F
76 changed files with 1048 additions and 1315 deletions

View file

@ -139,7 +139,7 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.
return new HeaderViewHolder(LayoutDmHeaderBinding.inflate(layoutInflater, parent, false));
}
final LayoutDmBaseBinding baseBinding = LayoutDmBaseBinding.inflate(layoutInflater, parent, false);
final DirectItemType directItemType = DirectItemType.Companion.getId(type);
final DirectItemType directItemType = DirectItemType.Companion.getTypeFromId(type);
final DirectItemViewHolder itemViewHolder = getItemViewHolder(layoutInflater, baseBinding, directItemType);
itemViewHolder.setLongClickListener(longClickListener);
return itemViewHolder;

View file

@ -208,15 +208,11 @@ public final class FeedAdapterV2 extends ListAdapter<Media, RecyclerView.ViewHol
// }
public interface FeedItemCallback {
void onPostClick(final Media feedModel,
final View profilePicView,
final View mainPostImage);
void onPostClick(final Media feedModel);
void onProfilePicClick(final Media feedModel,
final View profilePicView);
void onProfilePicClick(final Media feedModel);
void onNameClick(final Media feedModel,
final View profilePicView);
void onNameClick(final Media feedModel);
void onLocationClick(final Media feedModel);

View file

@ -7,13 +7,13 @@ import awais.instagrabber.repositories.responses.Media;
public class FeedItemCallbackAdapter implements FeedAdapterV2.FeedItemCallback {
@Override
public void onPostClick(final Media media, final View profilePicView, final View mainPostImage) {}
public void onPostClick(final Media media) {}
@Override
public void onProfilePicClick(final Media media, final View profilePicView) {}
public void onProfilePicClick(final Media media) {}
@Override
public void onNameClick(final Media media, final View profilePicView) {}
public void onNameClick(final Media media) {}
@Override
public void onLocationClick(final Media media) {}

View file

@ -12,12 +12,13 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.FollowsViewHolder;
import awais.instagrabber.databinding.ItemFollowBinding;
import awais.instagrabber.interfaces.OnGroupClickListener;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.TextUtils;
import thoughtbot.expandableadapter.ExpandableGroup;
import thoughtbot.expandableadapter.ExpandableList;
@ -27,28 +28,33 @@ import thoughtbot.expandableadapter.GroupViewHolder;
// thanks to ThoughtBot's ExpandableRecyclerViewAdapter
// https://github.com/thoughtbot/expandable-recycler-view
public final class FollowAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements OnGroupClickListener, Filterable {
private final View.OnClickListener onClickListener;
private final ExpandableList expandableListOriginal;
private final boolean hasManyGroups;
private ExpandableList expandableList;
private final Filter filter = new Filter() {
@Nullable
@Override
protected FilterResults performFiltering(final CharSequence filter) {
if (expandableList.groups != null) {
final boolean isFilterEmpty = TextUtils.isEmpty(filter);
final String query = isFilterEmpty ? null : filter.toString().toLowerCase();
for (int x = 0; x < expandableList.groups.size(); ++x) {
final ExpandableGroup expandableGroup = expandableList.groups.get(x);
final List<FollowModel> items = expandableGroup.getItems(false);
final int itemCount = expandableGroup.getItemCount(false);
for (int i = 0; i < itemCount; ++i) {
final FollowModel followModel = items.get(i);
if (isFilterEmpty) followModel.setShown(true);
else followModel.setShown(hasKey(query, followModel.getUsername(), followModel.getFullName()));
}
final List<User> filteredItems = new ArrayList<User>();
if (expandableListOriginal.groups == null || TextUtils.isEmpty(filter)) return null;
final String query = filter.toString().toLowerCase();
final ArrayList<ExpandableGroup> groups = new ArrayList<ExpandableGroup>();
for (int x = 0; x < expandableListOriginal.groups.size(); ++x) {
final ExpandableGroup expandableGroup = expandableListOriginal.groups.get(x);
final String title = expandableGroup.getTitle();
final List<User> items = expandableGroup.getItems();
if (items != null) {
final List<User> toReturn = items.stream()
.filter(u -> hasKey(query, u.getUsername(), u.getFullName()))
.collect(Collectors.toList());
groups.add(new ExpandableGroup(title, toReturn));
}
}
return null;
final FilterResults filterResults = new FilterResults();
filterResults.values = new ExpandableList(groups, expandableList.expandedGroupIndexes);
return filterResults;
}
private boolean hasKey(final String key, final String username, final String name) {
@ -60,15 +66,20 @@ public final class FollowAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
@Override
protected void publishResults(final CharSequence constraint, final FilterResults results) {
if (results == null) {
expandableList = expandableListOriginal;
}
else {
final ExpandableList filteredList = (ExpandableList) results.values;
expandableList = filteredList;
}
notifyDataSetChanged();
}
};
private final View.OnClickListener onClickListener;
private final ExpandableList expandableList;
private final boolean hasManyGroups;
public FollowAdapter(final View.OnClickListener onClickListener, @NonNull final ArrayList<ExpandableGroup> groups) {
this.expandableList = new ExpandableList(groups);
this.expandableListOriginal = new ExpandableList(groups);
expandableList = this.expandableListOriginal;
this.onClickListener = onClickListener;
this.hasManyGroups = groups.size() > 1;
}
@ -104,7 +115,7 @@ public final class FollowAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
gvh.toggle(isGroupExpanded(group));
return;
}
final FollowModel model = group.getItems(true).get(hasManyGroups ? listPos.childPos : position);
final User model = group.getItems().get(hasManyGroups ? listPos.childPos : position);
((FollowsViewHolder) holder).bind(model, onClickListener);
}
@ -124,7 +135,7 @@ public final class FollowAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
final int groupPos = listPosition.groupPos;
final int positionStart = expandableList.getFlattenedGroupIndex(listPosition) + 1;
final int positionEnd = expandableList.groups.get(groupPos).getItemCount(true);
final int positionEnd = expandableList.groups.get(groupPos).getItemCount();
final boolean isExpanded = expandableList.expandedGroupIndexes[groupPos];
expandableList.expandedGroupIndexes[groupPos] = !isExpanded;

View file

@ -24,12 +24,12 @@ public final class NotificationsAdapter extends ListAdapter<Notification, Notifi
private static final DiffUtil.ItemCallback<Notification> DIFF_CALLBACK = new DiffUtil.ItemCallback<Notification>() {
@Override
public boolean areItemsTheSame(final Notification oldItem, final Notification newItem) {
return oldItem != null && newItem != null && oldItem.getPk().equals(newItem.getPk());
return oldItem.getPk().equals(newItem.getPk());
}
@Override
public boolean areContentsTheSame(@NonNull final Notification oldItem, @NonNull final Notification newItem) {
return oldItem.getPk().equals(newItem.getPk());
return oldItem.getPk().equals(newItem.getPk()) && oldItem.getType() == newItem.getType();
}
};

View file

@ -49,7 +49,7 @@ public class FeedGridItemViewHolder extends RecyclerView.ViewHolder {
final boolean selected) {
itemView.setOnClickListener(v -> {
if (!selectionModeActive && feedItemCallback != null) {
feedItemCallback.onPostClick(media, binding.profilePic, binding.postImage);
feedItemCallback.onPostClick(media);
return;
}
if (selectionModeActive && adapterSelectionCallback != null) {

View file

@ -6,7 +6,6 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.databinding.ItemFollowBinding;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.repositories.responses.User;
public final class FollowsViewHolder extends RecyclerView.ViewHolder {
@ -27,14 +26,4 @@ public final class FollowsViewHolder extends RecyclerView.ViewHolder {
binding.fullName.setText(model.getFullName());
binding.profilePic.setImageURI(model.getProfilePicUrl());
}
public void bind(final FollowModel model,
final View.OnClickListener onClickListener) {
if (model == null) return;
itemView.setTag(model);
itemView.setOnClickListener(onClickListener);
binding.username.setUsername("@" + model.getUsername());
binding.fullName.setText(model.getFullName());
binding.profilePic.setImageURI(model.getProfilePicUrl());
}
}

View file

@ -1,39 +1,45 @@
package awais.instagrabber.adapters.viewholder.feed;
import android.text.method.LinkMovementMethod;
import android.graphics.drawable.Drawable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.transition.TransitionManager;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.databinding.ItemFeedBottomBinding;
import awais.instagrabber.customviews.VerticalImageSpan;
import awais.instagrabber.databinding.ItemFeedTopBinding;
import awais.instagrabber.databinding.LayoutPostViewBottomBinding;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Caption;
import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import static android.text.TextUtils.TruncateAt.END;
public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder {
public static final int MAX_LINES_COLLAPSED = 5;
private final ItemFeedTopBinding topBinding;
private final ItemFeedBottomBinding bottomBinding;
private final LayoutPostViewBottomBinding bottomBinding;
private final ViewGroup bottomFrame;
private final FeedAdapterV2.FeedItemCallback feedItemCallback;
public FeedItemViewHolder(@NonNull final View root,
final ItemFeedTopBinding topBinding,
final ItemFeedBottomBinding bottomBinding,
public FeedItemViewHolder(@NonNull final ViewGroup root,
final FeedAdapterV2.FeedItemCallback feedItemCallback) {
super(root);
this.topBinding = topBinding;
this.bottomBinding = bottomBinding;
topBinding.title.setMovementMethod(new LinkMovementMethod());
this.bottomFrame = root;
this.topBinding = ItemFeedTopBinding.bind(root);
this.bottomBinding = LayoutPostViewBottomBinding.bind(root);
this.feedItemCallback = feedItemCallback;
}
@ -42,33 +48,35 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder {
return;
}
setupProfilePic(media);
setupLocation(media);
bottomBinding.tvPostDate.setText(media.getDate());
bottomBinding.date.setText(media.getDate());
setupComments(media);
setupCaption(media);
setupActions(media);
if (media.getType() != MediaItemType.MEDIA_TYPE_SLIDER) {
bottomBinding.btnDownload.setOnClickListener(v ->
bottomBinding.download.setOnClickListener(v ->
feedItemCallback.onDownloadClick(media, -1, null)
);
}
bindItem(media);
bottomFrame.post(() -> setupLocation(media));
}
private void setupComments(@NonNull final Media feedModel) {
final long commentsCount = feedModel.getCommentCount();
bottomBinding.commentsCount.setText(String.valueOf(commentsCount));
bottomBinding.btnComments.setOnClickListener(v -> feedItemCallback.onCommentsClick(feedModel));
bottomBinding.comment.setOnClickListener(v -> feedItemCallback.onCommentsClick(feedModel));
}
private void setupProfilePic(@NonNull final Media media) {
final User user = media.getUser();
if (user == null) {
topBinding.ivProfilePic.setVisibility(View.GONE);
topBinding.profilePic.setVisibility(View.GONE);
topBinding.title.setVisibility(View.GONE);
topBinding.subtitle.setVisibility(View.GONE);
return;
}
topBinding.ivProfilePic.setOnClickListener(v -> feedItemCallback.onProfilePicClick(media, topBinding.ivProfilePic));
topBinding.ivProfilePic.setImageURI(user.getProfilePicUrl());
topBinding.profilePic.setOnClickListener(v -> feedItemCallback.onProfilePicClick(media));
topBinding.profilePic.setImageURI(user.getProfilePicUrl());
setupTitle(media);
}
@ -78,68 +86,97 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder {
// spannableString.setSpan(new CommentMentionClickSpan(), 0, titleLen, 0);
final User user = media.getUser();
if (user == null) return;
final String title = "@" + user.getUsername();
topBinding.title.setText(title);
topBinding.title.setOnClickListener(v -> feedItemCallback.onNameClick(media, topBinding.ivProfilePic));
setUsername(user);
topBinding.title.setOnClickListener(v -> feedItemCallback.onNameClick(media));
final String fullName = user.getFullName();
if (TextUtils.isEmpty(fullName)) {
topBinding.subtitle.setVisibility(View.GONE);
} else {
topBinding.subtitle.setVisibility(View.VISIBLE);
topBinding.subtitle.setText(fullName);
}
topBinding.subtitle.setOnClickListener(v -> feedItemCallback.onNameClick(media));
}
private void setupCaption(final Media media) {
bottomBinding.viewerCaption.clearOnMentionClickListeners();
bottomBinding.viewerCaption.clearOnHashtagClickListeners();
bottomBinding.viewerCaption.clearOnURLClickListeners();
bottomBinding.viewerCaption.clearOnEmailClickListeners();
bottomBinding.caption.clearOnMentionClickListeners();
bottomBinding.caption.clearOnHashtagClickListeners();
bottomBinding.caption.clearOnURLClickListeners();
bottomBinding.caption.clearOnEmailClickListeners();
final Caption caption = media.getCaption();
if (caption == null) {
bottomBinding.viewerCaption.setVisibility(View.GONE);
bottomBinding.caption.setVisibility(View.GONE);
return;
}
final CharSequence postCaption = caption.getText();
final boolean captionEmpty = TextUtils.isEmpty(postCaption);
bottomBinding.viewerCaption.setVisibility(captionEmpty ? View.GONE : View.VISIBLE);
bottomBinding.caption.setVisibility(captionEmpty ? View.GONE : View.VISIBLE);
if (captionEmpty) return;
bottomBinding.viewerCaption.setText(postCaption);
bottomBinding.viewerCaption.setMaxLines(MAX_LINES_COLLAPSED);
bottomBinding.viewerCaption.setEllipsize(END);
bottomBinding.viewerCaption.setOnClickListener(v -> bottomBinding.getRoot().post(() -> {
TransitionManager.beginDelayedTransition(bottomBinding.getRoot());
if (bottomBinding.viewerCaption.getMaxLines() == MAX_LINES_COLLAPSED) {
bottomBinding.viewerCaption.setMaxLines(Integer.MAX_VALUE);
bottomBinding.viewerCaption.setEllipsize(null);
bottomBinding.caption.setText(postCaption);
bottomBinding.caption.setMaxLines(MAX_LINES_COLLAPSED);
bottomBinding.caption.setEllipsize(END);
bottomBinding.caption.setOnClickListener(v -> bottomFrame.post(() -> {
TransitionManager.beginDelayedTransition(bottomFrame);
if (bottomBinding.caption.getMaxLines() == MAX_LINES_COLLAPSED) {
bottomBinding.caption.setMaxLines(Integer.MAX_VALUE);
bottomBinding.caption.setEllipsize(null);
return;
}
bottomBinding.viewerCaption.setMaxLines(MAX_LINES_COLLAPSED);
bottomBinding.viewerCaption.setEllipsize(END);
bottomBinding.caption.setMaxLines(MAX_LINES_COLLAPSED);
bottomBinding.caption.setEllipsize(END);
}));
bottomBinding.viewerCaption.addOnMentionClickListener(autoLinkItem -> feedItemCallback.onMentionClick(autoLinkItem.getOriginalText()));
bottomBinding.viewerCaption.addOnHashtagListener(autoLinkItem -> feedItemCallback.onHashtagClick(autoLinkItem.getOriginalText()));
bottomBinding.viewerCaption.addOnEmailClickListener(autoLinkItem -> feedItemCallback.onEmailClick(autoLinkItem.getOriginalText()));
bottomBinding.viewerCaption.addOnURLClickListener(autoLinkItem -> feedItemCallback.onURLClick(autoLinkItem.getOriginalText()));
bottomBinding.caption.addOnMentionClickListener(autoLinkItem -> feedItemCallback.onMentionClick(autoLinkItem.getOriginalText()));
bottomBinding.caption.addOnHashtagListener(autoLinkItem -> feedItemCallback.onHashtagClick(autoLinkItem.getOriginalText()));
bottomBinding.caption.addOnEmailClickListener(autoLinkItem -> feedItemCallback.onEmailClick(autoLinkItem.getOriginalText()));
bottomBinding.caption.addOnURLClickListener(autoLinkItem -> feedItemCallback.onURLClick(autoLinkItem.getOriginalText()));
}
private void setupLocation(@NonNull final Media media) {
final Location location = media.getLocation();
if (location == null) {
topBinding.location.setVisibility(View.GONE);
topBinding.title.setLayoutParams(new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT
));
} else {
final String locationName = location.getName();
if (TextUtils.isEmpty(locationName)) {
topBinding.location.setVisibility(View.GONE);
topBinding.title.setLayoutParams(new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT
));
} else {
topBinding.location.setVisibility(View.VISIBLE);
topBinding.location.setText(locationName);
topBinding.title.setLayoutParams(new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT
));
topBinding.location.setOnClickListener(v -> feedItemCallback.onLocationClick(media));
}
}
}
private void setupActions(@NonNull final Media media) {
// temporary - to be set up later
bottomBinding.like.setVisibility(View.GONE);
bottomBinding.save.setVisibility(View.GONE);
bottomBinding.translate.setVisibility(View.GONE);
bottomBinding.share.setVisibility(View.GONE);
}
private void setUsername(final User user) {
final SpannableStringBuilder sb = new SpannableStringBuilder(user.getUsername());
final int drawableSize = Utils.convertDpToPx(24);
if (user.isVerified()) {
final Drawable verifiedDrawable = itemView.getResources().getDrawable(R.drawable.verified);
VerticalImageSpan verifiedSpan = null;
if (verifiedDrawable != null) {
final Drawable drawable = verifiedDrawable.mutate();
drawable.setBounds(0, 0, drawableSize, drawableSize);
verifiedSpan = new VerticalImageSpan(drawable);
}
try {
if (verifiedSpan != null) {
sb.append(" ");
sb.setSpan(verifiedSpan, sb.length() - 1, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} catch (Exception e) {
Log.e("FeedItemViewHolder", "setUsername: ", e);
}
}
topBinding.title.setText(sb);
}
public abstract void bindItem(final Media media);
}

View file

@ -16,6 +16,7 @@ import com.facebook.imagepipeline.request.ImageRequestBuilder;
import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.databinding.ItemFeedPhotoBinding;
import awais.instagrabber.databinding.LayoutPostViewBottomBinding;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
@ -28,10 +29,11 @@ public class FeedPhotoViewHolder extends FeedItemViewHolder {
public FeedPhotoViewHolder(@NonNull final ItemFeedPhotoBinding binding,
final FeedAdapterV2.FeedItemCallback feedItemCallback) {
super(binding.getRoot(), binding.itemFeedTop, binding.itemFeedBottom, feedItemCallback);
super(binding.getRoot(), feedItemCallback);
this.binding = binding;
this.feedItemCallback = feedItemCallback;
binding.itemFeedBottom.btnViews.setVisibility(View.GONE);
final LayoutPostViewBottomBinding bottom = LayoutPostViewBottomBinding.bind(binding.getRoot());
bottom.viewsCount.setVisibility(View.GONE);
// binding.itemFeedBottom.btnMute.setVisibility(View.GONE);
binding.imageViewer.setAllowTouchInterceptionWhileZoomed(false);
final GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(itemView.getContext().getResources())
@ -61,7 +63,7 @@ public class FeedPhotoViewHolder extends FeedItemViewHolder {
@Override
public boolean onSingleTapConfirmed(final MotionEvent e) {
if (feedItemCallback != null) {
feedItemCallback.onPostClick(media, binding.itemFeedTop.ivProfilePic, binding.imageViewer);
feedItemCallback.onPostClick(media);
return true;
}
return false;

View file

@ -13,6 +13,7 @@ import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.adapters.SliderCallbackAdapter;
import awais.instagrabber.adapters.SliderItemsAdapter;
import awais.instagrabber.databinding.ItemFeedSliderBinding;
import awais.instagrabber.databinding.LayoutPostViewBottomBinding;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.Utils;
@ -23,14 +24,16 @@ public class FeedSliderViewHolder extends FeedItemViewHolder {
private final ItemFeedSliderBinding binding;
private final FeedAdapterV2.FeedItemCallback feedItemCallback;
private final LayoutPostViewBottomBinding bottom;
public FeedSliderViewHolder(@NonNull final ItemFeedSliderBinding binding,
final FeedAdapterV2.FeedItemCallback feedItemCallback) {
super(binding.getRoot(), binding.itemFeedTop, binding.itemFeedBottom, feedItemCallback);
super(binding.getRoot(), feedItemCallback);
this.binding = binding;
this.feedItemCallback = feedItemCallback;
binding.itemFeedBottom.btnViews.setVisibility(View.GONE);
// binding.itemFeedBottom.btnMute.setVisibility(View.GONE);
bottom = LayoutPostViewBottomBinding.bind(binding.getRoot());
bottom.viewsCount.setVisibility(View.GONE);
// bottom.btnMute.setVisibility(View.GONE);
final ViewGroup.LayoutParams layoutParams = binding.mediaList.getLayoutParams();
layoutParams.height = Utils.displayMetrics.widthPixels + 1;
binding.mediaList.setLayoutParams(layoutParams);
@ -59,14 +62,14 @@ public class FeedSliderViewHolder extends FeedItemViewHolder {
final String text = (position + 1) + "/" + sliderItemLen;
binding.mediaCounter.setText(text);
setDimensions(binding.mediaList, sliderItems.get(position));
binding.itemFeedBottom.btnDownload.setOnClickListener(v ->
feedItemCallback.onDownloadClick(feedModel, position, binding.itemFeedBottom.btnDownload)
bottom.download.setOnClickListener(v ->
feedItemCallback.onDownloadClick(feedModel, position, bottom.download)
);
}
});
setDimensions(binding.mediaList, sliderItems.get(0));
binding.itemFeedBottom.btnDownload.setOnClickListener(v ->
feedItemCallback.onDownloadClick(feedModel, 0, binding.itemFeedBottom.btnDownload)
bottom.download.setOnClickListener(v ->
feedItemCallback.onDownloadClick(feedModel, 0, bottom.download)
);
adapter.submitList(sliderItems);
}

View file

@ -3,10 +3,12 @@ package awais.instagrabber.adapters.viewholder.feed;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
@ -14,13 +16,17 @@ import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.customviews.VideoPlayerCallbackAdapter;
import awais.instagrabber.customviews.VideoPlayerViewHelper;
import awais.instagrabber.databinding.ItemFeedVideoBinding;
import awais.instagrabber.databinding.LayoutPostViewBottomBinding;
import awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding;
import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.MediaCandidate;
import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils;
@ -35,6 +41,7 @@ public class FeedVideoViewHolder extends FeedItemViewHolder {
private final Handler handler;
private final DefaultDataSourceFactory dataSourceFactory;
private final LayoutPostViewBottomBinding bottom;
private CacheDataSourceFactory cacheDataSourceFactory;
private Media media;
@ -47,10 +54,11 @@ public class FeedVideoViewHolder extends FeedItemViewHolder {
public FeedVideoViewHolder(@NonNull final ItemFeedVideoBinding binding,
final FeedAdapterV2.FeedItemCallback feedItemCallback) {
super(binding.getRoot(), binding.itemFeedTop, binding.itemFeedBottom, feedItemCallback);
super(binding.getRoot(), feedItemCallback);
bottom = LayoutPostViewBottomBinding.bind(binding.getRoot());
this.binding = binding;
this.feedItemCallback = feedItemCallback;
binding.itemFeedBottom.btnViews.setVisibility(View.VISIBLE);
bottom.viewsCount.setVisibility(View.VISIBLE);
handler = new Handler(Looper.getMainLooper());
final Context context = binding.getRoot().getContext();
dataSourceFactory = new DefaultDataSourceFactory(context, "instagram");
@ -64,23 +72,35 @@ public class FeedVideoViewHolder extends FeedItemViewHolder {
public void bindItem(final Media media) {
// Log.d(TAG, "Binding post: " + feedModel.getPostId());
this.media = media;
binding.itemFeedBottom.tvVideoViews.setText(String.valueOf(media.getViewCount()));
final String viewCount = itemView.getResources().getQuantityString(R.plurals.views_count, (int) media.getViewCount(), media.getViewCount());
bottom.viewsCount.setText(viewCount);
final LayoutVideoPlayerWithThumbnailBinding videoPost =
LayoutVideoPlayerWithThumbnailBinding.inflate(LayoutInflater.from(itemView.getContext()), binding.getRoot(), false);
final ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) videoPost.getRoot().getLayoutParams();
final NullSafePair<Integer, Integer> widthHeight = NumberUtils.calculateWidthHeight(media.getOriginalHeight(),
media.getOriginalWidth(),
(int) (Utils.displayMetrics.heightPixels * 0.8),
Utils.displayMetrics.widthPixels);
layoutParams.width = ConstraintLayout.LayoutParams.MATCH_PARENT;
layoutParams.height = widthHeight.second;
final View postView = videoPost.getRoot();
binding.postContainer.addView(postView);
final float vol = settingsHelper.getBoolean(PreferenceKeys.MUTED_VIDEOS) ? 0f : 1f;
final VideoPlayerViewHelper.VideoPlayerCallback videoPlayerCallback = new VideoPlayerCallbackAdapter() {
@Override
public void onThumbnailClick() {
feedItemCallback.onPostClick(media, binding.itemFeedTop.ivProfilePic, binding.videoPost.thumbnail);
feedItemCallback.onPostClick(media);
}
@Override
public void onPlayerViewLoaded() {
final ViewGroup.LayoutParams layoutParams = binding.videoPost.playerView.getLayoutParams();
final ViewGroup.LayoutParams layoutParams = videoPost.playerView.getLayoutParams();
final int requiredWidth = Utils.displayMetrics.widthPixels;
final int resultingHeight = NumberUtils.getResultingHeight(requiredWidth, media.getOriginalHeight(), media.getOriginalWidth());
layoutParams.width = requiredWidth;
layoutParams.height = resultingHeight;
binding.videoPost.playerView.requestLayout();
videoPost.playerView.requestLayout();
}
};
final float aspectRatio = (float) media.getOriginalWidth() / media.getOriginalHeight();
@ -91,7 +111,7 @@ public class FeedVideoViewHolder extends FeedItemViewHolder {
videoUrl = videoVersion.getUrl();
}
final VideoPlayerViewHelper videoPlayerViewHelper = new VideoPlayerViewHelper(binding.getRoot().getContext(),
binding.videoPost,
videoPost,
videoUrl,
vol,
aspectRatio,
@ -99,11 +119,11 @@ public class FeedVideoViewHolder extends FeedItemViewHolder {
false,
// null,
videoPlayerCallback);
binding.videoPost.thumbnail.post(() -> {
videoPost.thumbnail.post(() -> {
if (media.getOriginalHeight() > 0.8 * Utils.displayMetrics.heightPixels) {
final ViewGroup.LayoutParams layoutParams = binding.videoPost.thumbnail.getLayoutParams();
layoutParams.height = (int) (0.8 * Utils.displayMetrics.heightPixels);
binding.videoPost.thumbnail.requestLayout();
final ViewGroup.LayoutParams tLayoutParams = videoPost.thumbnail.getLayoutParams();
tLayoutParams.height = (int) (0.8 * Utils.displayMetrics.heightPixels);
videoPost.thumbnail.requestLayout();
}
});
}
@ -118,8 +138,8 @@ public class FeedVideoViewHolder extends FeedItemViewHolder {
// if (player != null) {
// player.release();
// }
// if (binding.videoPost.root.getDisplayedChild() == 1) {
// binding.videoPost.root.showPrevious();
// if (videoPost.root.getDisplayedChild() == 1) {
// videoPost.root.showPrevious();
// }
// }
//

View file

@ -94,7 +94,7 @@ public class ChatMessageLayout extends FrameLayout {
heightSize += viewPartMainHeight;
} else if (firstChildId == R.id.raven_media_container || firstChildId == R.id.profile_container || firstChildId == R.id.voice_media
|| firstChildId == R.id.story_container || firstChildId == R.id.media_share_container || firstChildId == R.id.link_container
|| firstChildId == R.id.ivAnimatedMessage) {
|| firstChildId == R.id.ivAnimatedMessage || firstChildId == R.id.reel_share_container) {
widthSize += viewPartMainWidth;
heightSize += viewPartMainHeight + viewPartInfoHeight;
} else {
@ -104,12 +104,6 @@ public class ChatMessageLayout extends FrameLayout {
if (firstChild instanceof TextView) {
textMessage = (TextView) firstChild;
}
else if (firstChildId == R.id.reel_share_container) {
textMessage = (TextView) ((ConstraintLayout) firstChild).getChildAt(5);
}
else if (firstChildId == R.id.story_container) {
textMessage = (TextView) ((ConstraintLayout) firstChild).getChildAt(2);
}
else textMessage = null;
if (textMessage != null) {
viewPartMainLineCount = textMessage.getLineCount();

View file

@ -56,6 +56,7 @@ import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.saved.SavedCollection;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.DownloadUtils;
@ -105,7 +106,7 @@ public class CollectionPostsFragment extends Fragment implements SwipeRefreshLay
});
private final FeedAdapterV2.FeedItemCallback feedItemCallback = new FeedAdapterV2.FeedItemCallback() {
@Override
public void onPostClick(final Media feedModel, final View profilePicView, final View mainPostImage) {
public void onPostClick(final Media feedModel) {
openPostDialog(feedModel, -1);
}
@ -165,14 +166,14 @@ public class CollectionPostsFragment extends Fragment implements SwipeRefreshLay
}
@Override
public void onNameClick(final Media feedModel, final View profilePicView) {
public void onNameClick(final Media feedModel) {
final User user = feedModel.getUser();
if (user == null) return;
navigateToProfile("@" + user.getUsername());
}
@Override
public void onProfilePicClick(final Media feedModel, final View profilePicView) {
public void onProfilePicClick(final Media feedModel) {
final User user = feedModel.getUser();
if (user == null) return;
navigateToProfile("@" + user.getUsername());
@ -461,7 +462,9 @@ public class CollectionPostsFragment extends Fragment implements SwipeRefreshLay
}
private void updateSwipeRefreshState() {
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching());
AppExecutors.INSTANCE.getMainThread().execute(() ->
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching())
);
}
private void navigateToProfile(final String username) {

View file

@ -1,456 +0,0 @@
package awais.instagrabber.fragments;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
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 android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SearchView;
import androidx.fragment.app.Fragment;
import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import java.util.ArrayList;
import awais.instagrabber.R;
import awais.instagrabber.adapters.FollowAdapter;
import awais.instagrabber.customviews.helpers.RecyclerLazyLoader;
import awais.instagrabber.databinding.FragmentFollowersViewerBinding;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.repositories.responses.FriendshipListFetchResponse;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.webservices.FriendshipRepository;
import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
import thoughtbot.expandableadapter.ExpandableGroup;
public final class FollowViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = "FollowViewerFragment";
private final ArrayList<FollowModel> followModels = new ArrayList<>();
private final ArrayList<FollowModel> followingModels = new ArrayList<>();
private final ArrayList<FollowModel> followersModels = new ArrayList<>();
private final ArrayList<FollowModel> allFollowing = new ArrayList<>();
private boolean moreAvailable = true, isFollowersList, isCompare = false, loading = false, shouldRefresh = true, searching = false;
private long profileId;
private String username;
private String namePost;
private String type;
private String endCursor;
private Resources resources;
private LinearLayoutManager layoutManager;
private RecyclerLazyLoader lazyLoader;
private FollowModel model;
private FollowAdapter adapter;
private View.OnClickListener clickListener;
private FragmentFollowersViewerBinding binding;
private SwipeRefreshLayout root;
private FriendshipRepository friendshipRepository;
private AppCompatActivity fragmentActivity;
final ServiceCallback<FriendshipListFetchResponse> followingFetchCb = new ServiceCallback<FriendshipListFetchResponse>() {
@Override
public void onSuccess(final FriendshipListFetchResponse result) {
if (result != null && isCompare) {
followingModels.addAll(result.getItems());
if (!isFollowersList) followModels.addAll(result.getItems());
if (result.isMoreAvailable()) {
endCursor = result.getNextMaxId();
friendshipRepository.getList(
false,
profileId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
onFailure(throwable);
return;
}
onSuccess(response);
}), Dispatchers.getIO())
);
} else if (followersModels.size() == 0) {
if (!isFollowersList) moreAvailable = false;
friendshipRepository.getList(
true,
profileId,
null,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
followingFetchCb.onFailure(throwable);
return;
}
followingFetchCb.onSuccess(response);
}), Dispatchers.getIO())
);
} else {
if (!isFollowersList) moreAvailable = false;
showCompare();
}
} else if (isCompare) binding.swipeRefreshLayout.setRefreshing(false);
}
@Override
public void onFailure(final Throwable t) {
try {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
} catch (Throwable ignored) {}
Log.e(TAG, "Error fetching list (double, following)", t);
}
};
final ServiceCallback<FriendshipListFetchResponse> followersFetchCb = new ServiceCallback<FriendshipListFetchResponse>() {
@Override
public void onSuccess(final FriendshipListFetchResponse result) {
if (result != null && isCompare) {
followersModels.addAll(result.getItems());
if (isFollowersList) followModels.addAll(result.getItems());
if (result.isMoreAvailable()) {
endCursor = result.getNextMaxId();
friendshipRepository.getList(
true,
profileId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
onFailure(throwable);
return;
}
onSuccess(response);
}), Dispatchers.getIO())
);
} else if (followingModels.size() == 0) {
if (isFollowersList) moreAvailable = false;
friendshipRepository.getList(
false,
profileId,
null,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
followingFetchCb.onFailure(throwable);
return;
}
followingFetchCb.onSuccess(response);
}), Dispatchers.getIO())
);
} else {
if (isFollowersList) moreAvailable = false;
showCompare();
}
} else if (isCompare) binding.swipeRefreshLayout.setRefreshing(false);
}
@Override
public void onFailure(final Throwable t) {
try {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
} catch (Throwable ignored) {}
Log.e(TAG, "Error fetching list (double, follower)", t);
}
};
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
friendshipRepository = FriendshipRepository.Companion.getInstance();
fragmentActivity = (AppCompatActivity) getActivity();
setHasOptionsMenu(true);
}
@NonNull
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) {
if (root != null) {
shouldRefresh = false;
return root;
}
binding = FragmentFollowersViewerBinding.inflate(getLayoutInflater());
root = binding.getRoot();
return root;
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
if (!shouldRefresh) return;
init();
shouldRefresh = false;
}
private void init() {
if (getArguments() == null) return;
final FollowViewerFragmentArgs fragmentArgs = FollowViewerFragmentArgs.fromBundle(getArguments());
profileId = fragmentArgs.getProfileId();
isFollowersList = fragmentArgs.getIsFollowersList();
username = fragmentArgs.getUsername();
namePost = username;
if (TextUtils.isEmpty(username)) {
// this usually should not occur
username = "You";
namePost = "You're";
}
setTitle(username);
resources = getResources();
clickListener = v -> {
final Object tag = v.getTag();
if (tag instanceof FollowModel) {
model = (FollowModel) tag;
try {
final NavDirections action = FollowViewerFragmentDirections.actionToProfile().setUsername(model.getUsername());
NavHostFragment.findNavController(this).navigate(action);
} catch (Exception e) {
Log.e(TAG, "init: ", e);
}
}
};
binding.swipeRefreshLayout.setOnRefreshListener(this);
onRefresh();
}
@Override
public void onResume() {
super.onResume();
setTitle(username);
setSubtitle(type);
}
private void setTitle(final String title) {
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
if (actionBar == null) return;
actionBar.setTitle(title);
}
private void setSubtitle(final String subtitle) {
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
if (actionBar == null) return;
actionBar.setSubtitle(subtitle);
}
private void setSubtitle(@SuppressWarnings("SameParameterValue") final int subtitleRes) {
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
if (actionBar == null) return;
actionBar.setSubtitle(subtitleRes);
}
@Override
public void onRefresh() {
if (isCompare) listCompare();
else listFollows();
endCursor = null;
lazyLoader.resetState();
}
private void listFollows() {
type = resources.getString(isFollowersList ? R.string.followers_type_followers : R.string.followers_type_following);
setSubtitle(type);
final ServiceCallback<FriendshipListFetchResponse> cb = new ServiceCallback<FriendshipListFetchResponse>() {
@Override
public void onSuccess(final FriendshipListFetchResponse result) {
if (result == null) {
binding.swipeRefreshLayout.setRefreshing(false);
return;
}
int oldSize = followModels.size() == 0 ? 0 : followModels.size() - 1;
followModels.addAll(result.getItems());
if (result.isMoreAvailable()) {
moreAvailable = true;
endCursor = result.getNextMaxId();
} else moreAvailable = false;
binding.swipeRefreshLayout.setRefreshing(false);
if (isFollowersList) followersModels.addAll(result.getItems());
else followingModels.addAll(result.getItems());
refreshAdapter(followModels, null, null, null);
layoutManager.scrollToPosition(oldSize);
}
@Override
public void onFailure(final Throwable t) {
try {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
} catch (Throwable ignored) {}
Log.e(TAG, "Error fetching list (single)", t);
}
};
layoutManager = new LinearLayoutManager(getContext());
lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> {
if (!TextUtils.isEmpty(endCursor) && !searching) {
binding.swipeRefreshLayout.setRefreshing(true);
layoutManager.setStackFromEnd(true);
friendshipRepository.getList(
isFollowersList,
profileId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
cb.onFailure(throwable);
return;
}
cb.onSuccess(response);
}), Dispatchers.getIO())
);
endCursor = null;
}
});
binding.rvFollow.addOnScrollListener(lazyLoader);
binding.rvFollow.setLayoutManager(layoutManager);
if (moreAvailable) {
binding.swipeRefreshLayout.setRefreshing(true);
friendshipRepository.getList(
isFollowersList,
profileId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
cb.onFailure(throwable);
return;
}
cb.onSuccess(response);
}), Dispatchers.getIO())
);
} else {
refreshAdapter(followModels, null, null, null);
layoutManager.scrollToPosition(0);
}
}
private void listCompare() {
layoutManager.setStackFromEnd(false);
binding.rvFollow.clearOnScrollListeners();
loading = true;
setSubtitle(R.string.followers_compare);
allFollowing.clear();
if (moreAvailable) {
binding.swipeRefreshLayout.setRefreshing(true);
Toast.makeText(getContext(), R.string.follower_start_compare, Toast.LENGTH_LONG).show();
friendshipRepository.getList(
isFollowersList,
profileId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
final ServiceCallback<FriendshipListFetchResponse> callback = isFollowersList ? followersFetchCb : followingFetchCb;
if (throwable != null) {
callback.onFailure(throwable);
return;
}
callback.onSuccess(response);
}), Dispatchers.getIO())
);
} else if (followersModels.size() == 0 || followingModels.size() == 0) {
binding.swipeRefreshLayout.setRefreshing(true);
Toast.makeText(getContext(), R.string.follower_start_compare, Toast.LENGTH_LONG).show();
friendshipRepository.getList(
!isFollowersList,
profileId,
null,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
final ServiceCallback<FriendshipListFetchResponse> callback = isFollowersList ? followingFetchCb : followersFetchCb;
if (throwable != null) {
callback.onFailure(throwable);
return;
}
callback.onSuccess(response);
}), Dispatchers.getIO()));
} else showCompare();
}
private void showCompare() {
allFollowing.addAll(followersModels);
allFollowing.retainAll(followingModels);
for (final FollowModel followModel : allFollowing) {
followersModels.remove(followModel);
followingModels.remove(followModel);
}
allFollowing.trimToSize();
followersModels.trimToSize();
followingModels.trimToSize();
binding.swipeRefreshLayout.setRefreshing(false);
refreshAdapter(null, followingModels, followersModels, allFollowing);
}
@Override
public void onCreateOptionsMenu(@NonNull final Menu menu, final MenuInflater inflater) {
inflater.inflate(R.menu.follow, menu);
final MenuItem menuSearch = menu.findItem(R.id.action_search);
final SearchView searchView = (SearchView) menuSearch.getActionView();
searchView.setQueryHint(getResources().getString(R.string.action_search));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(final String query) {
return false;
}
@Override
public boolean onQueryTextChange(final String query) {
if (TextUtils.isEmpty(query)) {
searching = false;
// refreshAdapter(followModels, followingModels, followersModels, allFollowing);
}
// else filter.filter(query.toLowerCase());
if (adapter != null) {
searching = true;
adapter.getFilter().filter(query);
}
return true;
}
});
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
if (item.getItemId() != R.id.action_compare) return super.onOptionsItemSelected(item);
binding.rvFollow.setAdapter(null);
final Context context = getContext();
if (loading) Toast.makeText(context, R.string.follower_wait_to_load, Toast.LENGTH_LONG).show();
else if (isCompare) {
isCompare = false;
listFollows();
} else {
isCompare = true;
listCompare();
}
return true;
}
private void refreshAdapter(final ArrayList<FollowModel> followModels,
final ArrayList<FollowModel> followingModels,
final ArrayList<FollowModel> followersModels,
final ArrayList<FollowModel> allFollowing) {
loading = false;
final ArrayList<ExpandableGroup> groups = new ArrayList<>(1);
if (isCompare && followingModels != null && followersModels != null && allFollowing != null) {
if (followingModels.size() > 0)
groups.add(new ExpandableGroup(resources.getString(R.string.followers_not_following, username), followingModels));
if (followersModels.size() > 0)
groups.add(new ExpandableGroup(resources.getString(R.string.followers_not_follower, namePost), followersModels));
if (allFollowing.size() > 0)
groups.add(new ExpandableGroup(resources.getString(R.string.followers_both_following), allFollowing));
} else if (followModels != null) {
groups.add(new ExpandableGroup(type, followModels));
} else return;
adapter = new FollowAdapter(clickListener, groups);
adapter.toggleGroup(0);
binding.rvFollow.setAdapter(adapter);
}
}

View file

@ -0,0 +1,260 @@
package awais.instagrabber.fragments
import android.content.Context
import android.content.res.Resources
import android.os.Bundle
import android.util.Log
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 android.widget.Toast
import androidx.annotation.NonNull
import androidx.annotation.Nullable
import androidx.appcompat.app.ActionBar
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.NavHostFragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.util.ArrayList
import awais.instagrabber.R
import awais.instagrabber.adapters.FollowAdapter
import awais.instagrabber.customviews.helpers.RecyclerLazyLoader
import awais.instagrabber.databinding.FragmentFollowersViewerBinding
import awais.instagrabber.models.Resource
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.utils.AppExecutors
import awais.instagrabber.viewmodels.FollowViewModel
import thoughtbot.expandableadapter.ExpandableGroup
class FollowViewerFragment : Fragment(), SwipeRefreshLayout.OnRefreshListener {
private val followModels: ArrayList<User> = ArrayList<User>()
private val followingModels: ArrayList<User> = ArrayList<User>()
private val followersModels: ArrayList<User> = ArrayList<User>()
private val allFollowing: ArrayList<User> = ArrayList<User>()
private val moreAvailable = true
private var isFollowersList = false
private var isCompare = false
private var shouldRefresh = true
private var searching = false
private var profileId: Long = 0
private var username: String? = null
private var namePost: String? = null
private var type = 0
private var root: SwipeRefreshLayout? = null
private var adapter: FollowAdapter? = null
private lateinit var lazyLoader: RecyclerLazyLoader
private lateinit var fragmentActivity: AppCompatActivity
private lateinit var viewModel: FollowViewModel
private lateinit var binding: FragmentFollowersViewerBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
fragmentActivity = activity as AppCompatActivity
viewModel = ViewModelProvider(this).get(FollowViewModel::class.java)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
if (root != null) {
shouldRefresh = false
return root!!
}
binding = FragmentFollowersViewerBinding.inflate(layoutInflater)
root = binding.root
return root!!
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (!shouldRefresh) return
init()
shouldRefresh = false
}
private fun init() {
val args = arguments ?: return
val fragmentArgs = FollowViewerFragmentArgs.fromBundle(args)
viewModel.userId.value = fragmentArgs.profileId
isFollowersList = fragmentArgs.isFollowersList
username = fragmentArgs.username
namePost = username
setTitle(username)
binding.swipeRefreshLayout.setOnRefreshListener(this)
if (isCompare) listCompare() else listFollows()
viewModel.fetch(isFollowersList, null)
}
override fun onResume() {
super.onResume()
setTitle(username)
setSubtitle(type)
}
private fun setTitle(title: String?) {
val actionBar: ActionBar = fragmentActivity.supportActionBar ?: return
actionBar.title = title
}
private fun setSubtitle(subtitleRes: Int) {
val actionBar: ActionBar = fragmentActivity.supportActionBar ?: return
actionBar.setSubtitle(subtitleRes)
}
override fun onRefresh() {
lazyLoader.resetState()
viewModel.clearProgress()
if (isCompare) listCompare()
else viewModel.fetch(isFollowersList, null)
}
private fun listFollows() {
viewModel.comparison.removeObservers(viewLifecycleOwner)
viewModel.status.removeObservers(viewLifecycleOwner)
type = if (isFollowersList) R.string.followers_type_followers else R.string.followers_type_following
setSubtitle(type)
val layoutManager = LinearLayoutManager(context)
lazyLoader = RecyclerLazyLoader(layoutManager, { _, totalItemsCount ->
binding.swipeRefreshLayout.isRefreshing = true
val liveData = if (searching) viewModel.search(isFollowersList)
else viewModel.fetch(isFollowersList, null)
liveData.observe(viewLifecycleOwner) {
binding.swipeRefreshLayout.isRefreshing = it.status != Resource.Status.SUCCESS
layoutManager.scrollToPosition(totalItemsCount)
}
})
binding.rvFollow.addOnScrollListener(lazyLoader)
binding.rvFollow.layoutManager = layoutManager
viewModel.getList(isFollowersList).observe(viewLifecycleOwner) {
binding.swipeRefreshLayout.isRefreshing = false
refreshAdapter(it, null, null, null)
}
}
private fun listCompare() {
viewModel.getList(isFollowersList).removeObservers(viewLifecycleOwner)
binding.rvFollow.clearOnScrollListeners()
binding.swipeRefreshLayout.isRefreshing = true
setSubtitle(R.string.followers_compare)
viewModel.status.observe(viewLifecycleOwner) {}
viewModel.comparison.observe(viewLifecycleOwner) {
if (it != null) {
binding.swipeRefreshLayout.isRefreshing = false
refreshAdapter(null, it.first, it.second, it.third)
}
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.follow, menu)
val menuSearch = menu.findItem(R.id.action_search)
val searchView = menuSearch.actionView as SearchView
searchView.queryHint = resources.getString(R.string.action_search)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return false
}
override fun onQueryTextChange(query: String): Boolean {
if (query.isNullOrEmpty()) {
if (!isCompare && searching) {
viewModel.setQuery(null, isFollowersList)
viewModel.getSearch().removeObservers(viewLifecycleOwner)
viewModel.getList(isFollowersList).observe(viewLifecycleOwner) {
refreshAdapter(it, null, null, null)
}
}
searching = false
return true
}
searching = true
if (isCompare && adapter != null) {
adapter!!.filter.filter(query)
return true
}
viewModel.getList(isFollowersList).removeObservers(viewLifecycleOwner)
binding.swipeRefreshLayout.isRefreshing = true
viewModel.setQuery(query, isFollowersList)
viewModel.getSearch().observe(viewLifecycleOwner) {
binding.swipeRefreshLayout.isRefreshing = false
refreshAdapter(it, null, null, null)
}
return true
}
})
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId != R.id.action_compare) return super.onOptionsItemSelected(item)
binding.rvFollow.adapter = null
if (isCompare) {
isCompare = false
listFollows()
} else {
isCompare = true
listCompare()
}
return true
}
private fun refreshAdapter(
followModels: List<User>?,
allFollowing: List<User>?,
followingModels: List<User>?,
followersModels: List<User>?
) {
val groups: ArrayList<ExpandableGroup> = ArrayList<ExpandableGroup>(1)
if (isCompare && followingModels != null && followersModels != null && allFollowing != null) {
if (followingModels.size > 0) groups.add(
ExpandableGroup(
getString(
R.string.followers_not_following,
username
), followingModels
)
)
if (followersModels.size > 0) groups.add(
ExpandableGroup(
getString(
R.string.followers_not_follower,
namePost
), followersModels
)
)
if (allFollowing.size > 0) groups.add(
ExpandableGroup(
getString(R.string.followers_both_following),
allFollowing
)
)
} else if (followModels != null) {
groups.add(ExpandableGroup(getString(type), followModels))
} else return
adapter = FollowAdapter({ v ->
val tag = v.tag
if (tag is User) {
val model = tag
val bundle = Bundle()
bundle.putString("username", model.username)
NavHostFragment.findNavController(this).navigate(R.id.action_global_profileFragment, bundle)
}
}, groups)
adapter!!.toggleGroup(0)
binding.rvFollow.adapter = adapter!!
}
companion object {
private const val TAG = "FollowViewerFragment"
}
}

View file

@ -51,6 +51,7 @@ import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment;
import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.models.enums.FollowingType;
//import awais.instagrabber.repositories.requests.StoryViewerOptions;
import awais.instagrabber.repositories.responses.Hashtag;
import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
@ -64,6 +65,7 @@ import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.webservices.GraphQLRepository;
import awais.instagrabber.webservices.ServiceCallback;
//import awais.instagrabber.webservices.StoriesRepository;
import awais.instagrabber.webservices.TagsService;
import kotlinx.coroutines.Dispatchers;
@ -80,9 +82,11 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
private String hashtag;
private Hashtag hashtagModel = null;
private ActionMode actionMode;
// private StoriesRepository storiesRepository;
private boolean isLoggedIn;
private TagsService tagsService;
private GraphQLRepository graphQLRepository;
// private boolean storiesFetching;
private Set<Media> selectedFeedModels;
private PostsLayoutPreferences layoutPreferences = Utils.getPostsLayoutPreferences(Constants.PREF_HASHTAG_POSTS_LAYOUT);
private LayoutHashtagDetailsBinding hashtagDetailsBinding;
@ -116,7 +120,7 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
});
private final FeedAdapterV2.FeedItemCallback feedItemCallback = new FeedAdapterV2.FeedItemCallback() {
@Override
public void onPostClick(final Media feedModel, final View profilePicView, final View mainPostImage) {
public void onPostClick(final Media feedModel) {
openPostDialog(feedModel, -1);
}
@ -176,14 +180,14 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
}
@Override
public void onNameClick(final Media feedModel, final View profilePicView) {
public void onNameClick(final Media feedModel) {
final User user = feedModel.getUser();
if (user == null) return;
navigateToProfile("@" + user.getUsername());
}
@Override
public void onProfilePicClick(final Media feedModel, final View profilePicView) {
public void onProfilePicClick(final Media feedModel) {
final User user = feedModel.getUser();
if (user == null) return;
navigateToProfile("@" + user.getUsername());
@ -199,8 +203,7 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
Utils.openEmailAddress(getContext(), emailId);
}
private void openPostDialog(@NonNull final Media feedModel,
final int position) {
private void openPostDialog(@NonNull final Media feedModel, final int position) {
if (opening) return;
final User user = feedModel.getUser();
if (user == null) return;
@ -286,6 +289,7 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
final String cookie = settingsHelper.getString(Constants.COOKIE);
isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0;
tagsService = isLoggedIn ? TagsService.getInstance() : null;
// storiesRepository = isLoggedIn ? StoriesRepository.Companion.getInstance() : null;
graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
setHasOptionsMenu(true);
}
@ -559,7 +563,9 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
}
private void updateSwipeRefreshState() {
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching());
AppExecutors.INSTANCE.getMainThread().execute(() ->
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching())
);
}
private void navigateToProfile(final String username) {

View file

@ -48,6 +48,7 @@ import awais.instagrabber.db.repositories.FavoriteRepository;
import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment;
import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.models.enums.FavoriteType;
//import awais.instagrabber.repositories.requests.StoryViewerOptions;
import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
@ -61,7 +62,7 @@ import awais.instagrabber.utils.Utils;
import awais.instagrabber.webservices.GraphQLRepository;
import awais.instagrabber.webservices.LocationService;
import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.StoriesRepository;
//import awais.instagrabber.webservices.StoriesRepository;
import kotlinx.coroutines.Dispatchers;
import static awais.instagrabber.utils.Utils.settingsHelper;
@ -78,11 +79,11 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
private long locationId;
private Location locationModel;
private ActionMode actionMode;
private StoriesRepository storiesRepository;
// private StoriesRepository storiesRepository;
private GraphQLRepository graphQLRepository;
private LocationService locationService;
private boolean isLoggedIn;
private boolean storiesFetching;
// private boolean storiesFetching;
private Set<Media> selectedFeedModels;
private PostsLayoutPreferences layoutPreferences = Utils.getPostsLayoutPreferences(Constants.PREF_LOCATION_POSTS_LAYOUT);
private LayoutLocationDetailsBinding locationDetailsBinding;
@ -116,13 +117,13 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
});
private final FeedAdapterV2.FeedItemCallback feedItemCallback = new FeedAdapterV2.FeedItemCallback() {
@Override
public void onPostClick(final Media feedModel, final View profilePicView, final View mainPostImage) {
openPostDialog(feedModel, profilePicView, mainPostImage, -1);
public void onPostClick(final Media feedModel) {
openPostDialog(feedModel, -1);
}
@Override
public void onSliderClick(final Media feedModel, final int position) {
openPostDialog(feedModel, null, null, position);
openPostDialog(feedModel, position);
}
@Override
@ -172,12 +173,12 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
}
@Override
public void onNameClick(final Media feedModel, final View profilePicView) {
public void onNameClick(final Media feedModel) {
navigateToProfile("@" + feedModel.getUser().getUsername());
}
@Override
public void onProfilePicClick(final Media feedModel, final View profilePicView) {
public void onProfilePicClick(final Media feedModel) {
navigateToProfile("@" + feedModel.getUser().getUsername());
}
@ -191,10 +192,7 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
Utils.openEmailAddress(getContext(), emailId);
}
private void openPostDialog(@NonNull final Media feedModel,
final View profilePicView,
final View mainPostImage,
final int position) {
private void openPostDialog(@NonNull final Media feedModel, final int position) {
if (opening) return;
final User user = feedModel.getUser();
if (user == null) return;
@ -209,7 +207,7 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
return;
}
if (media == null) return;
openPostDialog(media, profilePicView, mainPostImage, position);
openPostDialog(media, position);
}))
);
return;
@ -280,7 +278,7 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
final String cookie = settingsHelper.getString(Constants.COOKIE);
isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0;
locationService = isLoggedIn ? LocationService.getInstance() : null;
storiesRepository = StoriesRepository.Companion.getInstance();
// storiesRepository = StoriesRepository.Companion.getInstance();
graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
setHasOptionsMenu(true);
}
@ -584,7 +582,9 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
}
private void updateSwipeRefreshState() {
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching() || storiesFetching);
AppExecutors.INSTANCE.getMainThread().execute(() ->
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching())
);
}
private void navigateToProfile(final String username) {

View file

@ -758,6 +758,8 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme
popupMenu.setOnMenuItemClickListener(item -> {
final int itemId = item.getItemId();
if (itemId == R.id.share_dm) {
if (profileModel.isPrivate())
Toast.makeText(context, R.string.share_private_post, Toast.LENGTH_SHORT).show();
try {
final NavDirections actionGlobalUserSearch = PostViewV2FragmentDirections
.actionToUserSearch()

View file

@ -39,6 +39,7 @@ import awais.instagrabber.models.enums.PostItemType;
import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.DownloadUtils;
@ -89,7 +90,7 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL
});
private final FeedAdapterV2.FeedItemCallback feedItemCallback = new FeedAdapterV2.FeedItemCallback() {
@Override
public void onPostClick(final Media feedModel, final View profilePicView, final View mainPostImage) {
public void onPostClick(final Media feedModel) {
openPostDialog(feedModel, -1);
}
@ -149,14 +150,12 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL
}
@Override
public void onNameClick(final Media feedModel, final View profilePicView) {
final User user = feedModel.getUser();
if (user == null) return;
navigateToProfile("@" + user.getUsername());
public void onNameClick(final Media feedModel) {
navigateToProfile("@" + feedModel.getUser().getUsername());
}
@Override
public void onProfilePicClick(final Media feedModel, final View profilePicView) {
public void onProfilePicClick(final Media feedModel) {
final User user = feedModel.getUser();
if (user == null) return;
navigateToProfile("@" + user.getUsername());
@ -327,7 +326,9 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL
}
private void updateSwipeRefreshState() {
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching());
AppExecutors.INSTANCE.getMainThread().execute(() ->
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching())
);
}
private void navigateToProfile(final String username) {

View file

@ -17,10 +17,9 @@ import androidx.appcompat.view.ContextThemeWrapper
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.GestureDetectorCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
@ -40,9 +39,7 @@ import awais.instagrabber.models.enums.StoryPaginationType
import awais.instagrabber.repositories.requests.StoryViewerOptions
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
import awais.instagrabber.repositories.responses.stories.*
import awais.instagrabber.utils.Constants
import awais.instagrabber.utils.DownloadUtils.download
import awais.instagrabber.utils.TextUtils.epochSecondToString
import awais.instagrabber.utils.ResponseBodyUtils
import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.extensions.TAG
@ -78,7 +75,6 @@ class StoryViewerFragment : Fragment() {
private var gestureDetector: GestureDetectorCompat? = null
private val storiesRepository: StoriesRepository? = null
private val mediaRepository: MediaRepository? = null
private var live: Broadcast? = null
private var menuProfile: MenuItem? = null
private var profileVisible: Boolean = false
private var player: SimpleExoPlayer? = null
@ -218,7 +214,7 @@ class StoryViewerFragment : Fragment() {
}
binding.storiesList.adapter = storiesAdapter
storiesViewModel.getCurrentStory().observe(fragmentActivity, {
if (it?.items != null) {
if (it?.items != null && it.items.size > 1) {
val storyMedias = it.items.toMutableList()
val newItem = storyMedias.get(0)
newItem.isCurrentSlide = true
@ -230,6 +226,7 @@ class StoryViewerFragment : Fragment() {
else View.GONE
}
else {
if (it?.items != null) storiesViewModel.setMedia(0)
binding.listToggle.isEnabled = false
binding.storiesList.visibility = View.GONE
}
@ -266,65 +263,28 @@ class StoryViewerFragment : Fragment() {
@SuppressLint("ClickableViewAccessibility")
private fun setupListeners() {
var liveModels: LiveData<List<Story>?>? = null
if (currentFeedStoryIndex >= 0) {
val type = options!!.type
when (type) {
StoryViewerOptions.Type.HIGHLIGHT -> {
storiesViewModel.fetchHighlights(options!!.id)
liveModels = storiesViewModel.getHighlights()
storiesViewModel.highlights.observe(fragmentActivity) {
setupMultipage(it)
}
}
StoryViewerOptions.Type.FEED_STORY_POSITION -> {
val feedStoriesViewModel = listViewModel as FeedStoriesViewModel?
liveModels = feedStoriesViewModel!!.list
setupMultipage(feedStoriesViewModel!!.list.value)
}
StoryViewerOptions.Type.STORY_ARCHIVE -> {
val archivesViewModel = listViewModel as ArchivesViewModel?
liveModels = archivesViewModel!!.list
setupMultipage(archivesViewModel!!.list.value)
}
StoryViewerOptions.Type.USER -> {
resetView()
}
}
}
if (liveModels != null) liveModels.observe(viewLifecycleOwner, { models ->
storiesViewModel.getPagination().observe(fragmentActivity, {
if (models != null) {
when (it) {
StoryPaginationType.FORWARD -> {
if (currentFeedStoryIndex == models.size - 1)
Toast.makeText(
context,
R.string.no_more_stories,
Toast.LENGTH_SHORT
).show()
else paginateStories(false, currentFeedStoryIndex == models.size - 2)
}
StoryPaginationType.BACKWARD -> {
if (currentFeedStoryIndex == 0)
Toast.makeText(
context,
R.string.no_more_stories,
Toast.LENGTH_SHORT
).show()
else paginateStories(true, false)
}
StoryPaginationType.ERROR -> {
Toast.makeText(
context,
R.string.downloader_unknown_error,
Toast.LENGTH_SHORT
).show()
}
}
}
})
if (models != null && !models.isEmpty()) {
binding.btnBackward.isEnabled = currentFeedStoryIndex != 0
binding.btnForward.isEnabled = currentFeedStoryIndex != models.size - 1
resetView()
}
})
val context = context ?: return
swipeEvent = SwipeEvent { isRightSwipe: Boolean ->
@ -357,9 +317,46 @@ class StoryViewerFragment : Fragment() {
binding.imageViewer.setTapListener(simpleOnGestureListener)
}
private fun setupMultipage(models: List<Story>?) {
if (models == null) return
storiesViewModel.getPagination().observe(fragmentActivity, {
when (it) {
StoryPaginationType.FORWARD -> {
if (currentFeedStoryIndex == models.size - 1)
Toast.makeText(
context,
R.string.no_more_stories,
Toast.LENGTH_SHORT
).show()
else paginateStories(false, currentFeedStoryIndex == models.size - 2)
}
StoryPaginationType.BACKWARD -> {
if (currentFeedStoryIndex == 0)
Toast.makeText(
context,
R.string.no_more_stories,
Toast.LENGTH_SHORT
).show()
else paginateStories(true, false)
}
StoryPaginationType.ERROR -> {
Toast.makeText(
context,
R.string.downloader_unknown_error,
Toast.LENGTH_SHORT
).show()
}
}
})
if (!models.isEmpty()) {
binding.btnBackward.isEnabled = currentFeedStoryIndex != 0
binding.btnForward.isEnabled = currentFeedStoryIndex != models.size - 1
resetView()
}
}
private fun resetView() {
val context = context ?: return
live = null
if (menuProfile != null) menuProfile!!.isVisible = false
binding.imageViewer.controller = null
releasePlayer()
@ -367,7 +364,7 @@ class StoryViewerFragment : Fragment() {
var fetchOptions: StoryViewerOptions? = null
when (type) {
StoryViewerOptions.Type.HIGHLIGHT -> {
val models = storiesViewModel.getHighlights().value
val models = storiesViewModel.highlights.value
if (models == null || models.isEmpty() || currentFeedStoryIndex >= models.size || currentFeedStoryIndex < 0) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show()
return
@ -378,10 +375,15 @@ class StoryViewerFragment : Fragment() {
val feedStoriesViewModel = listViewModel as FeedStoriesViewModel?
val models = feedStoriesViewModel!!.list.value
if (models == null || currentFeedStoryIndex >= models.size || currentFeedStoryIndex < 0) return
val (_, _, _, _, user, _, _, _, _, _, _, broadcast) = models[currentFeedStoryIndex]
currentStoryUsername = user!!.username
fetchOptions = StoryViewerOptions.forUser(user.pk, currentStoryUsername)
live = broadcast
val userStory = models[currentFeedStoryIndex]
currentStoryUsername = userStory.user!!.username
fetchOptions = StoryViewerOptions.forUser(userStory.user.pk, currentStoryUsername)
val live = userStory.broadcast
if (live != null) {
storiesViewModel.setStory(userStory)
refreshLive(live)
return
}
}
StoryViewerOptions.Type.STORY_ARCHIVE -> {
val archivesViewModel = listViewModel as ArchivesViewModel?
@ -404,11 +406,7 @@ class StoryViewerFragment : Fragment() {
storiesViewModel.fetchSingleMedia(options!!.id)
return
}
if (live != null) {
refreshLive()
return
}
storiesViewModel.fetchStory(fetchOptions).observe(fragmentActivity, {
storiesViewModel.fetchStory(fetchOptions).observe(viewLifecycleOwner, {
if (it.status == Resource.Status.ERROR) {
Toast.makeText(context, "Error: " + it.message, Toast.LENGTH_SHORT).show()
}
@ -416,18 +414,14 @@ class StoryViewerFragment : Fragment() {
}
@Synchronized
private fun refreshLive() {
private fun refreshLive(live: Broadcast) {
binding.btnDownload.isEnabled = false
binding.stickers.isEnabled = false
binding.listToggle.isEnabled = false
binding.btnShare.isEnabled = false
binding.btnReply.isEnabled = false
releasePlayer()
setupLive(live!!.dashPlaybackUrl ?: live!!.dashAbrPlaybackUrl ?: return)
val actionBar = fragmentActivity.supportActionBar
actionBarSubtitle = epochSecondToString(live!!.publishedTime!!)
if (actionBar != null) {
try {
actionBar.setSubtitle(actionBarSubtitle)
} catch (e: Exception) {
Log.e(TAG, "refreshLive: ", e)
}
}
setupLive(live.dashPlaybackUrl ?: live.dashAbrPlaybackUrl ?: return)
}
@Synchronized
@ -446,14 +440,19 @@ class StoryViewerFragment : Fragment() {
binding.btnReply.isEnabled = currentStory.canReply
if (itemType === MediaItemType.MEDIA_TYPE_VIDEO) setupVideo(url) else setupImage(url)
// if (Utils.settingsHelper.getBoolean(MARK_AS_SEEN)) storiesRepository!!.seen(
// csrfToken,
// userId,
// deviceId,
// currentStory!!.id!!,
// currentStory!!.takenAt,
// System.currentTimeMillis() / 1000
// )
if (options!!.type == StoryViewerOptions.Type.FEED_STORY_POSITION
&& Utils.settingsHelper.getBoolean(PreferenceKeys.MARK_AS_SEEN)) {
val feedStoriesViewModel = listViewModel as FeedStoriesViewModel?
storiesViewModel.markAsSeen(currentStory).observe(viewLifecycleOwner) { m ->
if (m.status == Resource.Status.SUCCESS && m.data != null) {
val liveModels: MutableLiveData<List<Story>> = feedStoriesViewModel!!.list
val models = liveModels.value
val modelsCopy: MutableList<Story> = models!!.toMutableList()
modelsCopy.set(currentFeedStoryIndex, m.data)
liveModels.postValue(modelsCopy)
}
}
}
}
private fun downloadStory() {
@ -797,6 +796,13 @@ class StoryViewerFragment : Fragment() {
}
private fun shareStoryViaDm() {
val story = storiesViewModel.getCurrentStory().value ?: return
val context = context
if (story.user?.isPrivate == true && context != null) {
Toast.makeText(context, R.string.share_private_post, Toast.LENGTH_SHORT).show()
}
val actionBar = fragmentActivity.supportActionBar
if (actionBar != null) actionBar.subtitle = null
val actionGlobalUserSearch = StoryViewerFragmentDirections.actionToUserSearch().apply {
title = getString(R.string.share)
actionLabel = getString(R.string.send)

View file

@ -52,6 +52,7 @@ import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.discover.TopicCluster;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
@ -99,7 +100,7 @@ public class TopicPostsFragment extends Fragment implements SwipeRefreshLayout.O
});
private final FeedAdapterV2.FeedItemCallback feedItemCallback = new FeedAdapterV2.FeedItemCallback() {
@Override
public void onPostClick(final Media feedModel, final View profilePicView, final View mainPostImage) {
public void onPostClick(final Media feedModel) {
openPostDialog(feedModel, -1);
}
@ -147,12 +148,12 @@ public class TopicPostsFragment extends Fragment implements SwipeRefreshLayout.O
}
@Override
public void onNameClick(final Media feedModel, final View profilePicView) {
public void onNameClick(final Media feedModel) {
navigateToProfile("@" + feedModel.getUser().getUsername());
}
@Override
public void onProfilePicClick(final Media feedModel, final View profilePicView) {
public void onProfilePicClick(final Media feedModel) {
final User user = feedModel.getUser();
if (user == null) return;
navigateToProfile("@" + user.getUsername());
@ -377,7 +378,9 @@ public class TopicPostsFragment extends Fragment implements SwipeRefreshLayout.O
}
private void updateSwipeRefreshState() {
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching());
AppExecutors.INSTANCE.getMainThread().execute(() ->
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching())
);
}
private void navigateToProfile(final String username) {

View file

@ -97,7 +97,7 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre
private final FeedAdapterV2.FeedItemCallback feedItemCallback = new FeedAdapterV2.FeedItemCallback() {
@Override
public void onPostClick(final Media feedModel, final View profilePicView, final View mainPostImage) {
public void onPostClick(final Media feedModel) {
openPostDialog(feedModel, -1);
}
@ -157,13 +157,13 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre
}
@Override
public void onNameClick(final Media feedModel, final View profilePicView) {
public void onNameClick(final Media feedModel) {
if (feedModel.getUser() == null) return;
navigateToProfile("@" + feedModel.getUser().getUsername());
}
@Override
public void onProfilePicClick(final Media feedModel, final View profilePicView) {
public void onProfilePicClick(final Media feedModel) {
if (feedModel.getUser() == null) return;
navigateToProfile("@" + feedModel.getUser().getUsername());
}
@ -330,12 +330,6 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre
return super.onOptionsItemSelected(item);
}
@Override
public void onResume() {
super.onResume();
binding.getRoot().postDelayed(feedStoriesAdapter::notifyDataSetChanged, 1000);
}
@Override
public void onRefresh() {
binding.feedRecyclerView.refresh();
@ -370,7 +364,9 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre
}
private void updateSwipeRefreshState() {
binding.feedSwipeRefreshLayout.setRefreshing(binding.feedRecyclerView.isFetching() || storiesFetching);
AppExecutors.INSTANCE.getMainThread().execute(() ->
binding.feedSwipeRefreshLayout.setRefreshing(binding.feedRecyclerView.isFetching() || storiesFetching)
);
}
private void setupFeedStories() {
@ -381,7 +377,7 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre
storiesRecyclerView = binding.header;
storiesRecyclerView.setLayoutManager(new LinearLayoutManager(context, RecyclerView.HORIZONTAL, false));
storiesRecyclerView.setAdapter(feedStoriesAdapter);
feedStoriesViewModel.getList().observe(getViewLifecycleOwner(), feedStoriesAdapter::submitList);
feedStoriesViewModel.getList().observe(fragmentActivity, feedStoriesAdapter::submitList);
fetchStories();
}
@ -401,8 +397,6 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre
storiesFetching = false;
//noinspection unchecked
feedStoriesViewModel.getList().postValue((List<Story>) feedStoryModels);
//noinspection unchecked
feedStoriesAdapter.submitList((List<Story>) feedStoryModels);
if (storyListMenu != null) storyListMenu.setVisible(true);
updateSwipeRefreshState();
}), Dispatchers.getIO())

View file

@ -90,15 +90,15 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall
private val bioDialogRequestCode = 102
private val translationDialogRequestCode = 103
private val feedItemCallback: FeedAdapterV2.FeedItemCallback = object : FeedAdapterV2.FeedItemCallback {
override fun onPostClick(media: Media?, profilePicView: View?, mainPostImage: View?) {
override fun onPostClick(media: Media) {
openPostDialog(media ?: return, -1)
}
override fun onProfilePicClick(media: Media?, profilePicView: View?) {
override fun onProfilePicClick(media: Media) {
navigateToProfile(media?.user?.username)
}
override fun onNameClick(media: Media?, profilePicView: View?) {
override fun onNameClick(media: Media) {
navigateToProfile(media?.user?.username)
}
@ -860,7 +860,11 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall
.setLifeCycleOwner(this)
.setPostFetchService(ProfilePostFetchService(profile, currentUser != null))
.setLayoutPreferences(layoutPreferences)
.addFetchStatusChangeListener { binding.swipeRefreshLayout.isRefreshing = it }
.addFetchStatusChangeListener {
AppExecutors.mainThread.execute {
binding.swipeRefreshLayout.isRefreshing = it
}
}
.setFeedItemCallback(feedItemCallback)
.setSelectionModeCallback(selectionModeCallback)
.init()

View file

@ -240,6 +240,7 @@ public class SearchFragment extends Fragment implements SearchCategoryFragment.O
switch (resource.status) {
case SUCCESS:
viewModel.search("", type);
viewModel.search("", FavoriteType.TOP);
liveData.removeObserver(this);
break;
case ERROR:

View file

@ -104,7 +104,7 @@ public class DownloadsPreferencesFragment extends BasePreferencesFragment {
Utils.setupSelectedDir(context, data);
String path;
try {
path = URLDecoder.decode(data.getData().toString(), StandardCharsets.UTF_8.toString());
path = URLDecoder.decode(data.getData().toString(), StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
path = data.getData().toString();
}

View file

@ -1,49 +0,0 @@
package awais.instagrabber.models
import java.io.Serializable
class FollowModel(
val id: String,
val username: String,
val fullName: String,
val profilePicUrl: String
) : Serializable {
private var hasNextPage = false
get() = endCursor != null && field
var isShown = true
var endCursor: String? = null
private set
fun setPageCursor(hasNextPage: Boolean, endCursor: String?) {
this.endCursor = endCursor
this.hasNextPage = hasNextPage
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FollowModel
if (id != other.id) return false
if (username != other.username) return false
if (fullName != other.fullName) return false
if (profilePicUrl != other.profilePicUrl) return false
if (isShown != other.isShown) return false
if (endCursor != other.endCursor) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + username.hashCode()
result = 31 * result + fullName.hashCode()
result = 31 * result + profilePicUrl.hashCode()
result = 31 * result + isShown.hashCode()
result = 31 * result + (endCursor?.hashCode() ?: 0)
return result
}
}

View file

@ -64,8 +64,8 @@ enum class DirectItemType(val id: Int) : Serializable {
private val map: MutableMap<Int, DirectItemType> = mutableMapOf()
@JvmStatic
fun getId(id: Int): DirectItemType? {
return map[id]
fun getTypeFromId(id: Int): DirectItemType {
return map[id] ?: UNKNOWN
}
fun getName(directItemType: DirectItemType): String? {

View file

@ -1,6 +1,7 @@
package awais.instagrabber.repositories
import awais.instagrabber.repositories.responses.FriendshipChangeResponse
import awais.instagrabber.repositories.responses.FriendshipListFetchResponse
import awais.instagrabber.repositories.responses.FriendshipRestrictResponse
import retrofit2.http.*
@ -25,7 +26,7 @@ interface FriendshipService {
@Path("userId") userId: Long,
@Path("type") type: String, // following or followers
@QueryMap(encoded = true) queryParams: Map<String, String>,
): String
): FriendshipListFetchResponse
@FormUrlEncoded
@POST("/api/v1/friendships/{action}/")

View file

@ -1,27 +1,10 @@
package awais.instagrabber.repositories.responses
import awais.instagrabber.models.FollowModel
data class FriendshipListFetchResponse(
var nextMaxId: String?,
var status: String?,
var items: List<FollowModel>?
var users: List<User>?
) {
val isMoreAvailable: Boolean
get() = !nextMaxId.isNullOrBlank()
fun setNextMaxId(nextMaxId: String): FriendshipListFetchResponse {
this.nextMaxId = nextMaxId
return this
}
fun setStatus(status: String): FriendshipListFetchResponse {
this.status = status
return this
}
fun setItems(items: List<FollowModel>): FriendshipListFetchResponse {
this.items = items
return this
}
}

View file

@ -1,9 +1,9 @@
package awais.instagrabber.repositories.responses.notification
class NotificationCounts(val commentLikesCount: Int,
val userTagsCount: Int,
val likesCount: Int,
val commentsCount: Int,
val relationshipsCount: Int,
val pOYCount: Int,
val requestsCount: Int)
class NotificationCounts(val commentLikes: Int,
val usertags: Int,
val likes: Int,
val comments: Int,
val relationships: Int,
val photosOfYou: Int,
val requests: Int)

View file

@ -11,7 +11,7 @@ data class Story(
val latestReelMedia: Long? = null, // = timestamp
val mediaCount: Int? = null,
// for stories and highlights
var seen: Long? = null,
val seen: Long? = null,
val user: User? = null,
// for stories
val muted: Boolean? = null,

View file

@ -54,10 +54,9 @@ public class ActivityCheckerService extends Service {
public void onSuccess(final NotificationCounts result) {
try {
if (result == null) return;
final String notification = getNotificationString(result);
final List<String> notification = getNotificationString(result);
if (notification == null) return;
final String notificationString = getString(R.string.activity_count_prefix) + " " + notification + ".";
showNotification(notificationString);
showNotification(notification);
} finally {
handler.postDelayed(runnable, DELAY_MILLIS);
}
@ -88,42 +87,54 @@ public class ActivityCheckerService extends Service {
handler.removeCallbacks(runnable);
}
private String getNotificationString(final NotificationCounts result) {
private List<String> getNotificationString(final NotificationCounts result) {
final List<String> toReturn = new ArrayList<>(2);
final List<String> list = new ArrayList<>();
if (result.getRelationshipsCount() != 0) {
list.add(getString(R.string.activity_count_relationship, result.getRelationshipsCount()));
int count = 0;
if (result.getRelationships() != 0) {
list.add(getString(R.string.activity_count_relationship, result.getRelationships()));
count += result.getRelationships();
}
if (result.getRequestsCount() != 0) {
list.add(getString(R.string.activity_count_requests, result.getRequestsCount()));
if (result.getRequests() != 0) {
list.add(getString(R.string.activity_count_requests, result.getRequests()));
count += result.getRequests();
}
if (result.getUserTagsCount() != 0) {
list.add(getString(R.string.activity_count_usertags, result.getUserTagsCount()));
if (result.getUsertags() != 0) {
list.add(getString(R.string.activity_count_usertags, result.getUsertags()));
count += result.getUsertags();
}
if (result.getPOYCount() != 0) {
list.add(getString(R.string.activity_count_poy, result.getPOYCount()));
if (result.getPhotosOfYou() != 0) {
list.add(getString(R.string.activity_count_poy, result.getPhotosOfYou()));
count += result.getPhotosOfYou();
}
if (result.getCommentsCount() != 0) {
list.add(getString(R.string.activity_count_comments, result.getCommentsCount()));
if (result.getComments() != 0) {
list.add(getString(R.string.activity_count_comments, result.getComments()));
count += result.getComments();
}
if (result.getCommentLikesCount() != 0) {
list.add(getString(R.string.activity_count_commentlikes, result.getCommentLikesCount()));
if (result.getCommentLikes() != 0) {
list.add(getString(R.string.activity_count_commentlikes, result.getCommentLikes()));
count += result.getCommentLikes();
}
if (result.getLikesCount() != 0) {
list.add(getString(R.string.activity_count_likes, result.getLikesCount()));
if (result.getLikes() != 0) {
list.add(getString(R.string.activity_count_likes, result.getLikes()));
count += result.getLikes();
}
if (list.isEmpty()) return null;
return TextUtils.join(", ", list);
toReturn.add(TextUtils.join(", ", list));
toReturn.add(getResources().getQuantityString(R.plurals.activity_count_total, count, count));
return toReturn;
}
private void showNotification(final String notificationString) {
private void showNotification(final List<String> notificationString) {
final Notification notification = new NotificationCompat.Builder(this, Constants.ACTIVITY_CHANNEL_ID)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setSmallIcon(R.drawable.ic_notif)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentTitle(getString(R.string.action_notif))
.setContentText(notificationString)
.setContentTitle(notificationString.get(1))
.setContentText(notificationString.get(0))
.setStyle(new NotificationCompat.BigTextStyle().bigText(notificationString.get(0)))
.setContentIntent(getPendingIntent())
.build();
notificationManager.notify(Constants.ACTIVITY_NOTIFICATION_ID, notification);

View file

@ -15,48 +15,23 @@ import java.io.FileDescriptor;
public final class MediaUtils {
private static final String TAG = MediaUtils.class.getSimpleName();
private static final String[] PROJECTION_VIDEO = {
MediaStore.Video.Media.DURATION,
MediaStore.Video.Media.WIDTH,
MediaStore.Video.Media.HEIGHT,
MediaStore.Video.Media.SIZE
};
private static final String[] PROJECTION_AUDIO = {
MediaStore.Audio.Media.DURATION,
MediaStore.Audio.Media.SIZE
};
public static void getVideoInfo(@NonNull final ContentResolver contentResolver,
@NonNull final Uri uri,
@NonNull final OnInfoLoadListener<VideoInfo> listener) {
AppExecutors.INSTANCE.getTasksThread().submit(() -> {
try (Cursor cursor = MediaStore.Video.query(contentResolver, uri, PROJECTION_VIDEO)) {
if (cursor == null) {
listener.onLoad(null);
return;
}
int durationColumn = cursor.getColumnIndex(MediaStore.Video.Media.DURATION);
int widthColumn = cursor.getColumnIndex(MediaStore.Video.Media.WIDTH);
int heightColumn = cursor.getColumnIndex(MediaStore.Video.Media.HEIGHT);
int sizeColumn = cursor.getColumnIndex(MediaStore.Video.Media.SIZE);
if (cursor.moveToNext()) {
listener.onLoad(new VideoInfo(
cursor.getLong(durationColumn),
cursor.getInt(widthColumn),
cursor.getInt(heightColumn),
cursor.getLong(sizeColumn)
));
}
} catch (Exception e) {
Log.e(TAG, "getVideoInfo: ", e);
listener.onFailure(e);
}
});
getInfo(contentResolver, uri, listener, true);
}
public static void getVoiceInfo(@NonNull final ContentResolver contentResolver,
@NonNull final Uri uri,
@NonNull final OnInfoLoadListener<VideoInfo> listener) {
getInfo(contentResolver, uri, listener, false);
}
private static void getInfo(@NonNull final ContentResolver contentResolver,
@NonNull final Uri uri,
@NonNull final OnInfoLoadListener<VideoInfo> listener,
@NonNull final Boolean isVideo) {
AppExecutors.INSTANCE.getTasksThread().submit(() -> {
try (ParcelFileDescriptor parcelFileDescriptor = contentResolver.openFileDescriptor(uri, "r")) {
if (parcelFileDescriptor == null) {
@ -68,6 +43,23 @@ public final class MediaUtils {
mediaMetadataRetriever.setDataSource(fileDescriptor);
String duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
if (TextUtils.isEmpty(duration)) duration = "0";
if (isVideo) {
String width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
if (TextUtils.isEmpty(width)) width = "1";
String height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
if (TextUtils.isEmpty(height)) height = "1";
final Cursor cursor = contentResolver.query(uri, new String[]{MediaStore.Video.Media.SIZE}, null, null, null);
cursor.moveToFirst();
final long fileSize = cursor.getLong(0);
cursor.close();
listener.onLoad(new VideoInfo(
Long.parseLong(duration),
Integer.valueOf(width),
Integer.valueOf(height),
fileSize
));
return;
}
listener.onLoad(new VideoInfo(
Long.parseLong(duration),
0,
@ -75,7 +67,7 @@ public final class MediaUtils {
0
));
} catch (Exception e) {
Log.e(TAG, "getVoiceInfo: ", e);
Log.e(TAG, "getInfo: ", e);
listener.onFailure(e);
}
});

View file

@ -1,19 +0,0 @@
package awais.instagrabber.viewmodels;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import java.util.List;
import awais.instagrabber.models.FollowModel;
public class FollowViewModel extends ViewModel {
private MutableLiveData<List<FollowModel>> list;
public MutableLiveData<List<FollowModel>> getList() {
if (list == null) {
list = new MutableLiveData<>();
}
return list;
}
}

View file

@ -0,0 +1,167 @@
package awais.instagrabber.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import awais.instagrabber.models.Resource
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.webservices.FriendshipRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class FollowViewModel : ViewModel() {
// data
val userId = MutableLiveData<Long>()
private val followers = MutableLiveData<List<User>>()
private val followings = MutableLiveData<List<User>>()
private val searchResults = MutableLiveData<List<User>>()
// cursors
private val followersMaxId = MutableLiveData<String?>("")
private val followingMaxId = MutableLiveData<String?>("")
private val searchingMaxId = MutableLiveData<String?>("")
private val searchQuery = MutableLiveData<String?>()
// comparison
val status: LiveData<Pair<Boolean, Boolean>> = object : MediatorLiveData<Pair<Boolean, Boolean>>() {
init {
postValue(Pair(false, false))
addSource(followersMaxId) {
if (it == null) {
postValue(Pair(true, value!!.second))
}
else fetch(true, it)
}
addSource(followingMaxId) {
if (it == null) {
postValue(Pair(value!!.first, true))
}
else fetch(false, it)
}
}
}
val comparison: LiveData<Triple<List<User>, List<User>, List<User>>> =
object : MediatorLiveData<Triple<List<User>, List<User>, List<User>>>() {
init {
addSource(status) {
if (it.first && it.second) {
val followersList = followers.value!!
val followingList = followings.value!!
val allUsers: MutableList<User> = mutableListOf()
allUsers.addAll(followersList)
allUsers.addAll(followingList)
val followersMap = followersList.groupBy { it.pk }
val followingMap = followingList.groupBy { it.pk }
val mutual: MutableList<User> = mutableListOf()
val onlyFollowing: MutableList<User> = mutableListOf()
val onlyFollowers: MutableList<User> = mutableListOf()
allUsers.forEach {
val isFollowing = followingMap.get(it.pk) != null
val isFollower = followersMap.get(it.pk) != null
if (isFollowing && isFollower) mutual.add(it)
else if (isFollowing) onlyFollowing.add(it)
else if (isFollower) onlyFollowers.add(it)
}
postValue(Triple(mutual, onlyFollowing, onlyFollowers))
}
}
}
}
private val friendshipRepository: FriendshipRepository by lazy { FriendshipRepository.getInstance() }
// fetch: supply max ID for continuous fetch
fun fetch(follower: Boolean, nextMaxId: String?): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(Resource.loading(null))
val maxId = if (follower) followersMaxId else followingMaxId
if (maxId.value == null && nextMaxId == null) data.postValue(Resource.success(null))
else if (userId.value == null) data.postValue(Resource.error("No user ID supplied!", null))
else viewModelScope.launch(Dispatchers.IO) {
try {
val tempList = friendshipRepository.getList(
follower,
userId.value!!,
nextMaxId ?: maxId.value,
null
)
if (!tempList.status.equals("ok")) {
data.postValue(Resource.error("Status not ok!", null))
}
else {
if (tempList.users != null) {
val liveData = if (follower) followers else followings
val currentList = if (liveData.value != null) liveData.value!!.toMutableList()
else mutableListOf()
currentList.addAll(tempList.users!!)
liveData.postValue(currentList.toList())
}
maxId.postValue(tempList.nextMaxId)
data.postValue(Resource.success(null))
}
} catch (e: Exception) {
data.postValue(Resource.error(e.message, null))
}
}
return data
}
fun getList(follower: Boolean): LiveData<List<User>> {
return if (follower) followers else followings
}
fun search(follower: Boolean): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(Resource.loading(null))
val query = searchQuery.value
if (searchingMaxId.value == null) data.postValue(Resource.success(null))
else if (userId.value == null) data.postValue(Resource.error("No user ID supplied!", null))
else if (query.isNullOrEmpty()) data.postValue(Resource.error("No query supplied!", null))
else viewModelScope.launch(Dispatchers.IO) {
try {
val tempList = friendshipRepository.getList(
follower,
userId.value!!,
searchingMaxId.value,
query
)
if (!tempList.status.equals("ok")) {
data.postValue(Resource.error("Status not ok!", null))
}
else {
if (tempList.users != null) {
val currentList = if (searchResults.value != null) searchResults.value!!.toMutableList()
else mutableListOf()
currentList.addAll(tempList.users!!)
searchResults.postValue(currentList.toList())
}
searchingMaxId.postValue(tempList.nextMaxId)
data.postValue(Resource.success(null))
}
} catch (e: Exception) {
data.postValue(Resource.error(e.message, null))
}
}
return data
}
fun getSearch(): LiveData<List<User>> {
return searchResults
}
fun setQuery(query: String?, follower: Boolean) {
searchQuery.value = query
if (!query.isNullOrEmpty()) search(follower)
}
fun clearProgress() {
followersMaxId.value = ""
followingMaxId.value = ""
searchingMaxId.value = ""
followings.value = listOf<User>()
followers.value = listOf<User>()
searchResults.value = listOf<User>()
}
}

View file

@ -226,7 +226,7 @@ class ProfileFragmentViewModel(
private suspend fun fetchUser(
currentUser: User?,
stateUsername: String,
): User {
): User? {
if (currentUser != null) {
// logged in
val tempUser = userRepository.getUsernameInfo(stateUsername)

View file

@ -242,7 +242,7 @@ public class SearchFragmentViewModel extends AppStateViewModel {
@Override
public void onFailure(@NonNull final Throwable t) {
if (!TextUtils.isEmpty(tempQuery)) return;
topResults.postValue(Resource.success(Collections.emptyList()));
liveData.postValue(Resource.success(Collections.emptyList()));
Log.e(TAG, "onFailure: ", t);
}
}, AppExecutors.INSTANCE.getMainThread());

View file

@ -60,21 +60,22 @@ class StoryFragmentViewModel : ViewModel() {
private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() }
// for highlights ONLY
private val highlights = MutableLiveData<List<Story>?>()
val highlights = MutableLiveData<List<Story>?>()
/* set functions */
fun setStory(story: Story) {
if (story.items == null || story.items.size == 0) {
pagination.postValue(StoryPaginationType.ERROR)
return
}
currentStory.postValue(story)
storyTitle.postValue(story.title ?: story.user?.username)
if (story.broadcast != null) {
date.postValue(story.dateTime)
type.postValue(MediaItemType.MEDIA_TYPE_LIVE)
pagination.postValue(StoryPaginationType.DO_NOTHING)
return
}
if (story.items == null || story.items.size == 0) {
pagination.postValue(StoryPaginationType.ERROR)
return
}
}
@ -184,10 +185,6 @@ class StoryFragmentViewModel : ViewModel() {
/* get functions */
fun getHighlights(): LiveData<List<Story>?> {
return highlights
}
fun getCurrentStory(): LiveData<Story?> {
return currentStory
}
@ -472,4 +469,28 @@ class StoryFragmentViewModel : ViewModel() {
}
return data
}
fun markAsSeen(storyMedia: StoryMedia): LiveData<Resource<Story?>> {
val data = MutableLiveData<Resource<Story?>>()
data.postValue(loading(null))
val oldStory = currentStory.value!!
if (oldStory.seen != null && oldStory.seen >= storyMedia.takenAt) data.postValue(success(null))
else viewModelScope.launch(Dispatchers.IO) {
try {
storiesRepository.seen(
csrfToken!!,
userId,
deviceId,
storyMedia.id,
storyMedia.takenAt,
System.currentTimeMillis() / 1000
)
val newStory = oldStory.copy(seen = storyMedia.takenAt)
data.postValue(success(newStory))
} catch (e: Exception) {
data.postValue(error(e.message, null))
}
}
return data
}
}

View file

@ -1,15 +1,11 @@
package awais.instagrabber.webservices
import awais.instagrabber.models.FollowModel
import awais.instagrabber.repositories.FriendshipService
import awais.instagrabber.repositories.responses.FriendshipChangeResponse
import awais.instagrabber.repositories.responses.FriendshipListFetchResponse
import awais.instagrabber.repositories.responses.FriendshipRestrictResponse
import awais.instagrabber.utils.Utils
import awais.instagrabber.webservices.RetrofitFactory.retrofit
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
class FriendshipRepository(private val service: FriendshipService) {
@ -113,43 +109,12 @@ class FriendshipRepository(private val service: FriendshipService) {
follower: Boolean,
targetUserId: Long,
maxId: String?,
query: String?
): FriendshipListFetchResponse {
val queryMap = if (maxId != null) mapOf("max_id" to maxId) else emptyMap()
val response = service.getList(targetUserId, if (follower) "followers" else "following", queryMap)
return parseListResponse(response)
}
@Throws(JSONException::class)
private fun parseListResponse(body: String): FriendshipListFetchResponse {
val root = JSONObject(body)
val nextMaxId = root.optString("next_max_id")
val status = root.optString("status")
val itemsJson = root.optJSONArray("users")
val items = parseItems(itemsJson)
return FriendshipListFetchResponse(
nextMaxId,
status,
items
)
}
@Throws(JSONException::class)
private fun parseItems(items: JSONArray?): List<FollowModel> {
if (items == null) {
return emptyList()
}
val followModels = mutableListOf<FollowModel>()
for (i in 0 until items.length()) {
val itemJson = items.optJSONObject(i) ?: continue
val followModel = FollowModel(
itemJson.getString("pk"),
itemJson.getString("username"),
itemJson.optString("full_name"),
itemJson.getString("profile_pic_url")
)
followModels.add(followModel)
}
return followModels
val queryMap: MutableMap<String, String> = mutableMapOf()
if (!maxId.isNullOrEmpty()) queryMap.set("max_id", maxId)
if (!query.isNullOrEmpty()) queryMap.set("query", query)
return service.getList(targetUserId, if (follower) "followers" else "following", queryMap.toMap())
}
companion object {

View file

@ -178,51 +178,59 @@ open class GraphQLRepository(private val service: GraphQLService) {
// TODO convert string response to a response class
open suspend fun fetchUser(
username: String,
): User {
): User? {
val response = service.getUser(username)
val body = JSONObject(response
.split("<script type=\"text/javascript\">window._sharedData = ").get(1)
.split("</script>").get(0)
.trim().replace(Regex("\\};$"), "}"))
val userJson = body
.getJSONObject("entry_data")
.getJSONArray("ProfilePage")
.getJSONObject(0)
.getJSONObject("graphql")
.getJSONObject(Constants.EXTRAS_USER)
val isPrivate = userJson.getBoolean("is_private")
val id = userJson.optLong(Constants.EXTRAS_ID, 0)
val timelineMedia = userJson.getJSONObject("edge_owner_to_timeline_media")
// if (timelineMedia.has("edges")) {
// final JSONArray edges = timelineMedia.getJSONArray("edges");
// }
var url: String? = userJson.optString("external_url")
if (url.isNullOrBlank()) url = null
return User(
id,
username,
userJson.getString("full_name"),
isPrivate,
userJson.getString("profile_pic_url_hd"),
userJson.getBoolean("is_verified"),
friendshipStatus = FriendshipStatus(
userJson.optBoolean("followed_by_viewer"),
userJson.optBoolean("follows_viewer"),
userJson.optBoolean("blocked_by_viewer"),
false,
try {
val body = JSONObject(
response
.split("<script type=\"text/javascript\">window._sharedData = ").get(1)
.split("</script>").get(0)
.trim().replace(Regex("\\};$"), "}")
)
val userJson = body
.getJSONObject("entry_data")
.getJSONArray("ProfilePage")
.getJSONObject(0)
.getJSONObject("graphql")
.getJSONObject(Constants.EXTRAS_USER)
val isPrivate = userJson.getBoolean("is_private")
val id = userJson.optLong(Constants.EXTRAS_ID, 0)
val timelineMedia = userJson.getJSONObject("edge_owner_to_timeline_media")
// if (timelineMedia.has("edges")) {
// final JSONArray edges = timelineMedia.getJSONArray("edges");
// }
var url: String? = userJson.optString("external_url")
if (url.isNullOrBlank()) url = null
return User(
id,
username,
userJson.getString("full_name"),
isPrivate,
userJson.optBoolean("has_requested_viewer"),
userJson.optBoolean("requested_by_viewer"),
false,
userJson.optBoolean("restricted_by_viewer"),
false
),
mediaCount = timelineMedia.getLong("count"),
followerCount = userJson.getJSONObject("edge_followed_by").getLong("count"),
followingCount = userJson.getJSONObject("edge_follow").getLong("count"),
biography = userJson.getString("biography"),
externalUrl = url,
)
userJson.getString("profile_pic_url_hd"),
userJson.getBoolean("is_verified"),
friendshipStatus = FriendshipStatus(
userJson.optBoolean("followed_by_viewer"),
userJson.optBoolean("follows_viewer"),
userJson.optBoolean("blocked_by_viewer"),
false,
isPrivate,
userJson.optBoolean("has_requested_viewer"),
userJson.optBoolean("requested_by_viewer"),
false,
userJson.optBoolean("restricted_by_viewer"),
false
),
mediaCount = timelineMedia.getLong("count"),
followerCount = userJson.getJSONObject("edge_followed_by").getLong("count"),
followingCount = userJson.getJSONObject("edge_follow").getLong("count"),
biography = userJson.getString("biography"),
externalUrl = url,
)
}
catch (e: Exception) {
Log.e(TAG, "fetchUser failed", e)
return null
}
}
// TODO convert string response to a response class

View file

@ -3,13 +3,13 @@ package thoughtbot.expandableadapter;
import java.util.ArrayList;
import java.util.List;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.repositories.responses.User;
public class ExpandableGroup {
private final String title;
private final List<FollowModel> items;
private final List<User> items;
public ExpandableGroup(final String title, final List<FollowModel> items) {
public ExpandableGroup(final String title, final List<User> items) {
this.title = title;
this.items = items;
}
@ -18,22 +18,13 @@ public class ExpandableGroup {
return title;
}
public List<FollowModel> getItems(final boolean filtered) {
if (!filtered) return items;
final ArrayList<FollowModel> followModels = new ArrayList<>();
for (final FollowModel followModel : items) if (followModel.isShown()) followModels.add(followModel);
return followModels;
public List<User> getItems() {
return items;
}
public int getItemCount(final boolean filtered) {
public int getItemCount() {
if (items != null) {
final int size = items.size();
if (filtered) {
int finalSize = 0;
for (int i = 0; i < size; ++i) if (items.get(i).isShown()) ++finalSize;
return finalSize;
}
return size;
return items.size();
}
return 0;
}

View file

@ -1,6 +1,7 @@
package thoughtbot.expandableadapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
@ -15,6 +16,13 @@ public final class ExpandableList {
this.expandedGroupIndexes = new boolean[groupsSize];
}
public ExpandableList(@NonNull final ArrayList<ExpandableGroup> groups,
@Nullable final boolean[] expandedGroupIndexes) {
this.groups = groups;
this.groupsSize = groups.size();
this.expandedGroupIndexes = expandedGroupIndexes;
}
public int getVisibleItemCount() {
int count = 0;
for (int i = 0; i < groupsSize; i++) count = count + numberOfVisibleItemsInGroup(i);
@ -36,7 +44,7 @@ public final class ExpandableList {
}
private int numberOfVisibleItemsInGroup(final int group) {
return expandedGroupIndexes[group] ? groups.get(group).getItemCount(true) + 1 : 1;
return expandedGroupIndexes[group] ? groups.get(group).getItemCount() + 1 : 1;
}
public int getFlattenedGroupIndex(@NonNull final ExpandableListPosition listPosition) {