Merge branch 'master' into feature/multistack-navigation

This commit is contained in:
Ammar Githam 2021-07-11 02:56:24 +09:00
commit d7621e3d82
63 changed files with 750 additions and 909 deletions

View File

@ -139,7 +139,7 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.
return new HeaderViewHolder(LayoutDmHeaderBinding.inflate(layoutInflater, parent, false)); return new HeaderViewHolder(LayoutDmHeaderBinding.inflate(layoutInflater, parent, false));
} }
final LayoutDmBaseBinding baseBinding = LayoutDmBaseBinding.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); final DirectItemViewHolder itemViewHolder = getItemViewHolder(layoutInflater, baseBinding, directItemType);
itemViewHolder.setLongClickListener(longClickListener); itemViewHolder.setLongClickListener(longClickListener);
return itemViewHolder; return itemViewHolder;

View File

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

@ -6,7 +6,6 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.databinding.ItemFollowBinding; import awais.instagrabber.databinding.ItemFollowBinding;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.User;
public final class FollowsViewHolder extends RecyclerView.ViewHolder { public final class FollowsViewHolder extends RecyclerView.ViewHolder {
@ -27,14 +26,4 @@ public final class FollowsViewHolder extends RecyclerView.ViewHolder {
binding.fullName.setText(model.getFullName()); binding.fullName.setText(model.getFullName());
binding.profilePic.setImageURI(model.getProfilePicUrl()); 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

@ -9,7 +9,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import awais.instagrabber.R; import awais.instagrabber.R;
@ -94,7 +93,7 @@ public class ChatMessageLayout extends FrameLayout {
heightSize += viewPartMainHeight; heightSize += viewPartMainHeight;
} else if (firstChildId == R.id.raven_media_container || firstChildId == R.id.profile_container || firstChildId == R.id.voice_media } 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.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; widthSize += viewPartMainWidth;
heightSize += viewPartMainHeight + viewPartInfoHeight; heightSize += viewPartMainHeight + viewPartInfoHeight;
} else { } else {
@ -104,12 +103,6 @@ public class ChatMessageLayout extends FrameLayout {
if (firstChild instanceof TextView) { if (firstChild instanceof TextView) {
textMessage = (TextView) firstChild; 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; else textMessage = null;
if (textMessage != null) { if (textMessage != null) {
viewPartMainLineCount = textMessage.getLineCount(); 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.Media;
import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.saved.SavedCollection; import awais.instagrabber.repositories.responses.saved.SavedCollection;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.DownloadUtils;
@ -461,7 +462,9 @@ public class CollectionPostsFragment extends Fragment implements SwipeRefreshLay
} }
private void updateSwipeRefreshState() { 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) { 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,233 @@
package awais.instagrabber.fragments
import android.os.Bundle
import android.view.*
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.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
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.viewmodels.FollowViewModel
import thoughtbot.expandableadapter.ExpandableGroup
import java.util.*
class FollowViewerFragment : Fragment(), SwipeRefreshLayout.OnRefreshListener {
private var isFollowersList = false
private var isCompare = false
private var shouldRefresh = true
private var searching = false
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.isEmpty()) {
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.isNotEmpty()) groups.add(
ExpandableGroup(
getString(
R.string.followers_not_following,
username
), followingModels
)
)
if (followersModels.isNotEmpty()) groups.add(
ExpandableGroup(
getString(
R.string.followers_not_follower,
namePost
), followersModels
)
)
if (allFollowing.isNotEmpty()) 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) {
findNavController().navigate(FollowViewerFragmentDirections.actionToProfile().setUsername(tag.username))
}
}, groups).also {
it.toggleGroup(0)
binding.rvFollow.adapter = it
}
}
}

View File

@ -390,7 +390,6 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
} }
setTitle(); setTitle();
setupPosts(); setupPosts();
// fetchStories();
if (isLoggedIn) { if (isLoggedIn) {
hashtagDetailsBinding.btnFollowTag.setVisibility(View.VISIBLE); hashtagDetailsBinding.btnFollowTag.setVisibility(View.VISIBLE);
hashtagDetailsBinding.btnFollowTag.setText(hashtagModel.getFollowing() == FollowingType.FOLLOWING hashtagDetailsBinding.btnFollowTag.setText(hashtagModel.getFollowing() == FollowingType.FOLLOWING
@ -559,7 +558,9 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
} }
private void updateSwipeRefreshState() { 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) { private void navigateToProfile(final String username) {

View File

@ -61,7 +61,6 @@ import awais.instagrabber.utils.Utils;
import awais.instagrabber.webservices.GraphQLRepository; import awais.instagrabber.webservices.GraphQLRepository;
import awais.instagrabber.webservices.LocationService; import awais.instagrabber.webservices.LocationService;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.StoriesRepository;
import kotlinx.coroutines.Dispatchers; import kotlinx.coroutines.Dispatchers;
import static awais.instagrabber.utils.Utils.settingsHelper; import static awais.instagrabber.utils.Utils.settingsHelper;
@ -78,11 +77,11 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
private long locationId; private long locationId;
private Location locationModel; private Location locationModel;
private ActionMode actionMode; private ActionMode actionMode;
private StoriesRepository storiesRepository; // private StoriesRepository storiesRepository;
private GraphQLRepository graphQLRepository; private GraphQLRepository graphQLRepository;
private LocationService locationService; private LocationService locationService;
private boolean isLoggedIn; private boolean isLoggedIn;
private boolean storiesFetching; // private boolean storiesFetching;
private Set<Media> selectedFeedModels; private Set<Media> selectedFeedModels;
private PostsLayoutPreferences layoutPreferences = Utils.getPostsLayoutPreferences(Constants.PREF_LOCATION_POSTS_LAYOUT); private PostsLayoutPreferences layoutPreferences = Utils.getPostsLayoutPreferences(Constants.PREF_LOCATION_POSTS_LAYOUT);
private LayoutLocationDetailsBinding locationDetailsBinding; private LayoutLocationDetailsBinding locationDetailsBinding;
@ -280,7 +279,7 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
final String cookie = settingsHelper.getString(Constants.COOKIE); final String cookie = settingsHelper.getString(Constants.COOKIE);
isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0;
locationService = isLoggedIn ? LocationService.getInstance() : null; locationService = isLoggedIn ? LocationService.getInstance() : null;
storiesRepository = StoriesRepository.Companion.getInstance(); // storiesRepository = StoriesRepository.Companion.getInstance();
graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance(); graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
setHasOptionsMenu(true); setHasOptionsMenu(true);
} }
@ -584,7 +583,9 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
} }
private void updateSwipeRefreshState() { 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) { private void navigateToProfile(final String username) {

View File

@ -758,15 +758,19 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme
popupMenu.setOnMenuItemClickListener(item -> { popupMenu.setOnMenuItemClickListener(item -> {
final int itemId = item.getItemId(); final int itemId = item.getItemId();
if (itemId == R.id.share_dm) { if (itemId == R.id.share_dm) {
if (profileModel.isPrivate()) {
Toast.makeText(context, R.string.share_private_post, Toast.LENGTH_SHORT).show();
}
final NavDirections actionGlobalUserSearch = PostViewV2FragmentDirections
.actionToUserSearch()
.setTitle(getString(R.string.share))
.setActionLabel(getString(R.string.send))
.setShowGroups(true)
.setMultiple(true)
.setSearchMode(UserSearchMode.RAVEN);
final NavController navController = NavHostFragment.findNavController(PostViewV2Fragment.this);
try { try {
final NavDirections actionGlobalUserSearch = PostViewV2FragmentDirections navController.navigate(actionGlobalUserSearch);
.actionToUserSearch()
.setTitle(getString(R.string.share))
.setActionLabel(getString(R.string.send))
.setShowGroups(true)
.setMultiple(true)
.setSearchMode(UserSearchMode.RAVEN);
NavHostFragment.findNavController(this).navigate(actionGlobalUserSearch);
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "setupShare: ", e); Log.e(TAG, "setupShare: ", e);
} }

View File

@ -39,6 +39,7 @@ import awais.instagrabber.models.enums.PostItemType;
import awais.instagrabber.repositories.responses.Location; import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.DownloadUtils;
@ -327,7 +328,9 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL
} }
private void updateSwipeRefreshState() { 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) { private void navigateToProfile(final String username) {

View File

@ -18,9 +18,7 @@ import androidx.appcompat.widget.PopupMenu
import androidx.core.view.GestureDetectorCompat import androidx.core.view.GestureDetectorCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController import androidx.navigation.NavController
@ -40,9 +38,7 @@ import awais.instagrabber.models.enums.StoryPaginationType
import awais.instagrabber.repositories.requests.StoryViewerOptions import awais.instagrabber.repositories.requests.StoryViewerOptions
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
import awais.instagrabber.repositories.responses.stories.* import awais.instagrabber.repositories.responses.stories.*
import awais.instagrabber.utils.Constants
import awais.instagrabber.utils.DownloadUtils.download import awais.instagrabber.utils.DownloadUtils.download
import awais.instagrabber.utils.TextUtils.epochSecondToString
import awais.instagrabber.utils.ResponseBodyUtils import awais.instagrabber.utils.ResponseBodyUtils
import awais.instagrabber.utils.Utils import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.extensions.TAG import awais.instagrabber.utils.extensions.TAG
@ -78,7 +74,6 @@ class StoryViewerFragment : Fragment() {
private var gestureDetector: GestureDetectorCompat? = null private var gestureDetector: GestureDetectorCompat? = null
private val storiesRepository: StoriesRepository? = null private val storiesRepository: StoriesRepository? = null
private val mediaRepository: MediaRepository? = null private val mediaRepository: MediaRepository? = null
private var live: Broadcast? = null
private var menuProfile: MenuItem? = null private var menuProfile: MenuItem? = null
private var profileVisible: Boolean = false private var profileVisible: Boolean = false
private var player: SimpleExoPlayer? = null private var player: SimpleExoPlayer? = null
@ -218,7 +213,7 @@ class StoryViewerFragment : Fragment() {
} }
binding.storiesList.adapter = storiesAdapter binding.storiesList.adapter = storiesAdapter
storiesViewModel.getCurrentStory().observe(fragmentActivity, { storiesViewModel.getCurrentStory().observe(fragmentActivity, {
if (it?.items != null) { if (it?.items != null && it.items.size > 1) {
val storyMedias = it.items.toMutableList() val storyMedias = it.items.toMutableList()
val newItem = storyMedias.get(0) val newItem = storyMedias.get(0)
newItem.isCurrentSlide = true newItem.isCurrentSlide = true
@ -230,6 +225,7 @@ class StoryViewerFragment : Fragment() {
else View.GONE else View.GONE
} }
else { else {
if (it?.items != null) storiesViewModel.setMedia(0)
binding.listToggle.isEnabled = false binding.listToggle.isEnabled = false
binding.storiesList.visibility = View.GONE binding.storiesList.visibility = View.GONE
} }
@ -266,65 +262,28 @@ class StoryViewerFragment : Fragment() {
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun setupListeners() { private fun setupListeners() {
var liveModels: LiveData<List<Story>?>? = null
if (currentFeedStoryIndex >= 0) { if (currentFeedStoryIndex >= 0) {
val type = options!!.type val type = options!!.type
when (type) { when (type) {
StoryViewerOptions.Type.HIGHLIGHT -> { StoryViewerOptions.Type.HIGHLIGHT -> {
storiesViewModel.fetchHighlights(options!!.id) storiesViewModel.fetchHighlights(options!!.id)
liveModels = storiesViewModel.getHighlights() storiesViewModel.highlights.observe(fragmentActivity) {
setupMultipage(it)
}
} }
StoryViewerOptions.Type.FEED_STORY_POSITION -> { StoryViewerOptions.Type.FEED_STORY_POSITION -> {
val feedStoriesViewModel = listViewModel as FeedStoriesViewModel? val feedStoriesViewModel = listViewModel as FeedStoriesViewModel?
liveModels = feedStoriesViewModel!!.list setupMultipage(feedStoriesViewModel!!.list.value)
} }
StoryViewerOptions.Type.STORY_ARCHIVE -> { StoryViewerOptions.Type.STORY_ARCHIVE -> {
val archivesViewModel = listViewModel as ArchivesViewModel? val archivesViewModel = listViewModel as ArchivesViewModel?
liveModels = archivesViewModel!!.list setupMultipage(archivesViewModel!!.list.value)
} }
StoryViewerOptions.Type.USER -> { StoryViewerOptions.Type.USER -> {
resetView() 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 val context = context ?: return
swipeEvent = SwipeEvent { isRightSwipe: Boolean -> swipeEvent = SwipeEvent { isRightSwipe: Boolean ->
@ -357,9 +316,46 @@ class StoryViewerFragment : Fragment() {
binding.imageViewer.setTapListener(simpleOnGestureListener) 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() { private fun resetView() {
val context = context ?: return val context = context ?: return
live = null
if (menuProfile != null) menuProfile!!.isVisible = false if (menuProfile != null) menuProfile!!.isVisible = false
binding.imageViewer.controller = null binding.imageViewer.controller = null
releasePlayer() releasePlayer()
@ -367,7 +363,7 @@ class StoryViewerFragment : Fragment() {
var fetchOptions: StoryViewerOptions? = null var fetchOptions: StoryViewerOptions? = null
when (type) { when (type) {
StoryViewerOptions.Type.HIGHLIGHT -> { StoryViewerOptions.Type.HIGHLIGHT -> {
val models = storiesViewModel.getHighlights().value val models = storiesViewModel.highlights.value
if (models == null || models.isEmpty() || currentFeedStoryIndex >= models.size || currentFeedStoryIndex < 0) { if (models == null || models.isEmpty() || currentFeedStoryIndex >= models.size || currentFeedStoryIndex < 0) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show()
return return
@ -378,10 +374,15 @@ class StoryViewerFragment : Fragment() {
val feedStoriesViewModel = listViewModel as FeedStoriesViewModel? val feedStoriesViewModel = listViewModel as FeedStoriesViewModel?
val models = feedStoriesViewModel!!.list.value val models = feedStoriesViewModel!!.list.value
if (models == null || currentFeedStoryIndex >= models.size || currentFeedStoryIndex < 0) return if (models == null || currentFeedStoryIndex >= models.size || currentFeedStoryIndex < 0) return
val (_, _, _, _, user, _, _, _, _, _, _, broadcast) = models[currentFeedStoryIndex] val userStory = models[currentFeedStoryIndex]
currentStoryUsername = user!!.username currentStoryUsername = userStory.user!!.username
fetchOptions = StoryViewerOptions.forUser(user.pk, currentStoryUsername) fetchOptions = StoryViewerOptions.forUser(userStory.user.pk, currentStoryUsername)
live = broadcast val live = userStory.broadcast
if (live != null) {
storiesViewModel.setStory(userStory)
refreshLive(live)
return
}
} }
StoryViewerOptions.Type.STORY_ARCHIVE -> { StoryViewerOptions.Type.STORY_ARCHIVE -> {
val archivesViewModel = listViewModel as ArchivesViewModel? val archivesViewModel = listViewModel as ArchivesViewModel?
@ -404,11 +405,7 @@ class StoryViewerFragment : Fragment() {
storiesViewModel.fetchSingleMedia(options!!.id) storiesViewModel.fetchSingleMedia(options!!.id)
return return
} }
if (live != null) { storiesViewModel.fetchStory(fetchOptions).observe(viewLifecycleOwner, {
refreshLive()
return
}
storiesViewModel.fetchStory(fetchOptions).observe(fragmentActivity, {
if (it.status == Resource.Status.ERROR) { if (it.status == Resource.Status.ERROR) {
Toast.makeText(context, "Error: " + it.message, Toast.LENGTH_SHORT).show() Toast.makeText(context, "Error: " + it.message, Toast.LENGTH_SHORT).show()
} }
@ -416,18 +413,14 @@ class StoryViewerFragment : Fragment() {
} }
@Synchronized @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() releasePlayer()
setupLive(live!!.dashPlaybackUrl ?: live!!.dashAbrPlaybackUrl ?: return) 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)
}
}
} }
@Synchronized @Synchronized
@ -446,14 +439,19 @@ class StoryViewerFragment : Fragment() {
binding.btnReply.isEnabled = currentStory.canReply binding.btnReply.isEnabled = currentStory.canReply
if (itemType === MediaItemType.MEDIA_TYPE_VIDEO) setupVideo(url) else setupImage(url) if (itemType === MediaItemType.MEDIA_TYPE_VIDEO) setupVideo(url) else setupImage(url)
// if (Utils.settingsHelper.getBoolean(MARK_AS_SEEN)) storiesRepository!!.seen( if (options!!.type == StoryViewerOptions.Type.FEED_STORY_POSITION
// csrfToken, && Utils.settingsHelper.getBoolean(PreferenceKeys.MARK_AS_SEEN)) {
// userId, val feedStoriesViewModel = listViewModel as FeedStoriesViewModel?
// deviceId, storiesViewModel.markAsSeen(currentStory).observe(viewLifecycleOwner) { m ->
// currentStory!!.id!!, if (m.status == Resource.Status.SUCCESS && m.data != null) {
// currentStory!!.takenAt, val liveModels: MutableLiveData<List<Story>> = feedStoriesViewModel!!.list
// System.currentTimeMillis() / 1000 val models = liveModels.value
// ) val modelsCopy: MutableList<Story> = models!!.toMutableList()
modelsCopy.set(currentFeedStoryIndex, m.data)
liveModels.postValue(modelsCopy)
}
}
}
} }
private fun downloadStory() { private fun downloadStory() {
@ -797,6 +795,13 @@ class StoryViewerFragment : Fragment() {
} }
private fun shareStoryViaDm() { 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 { val actionGlobalUserSearch = StoryViewerFragmentDirections.actionToUserSearch().apply {
title = getString(R.string.share) title = getString(R.string.share)
actionLabel = getString(R.string.send) 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.Media;
import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.discover.TopicCluster; import awais.instagrabber.repositories.responses.discover.TopicCluster;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.ResponseBodyUtils; import awais.instagrabber.utils.ResponseBodyUtils;
@ -377,7 +378,9 @@ public class TopicPostsFragment extends Fragment implements SwipeRefreshLayout.O
} }
private void updateSwipeRefreshState() { 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) { private void navigateToProfile(final String username) {

View File

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

View File

@ -875,7 +875,11 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall
.setLifeCycleOwner(this) .setLifeCycleOwner(this)
.setPostFetchService(ProfilePostFetchService(profile, currentUser != null)) .setPostFetchService(ProfilePostFetchService(profile, currentUser != null))
.setLayoutPreferences(layoutPreferences) .setLayoutPreferences(layoutPreferences)
.addFetchStatusChangeListener { binding.swipeRefreshLayout.isRefreshing = it } .addFetchStatusChangeListener {
AppExecutors.mainThread.execute {
binding.swipeRefreshLayout.isRefreshing = it
}
}
.setFeedItemCallback(feedItemCallback) .setFeedItemCallback(feedItemCallback)
.setSelectionModeCallback(selectionModeCallback) .setSelectionModeCallback(selectionModeCallback)
.init() .init()

View File

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

View File

@ -104,7 +104,7 @@ public class DownloadsPreferencesFragment extends BasePreferencesFragment {
Utils.setupSelectedDir(context, data); Utils.setupSelectedDir(context, data);
String path; String path;
try { try {
path = URLDecoder.decode(data.getData().toString(), StandardCharsets.UTF_8.toString()); path = URLDecoder.decode(data.getData().toString(), StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
path = data.getData().toString(); 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() private val map: MutableMap<Int, DirectItemType> = mutableMapOf()
@JvmStatic @JvmStatic
fun getId(id: Int): DirectItemType? { fun getTypeFromId(id: Int): DirectItemType {
return map[id] return map[id] ?: UNKNOWN
} }
fun getName(directItemType: DirectItemType): String? { fun getName(directItemType: DirectItemType): String? {

View File

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

View File

@ -1,27 +1,10 @@
package awais.instagrabber.repositories.responses package awais.instagrabber.repositories.responses
import awais.instagrabber.models.FollowModel
data class FriendshipListFetchResponse( data class FriendshipListFetchResponse(
var nextMaxId: String?, var nextMaxId: String?,
var status: String?, var status: String?,
var items: List<FollowModel>? var users: List<User>?
) { ) {
val isMoreAvailable: Boolean val isMoreAvailable: Boolean
get() = !nextMaxId.isNullOrBlank() 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 package awais.instagrabber.repositories.responses.notification
class NotificationCounts(val commentLikesCount: Int, class NotificationCounts(val commentLikes: Int,
val userTagsCount: Int, val usertags: Int,
val likesCount: Int, val likes: Int,
val commentsCount: Int, val comments: Int,
val relationshipsCount: Int, val relationships: Int,
val pOYCount: Int, val photosOfYou: Int,
val requestsCount: Int) val requests: Int)

View File

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

View File

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

View File

@ -15,48 +15,23 @@ import java.io.FileDescriptor;
public final class MediaUtils { public final class MediaUtils {
private static final String TAG = MediaUtils.class.getSimpleName(); 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, public static void getVideoInfo(@NonNull final ContentResolver contentResolver,
@NonNull final Uri uri, @NonNull final Uri uri,
@NonNull final OnInfoLoadListener<VideoInfo> listener) { @NonNull final OnInfoLoadListener<VideoInfo> listener) {
AppExecutors.INSTANCE.getTasksThread().submit(() -> { getInfo(contentResolver, uri, listener, true);
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);
}
});
} }
public static void getVoiceInfo(@NonNull final ContentResolver contentResolver, public static void getVoiceInfo(@NonNull final ContentResolver contentResolver,
@NonNull final Uri uri, @NonNull final Uri uri,
@NonNull final OnInfoLoadListener<VideoInfo> listener) { @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(() -> { AppExecutors.INSTANCE.getTasksThread().submit(() -> {
try (ParcelFileDescriptor parcelFileDescriptor = contentResolver.openFileDescriptor(uri, "r")) { try (ParcelFileDescriptor parcelFileDescriptor = contentResolver.openFileDescriptor(uri, "r")) {
if (parcelFileDescriptor == null) { if (parcelFileDescriptor == null) {
@ -68,6 +43,23 @@ public final class MediaUtils {
mediaMetadataRetriever.setDataSource(fileDescriptor); mediaMetadataRetriever.setDataSource(fileDescriptor);
String duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); String duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
if (TextUtils.isEmpty(duration)) duration = "0"; 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( listener.onLoad(new VideoInfo(
Long.parseLong(duration), Long.parseLong(duration),
0, 0,
@ -75,7 +67,7 @@ public final class MediaUtils {
0 0
)); ));
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "getVoiceInfo: ", e); Log.e(TAG, "getInfo: ", e);
listener.onFailure(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,163 @@
package awais.instagrabber.viewmodels
import androidx.lifecycle.*
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

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

View File

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

View File

@ -7,19 +7,22 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import awais.instagrabber.R import awais.instagrabber.R
import awais.instagrabber.managers.DirectMessagesManager import awais.instagrabber.managers.DirectMessagesManager
import awais.instagrabber.models.enums.FavoriteType
import awais.instagrabber.models.enums.MediaItemType
import awais.instagrabber.models.enums.StoryPaginationType
import awais.instagrabber.models.Resource import awais.instagrabber.models.Resource
import awais.instagrabber.models.Resource.Companion.error import awais.instagrabber.models.Resource.Companion.error
import awais.instagrabber.models.Resource.Companion.loading import awais.instagrabber.models.Resource.Companion.loading
import awais.instagrabber.models.Resource.Companion.success import awais.instagrabber.models.Resource.Companion.success
import awais.instagrabber.models.enums.BroadcastItemType import awais.instagrabber.models.enums.BroadcastItemType
import awais.instagrabber.models.enums.FavoriteType
import awais.instagrabber.models.enums.MediaItemType
import awais.instagrabber.models.enums.StoryPaginationType
import awais.instagrabber.repositories.requests.StoryViewerOptions import awais.instagrabber.repositories.requests.StoryViewerOptions
import awais.instagrabber.repositories.responses.Media
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
import awais.instagrabber.repositories.responses.stories.* import awais.instagrabber.repositories.responses.stories.*
import awais.instagrabber.repositories.responses.Media import awais.instagrabber.utils.Constants
import awais.instagrabber.utils.* import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.getCsrfTokenFromCookie
import awais.instagrabber.utils.getUserIdFromCookie
import awais.instagrabber.webservices.MediaRepository import awais.instagrabber.webservices.MediaRepository
import awais.instagrabber.webservices.StoriesRepository import awais.instagrabber.webservices.StoriesRepository
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
@ -60,21 +63,22 @@ class StoryFragmentViewModel : ViewModel() {
private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() } private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() }
// for highlights ONLY // for highlights ONLY
private val highlights = MutableLiveData<List<Story>?>() val highlights = MutableLiveData<List<Story>?>()
/* set functions */ /* set functions */
fun setStory(story: Story) { fun setStory(story: Story) {
if (story.items == null || story.items.size == 0) {
pagination.postValue(StoryPaginationType.ERROR)
return
}
currentStory.postValue(story) currentStory.postValue(story)
storyTitle.postValue(story.title ?: story.user?.username) storyTitle.postValue(story.title ?: story.user?.username)
if (story.broadcast != null) { if (story.broadcast != null) {
date.postValue(story.dateTime) date.postValue(story.dateTime)
type.postValue(MediaItemType.MEDIA_TYPE_LIVE) type.postValue(MediaItemType.MEDIA_TYPE_LIVE)
pagination.postValue(StoryPaginationType.DO_NOTHING) pagination.postValue(StoryPaginationType.DO_NOTHING)
return
}
if (story.items == null || story.items.size == 0) {
pagination.postValue(StoryPaginationType.ERROR)
return
} }
} }
@ -184,10 +188,6 @@ class StoryFragmentViewModel : ViewModel() {
/* get functions */ /* get functions */
fun getHighlights(): LiveData<List<Story>?> {
return highlights
}
fun getCurrentStory(): LiveData<Story?> { fun getCurrentStory(): LiveData<Story?> {
return currentStory return currentStory
} }
@ -472,4 +472,28 @@ class StoryFragmentViewModel : ViewModel() {
} }
return data 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 package awais.instagrabber.webservices
import awais.instagrabber.models.FollowModel
import awais.instagrabber.repositories.FriendshipService import awais.instagrabber.repositories.FriendshipService
import awais.instagrabber.repositories.responses.FriendshipChangeResponse import awais.instagrabber.repositories.responses.FriendshipChangeResponse
import awais.instagrabber.repositories.responses.FriendshipListFetchResponse import awais.instagrabber.repositories.responses.FriendshipListFetchResponse
import awais.instagrabber.repositories.responses.FriendshipRestrictResponse import awais.instagrabber.repositories.responses.FriendshipRestrictResponse
import awais.instagrabber.utils.Utils import awais.instagrabber.utils.Utils
import awais.instagrabber.webservices.RetrofitFactory.retrofit import awais.instagrabber.webservices.RetrofitFactory.retrofit
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
class FriendshipRepository(private val service: FriendshipService) { class FriendshipRepository(private val service: FriendshipService) {
@ -113,43 +109,12 @@ class FriendshipRepository(private val service: FriendshipService) {
follower: Boolean, follower: Boolean,
targetUserId: Long, targetUserId: Long,
maxId: String?, maxId: String?,
query: String?
): FriendshipListFetchResponse { ): FriendshipListFetchResponse {
val queryMap = if (maxId != null) mapOf("max_id" to maxId) else emptyMap() val queryMap: MutableMap<String, String> = mutableMapOf()
val response = service.getList(targetUserId, if (follower) "followers" else "following", queryMap) if (!maxId.isNullOrEmpty()) queryMap.set("max_id", maxId)
return parseListResponse(response) if (!query.isNullOrEmpty()) queryMap.set("query", query)
} return service.getList(targetUserId, if (follower) "followers" else "following", queryMap.toMap())
@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
} }
companion object { companion object {

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package thoughtbot.expandableadapter; package thoughtbot.expandableadapter;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
@ -15,6 +16,13 @@ public final class ExpandableList {
this.expandedGroupIndexes = new boolean[groupsSize]; 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() { public int getVisibleItemCount() {
int count = 0; int count = 0;
for (int i = 0; i < groupsSize; i++) count = count + numberOfVisibleItemsInGroup(i); 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) { 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) { public int getFlattenedGroupIndex(@NonNull final ExpandableListPosition listPosition) {

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.swiperefreshlayout.widget.SwipeRefreshLayout 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" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/swipeRefreshLayout" android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -16,6 +15,5 @@
android:paddingLeft="8dp" android:paddingLeft="8dp"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingRight="8dp" android:paddingRight="8dp"
app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/item_follow" /> tools:listitem="@layout/item_follow" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -253,7 +253,6 @@
<string name="action_ayml">Suggested users</string> <string name="action_ayml">Suggested users</string>
<string name="select_picture">Select Picture</string> <string name="select_picture">Select Picture</string>
<string name="uploading">Uploading…</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_relationship">%d follows</string>
<string name="activity_count_comments">%d comments</string> <string name="activity_count_comments">%d comments</string>
<string name="activity_count_commentlikes">%d comment likes</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="action_ayml">Usuaris suggerits</string>
<string name="select_picture">Seleccionar imatge</string> <string name="select_picture">Seleccionar imatge</string>
<string name="uploading">S\'està pujant…</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_relationship">%d seguidors</string>
<string name="activity_count_comments">%d comentaris</string> <string name="activity_count_comments">%d comentaris</string>
<string name="activity_count_commentlikes">%d m\'agrades al comentari</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="action_ayml">Navrhovaní uživatelé</string>
<string name="select_picture">Vybrat obrázek</string> <string name="select_picture">Vybrat obrázek</string>
<string name="uploading">Nahrávání…</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_relationship">%d sleduje</string>
<string name="activity_count_comments">%d komentářů</string> <string name="activity_count_comments">%d komentářů</string>
<string name="activity_count_commentlikes">%d lajků komentáře</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="action_ayml">Vorgeschlagene Benutzer</string>
<string name="select_picture">Bild auswählen</string> <string name="select_picture">Bild auswählen</string>
<string name="uploading">Hochladen…</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_relationship">%d Abonnenten</string>
<string name="activity_count_comments">%d Kommentare</string> <string name="activity_count_comments">%d Kommentare</string>
<string name="activity_count_commentlikes">%d gelikte Kommentare</string> <string name="activity_count_commentlikes">%d gelikte Kommentare</string>

View File

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

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Usuarios sugeridos</string> <string name="action_ayml">Usuarios sugeridos</string>
<string name="select_picture">Seleccionar imagen</string> <string name="select_picture">Seleccionar imagen</string>
<string name="uploading">Subiendo…</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_relationship">%d sigue</string>
<string name="activity_count_comments">%d comentarios</string> <string name="activity_count_comments">%d comentarios</string>
<string name="activity_count_commentlikes">%d me gustas en 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="action_ayml">Iradokitutako erabiltzaileak</string>
<string name="select_picture">Hautatu irudia</string> <string name="select_picture">Hautatu irudia</string>
<string name="uploading">Igotzen…</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_relationship">%d jarraitzaile</string>
<string name="activity_count_comments">%d iruzkin</string> <string name="activity_count_comments">%d iruzkin</string>
<string name="activity_count_commentlikes">%d iruzkin-atsegite</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="action_ayml">Suggested users</string>
<string name="select_picture">انتخاب تصویر</string> <string name="select_picture">انتخاب تصویر</string>
<string name="uploading">Uploading…</string> <string name="uploading">Uploading…</string>
<string name="activity_count_prefix">شما باید:</string>
<string name="activity_count_relationship">%d دنبال کننده‌</string> <string name="activity_count_relationship">%d دنبال کننده‌</string>
<string name="activity_count_comments">%d دیدگاه</string> <string name="activity_count_comments">%d دیدگاه</string>
<string name="activity_count_commentlikes">%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="action_ayml">Utilisateurs suggérés</string>
<string name="select_picture">Sélectionnez une image</string> <string name="select_picture">Sélectionnez une image</string>
<string name="uploading">Envoi en cours…</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_relationship">%d abonné(e)s</string>
<string name="activity_count_comments">%d commentaires</string> <string name="activity_count_comments">%d commentaires</string>
<string name="activity_count_commentlikes">%d j\'aime(s) sur le commentaire</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="action_ayml">सुझायें ऊपयोगकर्ता</string>
<string name="select_picture">चित्र का चयन करें</string> <string name="select_picture">चित्र का चयन करें</string>
<string name="uploading">अपलोड हो रहा है...</string> <string name="uploading">अपलोड हो रहा है...</string>
<string name="activity_count_prefix">आपके पास है:</string>
<string name="activity_count_relationship">%d अनुगामी</string> <string name="activity_count_relationship">%d अनुगामी</string>
<string name="activity_count_comments">%d टिप्पणियाँ</string> <string name="activity_count_comments">%d टिप्पणियाँ</string>
<string name="activity_count_commentlikes">%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="action_ayml">Pengguna yang disarankan</string>
<string name="select_picture">Pilih Gambar</string> <string name="select_picture">Pilih Gambar</string>
<string name="uploading">Mengunggah…</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_relationship">%d mengikuti</string>
<string name="activity_count_comments">%d komentar</string> <string name="activity_count_comments">%d komentar</string>
<string name="activity_count_commentlikes">%d suka 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="action_ayml">Utenti suggeriti</string>
<string name="select_picture">Seleziona Immagine</string> <string name="select_picture">Seleziona Immagine</string>
<string name="uploading">Caricamento…</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_relationship">%d seguaci</string>
<string name="activity_count_comments">%d commenti</string> <string name="activity_count_comments">%d commenti</string>
<string name="activity_count_commentlikes">%d mi piace al commento</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="action_ayml">おすすめのユーザー</string>
<string name="select_picture">画像を選択</string> <string name="select_picture">画像を選択</string>
<string name="uploading">アップロード中…</string> <string name="uploading">アップロード中…</string>
<string name="activity_count_prefix">あなたのステータス:</string>
<string name="activity_count_relationship">%d 人のフォロワー</string> <string name="activity_count_relationship">%d 人のフォロワー</string>
<string name="activity_count_comments">%d コメント</string> <string name="activity_count_comments">%d コメント</string>
<string name="activity_count_commentlikes">%d 個のコメントへのいいね!</string> <string name="activity_count_commentlikes">%d 個のコメントへのいいね!</string>

View File

@ -233,7 +233,6 @@
<string name="action_ayml">프로필 추천</string> <string name="action_ayml">프로필 추천</string>
<string name="select_picture">사진 선택</string> <string name="select_picture">사진 선택</string>
<string name="uploading">업로드 중…</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_relationship">%d follows</string>
<string name="activity_count_comments">%d comments</string> <string name="activity_count_comments">%d comments</string>
<string name="activity_count_commentlikes">%d comment likes</string> <string name="activity_count_commentlikes">%d comment likes</string>

View File

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

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Voorgestelde gebruikers</string> <string name="action_ayml">Voorgestelde gebruikers</string>
<string name="select_picture">Selecteer Afbeelding</string> <string name="select_picture">Selecteer Afbeelding</string>
<string name="uploading">Bezig met uploaden…</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_relationship">%d volgers</string>
<string name="activity_count_comments">%d opmerkingen</string> <string name="activity_count_comments">%d opmerkingen</string>
<string name="activity_count_commentlikes">%d opmerking-likes</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="action_ayml">Suggested users</string>
<string name="select_picture">Select Picture</string> <string name="select_picture">Select Picture</string>
<string name="uploading">Uploading…</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_relationship">%d follows</string>
<string name="activity_count_comments">%d comments</string> <string name="activity_count_comments">%d comments</string>
<string name="activity_count_commentlikes">%d comment likes</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="action_ayml">Proponowani użytkownicy</string>
<string name="select_picture">Wybierz obraz</string> <string name="select_picture">Wybierz obraz</string>
<string name="uploading">Przesyłanie…</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_relationship">%d obserwujących</string>
<string name="activity_count_comments">%d komentarzy</string> <string name="activity_count_comments">%d komentarzy</string>
<string name="activity_count_commentlikes">%d polubionych 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="action_ayml">Usuários sugeridos</string>
<string name="select_picture">Selecionar imagem</string> <string name="select_picture">Selecionar imagem</string>
<string name="uploading">Enviando…</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_relationship">%d seguidores</string>
<string name="activity_count_comments">%d comentários</string> <string name="activity_count_comments">%d comentários</string>
<string name="activity_count_commentlikes">%d comentários curtidos</string> <string name="activity_count_commentlikes">%d comentários curtidos</string>

View File

@ -245,7 +245,6 @@
<string name="action_ayml">Предлагаемые пользователи</string> <string name="action_ayml">Предлагаемые пользователи</string>
<string name="select_picture">Выберите изображение</string> <string name="select_picture">Выберите изображение</string>
<string name="uploading">Загрузка…</string> <string name="uploading">Загрузка…</string>
<string name="activity_count_prefix">У вас есть:</string>
<string name="activity_count_relationship">%d подписано</string> <string name="activity_count_relationship">%d подписано</string>
<string name="activity_count_comments">%d комментариев</string> <string name="activity_count_comments">%d комментариев</string>
<string name="activity_count_commentlikes">%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="action_ayml">Používatelia ktorých možno poznáte</string>
<string name="select_picture">Vybrať fotografiu</string> <string name="select_picture">Vybrať fotografiu</string>
<string name="uploading">Nahráva sa…</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_relationship">%d sledovaní</string>
<string name="activity_count_comments">%d komentárov</string> <string name="activity_count_comments">%d komentárov</string>
<string name="activity_count_commentlikes">%d komentárov ktoré sa niekomu páčia</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="action_ayml">Suggested users</string>
<string name="select_picture">Välj bild</string> <string name="select_picture">Välj bild</string>
<string name="uploading">Laddar upp…</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_relationship">%d följer</string>
<string name="activity_count_comments">%d kommentarer</string> <string name="activity_count_comments">%d kommentarer</string>
<string name="activity_count_commentlikes">%d gillade 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="action_ayml">Önerilen kullanıcılar</string>
<string name="select_picture">Resim Seç</string> <string name="select_picture">Resim Seç</string>
<string name="uploading">Yükleniyor…</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_relationship">%d takip</string>
<string name="activity_count_comments">%d yorum</string> <string name="activity_count_comments">%d yorum</string>
<string name="activity_count_commentlikes">%d yorum beğenisi</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="action_ayml">Người dùng được đề xuất</string>
<string name="select_picture">Chọn hình ảnh</string> <string name="select_picture">Chọn hình ảnh</string>
<string name="uploading">Đang tải lên…</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_relationship">%d người theo dõi</string>
<string name="activity_count_comments">%d bình luận</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> <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="action_ayml">推荐用户</string>
<string name="select_picture">选择图片</string> <string name="select_picture">选择图片</string>
<string name="uploading">上传中...</string> <string name="uploading">上传中...</string>
<string name="activity_count_prefix">您有:</string>
<string name="activity_count_relationship">%d 位新粉丝</string> <string name="activity_count_relationship">%d 位新粉丝</string>
<string name="activity_count_comments">%d 个评论回复</string> <string name="activity_count_comments">%d 个评论回复</string>
<string name="activity_count_commentlikes">%d 个评论点赞</string> <string name="activity_count_commentlikes">%d 个评论点赞</string>

View File

@ -233,7 +233,6 @@
<string name="action_ayml">推薦用戶</string> <string name="action_ayml">推薦用戶</string>
<string name="select_picture">選擇圖片</string> <string name="select_picture">選擇圖片</string>
<string name="uploading">上傳中…</string> <string name="uploading">上傳中…</string>
<string name="activity_count_prefix">您有</string>
<string name="activity_count_relationship">%d 個追蹤者</string> <string name="activity_count_relationship">%d 個追蹤者</string>
<string name="activity_count_comments">%d 個評論</string> <string name="activity_count_comments">%d 個評論</string>
<string name="activity_count_commentlikes">%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="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="select_picture">Select Picture</string>
<string name="uploading">Uploading…</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_relationship">%d follows</string>
<string name="activity_count_comments">%d comments</string> <string name="activity_count_comments">%d comments</string>
<string name="activity_count_commentlikes">%d comment likes</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_subject">Barinsta Crash Report</string>
<string name="crash_report_title">Select an email app to send crash logs</string> <string name="crash_report_title">Select an email app to send crash logs</string>
<string name="not_found">Not found!</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="skip_update">Skip this update</string>
<string name="on_latest_version">You\'re already on the latest version</string> <string name="on_latest_version">You\'re already on the latest version</string>
<string name="tab_order">Screen order</string> <string name="tab_order">Screen order</string>