1
0
mirror of https://github.com/KokaKiwi/BarInsta synced 2024-11-22 06:37:30 +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) {

View File

@ -16,6 +16,5 @@
android:paddingLeft="8dp"
android:paddingEnd="8dp"
android:paddingRight="8dp"
app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/item_follow" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -1,113 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/top_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/btnComments"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?android:selectableItemBackground"
android:gravity="center"
android:orientation="horizontal"
android:padding="4dp">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
app:srcCompat="@drawable/ic_outline_comments_24"
app:tint="?android:textColorPrimary" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/commentsCount"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:textAppearance="?attr/textAppearanceButton"
tools:text="690000" />
</LinearLayout>
<LinearLayout
android:id="@+id/btnViews"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal"
android:padding="4dp">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
app:srcCompat="@drawable/ic_outline_views_24"
app:tint="?android:textColorPrimary" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tvVideoViews"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:textAppearance="?attr/textAppearanceButton"
tools:text="690000" />
</LinearLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tvPostDate"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:maxLines="1"
android:padding="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
tools:text="2020-01-01 12:00:00" />
<!--<androidx.appcompat.widget.AppCompatImageView-->
<!-- android:id="@+id/btnMute"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="match_parent"-->
<!-- android:background="?selectableItemBackgroundBorderless"-->
<!-- android:padding="4dp"-->
<!-- android:visibility="gone"-->
<!-- app:srcCompat="@drawable/ic_volume_up_24"-->
<!-- app:tint="?android:textColorPrimary"-->
<!-- tools:visibility="visible" />-->
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/btnDownload"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?selectableItemBackgroundBorderless"
android:padding="4dp"
app:srcCompat="@drawable/ic_download"
app:tint="?android:textColorPrimary" />
</LinearLayout>
<awais.instagrabber.customviews.RamboTextViewV2
android:id="@+id/viewerCaption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:selectableItemBackground"
android:clickable="true"
android:clipToPadding="false"
android:ellipsize="end"
android:focusable="true"
android:maxLines="5"
android:paddingStart="8dp"
android:paddingLeft="8dp"
android:paddingEnd="8dp"
android:paddingRight="8dp"
android:paddingBottom="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
tools:text="Bottom text with hashtags etc." />
</LinearLayout>

View File

@ -1,13 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_height="wrap_content">
<include
android:id="@+id/item_feed_top"
layout="@layout/item_feed_top" />
<include layout="@layout/item_feed_top" />
<awais.instagrabber.customviews.drawee.ZoomableDraweeView
android:id="@+id/imageViewer"
@ -17,9 +15,9 @@
android:clickable="true"
android:focusable="true"
app:actualImageScaleType="fitCenter"
app:viewAspectRatio="1" />
app:viewAspectRatio="1"
app:layout_constraintTop_toBottomOf="@id/top_barrier"
app:layout_constraintBottom_toTopOf="@id/buttons_top_barrier"/>
<include
android:id="@+id/item_feed_bottom"
layout="@layout/item_feed_bottom" />
</LinearLayout>
<include layout="@layout/layout_post_view_bottom" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_height="wrap_content">
<include
android:id="@+id/item_feed_top"
layout="@layout/item_feed_top" />
<include layout="@layout/item_feed_top" />
<FrameLayout
android:id="@+id/post_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/top_barrier"
app:layout_constraintBottom_toTopOf="@id/buttons_top_barrier">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/media_list"
@ -39,7 +39,5 @@
android:textColor="@android:color/white" />
</FrameLayout>
<include
android:id="@+id/item_feed_bottom"
layout="@layout/item_feed_bottom" />
</LinearLayout>
<include layout="@layout/layout_post_view_bottom" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,63 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/ivProfilePic"
android:layout_width="@dimen/profile_pic_size_regular"
android:layout_height="@dimen/profile_pic_size_regular"
android:background="?selectableItemBackgroundBorderless"
app:roundAsCircle="true" />
<awais.instagrabber.customviews.ProfilePicView
android:id="@+id/profile_pic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:transitionName="profile_pic"
app:layout_constraintBottom_toTopOf="@id/top_barrier"
app:layout_constraintEnd_toStartOf="@id/title"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:size="regular" />
<RelativeLayout
android:id="@+id/infoContainer"
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:animateLayoutChanges="true"
android:background="@null"
android:gravity="center"
android:orientation="vertical"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@id/subtitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/profile_pic"
app:layout_constraintTop_toTopOf="@id/profile_pic"
tools:text="Username Username Username" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
app:layout_constraintBottom_toBottomOf="@id/profile_pic"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/title"
tools:text="Full name Full name Full name Full name Full name Full name Full name "
tools:visibility="gone" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/top_barrier"
android:layout_width="0dp"
android:layout_height="0dp"
app:barrierAllowsGoneWidgets="true"
app:barrierDirection="bottom"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/location"
style="?borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:elevation="0dp"
android:ellipsize="end"
android:insetTop="0dp"
android:insetBottom="0dp"
android:maxWidth="200dp"
android:maxLines="1"
android:minHeight="32dp"
android:paddingStart="8dp"
android:paddingLeft="8dp"
android:paddingEnd="8dp"
android:paddingRight="8dp"
android:weightSum="2">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
tools:text="username" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/title"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
android:textSize="15sp"
android:visibility="visible"
tools:text="location" />
</RelativeLayout>
<!--<androidx.appcompat.widget.AppCompatImageView-->
<!-- android:id="@+id/viewStoryPost"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="match_parent"-->
<!-- android:layout_gravity="center"-->
<!-- android:background="?selectableItemBackgroundBorderless"-->
<!-- app:srcCompat="@drawable/ic_open_in_new_24"-->
<!-- app:tint="?android:textColorPrimary" />-->
</LinearLayout>
android:textAlignment="viewStart"
android:textAllCaps="false"
android:textColor="@android:color/white"
android:visibility="gone"
app:backgroundTint="@color/black_a50"
app:elevation="0dp"
app:icon="@drawable/ic_round_location_on_24"
app:iconSize="16dp"
app:iconTint="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_barrier"
app:rippleColor="@color/grey_600"
tools:text="Location, Location, Location, Location, "
tools:visibility="visible" />
</merge>

View File

@ -1,19 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/videoHolder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_height="wrap_content">
<include
android:id="@+id/item_feed_top"
layout="@layout/item_feed_top" />
<include layout="@layout/item_feed_top" />
<include
android:id="@+id/video_post"
layout="@layout/layout_video_player_with_thumbnail" />
<FrameLayout
android:id="@+id/post_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/buttons_top_barrier"
app:layout_constraintTop_toBottomOf="@id/top_barrier"
tools:layout_height="100dp" />
<include
android:id="@+id/item_feed_bottom"
layout="@layout/item_feed_bottom" />
</LinearLayout>
<include layout="@layout/layout_post_view_bottom" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,105 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize"
android:animateLayoutChanges="true"
android:orientation="vertical"
android:weightSum="3.2">
<include
android:id="@+id/topPanel"
layout="@layout/item_feed_top" />
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.9">
<awais.instagrabber.customviews.helpers.NestedScrollableHost
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/mediaViewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</awais.instagrabber.customviews.helpers.NestedScrollableHost>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/mediaCounter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|bottom"
android:background="@drawable/rounder_corner_semi_black_bg"
android:gravity="center"
android:padding="5dp"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
android:textColor="@android:color/white"
android:visibility="gone" />
<ProgressBar
android:id="@+id/progressView"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:visibility="gone" />
<!-- Removing for now, will add in later versions -->
<!--<androidx.appcompat.widget.AppCompatImageView-->
<!-- android:id="@+id/ivToggleFullScreen"-->
<!-- android:layout_width="48dp"-->
<!-- android:layout_height="48dp"-->
<!-- android:layout_gravity="end|top"-->
<!-- android:background="?selectableItemBackgroundBorderless"-->
<!-- android:padding="4dp"-->
<!-- app:srcCompat="@drawable/ic_fullscreen"-->
<!-- app:tint="?android:textColorPrimary" />-->
</FrameLayout>
<include
android:id="@+id/bottomPanel"
layout="@layout/item_feed_bottom"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/postActions"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.3"
android:background="#0000"
android:weightSum="2">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnLike"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_weight="1"
android:textColor="@color/btn_lightpink_text_color"
android:textSize="18sp"
app:backgroundTint="@color/btn_lightpink_background"
tools:text="@string/like" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnBookmark"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginLeft="8dp"
android:layout_marginEnd="6dp"
android:layout_marginRight="8dp"
android:layout_weight="1"
android:text="@string/bookmark"
android:textColor="@color/btn_lightorange_text_color"
android:textSize="18sp"
app:backgroundTint="@color/btn_lightorange_background" />
</androidx.appcompat.widget.LinearLayoutCompat>
</LinearLayout>

View File

@ -253,7 +253,6 @@
<string name="action_ayml">Suggested users</string>
<string name="select_picture">Select Picture</string>
<string name="uploading">Uploading…</string>
<string name="activity_count_prefix">You have:</string>
<string name="activity_count_relationship">%d follows</string>
<string name="activity_count_comments">%d comments</string>
<string name="activity_count_commentlikes">%d comment likes</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Usuaris suggerits</string>
<string name="select_picture">Seleccionar imatge</string>
<string name="uploading">S\'està pujant…</string>
<string name="activity_count_prefix">Tens:</string>
<string name="activity_count_relationship">%d seguidors</string>
<string name="activity_count_comments">%d comentaris</string>
<string name="activity_count_commentlikes">%d m\'agrades al comentari</string>

View File

@ -245,7 +245,6 @@
<string name="action_ayml">Navrhovaní uživatelé</string>
<string name="select_picture">Vybrat obrázek</string>
<string name="uploading">Nahrávání…</string>
<string name="activity_count_prefix">Máte:</string>
<string name="activity_count_relationship">%d sleduje</string>
<string name="activity_count_comments">%d komentářů</string>
<string name="activity_count_commentlikes">%d lajků komentáře</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Vorgeschlagene Benutzer</string>
<string name="select_picture">Bild auswählen</string>
<string name="uploading">Hochladen…</string>
<string name="activity_count_prefix">Du hast:</string>
<string name="activity_count_relationship">%d Abonnenten</string>
<string name="activity_count_comments">%d Kommentare</string>
<string name="activity_count_commentlikes">%d gelikte Kommentare</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Προτεινόμενοι χρήστες</string>
<string name="select_picture">Επιλογή εικόνας</string>
<string name="uploading">Μεταφόρτωση…</string>
<string name="activity_count_prefix">Έχετε:</string>
<string name="activity_count_relationship">%d ακόλουθοι</string>
<string name="activity_count_comments">%d σχόλια</string>
<string name="activity_count_commentlikes">Το σχόλιο αρέσει σε %d</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Usuarios sugeridos</string>
<string name="select_picture">Seleccionar imagen</string>
<string name="uploading">Subiendo…</string>
<string name="activity_count_prefix">Tienes:</string>
<string name="activity_count_relationship">%d sigue</string>
<string name="activity_count_comments">%d comentarios</string>
<string name="activity_count_commentlikes">%d me gustas en comentarios</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Iradokitutako erabiltzaileak</string>
<string name="select_picture">Hautatu irudia</string>
<string name="uploading">Igotzen…</string>
<string name="activity_count_prefix">Duzuna:</string>
<string name="activity_count_relationship">%d jarraitzaile</string>
<string name="activity_count_comments">%d iruzkin</string>
<string name="activity_count_commentlikes">%d iruzkin-atsegite</string>

View File

@ -238,7 +238,6 @@
<string name="action_ayml">Suggested users</string>
<string name="select_picture">انتخاب تصویر</string>
<string name="uploading">Uploading…</string>
<string name="activity_count_prefix">شما باید:</string>
<string name="activity_count_relationship">%d دنبال کننده‌</string>
<string name="activity_count_comments">%d دیدگاه</string>
<string name="activity_count_commentlikes">%d پسند دیدگاه</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Utilisateurs suggérés</string>
<string name="select_picture">Sélectionnez une image</string>
<string name="uploading">Envoi en cours…</string>
<string name="activity_count_prefix">Vous avez :</string>
<string name="activity_count_relationship">%d abonné(e)s</string>
<string name="activity_count_comments">%d commentaires</string>
<string name="activity_count_commentlikes">%d j\'aime(s) sur le commentaire</string>

View File

@ -238,7 +238,6 @@
<string name="action_ayml">सुझायें ऊपयोगकर्ता</string>
<string name="select_picture">चित्र का चयन करें</string>
<string name="uploading">अपलोड हो रहा है...</string>
<string name="activity_count_prefix">आपके पास है:</string>
<string name="activity_count_relationship">%d अनुगामी</string>
<string name="activity_count_comments">%d टिप्पणियाँ</string>
<string name="activity_count_commentlikes">%d टिप्पणीयाँ पसन्दीत</string>

View File

@ -233,7 +233,6 @@
<string name="action_ayml">Pengguna yang disarankan</string>
<string name="select_picture">Pilih Gambar</string>
<string name="uploading">Mengunggah…</string>
<string name="activity_count_prefix">Anda memiliki:</string>
<string name="activity_count_relationship">%d mengikuti</string>
<string name="activity_count_comments">%d komentar</string>
<string name="activity_count_commentlikes">%d suka komentar</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Utenti suggeriti</string>
<string name="select_picture">Seleziona Immagine</string>
<string name="uploading">Caricamento…</string>
<string name="activity_count_prefix">Hai:</string>
<string name="activity_count_relationship">%d seguaci</string>
<string name="activity_count_comments">%d commenti</string>
<string name="activity_count_commentlikes">%d mi piace al commento</string>

View File

@ -233,7 +233,6 @@
<string name="action_ayml">おすすめのユーザー</string>
<string name="select_picture">画像を選択</string>
<string name="uploading">アップロード中…</string>
<string name="activity_count_prefix">あなたのステータス:</string>
<string name="activity_count_relationship">%d 人のフォロワー</string>
<string name="activity_count_comments">%d コメント</string>
<string name="activity_count_commentlikes">%d 個のコメントへのいいね!</string>

View File

@ -233,7 +233,6 @@
<string name="action_ayml">프로필 추천</string>
<string name="select_picture">사진 선택</string>
<string name="uploading">업로드 중…</string>
<string name="activity_count_prefix">You have:</string>
<string name="activity_count_relationship">%d follows</string>
<string name="activity_count_comments">%d comments</string>
<string name="activity_count_commentlikes">%d comment likes</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Препорачани кориснчки сметки</string>
<string name="select_picture">Селектирај слика</string>
<string name="uploading">Се Прикачува…</string>
<string name="activity_count_prefix">Вие имате:</string>
<string name="activity_count_relationship">%d следачи</string>
<string name="activity_count_comments">%d коментари</string>
<string name="activity_count_commentlikes">%d лајкови на коментари</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Voorgestelde gebruikers</string>
<string name="select_picture">Selecteer Afbeelding</string>
<string name="uploading">Bezig met uploaden…</string>
<string name="activity_count_prefix">Je hebt:</string>
<string name="activity_count_relationship">%d volgers</string>
<string name="activity_count_comments">%d opmerkingen</string>
<string name="activity_count_commentlikes">%d opmerking-likes</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Suggested users</string>
<string name="select_picture">Select Picture</string>
<string name="uploading">Uploading…</string>
<string name="activity_count_prefix">You have:</string>
<string name="activity_count_relationship">%d follows</string>
<string name="activity_count_comments">%d comments</string>
<string name="activity_count_commentlikes">%d comment likes</string>

View File

@ -245,7 +245,6 @@
<string name="action_ayml">Proponowani użytkownicy</string>
<string name="select_picture">Wybierz obraz</string>
<string name="uploading">Przesyłanie…</string>
<string name="activity_count_prefix">Masz:</string>
<string name="activity_count_relationship">%d obserwujących</string>
<string name="activity_count_comments">%d komentarzy</string>
<string name="activity_count_commentlikes">%d polubionych komentarzy</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Usuários sugeridos</string>
<string name="select_picture">Selecionar imagem</string>
<string name="uploading">Enviando…</string>
<string name="activity_count_prefix">Você tem:</string>
<string name="activity_count_relationship">%d seguidores</string>
<string name="activity_count_comments">%d comentários</string>
<string name="activity_count_commentlikes">%d comentários curtidos</string>

View File

@ -245,7 +245,6 @@
<string name="action_ayml">Предлагаемые пользователи</string>
<string name="select_picture">Выберите изображение</string>
<string name="uploading">Загрузка…</string>
<string name="activity_count_prefix">У вас есть:</string>
<string name="activity_count_relationship">%d подписано</string>
<string name="activity_count_comments">%d комментариев</string>
<string name="activity_count_commentlikes">%d симпатий к комментарию</string>

View File

@ -245,7 +245,6 @@
<string name="action_ayml">Používatelia ktorých možno poznáte</string>
<string name="select_picture">Vybrať fotografiu</string>
<string name="uploading">Nahráva sa…</string>
<string name="activity_count_prefix">Máš:</string>
<string name="activity_count_relationship">%d sledovaní</string>
<string name="activity_count_comments">%d komentárov</string>
<string name="activity_count_commentlikes">%d komentárov ktoré sa niekomu páčia</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Suggested users</string>
<string name="select_picture">Välj bild</string>
<string name="uploading">Laddar upp…</string>
<string name="activity_count_prefix">Du har:</string>
<string name="activity_count_relationship">%d följer</string>
<string name="activity_count_comments">%d kommentarer</string>
<string name="activity_count_commentlikes">%d gillade kommentarer</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Önerilen kullanıcılar</string>
<string name="select_picture">Resim Seç</string>
<string name="uploading">Yükleniyor…</string>
<string name="activity_count_prefix">Sahip olduğun:</string>
<string name="activity_count_relationship">%d takip</string>
<string name="activity_count_comments">%d yorum</string>
<string name="activity_count_commentlikes">%d yorum beğenisi</string>

View File

@ -233,7 +233,6 @@
<string name="action_ayml">Người dùng được đề xuất</string>
<string name="select_picture">Chọn hình ảnh</string>
<string name="uploading">Đang tải lên…</string>
<string name="activity_count_prefix">Bạn có:</string>
<string name="activity_count_relationship">%d người theo dõi</string>
<string name="activity_count_comments">%d bình luận</string>
<string name="activity_count_commentlikes">%d lượt thích bình luận</string>

View File

@ -233,7 +233,6 @@
<string name="action_ayml">推荐用户</string>
<string name="select_picture">选择图片</string>
<string name="uploading">上传中...</string>
<string name="activity_count_prefix">您有:</string>
<string name="activity_count_relationship">%d 位新粉丝</string>
<string name="activity_count_comments">%d 个评论回复</string>
<string name="activity_count_commentlikes">%d 个评论点赞</string>

View File

@ -233,7 +233,6 @@
<string name="action_ayml">推薦用戶</string>
<string name="select_picture">選擇圖片</string>
<string name="uploading">上傳中…</string>
<string name="activity_count_prefix">您有</string>
<string name="activity_count_relationship">%d 個追蹤者</string>
<string name="activity_count_comments">%d 個評論</string>
<string name="activity_count_commentlikes">%d 個評論的讚</string>

View File

@ -242,7 +242,10 @@
<string name="liability" translatable="false">This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.</string>
<string name="select_picture">Select Picture</string>
<string name="uploading">Uploading…</string>
<string name="activity_count_prefix">You have:</string>
<plurals name="activity_count_total">
<item quantity="one">You have %d notification</item>
<item quantity="other">You have %d notifications</item>
</plurals>
<string name="activity_count_relationship">%d follows</string>
<string name="activity_count_comments">%d comments</string>
<string name="activity_count_commentlikes">%d comment likes</string>
@ -486,7 +489,7 @@
<string name="crash_report_subject">Barinsta Crash Report</string>
<string name="crash_report_title">Select an email app to send crash logs</string>
<string name="not_found">Not found!</string>
<string name="rate_limit">Your IP has been rate limited by Instagram. Wait for an hour and try again. &lt;a href=\"https://redd.it/msxlko\">Learn more.&lt;/a></string>
<string name="rate_limit">Your IP has been rate limited by Instagram. &lt;a href=\"https://barinsta.austinhuang.me/en/latest/faq.html#ratelimits\">Learn more.&lt;/a></string>
<string name="skip_update">Skip this update</string>
<string name="on_latest_version">You\'re already on the latest version</string>
<string name="tab_order">Screen order</string>