mirror of
https://github.com/KokaKiwi/BarInsta
synced 2024-11-22 22:57:29 +00:00
Add PostsRecyclerView to ProfileFragment
This commit is contained in:
parent
9b83c5e832
commit
0a67e859e0
@ -5,7 +5,7 @@ import java.util.List;
|
||||
import awais.instagrabber.customviews.helpers.PostFetcher;
|
||||
import awais.instagrabber.interfaces.FetchListener;
|
||||
import awais.instagrabber.models.FeedModel;
|
||||
import awais.instagrabber.repositories.responses.FeedFetchResponse;
|
||||
import awais.instagrabber.repositories.responses.PostsFetchResponse;
|
||||
import awais.instagrabber.webservices.FeedService;
|
||||
import awais.instagrabber.webservices.ServiceCallback;
|
||||
|
||||
@ -21,9 +21,9 @@ public class FeedPostFetchService implements PostFetcher.PostFetchService {
|
||||
|
||||
@Override
|
||||
public void fetch(final String cursor, final FetchListener<List<FeedModel>> fetchListener) {
|
||||
feedService.fetch(25, cursor, new ServiceCallback<FeedFetchResponse>() {
|
||||
feedService.fetch(25, cursor, new ServiceCallback<PostsFetchResponse>() {
|
||||
@Override
|
||||
public void onSuccess(final FeedFetchResponse result) {
|
||||
public void onSuccess(final PostsFetchResponse result) {
|
||||
if (result == null) return;
|
||||
nextCursor = result.getNextCursor();
|
||||
hasNextPage = result.hasNextPage();
|
||||
|
@ -0,0 +1,57 @@
|
||||
package awais.instagrabber.asyncs;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import awais.instagrabber.customviews.helpers.PostFetcher;
|
||||
import awais.instagrabber.interfaces.FetchListener;
|
||||
import awais.instagrabber.models.FeedModel;
|
||||
import awais.instagrabber.models.ProfileModel;
|
||||
import awais.instagrabber.repositories.responses.PostsFetchResponse;
|
||||
import awais.instagrabber.webservices.ProfileService;
|
||||
import awais.instagrabber.webservices.ServiceCallback;
|
||||
|
||||
public class ProfilePostFetchService implements PostFetcher.PostFetchService {
|
||||
private static final String TAG = "ProfilePostFetchService";
|
||||
private final ProfileService profileService;
|
||||
private final ProfileModel profileModel;
|
||||
private String nextCursor;
|
||||
private boolean hasNextPage;
|
||||
|
||||
public ProfilePostFetchService(final ProfileModel profileModel) {
|
||||
this.profileModel = profileModel;
|
||||
profileService = ProfileService.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fetch(final String cursor, final FetchListener<List<FeedModel>> fetchListener) {
|
||||
profileService.fetchPosts(profileModel, 30, cursor, new ServiceCallback<PostsFetchResponse>() {
|
||||
@Override
|
||||
public void onSuccess(final PostsFetchResponse result) {
|
||||
if (result == null) return;
|
||||
nextCursor = result.getNextCursor();
|
||||
hasNextPage = result.hasNextPage();
|
||||
if (fetchListener != null) {
|
||||
fetchListener.onResult(result.getFeedModels());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(final Throwable t) {
|
||||
// Log.e(TAG, "onFailure: ", t);
|
||||
if (fetchListener != null) {
|
||||
fetchListener.onFailure(t);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNextCursor() {
|
||||
return nextCursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNextPage() {
|
||||
return hasNextPage;
|
||||
}
|
||||
}
|
@ -25,7 +25,6 @@ import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtBottom;
|
||||
import awais.instagrabber.interfaces.FetchListener;
|
||||
import awais.instagrabber.models.FeedModel;
|
||||
import awais.instagrabber.models.PostsLayoutPreferences;
|
||||
import awais.instagrabber.utils.Constants;
|
||||
import awais.instagrabber.utils.Utils;
|
||||
import awais.instagrabber.viewmodels.FeedViewModel;
|
||||
|
||||
@ -130,16 +129,8 @@ public class PostsRecyclerView extends RecyclerView {
|
||||
throw new IllegalArgumentException("PostFetchService cannot be null");
|
||||
}
|
||||
if (layoutPreferences == null) {
|
||||
layoutPreferences = PostsLayoutPreferences.builder()
|
||||
.setType(PostsLayoutPreferences.PostsLayoutType.GRID)
|
||||
.setColCount(3)
|
||||
.setAvatarVisible(true)
|
||||
.setNameVisible(false)
|
||||
.setProfilePicSize(PostsLayoutPreferences.ProfilePicSize.TINY)
|
||||
.setHasGap(true)
|
||||
.setHasRoundedCorners(true)
|
||||
.build();
|
||||
Utils.settingsHelper.putString(Constants.PREF_POSTS_LAYOUT, layoutPreferences.getJson());
|
||||
layoutPreferences = PostsLayoutPreferences.builder().build();
|
||||
// Utils.settingsHelper.putString(Constants.PREF_POSTS_LAYOUT, layoutPreferences.getJson());
|
||||
}
|
||||
gridSpacingItemDecoration = new GridSpacingItemDecoration(Utils.convertDpToPx(2));
|
||||
initTransition();
|
||||
@ -156,9 +147,6 @@ public class PostsRecyclerView extends RecyclerView {
|
||||
|
||||
private void initLayoutManager() {
|
||||
layoutManager = new StaggeredGridLayoutManager(layoutPreferences.getColCount(), StaggeredGridLayoutManager.VERTICAL);
|
||||
if (layoutPreferences.getHasGap()) {
|
||||
addItemDecoration(gridSpacingItemDecoration);
|
||||
}
|
||||
setLayoutManager(layoutManager);
|
||||
}
|
||||
|
||||
@ -172,7 +160,9 @@ public class PostsRecyclerView extends RecyclerView {
|
||||
feedViewModel = new ViewModelProvider(viewModelStoreOwner).get(FeedViewModel.class);
|
||||
feedViewModel.getList().observe(lifeCycleOwner, feedAdapter::submitList);
|
||||
postFetcher = new PostFetcher(postFetchService, fetchListener);
|
||||
if (layoutPreferences.getHasGap()) {
|
||||
addItemDecoration(gridSpacingItemDecoration);
|
||||
}
|
||||
setHasFixedSize(true);
|
||||
setNestedScrollingEnabled(true);
|
||||
lazyLoader = new RecyclerLazyLoaderAtBottom(layoutManager, (page) -> {
|
||||
|
@ -16,20 +16,21 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import awais.instagrabber.R;
|
||||
import awais.instagrabber.databinding.DialogPostLayoutPreferencesBinding;
|
||||
import awais.instagrabber.models.PostsLayoutPreferences;
|
||||
import awais.instagrabber.utils.Constants;
|
||||
|
||||
import static awais.instagrabber.utils.Utils.settingsHelper;
|
||||
|
||||
public class PostsLayoutPreferencesDialogFragment extends DialogFragment {
|
||||
|
||||
private final PostsLayoutPreferences.Builder preferencesBuilder;
|
||||
@NonNull
|
||||
private final OnApplyListener onApplyListener;
|
||||
private final PostsLayoutPreferences.Builder preferencesBuilder;
|
||||
private final String layoutPreferenceKey;
|
||||
private DialogPostLayoutPreferencesBinding binding;
|
||||
private Context context;
|
||||
|
||||
public PostsLayoutPreferencesDialogFragment(@NonNull final OnApplyListener onApplyListener) {
|
||||
final PostsLayoutPreferences preferences = PostsLayoutPreferences.fromJson(settingsHelper.getString(Constants.PREF_POSTS_LAYOUT));
|
||||
public PostsLayoutPreferencesDialogFragment(final String layoutPreferenceKey,
|
||||
@NonNull final OnApplyListener onApplyListener) {
|
||||
this.layoutPreferenceKey = layoutPreferenceKey;
|
||||
final PostsLayoutPreferences preferences = PostsLayoutPreferences.fromJson(settingsHelper.getString(layoutPreferenceKey));
|
||||
this.preferencesBuilder = PostsLayoutPreferences.builder().mergeFrom(preferences);
|
||||
this.onApplyListener = onApplyListener;
|
||||
}
|
||||
@ -50,7 +51,7 @@ public class PostsLayoutPreferencesDialogFragment extends DialogFragment {
|
||||
.setPositiveButton(R.string.apply, (dialog, which) -> {
|
||||
final PostsLayoutPreferences preferences = preferencesBuilder.build();
|
||||
final String json = preferences.getJson();
|
||||
settingsHelper.putString(Constants.PREF_POSTS_LAYOUT, json);
|
||||
settingsHelper.putString(layoutPreferenceKey, json);
|
||||
onApplyListener.onApply(preferences);
|
||||
})
|
||||
.create();
|
||||
|
@ -417,8 +417,10 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment {
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
wasPaused = true;
|
||||
if (bottomSheetBehavior != null) {
|
||||
captionState = bottomSheetBehavior.getState();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
@ -494,7 +496,9 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment {
|
||||
binding.postImage.setLayoutParams(new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
binding.postImage.requestLayout();
|
||||
if (bottomSheetBehavior != null) {
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (destView == binding.sliderParent) {
|
||||
@ -778,6 +782,9 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment {
|
||||
|
||||
private void setupCaption() {
|
||||
final CharSequence postCaption = feedModel.getPostCaption();
|
||||
if (TextUtils.isEmpty(postCaption)) {
|
||||
return;
|
||||
}
|
||||
binding.caption.addOnHashtagListener(autoLinkItem -> {
|
||||
final NavController navController = NavHostFragment.findNavController(this);
|
||||
final Bundle bundle = new Bundle();
|
||||
|
@ -331,7 +331,7 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre
|
||||
}
|
||||
|
||||
private void showPostsLayoutPreferences() {
|
||||
final PostsLayoutPreferencesDialogFragment fragment = new PostsLayoutPreferencesDialogFragment(preferences -> new Handler()
|
||||
final PostsLayoutPreferencesDialogFragment fragment = new PostsLayoutPreferencesDialogFragment(Constants.PREF_POSTS_LAYOUT, preferences -> new Handler()
|
||||
.postDelayed(() -> binding.feedRecyclerView.setLayoutPreferences(preferences), 200));
|
||||
fragment.show(getChildFragmentManager(), "posts_layout_preferences");
|
||||
}
|
||||
|
@ -24,16 +24,17 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.activity.OnBackPressedDispatcher;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import androidx.core.content.PermissionChecker;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.NavDirections;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
@ -46,7 +47,6 @@ import com.facebook.imagepipeline.image.ImageInfo;
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@ -54,24 +54,25 @@ import java.util.List;
|
||||
import awais.instagrabber.ProfileNavGraphDirections;
|
||||
import awais.instagrabber.R;
|
||||
import awais.instagrabber.activities.MainActivity;
|
||||
import awais.instagrabber.adapters.FeedAdapterV2;
|
||||
import awais.instagrabber.adapters.HighlightsAdapter;
|
||||
import awais.instagrabber.adapters.PostsAdapter;
|
||||
import awais.instagrabber.asyncs.HighlightsFetcher;
|
||||
import awais.instagrabber.asyncs.PostsFetcher;
|
||||
import awais.instagrabber.asyncs.ProfileFetcher;
|
||||
import awais.instagrabber.asyncs.ProfilePostFetchService;
|
||||
import awais.instagrabber.asyncs.UsernameFetcher;
|
||||
import awais.instagrabber.asyncs.direct_messages.CreateThreadAction;
|
||||
import awais.instagrabber.customviews.PrimaryActionModeCallback;
|
||||
import awais.instagrabber.customviews.PrimaryActionModeCallback.CallbacksHelper;
|
||||
import awais.instagrabber.customviews.helpers.GridAutofitLayoutManager;
|
||||
import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration;
|
||||
import awais.instagrabber.customviews.helpers.NestedCoordinatorLayout;
|
||||
import awais.instagrabber.customviews.helpers.RecyclerLazyLoader;
|
||||
import awais.instagrabber.databinding.FragmentProfileBinding;
|
||||
import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment;
|
||||
import awais.instagrabber.dialogs.ProfilePicDialogFragment;
|
||||
import awais.instagrabber.fragments.PostViewV2Fragment;
|
||||
import awais.instagrabber.interfaces.FetchListener;
|
||||
import awais.instagrabber.interfaces.MentionClickListener;
|
||||
import awais.instagrabber.models.PostModel;
|
||||
import awais.instagrabber.models.FeedModel;
|
||||
import awais.instagrabber.models.PostsLayoutPreferences;
|
||||
import awais.instagrabber.models.ProfileModel;
|
||||
import awais.instagrabber.models.StoryModel;
|
||||
import awais.instagrabber.models.enums.DownloadMethod;
|
||||
@ -86,17 +87,17 @@ import awais.instagrabber.utils.DownloadUtils;
|
||||
import awais.instagrabber.utils.TextUtils;
|
||||
import awais.instagrabber.utils.Utils;
|
||||
import awais.instagrabber.viewmodels.HighlightsViewModel;
|
||||
import awais.instagrabber.viewmodels.PostsViewModel;
|
||||
import awais.instagrabber.webservices.FriendshipService;
|
||||
import awais.instagrabber.webservices.ServiceCallback;
|
||||
import awais.instagrabber.webservices.StoriesService;
|
||||
import awaisomereport.LogCollector;
|
||||
|
||||
import static awais.instagrabber.utils.Utils.logCollector;
|
||||
import static androidx.core.content.PermissionChecker.checkSelfPermission;
|
||||
import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION;
|
||||
import static awais.instagrabber.utils.Utils.settingsHelper;
|
||||
|
||||
public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
|
||||
private static final String TAG = "ProfileFragment";
|
||||
private static final int STORAGE_PERM_REQUEST_CODE = 8020;
|
||||
|
||||
private MainActivity fragmentActivity;
|
||||
private CoordinatorLayout root;
|
||||
@ -105,21 +106,18 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
private String cookie;
|
||||
private String username;
|
||||
private ProfileModel profileModel;
|
||||
private PostsViewModel postsViewModel;
|
||||
private PostsAdapter postsAdapter;
|
||||
private ActionMode actionMode;
|
||||
private Handler usernameSettingHandler;
|
||||
private FriendshipService friendshipService;
|
||||
private StoriesService storiesService;
|
||||
private boolean shouldRefresh = true, hasStories = false;
|
||||
private boolean hasNextPage;
|
||||
private String endCursor;
|
||||
private AsyncTask<Void, Void, List<PostModel>> currentlyExecuting;
|
||||
private boolean isPullToRefresh;
|
||||
private boolean shouldRefresh = true;
|
||||
private boolean hasStories = false;
|
||||
private HighlightsAdapter highlightsAdapter;
|
||||
private HighlightsViewModel highlightsViewModel;
|
||||
private MenuItem blockMenuItem;
|
||||
private MenuItem restrictMenuItem;
|
||||
private boolean highlightsFetching;
|
||||
|
||||
private final Runnable usernameSettingRunnable = () -> {
|
||||
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
|
||||
@ -165,36 +163,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
return false;
|
||||
}
|
||||
});
|
||||
private final FetchListener<List<PostModel>> postsFetchListener = new FetchListener<List<PostModel>>() {
|
||||
@Override
|
||||
public void onResult(final List<PostModel> result) {
|
||||
binding.swipeRefreshLayout.setRefreshing(false);
|
||||
if (result == null || result.isEmpty()) {
|
||||
binding.privatePage1.setImageResource(R.drawable.ic_cancel);
|
||||
binding.privatePage2.setText(R.string.empty_acc);
|
||||
binding.privatePage.setVisibility(View.VISIBLE);
|
||||
return;
|
||||
} else {
|
||||
binding.privatePage.setVisibility(View.GONE);
|
||||
}
|
||||
binding.mainPosts.post(() -> binding.mainPosts.setVisibility(View.VISIBLE));
|
||||
final List<PostModel> postModels = postsViewModel.getList().getValue();
|
||||
List<PostModel> finalList = postModels == null || postModels.isEmpty() ? new ArrayList<>()
|
||||
: new ArrayList<>(postModels);
|
||||
if (isPullToRefresh) {
|
||||
finalList = result;
|
||||
isPullToRefresh = false;
|
||||
} else {
|
||||
finalList.addAll(result);
|
||||
}
|
||||
postsViewModel.getList().postValue(finalList);
|
||||
final PostModel lastPostModel = result.get(result.size() - 1);
|
||||
if (lastPostModel == null) return;
|
||||
endCursor = lastPostModel.getEndCursor();
|
||||
hasNextPage = lastPostModel.hasNextPage();
|
||||
lastPostModel.setPageCursor(false, null);
|
||||
}
|
||||
};
|
||||
private final MentionClickListener mentionClickListener = (view, text, isHashtag, isLocation) -> {
|
||||
Log.d(TAG, "action...");
|
||||
if (isHashtag) {
|
||||
@ -213,6 +181,92 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
action.setUsername("@" + text);
|
||||
NavHostFragment.findNavController(this).navigate(action);
|
||||
};
|
||||
private final FeedAdapterV2.FeedItemCallback feedItemCallback = new FeedAdapterV2.FeedItemCallback() {
|
||||
@Override
|
||||
public void onPostClick(final FeedModel feedModel, final View profilePicView, final View mainPostImage) {
|
||||
openPostDialog(feedModel, profilePicView, mainPostImage, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSliderClick(final FeedModel feedModel, final int position) {
|
||||
openPostDialog(feedModel, null, null, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCommentsClick(final FeedModel feedModel) {
|
||||
final NavDirections commentsAction = FeedFragmentDirections.actionGlobalCommentsViewerFragment(
|
||||
feedModel.getShortCode(),
|
||||
feedModel.getPostId(),
|
||||
feedModel.getProfileModel().getId()
|
||||
);
|
||||
NavHostFragment.findNavController(ProfileFragment.this).navigate(commentsAction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadClick(final FeedModel feedModel) {
|
||||
final Context context = getContext();
|
||||
if (context == null) return;
|
||||
if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) {
|
||||
showDownloadDialog(feedModel);
|
||||
return;
|
||||
}
|
||||
requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHashtagClick(final String hashtag) {
|
||||
final NavDirections action = FeedFragmentDirections.actionGlobalHashTagFragment(hashtag);
|
||||
NavHostFragment.findNavController(ProfileFragment.this).navigate(action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationClick(final FeedModel feedModel) {
|
||||
final NavDirections action = FeedFragmentDirections.actionGlobalLocationFragment(feedModel.getLocationId());
|
||||
NavHostFragment.findNavController(ProfileFragment.this).navigate(action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMentionClick(final String mention) {
|
||||
navigateToProfile(mention.trim());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNameClick(final FeedModel feedModel, final View profilePicView) {
|
||||
navigateToProfile("@" + feedModel.getProfileModel().getUsername());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProfilePicClick(final FeedModel feedModel, final View profilePicView) {
|
||||
navigateToProfile("@" + feedModel.getProfileModel().getUsername());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onURLClick(final String url) {
|
||||
Utils.openURL(getContext(), url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEmailClick(final String emailId) {
|
||||
Utils.openEmailAddress(getContext(), emailId);
|
||||
}
|
||||
|
||||
private void openPostDialog(final FeedModel feedModel,
|
||||
final View profilePicView,
|
||||
final View mainPostImage,
|
||||
final int position) {
|
||||
final PostViewV2Fragment.Builder builder = PostViewV2Fragment
|
||||
.builder(feedModel);
|
||||
if (position >= 0) {
|
||||
builder.setPosition(position);
|
||||
}
|
||||
final PostViewV2Fragment fragment = builder
|
||||
.setSharedProfilePicElement(profilePicView)
|
||||
.setSharedMainPostElement(mainPostImage)
|
||||
.build();
|
||||
fragment.show(getChildFragmentManager(), "post_view");
|
||||
}
|
||||
};
|
||||
private boolean postsSetupDone = false;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
@ -267,7 +321,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.profile_menu, menu);
|
||||
// favMenuItem = menu.findItem(R.id.favourites);
|
||||
blockMenuItem = menu.findItem(R.id.block);
|
||||
if (blockMenuItem != null) {
|
||||
blockMenuItem.setVisible(false);
|
||||
@ -280,6 +333,10 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||
if (item.getItemId() == R.id.layout) {
|
||||
showPostsLayoutPreferences();
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.restrict) {
|
||||
if (!isLoggedIn) return false;
|
||||
final String action = profileModel.getRestricted() ? "Unrestrict" : "Restrict";
|
||||
@ -346,10 +403,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
isPullToRefresh = true;
|
||||
endCursor = null;
|
||||
fetchProfileDetails();
|
||||
fetchPosts();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -359,9 +413,9 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
if (usernameSettingHandler != null) {
|
||||
usernameSettingHandler.removeCallbacks(usernameSettingRunnable);
|
||||
}
|
||||
if (postsViewModel != null) {
|
||||
postsViewModel.getList().postValue(Collections.emptyList());
|
||||
}
|
||||
// if (postsViewModel != null) {
|
||||
// postsViewModel.getList().postValue(Collections.emptyList());
|
||||
// }
|
||||
if (highlightsViewModel != null) {
|
||||
highlightsViewModel.getList().postValue(Collections.emptyList());
|
||||
}
|
||||
@ -385,7 +439,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
return;
|
||||
}
|
||||
binding.swipeRefreshLayout.setEnabled(true);
|
||||
setupPosts();
|
||||
setupHighlights();
|
||||
setupCommonListeners();
|
||||
fetchUsername();
|
||||
@ -444,87 +497,18 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
Toast.makeText(context, R.string.error_loading_profile, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
if (!postsSetupDone) {
|
||||
setupPosts();
|
||||
} else {
|
||||
binding.postsRecyclerView.refresh();
|
||||
}
|
||||
binding.isVerified.setVisibility(profileModel.isVerified() ? View.VISIBLE : View.GONE);
|
||||
final String profileId = profileModel.getId();
|
||||
|
||||
final String myId = CookieUtils.getUserIdFromCookie(cookie);
|
||||
if (isLoggedIn) {
|
||||
storiesService.getUserStory(profileId,
|
||||
profileModel.getUsername(),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
new ServiceCallback<List<StoryModel>>() {
|
||||
@Override
|
||||
public void onSuccess(final List<StoryModel> storyModels) {
|
||||
if (storyModels != null && !storyModels.isEmpty()) {
|
||||
binding.mainProfileImage.setStoriesBorder();
|
||||
hasStories = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(final Throwable t) {
|
||||
Log.e(TAG, "Error", t);
|
||||
}
|
||||
});
|
||||
new HighlightsFetcher(profileId,
|
||||
result -> {
|
||||
if (result != null) {
|
||||
binding.highlightsList.setVisibility(View.VISIBLE);
|
||||
highlightsViewModel.getList().postValue(result);
|
||||
} else binding.highlightsList.setVisibility(View.GONE);
|
||||
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
if (profileId.equals(myId)) {
|
||||
binding.btnTagged.setVisibility(View.VISIBLE);
|
||||
binding.btnSaved.setVisibility(View.VISIBLE);
|
||||
binding.btnLiked.setVisibility(View.VISIBLE);
|
||||
binding.btnDM.setVisibility(View.GONE);
|
||||
binding.btnSaved.setText(R.string.saved);
|
||||
} else {
|
||||
binding.btnTagged.setVisibility(View.GONE);
|
||||
binding.btnSaved.setVisibility(View.GONE);
|
||||
binding.btnLiked.setVisibility(View.GONE);
|
||||
binding.btnDM.setVisibility(View.VISIBLE); // maybe there is a judgment mechanism?
|
||||
binding.btnFollow.setVisibility(View.VISIBLE);
|
||||
if (profileModel.getFollowing()) {
|
||||
binding.btnFollow.setText(R.string.unfollow);
|
||||
binding.btnFollow.setIconResource(R.drawable.ic_outline_person_add_disabled_24);
|
||||
} else if (profileModel.getRequested()) {
|
||||
binding.btnFollow.setText(R.string.cancel);
|
||||
binding.btnFollow.setIconResource(R.drawable.ic_outline_person_add_disabled_24);
|
||||
} else {
|
||||
binding.btnFollow.setText(R.string.follow);
|
||||
binding.btnFollow.setIconResource(R.drawable.ic_outline_person_add_24);
|
||||
}
|
||||
if (restrictMenuItem != null) {
|
||||
restrictMenuItem.setVisible(true);
|
||||
if (profileModel.getRestricted()) {
|
||||
restrictMenuItem.setTitle(R.string.unrestrict);
|
||||
} else {
|
||||
restrictMenuItem.setTitle(R.string.restrict);
|
||||
}
|
||||
}
|
||||
binding.btnTagged.setVisibility(profileModel.isReallyPrivate() ? View.GONE : View.VISIBLE);
|
||||
if (blockMenuItem != null) {
|
||||
blockMenuItem.setVisible(true);
|
||||
if (profileModel.getBlocked()) {
|
||||
blockMenuItem.setTitle(R.string.unblock);
|
||||
} else {
|
||||
blockMenuItem.setTitle(R.string.block);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!profileModel.isReallyPrivate() && restrictMenuItem != null) {
|
||||
restrictMenuItem.setVisible(true);
|
||||
if (profileModel.getRestricted()) {
|
||||
restrictMenuItem.setTitle(R.string.unrestrict);
|
||||
} else {
|
||||
restrictMenuItem.setTitle(R.string.restrict);
|
||||
}
|
||||
}
|
||||
fetchStoryAndHighlights(profileId);
|
||||
}
|
||||
setupButtons(profileId, myId);
|
||||
if (!profileId.equals(myId)) {
|
||||
binding.favCb.setVisibility(View.VISIBLE);
|
||||
final boolean isFav = Utils.dataBox.getFavorite(username.substring(1), FavoriteType.USER) != null;
|
||||
@ -572,8 +556,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
: profileModel.getName());
|
||||
|
||||
CharSequence biography = profileModel.getBiography();
|
||||
// binding.mainBiography.setCaptionIsExpandable(true);
|
||||
// binding.mainBiography.setCaptionIsExpanded(true);
|
||||
if (TextUtils.hasMentions(biography)) {
|
||||
biography = TextUtils.getMentionText(biography);
|
||||
binding.mainBiography.setText(biography, TextView.BufferType.SPANNABLE);
|
||||
@ -618,7 +600,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
}
|
||||
|
||||
binding.swipeRefreshLayout.setRefreshing(true);
|
||||
binding.mainPosts.setVisibility(View.VISIBLE);
|
||||
binding.postsRecyclerView.setVisibility(View.VISIBLE);
|
||||
fetchPosts();
|
||||
} else {
|
||||
binding.mainFollowers.setClickable(false);
|
||||
@ -628,10 +610,94 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
binding.privatePage1.setImageResource(R.drawable.lock);
|
||||
binding.privatePage2.setText(R.string.priv_acc);
|
||||
binding.privatePage.setVisibility(View.VISIBLE);
|
||||
binding.mainPosts.setVisibility(View.GONE);
|
||||
binding.postsRecyclerView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupButtons(final String profileId, final String myId) {
|
||||
if (isLoggedIn) {
|
||||
if (profileId.equals(myId)) {
|
||||
binding.btnTagged.setVisibility(View.VISIBLE);
|
||||
binding.btnSaved.setVisibility(View.VISIBLE);
|
||||
binding.btnLiked.setVisibility(View.VISIBLE);
|
||||
binding.btnDM.setVisibility(View.GONE);
|
||||
binding.btnSaved.setText(R.string.saved);
|
||||
return;
|
||||
}
|
||||
binding.btnTagged.setVisibility(View.GONE);
|
||||
binding.btnSaved.setVisibility(View.GONE);
|
||||
binding.btnLiked.setVisibility(View.GONE);
|
||||
binding.btnDM.setVisibility(View.VISIBLE); // maybe there is a judgment mechanism?
|
||||
binding.btnFollow.setVisibility(View.VISIBLE);
|
||||
if (profileModel.getFollowing()) {
|
||||
binding.btnFollow.setText(R.string.unfollow);
|
||||
binding.btnFollow.setIconResource(R.drawable.ic_outline_person_add_disabled_24);
|
||||
} else if (profileModel.getRequested()) {
|
||||
binding.btnFollow.setText(R.string.cancel);
|
||||
binding.btnFollow.setIconResource(R.drawable.ic_outline_person_add_disabled_24);
|
||||
} else {
|
||||
binding.btnFollow.setText(R.string.follow);
|
||||
binding.btnFollow.setIconResource(R.drawable.ic_outline_person_add_24);
|
||||
}
|
||||
if (restrictMenuItem != null) {
|
||||
restrictMenuItem.setVisible(true);
|
||||
if (profileModel.getRestricted()) {
|
||||
restrictMenuItem.setTitle(R.string.unrestrict);
|
||||
} else {
|
||||
restrictMenuItem.setTitle(R.string.restrict);
|
||||
}
|
||||
}
|
||||
binding.btnTagged.setVisibility(profileModel.isReallyPrivate() ? View.GONE : View.VISIBLE);
|
||||
if (blockMenuItem != null) {
|
||||
blockMenuItem.setVisible(true);
|
||||
if (profileModel.getBlocked()) {
|
||||
blockMenuItem.setTitle(R.string.unblock);
|
||||
} else {
|
||||
blockMenuItem.setTitle(R.string.block);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!profileModel.isReallyPrivate() && restrictMenuItem != null) {
|
||||
restrictMenuItem.setVisible(true);
|
||||
if (profileModel.getRestricted()) {
|
||||
restrictMenuItem.setTitle(R.string.unrestrict);
|
||||
} else {
|
||||
restrictMenuItem.setTitle(R.string.restrict);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchStoryAndHighlights(final String profileId) {
|
||||
storiesService.getUserStory(profileId,
|
||||
profileModel.getUsername(),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
new ServiceCallback<List<StoryModel>>() {
|
||||
@Override
|
||||
public void onSuccess(final List<StoryModel> storyModels) {
|
||||
if (storyModels != null && !storyModels.isEmpty()) {
|
||||
binding.mainProfileImage.setStoriesBorder();
|
||||
hasStories = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(final Throwable t) {
|
||||
Log.e(TAG, "Error", t);
|
||||
}
|
||||
});
|
||||
new HighlightsFetcher(profileId,
|
||||
result -> {
|
||||
highlightsFetching = false;
|
||||
if (result != null) {
|
||||
binding.highlightsList.setVisibility(View.VISIBLE);
|
||||
highlightsViewModel.getList().postValue(result);
|
||||
} else binding.highlightsList.setVisibility(View.GONE);
|
||||
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private void setupCommonListeners() {
|
||||
|
||||
final String userIdFromCookie = CookieUtils.getUserIdFromCookie(cookie);
|
||||
@ -781,62 +847,60 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
}
|
||||
|
||||
private void setupPosts() {
|
||||
postsViewModel = new ViewModelProvider(this).get(PostsViewModel.class);
|
||||
final Context context = getContext();
|
||||
if (context == null) return;
|
||||
final GridAutofitLayoutManager layoutManager = new GridAutofitLayoutManager(context, Utils.convertDpToPx(110));
|
||||
binding.mainPosts.setLayoutManager(layoutManager);
|
||||
binding.mainPosts.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(4)));
|
||||
postsAdapter = new PostsAdapter((postModel, position) -> {
|
||||
if (postsAdapter.isSelecting()) {
|
||||
if (actionMode == null) return;
|
||||
final String title = getString(R.string.number_selected,
|
||||
postsAdapter.getSelectedModels().size());
|
||||
actionMode.setTitle(title);
|
||||
return;
|
||||
}
|
||||
if (checkAndResetAction()) return;
|
||||
final List<PostModel> postModels = postsViewModel.getList().getValue();
|
||||
if (postModels == null || postModels.size() == 0) return;
|
||||
if (postModels.get(0) == null) return;
|
||||
final String postId = isLoggedIn ? postModels.get(0).getPostId() : postModels.get(0).getShortCode();
|
||||
final boolean isId = isLoggedIn && postId != null;
|
||||
final String[] idsOrShortCodes = new String[postModels.size()];
|
||||
for (int i = 0; i < postModels.size(); i++) {
|
||||
idsOrShortCodes[i] = isId ? postModels.get(i).getPostId()
|
||||
: postModels.get(i).getShortCode();
|
||||
}
|
||||
final NavDirections action = ProfileFragmentDirections.actionGlobalPostViewFragment(
|
||||
position,
|
||||
idsOrShortCodes,
|
||||
isId);
|
||||
NavHostFragment.findNavController(this).navigate(action);
|
||||
|
||||
}, (model, position) -> {
|
||||
if (!postsAdapter.isSelecting()) {
|
||||
checkAndResetAction();
|
||||
return true;
|
||||
}
|
||||
if (onBackPressedCallback.isEnabled()) {
|
||||
return true;
|
||||
}
|
||||
final OnBackPressedDispatcher onBackPressedDispatcher = fragmentActivity.getOnBackPressedDispatcher();
|
||||
onBackPressedCallback.setEnabled(true);
|
||||
actionMode = fragmentActivity.startActionMode(multiSelectAction);
|
||||
final String title = getString(R.string.number_selected, 1);
|
||||
actionMode.setTitle(title);
|
||||
onBackPressedDispatcher.addCallback(getViewLifecycleOwner(), onBackPressedCallback);
|
||||
return true;
|
||||
});
|
||||
postsViewModel.getList().observe(fragmentActivity, postsAdapter::submitList);
|
||||
binding.mainPosts.setAdapter(postsAdapter);
|
||||
final RecyclerLazyLoader lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> {
|
||||
if (!hasNextPage) return;
|
||||
binding.postsRecyclerView.setViewModelStoreOwner(this)
|
||||
.setLifeCycleOwner(this)
|
||||
.setPostFetchService(new ProfilePostFetchService(profileModel))
|
||||
.setLayoutPreferences(PostsLayoutPreferences.fromJson(settingsHelper.getString(Constants.PREF_PROFILE_POSTS_LAYOUT)))
|
||||
.addFetchStatusChangeListener(fetching -> updateSwipeRefreshState())
|
||||
.setFeedItemCallback(feedItemCallback)
|
||||
.init();
|
||||
binding.swipeRefreshLayout.setRefreshing(true);
|
||||
fetchPosts();
|
||||
endCursor = null;
|
||||
});
|
||||
binding.mainPosts.addOnScrollListener(lazyLoader);
|
||||
postsSetupDone = true;
|
||||
// postsAdapter = new PostsAdapter((postModel, position) -> {
|
||||
// if (postsAdapter.isSelecting()) {
|
||||
// if (actionMode == null) return;
|
||||
// final String title = getString(R.string.number_selected,
|
||||
// postsAdapter.getSelectedModels().size());
|
||||
// actionMode.setTitle(title);
|
||||
// return;
|
||||
// }
|
||||
// if (checkAndResetAction()) return;
|
||||
// final List<PostModel> postModels = postsViewModel.getList().getValue();
|
||||
// if (postModels == null || postModels.size() == 0) return;
|
||||
// if (postModels.get(0) == null) return;
|
||||
// final String postId = isLoggedIn ? postModels.get(0).getPostId() : postModels.get(0).getShortCode();
|
||||
// final boolean isId = isLoggedIn && postId != null;
|
||||
// final String[] idsOrShortCodes = new String[postModels.size()];
|
||||
// for (int i = 0; i < postModels.size(); i++) {
|
||||
// idsOrShortCodes[i] = isId ? postModels.get(i).getPostId()
|
||||
// : postModels.get(i).getShortCode();
|
||||
// }
|
||||
// final NavDirections action = ProfileFragmentDirections.actionGlobalPostViewFragment(
|
||||
// position,
|
||||
// idsOrShortCodes,
|
||||
// isId);
|
||||
// NavHostFragment.findNavController(this).navigate(action);
|
||||
//
|
||||
// }, (model, position) -> {
|
||||
// if (!postsAdapter.isSelecting()) {
|
||||
// checkAndResetAction();
|
||||
// return true;
|
||||
// }
|
||||
// if (onBackPressedCallback.isEnabled()) {
|
||||
// return true;
|
||||
// }
|
||||
// final OnBackPressedDispatcher onBackPressedDispatcher = fragmentActivity.getOnBackPressedDispatcher();
|
||||
// onBackPressedCallback.setEnabled(true);
|
||||
// actionMode = fragmentActivity.startActionMode(multiSelectAction);
|
||||
// final String title = getString(R.string.number_selected, 1);
|
||||
// actionMode.setTitle(title);
|
||||
// onBackPressedDispatcher.addCallback(getViewLifecycleOwner(), onBackPressedCallback);
|
||||
// return true;
|
||||
// });
|
||||
}
|
||||
|
||||
private void updateSwipeRefreshState() {
|
||||
binding.swipeRefreshLayout.setRefreshing(binding.postsRecyclerView.isFetching() || highlightsFetching);
|
||||
}
|
||||
|
||||
private void setupHighlights() {
|
||||
@ -855,22 +919,11 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
}
|
||||
|
||||
private void fetchPosts() {
|
||||
stopCurrentExecutor();
|
||||
// stopCurrentExecutor();
|
||||
binding.swipeRefreshLayout.setRefreshing(true);
|
||||
currentlyExecuting = new PostsFetcher(profileModel.getId(), PostItemType.MAIN, endCursor, postsFetchListener)
|
||||
.setUsername(profileModel.getUsername())
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
public void stopCurrentExecutor() {
|
||||
if (currentlyExecuting != null) {
|
||||
try {
|
||||
currentlyExecuting.cancel(true);
|
||||
} catch (final Exception e) {
|
||||
if (logCollector != null) logCollector.appendException(e, LogCollector.LogFile.MAIN_HELPER, "stopCurrentExecutor");
|
||||
Log.e(TAG, "", e);
|
||||
}
|
||||
}
|
||||
// currentlyExecuting = new PostsFetcher(profileModel.getId(), PostItemType.MAIN, endCursor, postsFetchListener)
|
||||
// .setUsername(profileModel.getUsername())
|
||||
// .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private boolean checkAndResetAction() {
|
||||
@ -887,4 +940,60 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void navigateToProfile(final String username) {
|
||||
final NavController navController = NavHostFragment.findNavController(this);
|
||||
final Bundle bundle = new Bundle();
|
||||
bundle.putString("username", username);
|
||||
navController.navigate(R.id.action_global_profileFragment, bundle);
|
||||
}
|
||||
|
||||
private void showDownloadDialog(final FeedModel feedModel) {
|
||||
final Context context = getContext();
|
||||
if (context == null) return;
|
||||
DownloadUtils.download(context, feedModel);
|
||||
// switch (feedModel.getItemType()) {
|
||||
// case MEDIA_TYPE_IMAGE:
|
||||
// case MEDIA_TYPE_VIDEO:
|
||||
// break;
|
||||
// case MEDIA_TYPE_SLIDER:
|
||||
// break;
|
||||
// }
|
||||
// final List<ViewerPostModel> postModelsToDownload = new ArrayList<>();
|
||||
// // if (!session) {
|
||||
// final DialogInterface.OnClickListener clickListener = (dialog, which) -> {
|
||||
// if (which == DialogInterface.BUTTON_NEGATIVE) {
|
||||
// postModelsToDownload.addAll(postModels);
|
||||
// } else if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||
// postModelsToDownload.add(postModels.get(childPosition));
|
||||
// } else {
|
||||
// session = true;
|
||||
// postModelsToDownload.add(postModels.get(childPosition));
|
||||
// }
|
||||
// if (postModelsToDownload.size() > 0) {
|
||||
// DownloadUtils.batchDownload(context,
|
||||
// username,
|
||||
// DownloadMethod.DOWNLOAD_POST_VIEWER,
|
||||
// postModelsToDownload);
|
||||
// }
|
||||
// };
|
||||
// new AlertDialog.Builder(context)
|
||||
// .setTitle(R.string.post_viewer_download_dialog_title)
|
||||
// .setMessage(R.string.post_viewer_download_message)
|
||||
// .setNeutralButton(R.string.post_viewer_download_session, clickListener)
|
||||
// .setPositiveButton(R.string.post_viewer_download_current, clickListener)
|
||||
// .setNegativeButton(R.string.post_viewer_download_album, clickListener).show();
|
||||
// } else {
|
||||
// DownloadUtils.batchDownload(context,
|
||||
// username,
|
||||
// DownloadMethod.DOWNLOAD_POST_VIEWER,
|
||||
// Collections.singletonList(postModels.get(childPosition)));
|
||||
}
|
||||
|
||||
private void showPostsLayoutPreferences() {
|
||||
final PostsLayoutPreferencesDialogFragment fragment = new PostsLayoutPreferencesDialogFragment(
|
||||
Constants.PREF_PROFILE_POSTS_LAYOUT,
|
||||
preferences -> new Handler().postDelayed(() -> binding.postsRecyclerView.setLayoutPreferences(preferences), 200));
|
||||
fragment.show(getChildFragmentManager(), "posts_layout_preferences");
|
||||
}
|
||||
}
|
||||
|
@ -15,10 +15,10 @@ public final class PostsLayoutPreferences {
|
||||
|
||||
public static class Builder {
|
||||
private PostsLayoutType type = PostsLayoutType.GRID;
|
||||
private int colCount = 2;
|
||||
private boolean isAvatarVisible = false;
|
||||
private int colCount = 3;
|
||||
private boolean isAvatarVisible = true;
|
||||
private boolean isNameVisible = false;
|
||||
private ProfilePicSize profilePicSize = ProfilePicSize.REGULAR;
|
||||
private ProfilePicSize profilePicSize = ProfilePicSize.SMALL;
|
||||
private boolean hasRoundedCorners = true;
|
||||
private boolean hasGap = true;
|
||||
|
||||
@ -87,6 +87,9 @@ public final class PostsLayoutPreferences {
|
||||
}
|
||||
|
||||
public Builder mergeFrom(final PostsLayoutPreferences preferences) {
|
||||
if (preferences == null) {
|
||||
return this;
|
||||
}
|
||||
setColCount(preferences.getColCount());
|
||||
setAvatarVisible(preferences.isAvatarVisible());
|
||||
setNameVisible(preferences.isNameVisible());
|
||||
|
@ -1,11 +1,17 @@
|
||||
package awais.instagrabber.repositories;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.QueryMap;
|
||||
|
||||
public interface ProfileRepository {
|
||||
|
||||
@GET("api/v1/users/{uid}/info/")
|
||||
Call<String> getUserInfo(@Path("uid") final String uid);
|
||||
|
||||
@GET("/graphql/query/")
|
||||
Call<String> fetch(@QueryMap Map<String, String> queryMap);
|
||||
}
|
||||
|
@ -4,12 +4,12 @@ import java.util.List;
|
||||
|
||||
import awais.instagrabber.models.FeedModel;
|
||||
|
||||
public class FeedFetchResponse {
|
||||
public class PostsFetchResponse {
|
||||
private List<FeedModel> feedModels;
|
||||
private boolean hasNextPage;
|
||||
private String nextCursor;
|
||||
|
||||
public FeedFetchResponse(final List<FeedModel> feedModels, final boolean hasNextPage, final String nextCursor) {
|
||||
public PostsFetchResponse(final List<FeedModel> feedModels, final boolean hasNextPage, final String nextCursor) {
|
||||
this.feedModels = feedModels;
|
||||
this.hasNextPage = hasNextPage;
|
||||
this.nextCursor = nextCursor;
|
@ -88,4 +88,5 @@ public final class Constants {
|
||||
public static final String DEFAULT_HASH_TAG_PIC = "https://www.instagram.com/static/images/hashtag/search-hashtag-default-avatar.png/1d8417c9a4f5.png";
|
||||
public static final String SHARED_PREFERENCES_NAME = "settings";
|
||||
public static final String PREF_POSTS_LAYOUT = "posts_layout";
|
||||
public static final String PREF_PROFILE_POSTS_LAYOUT = "profile_posts_layout";
|
||||
}
|
@ -31,6 +31,7 @@ import static awais.instagrabber.utils.Constants.MUTED_VIDEOS;
|
||||
import static awais.instagrabber.utils.Constants.PREF_DARK_THEME;
|
||||
import static awais.instagrabber.utils.Constants.PREF_LIGHT_THEME;
|
||||
import static awais.instagrabber.utils.Constants.PREF_POSTS_LAYOUT;
|
||||
import static awais.instagrabber.utils.Constants.PREF_PROFILE_POSTS_LAYOUT;
|
||||
import static awais.instagrabber.utils.Constants.PREV_INSTALL_VERSION;
|
||||
import static awais.instagrabber.utils.Constants.SHOW_QUICK_ACCESS_DIALOG;
|
||||
import static awais.instagrabber.utils.Constants.SKIPPED_VERSION;
|
||||
@ -114,7 +115,7 @@ public final class SettingsHelper {
|
||||
|
||||
@StringDef(
|
||||
{APP_LANGUAGE, APP_THEME, COOKIE, FOLDER_PATH, DATE_TIME_FORMAT, DATE_TIME_SELECTION, CUSTOM_DATE_TIME_FORMAT,
|
||||
DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME, PREF_POSTS_LAYOUT})
|
||||
DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME, PREF_POSTS_LAYOUT, PREF_PROFILE_POSTS_LAYOUT})
|
||||
public @interface StringSettings {}
|
||||
|
||||
@StringDef({DOWNLOAD_USER_FOLDER, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, SHOW_QUICK_ACCESS_DIALOG, MUTED_VIDEOS,
|
||||
|
@ -25,7 +25,7 @@ import awais.instagrabber.models.PostChild;
|
||||
import awais.instagrabber.models.ProfileModel;
|
||||
import awais.instagrabber.models.enums.MediaItemType;
|
||||
import awais.instagrabber.repositories.FeedRepository;
|
||||
import awais.instagrabber.repositories.responses.FeedFetchResponse;
|
||||
import awais.instagrabber.repositories.responses.PostsFetchResponse;
|
||||
import awais.instagrabber.utils.Constants;
|
||||
import awais.instagrabber.utils.ResponseBodyUtils;
|
||||
import awais.instagrabber.utils.TextUtils;
|
||||
@ -58,7 +58,7 @@ public class FeedService extends BaseService {
|
||||
|
||||
public void fetch(final int maxItemsToLoad,
|
||||
final String cursor,
|
||||
final ServiceCallback<FeedFetchResponse> callback) {
|
||||
final ServiceCallback<PostsFetchResponse> callback) {
|
||||
if (loadFromMock) {
|
||||
final Handler handler = new Handler();
|
||||
handler.postDelayed(() -> {
|
||||
@ -96,9 +96,9 @@ public class FeedService extends BaseService {
|
||||
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {
|
||||
try {
|
||||
// Log.d(TAG, "onResponse: body: " + response.body());
|
||||
final FeedFetchResponse feedFetchResponse = parseResponse(response);
|
||||
final PostsFetchResponse postsFetchResponse = parseResponse(response);
|
||||
if (callback != null) {
|
||||
callback.onSuccess(feedFetchResponse);
|
||||
callback.onSuccess(postsFetchResponse);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "onResponse", e);
|
||||
@ -119,16 +119,16 @@ public class FeedService extends BaseService {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private FeedFetchResponse parseResponse(@NonNull final Response<String> response) throws JSONException {
|
||||
private PostsFetchResponse parseResponse(@NonNull final Response<String> response) throws JSONException {
|
||||
if (TextUtils.isEmpty(response.body())) {
|
||||
Log.e(TAG, "parseResponse: feed response body is empty with status code: " + response.code());
|
||||
return new FeedFetchResponse(Collections.emptyList(), false, null);
|
||||
return new PostsFetchResponse(Collections.emptyList(), false, null);
|
||||
}
|
||||
return parseResponseBody(response.body());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private FeedFetchResponse parseResponseBody(@NonNull final String body)
|
||||
private PostsFetchResponse parseResponseBody(@NonNull final String body)
|
||||
throws JSONException {
|
||||
final List<FeedModel> feedModels = new ArrayList<>();
|
||||
final JSONObject timelineFeed = new JSONObject(body)
|
||||
@ -161,7 +161,6 @@ public class FeedService extends BaseService {
|
||||
final String displayUrl = feedItem.optString("display_url");
|
||||
if (TextUtils.isEmpty(displayUrl)) continue;
|
||||
final String resourceUrl;
|
||||
|
||||
if (isVideo) {
|
||||
resourceUrl = feedItem.getString("video_url");
|
||||
} else {
|
||||
@ -265,7 +264,7 @@ public class FeedService extends BaseService {
|
||||
final FeedModel feedModel = feedModelBuilder.build();
|
||||
feedModels.add(feedModel);
|
||||
}
|
||||
return new FeedFetchResponse(feedModels, hasNextPage, endCursor);
|
||||
return new PostsFetchResponse(feedModels, hasNextPage, endCursor);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
@ -4,12 +4,26 @@ import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import awais.instagrabber.models.FeedModel;
|
||||
import awais.instagrabber.models.PostChild;
|
||||
import awais.instagrabber.models.ProfileModel;
|
||||
import awais.instagrabber.models.enums.MediaItemType;
|
||||
import awais.instagrabber.repositories.ProfileRepository;
|
||||
import awais.instagrabber.repositories.responses.PostsFetchResponse;
|
||||
import awais.instagrabber.repositories.responses.UserInfo;
|
||||
import awais.instagrabber.utils.Constants;
|
||||
import awais.instagrabber.utils.ResponseBodyUtils;
|
||||
import awais.instagrabber.utils.TextUtils;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
@ -19,6 +33,7 @@ public class ProfileService extends BaseService {
|
||||
private static final String TAG = "ProfileService";
|
||||
|
||||
private final ProfileRepository repository;
|
||||
private final ProfileRepository wwwRepository;
|
||||
|
||||
private static ProfileService instance;
|
||||
|
||||
@ -26,7 +41,11 @@ public class ProfileService extends BaseService {
|
||||
final Retrofit retrofit = getRetrofitBuilder()
|
||||
.baseUrl("https://i.instagram.com")
|
||||
.build();
|
||||
final Retrofit wwwRetrofit = getRetrofitBuilder()
|
||||
.baseUrl("https://www.instagram.com")
|
||||
.build();
|
||||
repository = retrofit.create(ProfileRepository.class);
|
||||
wwwRepository = wwwRetrofit.create(ProfileRepository.class);
|
||||
}
|
||||
|
||||
public static ProfileService getInstance() {
|
||||
@ -66,4 +85,201 @@ public class ProfileService extends BaseService {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public void fetchPosts(final ProfileModel profileModel,
|
||||
final int postsPerPage,
|
||||
final String cursor,
|
||||
final ServiceCallback<PostsFetchResponse> callback) {
|
||||
final Map<String, String> queryMap = new HashMap<>();
|
||||
queryMap.put("query_hash", "18a7b935ab438c4514b1f742d8fa07a7");
|
||||
queryMap.put("variables", "{" +
|
||||
"\"id\":\"" + profileModel.getId() + "\"," +
|
||||
"\"first\":" + postsPerPage + "," +
|
||||
"\"after\":\"" + (cursor == null ? "" : cursor) + "\"" +
|
||||
"}");
|
||||
final Call<String> request = wwwRepository.fetch(queryMap);
|
||||
request.enqueue(new Callback<String>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {
|
||||
try {
|
||||
// Log.d(TAG, "onResponse: body: " + response.body());
|
||||
final PostsFetchResponse postsFetchResponse = parseResponse(profileModel, response);
|
||||
if (callback != null) {
|
||||
callback.onSuccess(postsFetchResponse);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "onResponse", e);
|
||||
if (callback != null) {
|
||||
callback.onFailure(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) {
|
||||
if (callback != null) {
|
||||
callback.onFailure(t);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private PostsFetchResponse parseResponse(final ProfileModel profileModel, final Response<String> response) throws JSONException {
|
||||
if (TextUtils.isEmpty(response.body())) {
|
||||
Log.e(TAG, "parseResponse: feed response body is empty with status code: " + response.code());
|
||||
return new PostsFetchResponse(Collections.emptyList(), false, null);
|
||||
}
|
||||
return parseResponseBody(profileModel, response.body());
|
||||
}
|
||||
|
||||
private PostsFetchResponse parseResponseBody(final ProfileModel profileModel, final String body) throws JSONException {
|
||||
// Log.d(TAG, "parseResponseBody: body: " + body);
|
||||
final List<FeedModel> feedModels = new ArrayList<>();
|
||||
// return new FeedFetchResponse(feedModels, false, null);
|
||||
final JSONObject mediaPosts = new JSONObject(body)
|
||||
.getJSONObject("data")
|
||||
.getJSONObject(Constants.EXTRAS_USER)
|
||||
.getJSONObject("edge_owner_to_timeline_media");
|
||||
final String endCursor;
|
||||
final boolean hasNextPage;
|
||||
final JSONObject pageInfo = mediaPosts.getJSONObject("page_info");
|
||||
if (pageInfo.has("has_next_page")) {
|
||||
hasNextPage = pageInfo.getBoolean("has_next_page");
|
||||
endCursor = hasNextPage ? pageInfo.getString("end_cursor") : null;
|
||||
} else {
|
||||
hasNextPage = false;
|
||||
endCursor = null;
|
||||
}
|
||||
final JSONArray edges = mediaPosts.getJSONArray("edges");
|
||||
for (int i = 0; i < edges.length(); ++i) {
|
||||
final JSONObject mediaNode = edges.getJSONObject(i).getJSONObject("node");
|
||||
final String mediaType = mediaNode.optString("__typename");
|
||||
if (mediaType.isEmpty() || "GraphSuggestedUserFeedUnit".equals(mediaType))
|
||||
continue;
|
||||
final boolean isVideo = mediaNode.getBoolean("is_video");
|
||||
final long videoViews = mediaNode.optLong("video_view_count", 0);
|
||||
|
||||
final String displayUrl = mediaNode.optString("display_url");
|
||||
if (TextUtils.isEmpty(displayUrl)) continue;
|
||||
final String resourceUrl;
|
||||
|
||||
if (isVideo) {
|
||||
resourceUrl = mediaNode.getString("video_url");
|
||||
} else {
|
||||
resourceUrl = mediaNode.has("display_resources") ? ResponseBodyUtils.getHighQualityImage(mediaNode) : displayUrl;
|
||||
}
|
||||
JSONObject tempJsonObject = mediaNode.optJSONObject("edge_media_preview_comment");
|
||||
final long commentsCount = tempJsonObject != null ? tempJsonObject.optLong("count") : 0;
|
||||
tempJsonObject = mediaNode.optJSONObject("edge_media_preview_like");
|
||||
final long likesCount = tempJsonObject != null ? tempJsonObject.optLong("count") : 0;
|
||||
tempJsonObject = mediaNode.optJSONObject("edge_media_to_caption");
|
||||
final JSONArray captions = tempJsonObject != null ? tempJsonObject.getJSONArray("edges") : null;
|
||||
String captionText = null;
|
||||
if (captions != null && captions.length() > 0) {
|
||||
if ((tempJsonObject = captions.optJSONObject(0)) != null &&
|
||||
(tempJsonObject = tempJsonObject.optJSONObject("node")) != null) {
|
||||
captionText = tempJsonObject.getString("text");
|
||||
}
|
||||
}
|
||||
final JSONObject location = mediaNode.optJSONObject("location");
|
||||
// Log.d(TAG, "location: " + (location == null ? null : location.toString()));
|
||||
String locationId = null;
|
||||
String locationName = null;
|
||||
if (location != null) {
|
||||
locationName = location.optString("name");
|
||||
if (location.has("id")) {
|
||||
locationId = location.getString("id");
|
||||
} else if (location.has("pk")) {
|
||||
locationId = location.getString("pk");
|
||||
}
|
||||
// Log.d(TAG, "locationId: " + locationId);
|
||||
}
|
||||
int height = 0;
|
||||
int width = 0;
|
||||
final JSONObject dimensions = mediaNode.optJSONObject("dimensions");
|
||||
if (dimensions != null) {
|
||||
height = dimensions.optInt("height");
|
||||
width = dimensions.optInt("width");
|
||||
}
|
||||
String thumbnailUrl = null;
|
||||
try {
|
||||
thumbnailUrl = mediaNode.getJSONArray("display_resources")
|
||||
.getJSONObject(0)
|
||||
.getString("src");
|
||||
} catch (JSONException ignored) {}
|
||||
final FeedModel.Builder builder = new FeedModel.Builder()
|
||||
.setProfileModel(profileModel)
|
||||
.setItemType(isVideo ? MediaItemType.MEDIA_TYPE_VIDEO
|
||||
: MediaItemType.MEDIA_TYPE_IMAGE)
|
||||
.setViewCount(videoViews)
|
||||
.setPostId(mediaNode.getString(Constants.EXTRAS_ID))
|
||||
.setDisplayUrl(resourceUrl)
|
||||
.setThumbnailUrl(thumbnailUrl != null ? thumbnailUrl : displayUrl)
|
||||
.setShortCode(mediaNode.getString(Constants.EXTRAS_SHORTCODE))
|
||||
.setPostCaption(captionText)
|
||||
.setCommentsCount(commentsCount)
|
||||
.setTimestamp(mediaNode.optLong("taken_at_timestamp", -1))
|
||||
.setLiked(mediaNode.getBoolean("viewer_has_liked"))
|
||||
.setBookmarked(mediaNode.getBoolean("viewer_has_saved"))
|
||||
.setLikesCount(likesCount)
|
||||
.setLocationName(locationName)
|
||||
.setLocationId(locationId)
|
||||
.setImageHeight(height)
|
||||
.setImageWidth(width);
|
||||
final boolean isSlider = "GraphSidecar".equals(mediaType) && mediaNode.has("edge_sidecar_to_children");
|
||||
if (isSlider) {
|
||||
builder.setItemType(MediaItemType.MEDIA_TYPE_SLIDER);
|
||||
final JSONObject sidecar = mediaNode.optJSONObject("edge_sidecar_to_children");
|
||||
if (sidecar != null) {
|
||||
final JSONArray children = sidecar.optJSONArray("edges");
|
||||
if (children != null) {
|
||||
final List<PostChild> sliderItems = getSliderItems(children);
|
||||
builder.setSliderItems(sliderItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
final FeedModel feedModel = builder.build();
|
||||
feedModels.add(feedModel);
|
||||
// DownloadUtils.checkExistence(downloadDir, customDir, isSlider, model);
|
||||
}
|
||||
return new PostsFetchResponse(feedModels, hasNextPage, endCursor);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<PostChild> getSliderItems(final JSONArray children) throws JSONException {
|
||||
final List<PostChild> sliderItems = new ArrayList<>();
|
||||
for (int j = 0; j < children.length(); ++j) {
|
||||
final JSONObject childNode = children.optJSONObject(j).getJSONObject("node");
|
||||
final boolean isChildVideo = childNode.optBoolean("is_video");
|
||||
int height = 0;
|
||||
int width = 0;
|
||||
final JSONObject dimensions = childNode.optJSONObject("dimensions");
|
||||
if (dimensions != null) {
|
||||
height = dimensions.optInt("height");
|
||||
width = dimensions.optInt("width");
|
||||
}
|
||||
String thumbnailUrl = null;
|
||||
try {
|
||||
thumbnailUrl = childNode.getJSONArray("display_resources")
|
||||
.getJSONObject(0)
|
||||
.getString("src");
|
||||
} catch (JSONException ignored) {}
|
||||
final PostChild sliderItem = new PostChild.Builder()
|
||||
.setItemType(isChildVideo ? MediaItemType.MEDIA_TYPE_VIDEO
|
||||
: MediaItemType.MEDIA_TYPE_IMAGE)
|
||||
.setPostId(childNode.getString(Constants.EXTRAS_ID))
|
||||
.setDisplayUrl(isChildVideo ? childNode.getString("video_url")
|
||||
: childNode.getString("display_url"))
|
||||
.setThumbnailUrl(thumbnailUrl != null ? thumbnailUrl
|
||||
: childNode.getString("display_url"))
|
||||
.setVideoViews(childNode.optLong("video_view_count", 0))
|
||||
.setHeight(height)
|
||||
.setWidth(width)
|
||||
.build();
|
||||
// Log.d(TAG, "getSliderItems: sliderItem: " + sliderItem);
|
||||
sliderItems.add(sliderItem);
|
||||
}
|
||||
return sliderItems;
|
||||
}
|
||||
}
|
||||
|
@ -37,10 +37,4 @@
|
||||
android:clipToPadding="false"
|
||||
tools:listitem="@layout/item_feed_photo" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<!--<androidx.fragment.app.FragmentContainerView-->
|
||||
<!-- android:id="@+id/frag_container"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="match_parent"-->
|
||||
<!-- app:layout_behavior="@string/appbar_scrolling_view_behavior" />-->
|
||||
</awais.instagrabber.customviews.helpers.NestedCoordinatorLayout>
|
@ -272,19 +272,33 @@
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:id="@+id/swipe_refresh_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/mainPosts"
|
||||
<awais.instagrabber.customviews.PostsRecyclerView
|
||||
android:id="@+id/posts_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
tools:listitem="@layout/item_post" />
|
||||
tools:listitem="@layout/item_feed_photo" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<!--<androidx.swiperefreshlayout.widget.SwipeRefreshLayout-->
|
||||
<!-- android:id="@+id/swipeRefreshLayout"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="match_parent"-->
|
||||
<!-- app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">-->
|
||||
|
||||
<!-- <androidx.recyclerview.widget.RecyclerView-->
|
||||
<!-- android:id="@+id/mainPosts"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="match_parent"-->
|
||||
<!-- android:clipToPadding="false"-->
|
||||
<!-- tools:listitem="@layout/item_post" />-->
|
||||
<!--</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>-->
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/privatePage"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -3,6 +3,6 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/layout"
|
||||
android:title="Layout"
|
||||
android:title="@string/layout"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
@ -1,13 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<!--<item-->
|
||||
<!-- android:id="@+id/favourites"-->
|
||||
<!-- android:enabled="true"-->
|
||||
<!-- android:icon="@drawable/ic_star_24"-->
|
||||
<!-- android:title="@string/title_favorites"-->
|
||||
<!-- android:visible="false"-->
|
||||
<!-- app:showAsAction="ifRoom" />-->
|
||||
|
||||
<item
|
||||
android:id="@+id/layout"
|
||||
android:title="@string/layout"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/block"
|
||||
|
@ -324,6 +324,7 @@
|
||||
<string name="downloader_downloading_child">Download item %d of %d</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="comment">Comment</string>
|
||||
<string name="layout">Layout</string>
|
||||
<plurals name="likes_count">
|
||||
<item quantity="one">%d like</item>
|
||||
<item quantity="other">%d likes</item>
|
||||
|
Loading…
Reference in New Issue
Block a user