diff --git a/.all-contributorsrc b/.all-contributorsrc index 1bc26b73..3e8bb4a7 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -61,6 +61,15 @@ "translation" ] }, + { + "login": "CrazyMarvin", + "name": "CrazyMarvin", + "avatar_url": "https://avatars3.githubusercontent.com/u/15004217?v=4", + "profile": "https://github.com/CrazyMarvin", + "contributions": [ + "financial" + ] + }, { "login": "KevinNThomas", "name": "Kevin Thomas", @@ -82,16 +91,6 @@ "question" ] }, - { - "login": "e-edgren", - "name": "Airikr", - "avatar_url": "https://avatars0.githubusercontent.com/u/53869451", - "profile": "https://airikr.me/", - "contributions": [ - "ideas", - "question" - ] - }, { "login": "alin-1", "name": "ALIN", @@ -102,6 +101,26 @@ "ideas" ] }, + { + "login": "RickyM7", + "name": "Ricardo", + "avatar_url": "https://avatars3.githubusercontent.com/u/24703825?v=4", + "profile": "https://github.com/RickyM7", + "contributions": [ + "bug", + "translation" + ] + }, + { + "login": "e-edgren", + "name": "Airikr", + "avatar_url": "https://avatars0.githubusercontent.com/u/53869451", + "profile": "https://airikr.me/", + "contributions": [ + "ideas", + "question" + ] + }, { "login": "Akrai", "name": "Akrai", @@ -112,6 +131,15 @@ "translation" ] }, + { + "login": "cizordj", + "name": "Cézar Augusto", + "avatar_url": "https://avatars2.githubusercontent.com/u/32869222?v=4", + "profile": "https://github.com/cizordj", + "contributions": [ + "translation" + ] + }, { "login": "farzadx", "name": "farzadx", @@ -149,19 +177,28 @@ ] }, { - "login": "kernoeb", - "name": "kernoeb", - "avatar_url": "https://avatars3.githubusercontent.com/u/24623168", - "profile": "https://becauseofprog.fr/", + "login": "initdebugs", + "name": "Initdebugs", + "avatar_url": "https://avatars0.githubusercontent.com/u/75781464?v=4", + "profile": "https://github.com/initdebugs", "contributions": [ "translation" ] }, { - "login": "Lego8486", - "name": "Ten_Lego", - "avatar_url": "https://avatars1.githubusercontent.com/u/47414485", - "profile": "https://github.com/Lego8486", + "login": "CrafterSvK", + "name": "Jakub Janek", + "avatar_url": "https://avatars3.githubusercontent.com/u/8365659?v=4", + "profile": "https://janek.xyz/", + "contributions": [ + "translation" + ] + }, + { + "login": "kernoeb", + "name": "kernoeb", + "avatar_url": "https://avatars3.githubusercontent.com/u/24623168", + "profile": "https://becauseofprog.fr/", "contributions": [ "translation" ] @@ -229,16 +266,6 @@ "translation" ] }, - { - "login": "RickyM7", - "name": "Ricardo", - "avatar_url": "https://avatars3.githubusercontent.com/u/24703825?v=4", - "profile": "https://github.com/RickyM7", - "contributions": [ - "bug", - "translation" - ] - }, { "login": "rikishi0071", "name": "rikishi0071", @@ -257,6 +284,15 @@ "translation" ] }, + { + "login": "Lego8486", + "name": "Ten_Lego", + "avatar_url": "https://avatars1.githubusercontent.com/u/47414485", + "profile": "https://github.com/Lego8486", + "contributions": [ + "translation" + ] + }, { "login": "wagnim", "name": "wagnim", @@ -265,6 +301,15 @@ "contributions": [ "translation" ] + }, + { + "login": "ysakamoto", + "name": "ysakamoto", + "avatar_url": "https://avatars3.githubusercontent.com/u/1331642?v=4", + "profile": "https://github.com/ysakamoto", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 6, diff --git a/README.md b/README.md index 3e0e5552..1fe4a522 100755 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com) [![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](./LICENSE) [![GitHub stars](https://img.shields.io/github/stars/austinhuang0131/instagrabber.svg?style=social&label=Star)](https://GitHub.com/austinhuang0131/barinsta/stargazers/) -[![All Contributors](https://img.shields.io/badge/all_contributors-27-orange.svg)](#contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-32-orange.svg)](#contributors) We're previously known as InstaGrabber. @@ -32,11 +32,15 @@ Version status: ![F-Droid](https://img.shields.io/f-droid/v/me.austinhuang.insta Hashtag Discover Topics +## We need maintainers! + +To speed up development, we need more hands on deck. If you are proficient in Java and Android development, and are willing to perform such a public service, please [contact us](https://t.me/austinhuang). + ## Contact us * Use [GitHub issues](https://github.com/austinhuang0131/instagrabber/issues) when possible. * Email: [Barinsta@AustinHuang.me](mailto:barinsta@austinhuang.me?body=Please%20note%20that%20your%20email%20address%20and%20the%20entire%20content%20will%20be%20published%20onto%20GitHub%20issues.%20If%20you%20do%20not%20wish%20to%20do%20that%2C%20use%20other%20contact%20methods%20instead.) (Synced to GitHub issues) -* Reddit: [r/Barinsta](https://reddit.com/r/barinsta) +* Reddit: [![r/Barinsta](https://img.shields.io/reddit/subreddit-subscribers/Barinsta?style=social)](https://reddit.com/r/barinsta) * Chat (Bridged to each other): [![Matrix](https://img.shields.io/badge/Matrix-%23Barinsta:matrix.org-000000?logo=matrix)](https://matrix.to/#/#barinsta:matrix.org) [![Telegram](https://img.shields.io/badge/Telegram-@Grabber__App-2CA5E0?logo=telegram)](https://t.me/grabber_app) [![Discord](https://img.shields.io/badge/Discord-YtEDzN2-7289da?logo=discord&logoColor=white)](https://discord.gg/YtEDzN2) [![Gitter](https://img.shields.io/badge/Gitter-InstaGrabber/General-ed1965?logo=gitter)](https://gitter.im/instagrabber/general) ## Contributors @@ -53,36 +57,43 @@ Prominent contributors are listed here in the [all-contributors](https://allcont
Anderson Mesquita

💻 🐛
AWAiS

💻
Stefan Najdovski

🎨 🌍 -
Kevin Thomas

💵 +
CrazyMarvin

💵 +
Kevin Thomas

💵
Shadowspear123

📝 🐛 🤔 💬 -
Airikr

🤔 💬
ALIN

🐛 🤔 +
Ricardo

🐛 🌍 +
Airikr

🤔 💬
Akrai

🤔 🌍 + + +
Cézar Augusto

🌍
farzadx

🌍
Fatih Aydın

🌍 - -
fouze555

🌍
Galang23

🌍 -
kernoeb

🌍 -
Ten_Lego

🌍 -
MoaufmKlo

🌍 -
nalinalini

🌍 +
Initdebugs

🌍 +
Jakub Janek

🌍 +
kernoeb

🌍 +
MoaufmKlo

🌍 +
nalinalini

🌍
peterge1998

🌍
PierreM0

🌍 + +
RAMAR-RAR

🌍
rohang02

🌍
retiolus

🌍 -
Ricardo

🐛 🌍 - -
rikishi0071

🌍
Still Hsu

🌍 +
Ten_Lego

🌍 + +
wagnim

🌍 +
ysakamoto

🌍 @@ -92,12 +103,11 @@ Prominent contributors are listed here in the [all-contributors](https://allcont ## License -This app is originally made by [@AwaisKing](https://github.com/AwaisKing) on [GitLab](https://gitlab.com/AwaisKing/instagrabber). +This app's predecessor, InstaGrabber, is originally made by [@AwaisKing](https://github.com/AwaisKing) on [GitLab](https://gitlab.com/AwaisKing/instagrabber). - Barinsta (ex-InstaGrabber) - Copyright (C) 2019 AWAiS - Copyright (C) 2020 Austin Huang - Ammar Githam + Barinsta + Copyright (C) 2020-2021 Austin Huang + Ammar Githam This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/app/build.gradle b/app/build.gradle index 01d39a3a..8705ac87 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { minSdkVersion 21 targetSdkVersion 29 - versionCode 54 - versionName '19.0.2' + versionCode 56 + versionName '19.0.4' multiDexEnabled true @@ -98,6 +98,9 @@ dependencies { implementation "androidx.emoji:emoji:$emoji_compat_version" implementation "androidx.emoji:emoji-appcompat:$emoji_compat_version" + // implementation 'com.github.hendrawd:StorageUtil:1.1.0' + implementation 'me.austinhuang:AutoLinkTextViewV2:-SNAPSHOT' + implementation 'org.jsoup:jsoup:1.13.1' implementation 'com.facebook.fresco:fresco:2.3.0' implementation 'com.facebook.fresco:animated-webp:2.3.0' @@ -109,7 +112,6 @@ dependencies { implementation 'org.apache.commons:commons-imaging:1.0-alpha2' implementation 'com.ibm.icu:icu4j:68.1' - implementation 'com.github.ammargitham:AutoLinkTextViewV2:master-SNAPSHOT' implementation 'com.github.ammargitham:uCrop:2.3-native-beta-2' implementation 'com.github.ammargitham:android-gpuimage:2.1.1-beta4' diff --git a/app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java index e0ccb44e..d88e5f34 100755 --- a/app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java @@ -83,8 +83,8 @@ public final class CommentsAdapter extends ListAdapter { private final OnFeedStoryClickListener listener; @@ -22,7 +29,7 @@ public final class FeedStoriesAdapter extends ListAdapter { + private final OnFeedStoryClickListener listener; + + private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull final FeedStoryModel oldItem, @NonNull final FeedStoryModel newItem) { + return oldItem.getStoryMediaId().equals(newItem.getStoryMediaId()); + } + + @Override + public boolean areContentsTheSame(@NonNull final FeedStoryModel oldItem, @NonNull final FeedStoryModel newItem) { + return oldItem.getStoryMediaId().equals(newItem.getStoryMediaId()) && oldItem.isFullyRead().equals(newItem.isFullyRead()); + } + }; + + public FeedStoriesListAdapter(final OnFeedStoryClickListener listener) { + super(diffCallback); + this.listener = listener; + } + + @NonNull + @Override + public StoryListViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { + final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); + final ItemNotificationBinding binding = ItemNotificationBinding.inflate(layoutInflater, parent, false); + return new StoryListViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull final StoryListViewHolder holder, final int position) { + final FeedStoryModel model = getItem(position); + holder.bind(model, position, listener); + } + + public interface OnFeedStoryClickListener { + void onFeedStoryClick(final FeedStoryModel model, final int position); + + void onProfileClick(final String username); + } +} diff --git a/app/src/main/java/awais/instagrabber/adapters/HighlightStoriesListAdapter.java b/app/src/main/java/awais/instagrabber/adapters/HighlightStoriesListAdapter.java new file mode 100755 index 00000000..9547b9c1 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/HighlightStoriesListAdapter.java @@ -0,0 +1,53 @@ +package awais.instagrabber.adapters; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; + +import awais.instagrabber.adapters.viewholder.StoryListViewHolder; +import awais.instagrabber.databinding.ItemNotificationBinding; +import awais.instagrabber.models.HighlightModel; + +public final class HighlightStoriesListAdapter extends ListAdapter { + private final OnHighlightStoryClickListener listener; + + private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull final HighlightModel oldItem, @NonNull final HighlightModel newItem) { + return oldItem.getId().equals(newItem.getId()); + } + + @Override + public boolean areContentsTheSame(@NonNull final HighlightModel oldItem, @NonNull final HighlightModel newItem) { + return oldItem.getId().equals(newItem.getId()); + } + }; + + public HighlightStoriesListAdapter(final OnHighlightStoryClickListener listener) { + super(diffCallback); + this.listener = listener; + } + + @NonNull + @Override + public StoryListViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { + final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); + final ItemNotificationBinding binding = ItemNotificationBinding.inflate(layoutInflater, parent, false); + return new StoryListViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull final StoryListViewHolder holder, final int position) { + final HighlightModel model = getItem(position); + holder.bind(model, position, listener); + } + + public interface OnHighlightStoryClickListener { + void onHighlightClick(final HighlightModel model, final int position); + + void onProfileClick(final String username); + } +} diff --git a/app/src/main/java/awais/instagrabber/adapters/LikesAdapter.java b/app/src/main/java/awais/instagrabber/adapters/LikesAdapter.java new file mode 100755 index 00000000..5592ee6d --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/LikesAdapter.java @@ -0,0 +1,45 @@ +package awais.instagrabber.adapters; + +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +import awais.instagrabber.adapters.viewholder.FollowsViewHolder; +import awais.instagrabber.databinding.ItemFollowBinding; +import awais.instagrabber.models.ProfileModel; + +public final class LikesAdapter extends RecyclerView.Adapter { + private final List profileModels; + private final View.OnClickListener onClickListener; + + public LikesAdapter(final List profileModels, + final View.OnClickListener onClickListener) { + this.profileModels = profileModels; + this.onClickListener = onClickListener; + } + + @NonNull + @Override + public FollowsViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { + final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); + final ItemFollowBinding binding = ItemFollowBinding.inflate(layoutInflater, parent, false); + return new FollowsViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull final FollowsViewHolder holder, final int position) { + final ProfileModel model = profileModels.get(position); + holder.bind(model, null, onClickListener); + } + + @Override + public int getItemCount() { + return profileModels.size(); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/NotificationsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/NotificationsAdapter.java index 10e5867a..124abe73 100644 --- a/app/src/main/java/awais/instagrabber/adapters/NotificationsAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/NotificationsAdapter.java @@ -76,16 +76,23 @@ public final class NotificationsAdapter extends ListAdapter sort(final List list) { final List listCopy = new ArrayList<>(list); Collections.sort(listCopy, (o1, o2) -> { - if (o1.getType() == o2.getType()) return 0; // keep requests at top - if (o1.getType() == NotificationType.REQUEST) return -1; - if (o2.getType() == NotificationType.REQUEST) return 1; - return 0; + if (o1.getType() == o2.getType() + && o1.getType() == NotificationType.REQUEST + && o2.getType() == NotificationType.REQUEST) return 0; + else if (o1.getType() == NotificationType.REQUEST) return -1; + else if (o2.getType() == NotificationType.REQUEST) return 1; + // timestamp + return o1.getTimestamp() > o2.getTimestamp() ? -1 : (o1.getTimestamp() == o2.getTimestamp() ? 0 : 1); }); return listCopy; } public interface OnNotificationClickListener { void onNotificationClick(final NotificationModel model); + + void onProfileClick(final String username); + + void onPreviewClick(final NotificationModel model); } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/PostViewAdapter.java b/app/src/main/java/awais/instagrabber/adapters/PostViewAdapter.java deleted file mode 100644 index f49b8974..00000000 --- a/app/src/main/java/awais/instagrabber/adapters/PostViewAdapter.java +++ /dev/null @@ -1,75 +0,0 @@ -// package awais.instagrabber.adapters; -// -// import android.view.LayoutInflater; -// import android.view.View; -// import android.view.ViewGroup; -// -// import androidx.annotation.NonNull; -// import androidx.recyclerview.widget.DiffUtil; -// import androidx.recyclerview.widget.ListAdapter; -// -// import awais.instagrabber.adapters.viewholder.PostViewerViewHolder; -// import awais.instagrabber.databinding.ItemFullPostViewBinding; -// import awais.instagrabber.interfaces.MentionClickListener; -// import awais.instagrabber.models.ViewerPostModelWrapper; -// -// public class PostViewAdapter extends ListAdapter { -// private final OnPostViewChildViewClickListener clickListener; -// private final OnPostCaptionLongClickListener longClickListener; -// private final MentionClickListener mentionClickListener; -// -// private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() { -// @Override -// public boolean areItemsTheSame(@NonNull final ViewerPostModelWrapper oldItem, -// @NonNull final ViewerPostModelWrapper newItem) { -// return oldItem.getPosition() == newItem.getPosition(); -// } -// -// @Override -// public boolean areContentsTheSame(@NonNull final ViewerPostModelWrapper oldItem, -// @NonNull final ViewerPostModelWrapper newItem) { -// return oldItem.getViewerPostModels().equals(newItem.getViewerPostModels()); -// } -// }; -// -// public PostViewAdapter(final OnPostViewChildViewClickListener clickListener, -// final OnPostCaptionLongClickListener longClickListener, -// final MentionClickListener mentionClickListener) { -// super(diffCallback); -// this.clickListener = clickListener; -// this.longClickListener = longClickListener; -// this.mentionClickListener = mentionClickListener; -// } -// -// @Override -// public void onViewDetachedFromWindow(@NonNull final PostViewerViewHolder holder) { -// holder.stopPlayingVideo(); -// } -// -// @NonNull -// @Override -// public PostViewerViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, -// final int viewType) { -// final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); -// final ItemFullPostViewBinding binding = ItemFullPostViewBinding -// .inflate(layoutInflater, parent, false); -// return new PostViewerViewHolder(binding); -// } -// -// @Override -// public void onBindViewHolder(@NonNull final PostViewerViewHolder holder, final int position) { -// final ViewerPostModelWrapper item = getItem(position); -// holder.bind(item, position, clickListener, longClickListener, mentionClickListener); -// } -// -// public interface OnPostViewChildViewClickListener { -// void onClick(View v, -// ViewerPostModelWrapper viewerPostModelWrapper, -// int postPosition, -// int childPosition); -// } -// -// public interface OnPostCaptionLongClickListener { -// void onLongClick(String text); -// } -// } diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedStoryViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedStoryViewHolder.java index 10c00e69..76b309e9 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedStoryViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedStoryViewHolder.java @@ -24,10 +24,14 @@ public final class FeedStoryViewHolder extends RecyclerView.ViewHolder { if (listener == null) return; listener.onFeedStoryClick(model, position); }); + binding.getRoot().setOnLongClickListener(v -> { + if (listener != null) listener.onFeedStoryLongClick(model, position); + return true; + }); final ProfileModel profileModel = model.getProfileModel(); binding.title.setText(profileModel.getUsername()); - binding.title.setAlpha(model.getFullyRead() ? 0.5F : 1.0F); + binding.title.setAlpha(model.isFullyRead() ? 0.5F : 1.0F); binding.icon.setImageURI(profileModel.getSdProfilePic()); - binding.icon.setAlpha(model.getFullyRead() ? 0.5F : 1.0F); + binding.icon.setAlpha(model.isFullyRead() ? 0.5F : 1.0F); } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java index a9b09de0..1f38cecb 100755 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java @@ -1,5 +1,6 @@ package awais.instagrabber.adapters.viewholder; +import android.util.Log; import android.view.View; import androidx.recyclerview.widget.RecyclerView; diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/NotificationViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/NotificationViewHolder.java index ac1ef257..1c49dbee 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/NotificationViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/NotificationViewHolder.java @@ -24,10 +24,6 @@ public final class NotificationViewHolder extends RecyclerView.ViewHolder { public void bind(final NotificationModel model, final OnNotificationClickListener notificationClickListener) { if (model == null) return; - itemView.setOnClickListener(v -> { - if (notificationClickListener == null) return; - notificationClickListener.onNotificationClick(model); - }); int text = -1; CharSequence subtext = null; switch (model.getType()) { @@ -52,20 +48,54 @@ public final class NotificationViewHolder extends RecyclerView.ViewHolder { text = R.string.request_notif; subtext = model.getText(); break; + case COMMENT_LIKE: + case TAGGED_COMMENT: + case RESPONDED_STORY: + subtext = model.getText(); + break; + case AYML: + subtext = model.getPostId(); + break; } - binding.tvUsername.setText(model.getUsername()); - binding.tvComment.setText(text); - binding.tvSubComment.setText(subtext, subtext instanceof Spannable ? TextView.BufferType.SPANNABLE : TextView.BufferType.NORMAL); - // binding.tvSubComment.setMentionClickListener(mentionClickListener); - if (model.getType() != NotificationType.REQUEST) { + binding.tvSubComment.setText(model.getType() == NotificationType.AYML ? model.getText() : subtext); + if (text == -1 && subtext != null) { + binding.tvComment.setText(subtext); + binding.tvComment.setVisibility(TextUtils.isEmpty(subtext) ? View.GONE : View.VISIBLE); + binding.tvSubComment.setVisibility(model.getType() == NotificationType.AYML ? View.VISIBLE : View.GONE); + } + else if (text != -1) { + binding.tvComment.setText(text); + binding.tvSubComment.setVisibility(subtext == null ? View.GONE : View.VISIBLE); + } + + if (model.getType() != NotificationType.REQUEST && model.getType() != NotificationType.AYML) { binding.tvDate.setText(model.getDateTime()); } + + binding.tvUsername.setText(model.getUsername()); binding.ivProfilePic.setImageURI(model.getProfilePic()); - if (TextUtils.isEmpty(model.getPreviewPic())) { + binding.ivProfilePic.setOnClickListener(v -> { + if (notificationClickListener == null) return; + notificationClickListener.onProfileClick(model.getUsername()); + }); + + if (model.getType() == NotificationType.AYML) { binding.ivPreviewPic.setVisibility(View.GONE); + } + else if (TextUtils.isEmpty(model.getPreviewPic())) { + binding.ivPreviewPic.setVisibility(View.INVISIBLE); } else { binding.ivPreviewPic.setVisibility(View.VISIBLE); binding.ivPreviewPic.setImageURI(model.getPreviewPic()); + binding.ivPreviewPic.setOnClickListener(v -> { + if (notificationClickListener == null) return; + notificationClickListener.onPreviewClick(model); + }); } + + itemView.setOnClickListener(v -> { + if (notificationClickListener == null) return; + notificationClickListener.onNotificationClick(model); + }); } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/PostViewerViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/PostViewerViewHolder.java deleted file mode 100644 index dcb2f9fc..00000000 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/PostViewerViewHolder.java +++ /dev/null @@ -1,240 +0,0 @@ -// package awais.instagrabber.adapters.viewholder; -// -// import android.content.res.ColorStateList; -// import android.content.res.Resources; -// import android.util.Log; -// import android.view.View; -// import android.widget.TextView; -// -// import androidx.annotation.NonNull; -// import androidx.core.content.ContextCompat; -// import androidx.core.view.ViewCompat; -// import androidx.recyclerview.widget.RecyclerView; -// import androidx.viewpager2.widget.ViewPager2; -// -// import com.google.android.exoplayer2.Player; -// import com.google.android.exoplayer2.SimpleExoPlayer; -// import com.google.android.exoplayer2.ui.PlayerView; -// -// import java.util.ArrayList; -// import java.util.List; -// -// import awais.instagrabber.R; -// import awais.instagrabber.adapters.PostViewAdapter.OnPostCaptionLongClickListener; -// import awais.instagrabber.adapters.PostViewAdapter.OnPostViewChildViewClickListener; -// import awais.instagrabber.adapters.PostViewerChildAdapter; -// import awais.instagrabber.databinding.ItemFullPostViewBinding; -// import awais.instagrabber.interfaces.MentionClickListener; -// import awais.instagrabber.models.PostChild; -// import awais.instagrabber.models.ProfileModel; -// import awais.instagrabber.models.ViewerPostModel; -// import awais.instagrabber.models.ViewerPostModelWrapper; -// import awais.instagrabber.models.enums.MediaItemType; -// import awais.instagrabber.utils.TextUtils; -// import awais.instagrabber.utils.Utils; -// -// public class PostViewerViewHolder extends RecyclerView.ViewHolder { -// private static final String TAG = "PostViewerViewHolder"; -// -// private final ItemFullPostViewBinding binding; -// private int currentChildPosition; -// -// public PostViewerViewHolder(@NonNull final ItemFullPostViewBinding binding) { -// super(binding.getRoot()); -// this.binding = binding; -// binding.topPanel.viewStoryPost.setVisibility(View.GONE); -// } -// -// public void bind(final ViewerPostModelWrapper wrapper, -// final int position, -// final OnPostViewChildViewClickListener clickListener, -// final OnPostCaptionLongClickListener longClickListener, -// final MentionClickListener mentionClickListener) { -// if (wrapper == null) return; -// final List items = wrapper.getViewerPostModels(); -// if (items == null || items.size() == 0) return; -// if (items.get(0) == null) return; -// final PostViewerChildAdapter adapter = new PostViewerChildAdapter(); -// binding.mediaViewPager.setAdapter(adapter); -// final PostChild firstPost = items.get(0); -// setPostInfo(firstPost, mentionClickListener); -// setMediaItems(items, adapter); -// setupListeners(wrapper, -// position, -// clickListener, -// longClickListener, -// mentionClickListener, -// firstPost.getLocation()); -// } -// -// private void setPostInfo(final PostChild firstPost, -// final MentionClickListener mentionClickListener) { -// final ProfileModel profileModel = firstPost.getProfileModel(); -// if (profileModel == null) return; -// binding.topPanel.title.setText(profileModel.getUsername()); -// final String locationName = firstPost.getLocationName(); -// if (!TextUtils.isEmpty(locationName)) { -// binding.topPanel.location.setVisibility(View.VISIBLE); -// binding.topPanel.location.setText(locationName); -// } else binding.topPanel.location.setVisibility(View.GONE); -// binding.topPanel.ivProfilePic.setImageURI(profileModel.getSdProfilePic()); -// binding.bottomPanel.commentsCount.setText(String.valueOf(firstPost.getCommentsCount())); -// final CharSequence postCaption = firstPost.getPostCaption(); -// if (TextUtils.hasMentions(postCaption)) { -// binding.bottomPanel.viewerCaption.setMentionClickListener(mentionClickListener); -// binding.bottomPanel.viewerCaption -// .setText(TextUtils.getMentionText(postCaption), TextView.BufferType.SPANNABLE); -// } else { -// binding.bottomPanel.viewerCaption.setMentionClickListener(null); -// binding.bottomPanel.viewerCaption.setText(postCaption); -// } -// binding.bottomPanel.tvPostDate.setText(firstPost.getPostDate()); -// setupLikes(firstPost); -// setupSave(firstPost); -// } -// -// private void setupLikes(final ViewerPostModel firstPost) { -// final boolean liked = firstPost.getLike(); -// final long likeCount = firstPost.getLikes(); -// final Resources resources = itemView.getContext().getResources(); -// if (liked) { -// final String unlikeString = resources.getString(R.string.unlike, String.valueOf(likeCount)); -// binding.btnLike.setText(unlikeString); -// ViewCompat.setBackgroundTintList(binding.btnLike, -// ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.btn_pink_background))); -// } else { -// final String likeString = resources.getString(R.string.like, String.valueOf(likeCount)); -// binding.btnLike.setText(likeString); -// ViewCompat.setBackgroundTintList(binding.btnLike, -// ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.btn_lightpink_background))); -// } -// } -// -// private void setupSave(final ViewerPostModel firstPost) { -// final boolean saved = firstPost.isSaved(); -// if (saved) { -// binding.btnBookmark.setText(R.string.unbookmark); -// ViewCompat.setBackgroundTintList(binding.btnBookmark, -// ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.btn_orange_background))); -// } else { -// binding.btnBookmark.setText(R.string.bookmark); -// ViewCompat.setBackgroundTintList( -// binding.btnBookmark, -// ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.btn_lightorange_background))); -// } -// } -// -// private void setupListeners(final ViewerPostModelWrapper wrapper, -// final int position, -// final OnPostViewChildViewClickListener clickListener, -// final OnPostCaptionLongClickListener longClickListener, -// final MentionClickListener mentionClickListener, -// final String location) { -// final View.OnClickListener onClickListener = v -> clickListener -// .onClick(v, wrapper, position, currentChildPosition); -// binding.bottomPanel.btnComments.setOnClickListener(onClickListener); -// binding.topPanel.title.setOnClickListener(onClickListener); -// binding.topPanel.ivProfilePic.setOnClickListener(onClickListener); -// binding.bottomPanel.btnDownload.setOnClickListener(onClickListener); -// binding.bottomPanel.viewerCaption.setOnClickListener(onClickListener); -// binding.btnLike.setOnClickListener(onClickListener); -// binding.btnBookmark.setOnClickListener(onClickListener); -// binding.bottomPanel.viewerCaption.setOnLongClickListener(v -> { -// longClickListener.onLongClick(binding.bottomPanel.viewerCaption.getText().toString()); -// return true; -// }); -// if (!TextUtils.isEmpty(location)) { -// binding.topPanel.location.setOnClickListener(v -> mentionClickListener -// .onClick(binding.topPanel.location, location, false, true)); -// } -// } -// -// private void setMediaItems(final List items, -// final PostViewerChildAdapter adapter) { -// final List filteredList = new ArrayList<>(); -// for (final ViewerPostModel model : items) { -// final MediaItemType itemType = model.getItemType(); -// if (itemType == MediaItemType.MEDIA_TYPE_VIDEO || itemType == MediaItemType.MEDIA_TYPE_IMAGE) { -// filteredList.add(model); -// } -// } -// binding.mediaCounter.setVisibility(filteredList.size() > 1 ? View.VISIBLE : View.GONE); -// final String counter = "1/" + filteredList.size(); -// binding.mediaCounter.setText(counter); -// adapter.submitList(filteredList); -// binding.mediaViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { -// @Override -// public void onPageSelected(final int position) { -// if (filteredList.size() <= 0 || position >= filteredList.size()) return; -// currentChildPosition = position; -// final String counter = (position + 1) + "/" + filteredList.size(); -// binding.mediaCounter.setText(counter); -// final ViewerPostModel viewerPostModel = filteredList.get(position); -// if (viewerPostModel.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { -// setVideoDetails(viewerPostModel); -// setVolumeListener(position); -// return; -// } -// setImageDetails(); -// } -// }); -// } -// -// private void setVolumeListener(final int position) { -// binding.bottomPanel.btnMute.setOnClickListener(v -> { -// try { -// final RecyclerView.ViewHolder viewHolder = ((RecyclerView) binding.mediaViewPager -// .getChildAt(0)).findViewHolderForAdapterPosition(position); -// if (viewHolder != null) { -// final View itemView = viewHolder.itemView; -// if (itemView instanceof PlayerView) { -// final SimpleExoPlayer player = (SimpleExoPlayer) ((PlayerView) itemView) -// .getPlayer(); -// if (player == null) return; -// final float vol = player.getVolume() == 0f ? 1f : 0f; -// player.setVolume(vol); -// binding.bottomPanel.btnMute.setImageResource(vol == 0f ? R.drawable.ic_volume_up_24 -// : R.drawable.ic_volume_off_24); -// Utils.sessionVolumeFull = vol == 1f; -// } -// } -// } catch (Exception e) { -// Log.e(TAG, "Error", e); -// } -// }); -// } -// -// private void setImageDetails() { -// binding.bottomPanel.btnMute.setVisibility(View.GONE); -// binding.bottomPanel.videoViewsContainer.setVisibility(View.GONE); -// } -// -// private void setVideoDetails(final ViewerPostModel viewerPostModel) { -// binding.bottomPanel.btnMute.setVisibility(View.VISIBLE); -// final long videoViews = viewerPostModel.getVideoViews(); -// if (videoViews < 0) { -// binding.bottomPanel.videoViewsContainer.setVisibility(View.GONE); -// return; -// } -// binding.bottomPanel.tvVideoViews.setText(String.valueOf(videoViews)); -// binding.bottomPanel.videoViewsContainer.setVisibility(View.VISIBLE); -// } -// -// public void stopPlayingVideo() { -// try { -// final RecyclerView.ViewHolder viewHolder = ((RecyclerView) binding.mediaViewPager -// .getChildAt(0)).findViewHolderForAdapterPosition(currentChildPosition); -// if (viewHolder != null) { -// final View itemView = viewHolder.itemView; -// if (itemView instanceof PlayerView) { -// final Player player = ((PlayerView) itemView).getPlayer(); -// if (player != null) { -// player.setPlayWhenReady(false); -// } -// } -// } -// } catch (Exception e) { -// Log.e(TAG, "Error", e); -// } -// } -// } diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/StoryListViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/StoryListViewHolder.java new file mode 100644 index 00000000..b2bb5d17 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/StoryListViewHolder.java @@ -0,0 +1,85 @@ +package awais.instagrabber.adapters.viewholder; + +import android.text.TextUtils; +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +import awais.instagrabber.R; +import awais.instagrabber.adapters.FeedStoriesListAdapter.OnFeedStoryClickListener; +import awais.instagrabber.adapters.HighlightStoriesListAdapter.OnHighlightStoryClickListener; +import awais.instagrabber.databinding.ItemNotificationBinding; +import awais.instagrabber.models.FeedStoryModel; +import awais.instagrabber.models.HighlightModel; + +public final class StoryListViewHolder extends RecyclerView.ViewHolder { + private final ItemNotificationBinding binding; + + public StoryListViewHolder(final ItemNotificationBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(final FeedStoryModel model, + final int position, + final OnFeedStoryClickListener notificationClickListener) { + if (model == null) return; + + final int storiesCount = model.getMediaCount(); + binding.tvComment.setVisibility(View.VISIBLE); + binding.tvComment.setText(itemView.getResources().getQuantityString(R.plurals.stories_count, storiesCount, storiesCount)); + + binding.tvSubComment.setVisibility(View.GONE); + + binding.tvDate.setText(model.getDateTime()); + + binding.tvUsername.setText(model.getProfileModel().getUsername()); + binding.ivProfilePic.setImageURI(model.getProfileModel().getSdProfilePic()); + binding.ivProfilePic.setOnClickListener(v -> { + if (notificationClickListener == null) return; + notificationClickListener.onProfileClick(model.getProfileModel().getUsername()); + }); + + if (model.getFirstStoryModel() != null) { + binding.ivPreviewPic.setVisibility(View.VISIBLE); + binding.ivPreviewPic.setImageURI(model.getFirstStoryModel().getThumbnail()); + } + else binding.ivPreviewPic.setVisibility(View.INVISIBLE); + + float alpha = model.isFullyRead() ? 0.5F : 1.0F; + binding.ivProfilePic.setAlpha(alpha); + binding.ivPreviewPic.setAlpha(alpha); + binding.tvUsername.setAlpha(alpha); + binding.tvComment.setAlpha(alpha); + binding.tvDate.setAlpha(alpha); + + itemView.setOnClickListener(v -> { + if (notificationClickListener == null) return; + notificationClickListener.onFeedStoryClick(model, position); + }); + } + + public void bind(final HighlightModel model, + final int position, + final OnHighlightStoryClickListener notificationClickListener) { + if (model == null) return; + + final int storiesCount = model.getMediaCount(); + binding.tvComment.setVisibility(View.VISIBLE); + binding.tvComment.setText(itemView.getResources().getQuantityString(R.plurals.stories_count, storiesCount, storiesCount)); + + binding.tvSubComment.setVisibility(View.GONE); + + binding.tvUsername.setText(model.getDateTime()); + + binding.ivProfilePic.setVisibility(View.GONE); + + binding.ivPreviewPic.setVisibility(View.VISIBLE); + binding.ivPreviewPic.setImageURI(model.getThumbnailUrl()); + + itemView.setOnClickListener(v -> { + if (notificationClickListener == null) return; + notificationClickListener.onHighlightClick(model, position); + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ChildCommentViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ChildCommentViewHolder.java index bc5aecd8..8e9a7941 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ChildCommentViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ChildCommentViewHolder.java @@ -89,8 +89,7 @@ public final class ChildCommentViewHolder extends RecyclerView.ViewHolder { public final void setLiked(final boolean liked) { if (liked) { - // container.setBackgroundColor(0x40FF69B4); - return; + itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_liked)); } } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ParentCommentViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ParentCommentViewHolder.java index 2b595d9e..20637801 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ParentCommentViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ParentCommentViewHolder.java @@ -89,8 +89,7 @@ public final class ParentCommentViewHolder extends RecyclerView.ViewHolder { public final void setLiked(final boolean liked) { if (liked) { - // container.setBackgroundColor(0x40FF69B4); - return; + itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_liked)); } } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java index 7c1a5c43..d9f3cb65 100755 --- a/app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java +++ b/app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java @@ -28,11 +28,12 @@ import static awais.instagrabber.utils.Utils.logCollector; public final class CommentsFetcher extends AsyncTask> { private static final String TAG = "CommentsFetcher"; - private final String shortCode; + private final String shortCode, endCursor; private final FetchListener> fetchListener; - public CommentsFetcher(final String shortCode, final FetchListener> fetchListener) { + public CommentsFetcher(final String shortCode, final String endCursor, final FetchListener> fetchListener) { this.shortCode = shortCode; + this.endCursor = endCursor; this.fetchListener = fetchListener; } @@ -48,15 +49,17 @@ public final class CommentsFetcher extends AsyncTask commentModels = getParentComments(); - for (final CommentModel commentModel : commentModels) { - final List childCommentModels = commentModel.getChildCommentModels(); - if (childCommentModels != null) { - final int childCommentsLen = childCommentModels.size(); - final CommentModel lastChild = childCommentModels.get(childCommentsLen - 1); - if (lastChild != null && lastChild.hasNextPage() && !TextUtils.isEmpty(lastChild.getEndCursor())) { - final List remoteChildComments = getChildComments(commentModel.getId()); - commentModel.setChildCommentModels(remoteChildComments); - lastChild.setPageCursor(false, null); + if (commentModels != null) { + for (final CommentModel commentModel : commentModels) { + final List childCommentModels = commentModel.getChildCommentModels(); + if (childCommentModels != null) { + final int childCommentsLen = childCommentModels.size(); + final CommentModel lastChild = childCommentModels.get(childCommentsLen - 1); + if (lastChild != null && lastChild.hasNextPage() && !TextUtils.isEmpty(lastChild.getEndCursor())) { + final List remoteChildComments = getChildComments(commentModel.getId()); + commentModel.setChildCommentModels(remoteChildComments); + lastChild.setPageCursor(false, null); + } } } } @@ -76,11 +79,10 @@ public final class CommentsFetcher extends AsyncTask getChildComments(final String commentId) { final List commentModels = new ArrayList<>(); - String endCursor = ""; - while (endCursor != null) { + String childEndCursor = ""; + while (childEndCursor != null) { final String url = "https://www.instagram.com/graphql/query/?query_hash=51fdd02b67508306ad4484ff574a0b62&variables=" + - "{\"comment_id\":\"" + commentId + "\",\"first\":50,\"after\":\"" + endCursor + "\"}"; - + "{\"comment_id\":\"" + commentId + "\",\"first\":50,\"after\":\"" + childEndCursor + "\"}"; try { final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setUseCaches(false); @@ -93,8 +95,8 @@ public final class CommentsFetcher extends AsyncTask("commentModels.size", commentModels.size())); if (BuildConfig.DEBUG) Log.e(TAG, "", e); + if (fetchListener != null) fetchListener.onFailure(e); break; } } @@ -152,17 +156,14 @@ public final class CommentsFetcher extends AsyncTask getParentComments() { final List commentModels = new ArrayList<>(); - String endCursor = ""; - while (endCursor != null) { final String url = "https://www.instagram.com/graphql/query/?query_hash=bc3296d1ce80a24b1b6e40b1e72903f5&variables=" + - "{\"shortcode\":\"" + shortCode + "\",\"first\":50,\"after\":\"" + endCursor + "\"}"; - - try { + "{\"shortcode\":\"" + shortCode + "\",\"first\":50,\"after\":\"" + endCursor.replace("\"", "\\\"") + "\"}"; + try { final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setUseCaches(false); conn.connect(); - if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) break; + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) return null; else { final JSONObject parentComments = new JSONObject(NetworkUtils.readFromConnection(conn)).getJSONObject("data") .getJSONObject("shortcode_media") @@ -170,8 +171,8 @@ public final class CommentsFetcher extends AsyncTask 0) { final String childEndCursor; - final boolean hasNextPage; + final boolean childHasNextPage; if ((tempJsonObject = tempJsonObject.optJSONObject("page_info")) != null) { childEndCursor = tempJsonObject.optString("end_cursor"); - hasNextPage = tempJsonObject.optBoolean("has_next_page", !TextUtils.isEmpty(childEndCursor)); + childHasNextPage = tempJsonObject.optBoolean("has_next_page", !TextUtils.isEmpty(childEndCursor)); } else { childEndCursor = null; - hasNextPage = false; + childHasNextPage = false; } final List childCommentModels = new ArrayList<>(); @@ -257,6 +261,7 @@ public final class CommentsFetcher extends AsyncTask("commentModelsList.size", commentModels.size())); if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); - break; + if (fetchListener != null) fetchListener.onFailure(e); + return null; } - } return commentModels; } } diff --git a/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java index d2bba0ce..b45a224a 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java +++ b/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java @@ -1,34 +1,61 @@ package awais.instagrabber.asyncs; +//import android.os.Handler; +//import android.util.Log; + +import java.util.ArrayList; 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.PostsFetchResponse; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.webservices.FeedService; import awais.instagrabber.webservices.ServiceCallback; +import static awais.instagrabber.utils.Utils.settingsHelper; + public class FeedPostFetchService implements PostFetcher.PostFetchService { private static final String TAG = "FeedPostFetchService"; private final FeedService feedService; private String nextCursor; +// private final Handler handler; private boolean hasNextPage; +// private static final int DELAY_MILLIS = 500; public FeedPostFetchService() { feedService = FeedService.getInstance(); +// handler = new Handler(); } @Override public void fetch(final FetchListener> fetchListener) { - feedService.fetch(25, nextCursor, new ServiceCallback() { + final List feedModels = new ArrayList<>(); + final String cookie = settingsHelper.getString(Constants.COOKIE); + final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); + feedModels.clear(); + feedService.fetch(csrfToken, nextCursor, new ServiceCallback() { @Override public void onSuccess(final PostsFetchResponse result) { - if (result == null) return; + if (result == null && feedModels.size() > 0) { + fetchListener.onResult(feedModels); + return; + } + else if (result == null) return; nextCursor = result.getNextCursor(); hasNextPage = result.hasNextPage(); + feedModels.addAll(result.getFeedModels()); if (fetchListener != null) { - fetchListener.onResult(result.getFeedModels()); + if (feedModels.size() < 15 && hasNextPage) { +// handler.postDelayed(() -> { + feedService.fetch(csrfToken, nextCursor, this); +// }, DELAY_MILLIS); + } + else { + fetchListener.onResult(feedModels); + } } } diff --git a/app/src/main/java/awais/instagrabber/asyncs/FeedStoriesFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/FeedStoriesFetcher.java deleted file mode 100755 index c975dc5d..00000000 --- a/app/src/main/java/awais/instagrabber/asyncs/FeedStoriesFetcher.java +++ /dev/null @@ -1,93 +0,0 @@ -package awais.instagrabber.asyncs; - -import android.os.AsyncTask; -import android.util.Log; - -import org.json.JSONArray; -import org.json.JSONObject; - -import java.net.HttpURLConnection; -import java.net.URL; - -import awais.instagrabber.BuildConfig; -import awais.instagrabber.interfaces.FetchListener; -import awais.instagrabber.models.FeedStoryModel; -import awais.instagrabber.models.ProfileModel; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.NetworkUtils; -import awaisomereport.LogCollector.LogFile; - -import static awais.instagrabber.utils.Utils.logCollector; - -public final class FeedStoriesFetcher extends AsyncTask { - private final FetchListener fetchListener; - - public FeedStoriesFetcher(final FetchListener fetchListener) { - this.fetchListener = fetchListener; - } - - @Override - protected FeedStoryModel[] doInBackground(final Void... voids) { - FeedStoryModel[] result = null; - String url = "https://www.instagram.com/graphql/query/?query_hash=b7b84d884400bc5aa7cfe12ae843a091&variables=" + - "{\"only_stories\":true,\"stories_prefetch\":false,\"stories_video_dash_manifest\":false}"; - - try { - HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); - conn.setInstanceFollowRedirects(false); - conn.setUseCaches(false); - conn.connect(); - - if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { - final JSONArray feedStoriesReel = new JSONObject(NetworkUtils.readFromConnection(conn)) - .getJSONObject("data") - .getJSONObject(Constants.EXTRAS_USER) - .getJSONObject("feed_reels_tray") - .getJSONObject("edge_reels_tray_to_reel") - .getJSONArray("edges"); - - conn.disconnect(); - - final int storiesLen = feedStoriesReel.length(); - final FeedStoryModel[] feedStoryModels = new FeedStoryModel[storiesLen]; - final String[] feedStoryIDs = new String[storiesLen]; - - for (int i = 0; i < storiesLen; ++i) { - final JSONObject node = feedStoriesReel.getJSONObject(i).getJSONObject("node"); - - final JSONObject user = node.getJSONObject(node.has("user") ? "user" : "owner"); - final ProfileModel profileModel = new ProfileModel(false, false, false, - user.getString("id"), - user.getString("username"), - null, null, null, - user.getString("profile_pic_url"), - null, 0, 0, 0, false, false, false, false); - - final String id = node.getString("id"); - final boolean fullyRead = !node.isNull("seen") && node.getLong("seen") == node.getLong("latest_reel_media"); - feedStoryIDs[i] = id; - feedStoryModels[i] = new FeedStoryModel(id, profileModel, fullyRead); - } - result = feedStoryModels; - } - - conn.disconnect(); - } catch (final Exception e) { - if (logCollector != null) - logCollector.appendException(e, LogFile.ASYNC_FEED_STORY_FETCHER, "doInBackground"); - if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); - } - - return result; - } - - @Override - protected void onPreExecute() { - if (fetchListener != null) fetchListener.doBefore(); - } - - @Override - protected void onPostExecute(final FeedStoryModel[] result) { - if (fetchListener != null) fetchListener.onResult(result); - } -} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/GetActivityAsyncTask.java b/app/src/main/java/awais/instagrabber/asyncs/GetActivityAsyncTask.java index ff65a56b..1987443b 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/GetActivityAsyncTask.java +++ b/app/src/main/java/awais/instagrabber/asyncs/GetActivityAsyncTask.java @@ -16,12 +16,21 @@ import awais.instagrabber.utils.TextUtils; public class GetActivityAsyncTask extends AsyncTask { private static final String TAG = "GetActivityAsyncTask"; - private OnTaskCompleteListener onTaskCompleteListener; + private final OnTaskCompleteListener onTaskCompleteListener; public GetActivityAsyncTask(final OnTaskCompleteListener onTaskCompleteListener) { this.onTaskCompleteListener = onTaskCompleteListener; } + /* + This needs to be redone to fetch i inbox instead + Within inbox, data is (body JSON => counts) + Then we have these counts: + new_posts, activity_feed_dot_badge, relationships, campaign_notification + usertags, likes, comment_likes, shopping_notification, comments + photos_of_you (not sure about difference to usertags), requests + */ + protected NotificationCounts doInBackground(final String... cookiesArray) { if (cookiesArray == null) return null; final String cookie = cookiesArray[0]; @@ -70,11 +79,11 @@ public class GetActivityAsyncTask extends AsyncTask { if (logCollector != null) logCollector.appendException(e, LogCollector.LogFile.ASYNC_HASHTAG_FETCHER, "doInBackground"); if (BuildConfig.DEBUG) Log.e(TAG, "", e); + if (fetchListener != null) fetchListener.onFailure(e); } return result; diff --git a/app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java index 4c12f16e..55f1bc40 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java +++ b/app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java @@ -6,12 +6,14 @@ import awais.instagrabber.customviews.helpers.PostFetcher; import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.HashtagModel; +import awais.instagrabber.repositories.responses.PostsFetchResponse; +import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.TagsService; -import awais.instagrabber.webservices.TagsService.TagPostsFetchResponse; public class HashtagPostFetchService implements PostFetcher.PostFetchService { private final TagsService tagsService; + private final GraphQLService graphQLService; private final HashtagModel hashtagModel; private String nextMaxId; private boolean moreAvailable; @@ -20,19 +22,20 @@ public class HashtagPostFetchService implements PostFetcher.PostFetchService { public HashtagPostFetchService(final HashtagModel hashtagModel, final boolean isLoggedIn) { this.hashtagModel = hashtagModel; this.isLoggedIn = isLoggedIn; - tagsService = TagsService.getInstance(); + tagsService = isLoggedIn ? TagsService.getInstance() : null; + graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); } @Override public void fetch(final FetchListener> fetchListener) { - final ServiceCallback cb = new ServiceCallback() { + final ServiceCallback cb = new ServiceCallback() { @Override - public void onSuccess(final TagPostsFetchResponse result) { + public void onSuccess(final PostsFetchResponse result) { if (result == null) return; - nextMaxId = result.getNextMaxId(); - moreAvailable = result.isMoreAvailable(); + nextMaxId = result.getNextCursor(); + moreAvailable = result.hasNextPage(); if (fetchListener != null) { - fetchListener.onResult(result.getItems()); + fetchListener.onResult(result.getFeedModels()); } } @@ -45,7 +48,7 @@ public class HashtagPostFetchService implements PostFetcher.PostFetchService { } }; if (isLoggedIn) tagsService.fetchPosts(hashtagModel.getName().toLowerCase(), nextMaxId, cb); - else tagsService.fetchGraphQLPosts(hashtagModel.getName().toLowerCase(), nextMaxId, cb); + else graphQLService.fetchHashtagPosts(hashtagModel.getName().toLowerCase(), nextMaxId, cb); } @Override diff --git a/app/src/main/java/awais/instagrabber/asyncs/HighlightsFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/HighlightsFetcher.java deleted file mode 100755 index acb7b7c1..00000000 --- a/app/src/main/java/awais/instagrabber/asyncs/HighlightsFetcher.java +++ /dev/null @@ -1,73 +0,0 @@ -package awais.instagrabber.asyncs; - -import android.os.AsyncTask; -import android.util.Log; - -import org.json.JSONArray; -import org.json.JSONObject; - -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - -import awais.instagrabber.BuildConfig; -import awais.instagrabber.interfaces.FetchListener; -import awais.instagrabber.models.HighlightModel; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.NetworkUtils; - -public final class HighlightsFetcher extends AsyncTask> { - private final String id; - private final FetchListener> fetchListener; - - public HighlightsFetcher(final String id, final FetchListener> fetchListener) { - this.id = id; - this.fetchListener = fetchListener; - } - - @Override - protected List doInBackground(final Void... voids) { - List result = null; - String url = "https://i.instagram.com/api/v1/highlights/" + id + "/highlights_tray/"; - - try { - HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); - conn.setInstanceFollowRedirects(false); - conn.setUseCaches(false); - conn.setRequestProperty("User-Agent", Constants.I_USER_AGENT); - conn.connect(); - - if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { - final JSONArray highlightsReel = new JSONObject(NetworkUtils.readFromConnection(conn)).getJSONArray("tray"); - - final int length = highlightsReel.length(); - final List highlightModels = new ArrayList<>(); - // final String[] highlightIds = new String[length]; - for (int i = 0; i < length; ++i) { - final JSONObject highlightNode = highlightsReel.getJSONObject(i); - highlightModels.add(new HighlightModel( - highlightNode.getString("title"), - highlightNode.getString(Constants.EXTRAS_ID), - highlightNode.getJSONObject("cover_media") - .getJSONObject("cropped_image_version") - .getString("url") - )); - } - conn.disconnect(); - result = highlightModels; - } - - conn.disconnect(); - } catch (Exception e) { - if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); - } - - return result; - } - - @Override - protected void onPostExecute(final List result) { - if (fetchListener != null) fetchListener.onResult(result); - } -} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java index 2b504c50..7c7a12ce 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java +++ b/app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java @@ -6,12 +6,14 @@ import awais.instagrabber.customviews.helpers.PostFetcher; import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.LocationModel; +import awais.instagrabber.repositories.responses.PostsFetchResponse; +import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.LocationService; -import awais.instagrabber.webservices.LocationService.LocationPostsFetchResponse; import awais.instagrabber.webservices.ServiceCallback; public class LocationPostFetchService implements PostFetcher.PostFetchService { private final LocationService locationService; + private final GraphQLService graphQLService; private final LocationModel locationModel; private String nextMaxId; private boolean moreAvailable; @@ -20,19 +22,20 @@ public class LocationPostFetchService implements PostFetcher.PostFetchService { public LocationPostFetchService(final LocationModel locationModel, final boolean isLoggedIn) { this.locationModel = locationModel; this.isLoggedIn = isLoggedIn; - locationService = LocationService.getInstance(); + locationService = isLoggedIn ? LocationService.getInstance() : null; + graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); } @Override public void fetch(final FetchListener> fetchListener) { - final ServiceCallback cb = new ServiceCallback() { + final ServiceCallback cb = new ServiceCallback() { @Override - public void onSuccess(final LocationPostsFetchResponse result) { + public void onSuccess(final PostsFetchResponse result) { if (result == null) return; - nextMaxId = result.getNextMaxId(); - moreAvailable = result.isMoreAvailable(); + nextMaxId = result.getNextCursor(); + moreAvailable = result.hasNextPage(); if (fetchListener != null) { - fetchListener.onResult(result.getItems()); + fetchListener.onResult(result.getFeedModels()); } } @@ -45,7 +48,7 @@ public class LocationPostFetchService implements PostFetcher.PostFetchService { } }; if (isLoggedIn) locationService.fetchPosts(locationModel.getId(), nextMaxId, cb); - else locationService.fetchGraphQLPosts(locationModel.getId(), nextMaxId, cb); + else graphQLService.fetchLocationPosts(locationModel.getId(), nextMaxId, cb); } @Override diff --git a/app/src/main/java/awais/instagrabber/asyncs/NotificationsFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/NotificationsFetcher.java index 16e355df..714a2793 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/NotificationsFetcher.java +++ b/app/src/main/java/awais/instagrabber/asyncs/NotificationsFetcher.java @@ -3,21 +3,14 @@ package awais.instagrabber.asyncs; import android.os.AsyncTask; import android.util.Log; -import org.json.JSONArray; -import org.json.JSONObject; - -import java.net.HttpURLConnection; -import java.net.URL; import java.util.ArrayList; import java.util.List; import awais.instagrabber.BuildConfig; import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.models.NotificationModel; -import awais.instagrabber.models.enums.NotificationType; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.LocaleUtils; -import awais.instagrabber.utils.NetworkUtils; +import awais.instagrabber.webservices.NewsService; +import awais.instagrabber.webservices.ServiceCallback; import awaisomereport.LogCollector; import static awais.instagrabber.utils.Utils.logCollector; @@ -26,89 +19,48 @@ public final class NotificationsFetcher extends AsyncTask> fetchListener; + private final NewsService newsService; + private final boolean markAsSeen; + private boolean fetchedWeb = false; - public NotificationsFetcher(final FetchListener> fetchListener) { + public NotificationsFetcher(final boolean markAsSeen, + final FetchListener> fetchListener) { + this.markAsSeen = markAsSeen; this.fetchListener = fetchListener; + newsService = NewsService.getInstance(); } @Override protected List doInBackground(final Void... voids) { - List result = new ArrayList<>(); - final String url = "https://www.instagram.com/accounts/activity/?__a=1"; - try { - final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); - conn.setInstanceFollowRedirects(false); - conn.setUseCaches(false); - conn.setRequestProperty("Accept-Language", LocaleUtils.getCurrentLocale().getLanguage() + ",en-US;q=0.8"); - conn.connect(); + List notificationModels = new ArrayList<>(); - if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { - final JSONObject page = new JSONObject(NetworkUtils.readFromConnection(conn)) - .getJSONObject("graphql") - .getJSONObject("user"); - final JSONObject ewaf = page.getJSONObject("activity_feed") - .optJSONObject("edge_web_activity_feed"); - final JSONObject efr = page.optJSONObject("edge_follow_requests"); - JSONObject data; - JSONArray media; - if (ewaf != null - && (media = ewaf.optJSONArray("edges")) != null - && media.length() > 0 - && media.optJSONObject(0).optJSONObject("node") != null) { - for (int i = 0; i < media.length(); ++i) { - data = media.optJSONObject(i).optJSONObject("node"); - if (data == null) continue; - final String type = data.getString("__typename"); - final NotificationType notificationType = NotificationType.valueOfType(type); - if (notificationType == null) continue; - final JSONObject user = data.getJSONObject("user"); - result.add(new NotificationModel( - data.getString(Constants.EXTRAS_ID), - data.optString("text"), // comments or mentions - data.getLong("timestamp"), - user.getString("id"), - user.getString("username"), - user.getString("profile_pic_url"), - !data.isNull("media") ? data.getJSONObject("media").getString("shortcode") : null, - !data.isNull("media") ? data.getJSONObject("media").getString("thumbnail_src") : null, notificationType)); - } + newsService.fetchAppInbox(markAsSeen, new ServiceCallback>() { + @Override + public void onSuccess(final List result) { + if (result == null) return; + notificationModels.addAll(result); + if (fetchedWeb) { + fetchListener.onResult(notificationModels); } - - if (efr != null - && (media = efr.optJSONArray("edges")) != null - && media.length() > 0 - && media.optJSONObject(0).optJSONObject("node") != null) { - for (int i = 0; i < media.length(); ++i) { - data = media.optJSONObject(i).optJSONObject("node"); - if (data == null) continue; - result.add(new NotificationModel( - data.getString(Constants.EXTRAS_ID), - data.optString("full_name"), - 0L, - data.getString(Constants.EXTRAS_ID), - data.getString("username"), - data.getString("profile_pic_url"), - null, - null, NotificationType.REQUEST)); - } + else { + fetchedWeb = true; + newsService.fetchWebInbox(markAsSeen, this); } } - conn.disconnect(); - } catch (final Exception e) { - if (logCollector != null) - logCollector.appendException(e, LogCollector.LogFile.ASYNC_NOTIFICATION_FETCHER, "doInBackground"); - if (BuildConfig.DEBUG) Log.e(TAG, "", e); - } - return result; + + @Override + public void onFailure(final Throwable t) { + // Log.e(TAG, "onFailure: ", t); + if (fetchListener != null) { + fetchListener.onFailure(t); + } + } + }); + return notificationModels; } @Override protected void onPreExecute() { if (fetchListener != null) fetchListener.doBefore(); } - - @Override - protected void onPostExecute(final List result) { - if (fetchListener != null) fetchListener.onResult(result); - } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java index f87e5103..2a721a61 100755 --- a/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java +++ b/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java @@ -65,6 +65,7 @@ public final class PostFetcher extends AsyncTask { owner.optInt("edge_followed_by"), -1, owner.optBoolean("followed_by_viewer"), + false, owner.optBoolean("restricted_by_viewer"), owner.optBoolean("blocked_by_viewer"), owner.optBoolean("requested_by_viewer") diff --git a/app/src/main/java/awais/instagrabber/asyncs/ProfileFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/ProfileFetcher.java index e2e4fe2c..7e78b0b2 100755 --- a/app/src/main/java/awais/instagrabber/asyncs/ProfileFetcher.java +++ b/app/src/main/java/awais/instagrabber/asyncs/ProfileFetcher.java @@ -72,6 +72,7 @@ public final class ProfileFetcher extends AsyncTask { user.getJSONObject("edge_followed_by").getLong("count"), user.getJSONObject("edge_follow").getLong("count"), user.optBoolean("followed_by_viewer"), + user.optBoolean("follows_viewer"), user.optBoolean("restricted_by_viewer"), user.optBoolean("blocked_by_viewer"), user.optBoolean("requested_by_viewer")); diff --git a/app/src/main/java/awais/instagrabber/asyncs/ProfilePictureFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/ProfilePictureFetcher.java index 6fe750a1..433697de 100755 --- a/app/src/main/java/awais/instagrabber/asyncs/ProfilePictureFetcher.java +++ b/app/src/main/java/awais/instagrabber/asyncs/ProfilePictureFetcher.java @@ -40,51 +40,34 @@ public final class ProfilePictureFetcher extends AsyncTask { protected String doInBackground(final Void... voids) { String out = null; if (isHashtag) out = picUrl; - else try { - final HttpURLConnection conn = - (HttpURLConnection) new URL("https://i.instagram.com/api/v1/users/"+userId+"/info/").openConnection(); - conn.setUseCaches(false); - conn.setRequestMethod("GET"); - conn.setRequestProperty("User-Agent", Constants.USER_AGENT); + else if (Utils.settingsHelper.getBoolean(Constants.INSTADP)) try { + final HttpURLConnection backup = + (HttpURLConnection) new URL("https://instadp.com/fullsize/" + userName).openConnection(); + backup.setUseCaches(false); + backup.setRequestMethod("GET"); + backup.setRequestProperty("User-Agent", Constants.A_USER_AGENT); - final String result = conn.getResponseCode() == HttpURLConnection.HTTP_OK ? NetworkUtils.readFromConnection(conn) : null; - conn.disconnect(); + final String instadp = backup.getResponseCode() == HttpURLConnection.HTTP_OK ? NetworkUtils.readFromConnection(backup) : null; + backup.disconnect(); - if (!TextUtils.isEmpty(result)) { - JSONObject data = new JSONObject(result).getJSONObject("user"); - if (data.has("hd_profile_pic_url_info")) - out = data.getJSONObject("hd_profile_pic_url_info").optString("url"); - } + if (!TextUtils.isEmpty(instadp)) { + final Document doc = Jsoup.parse(instadp); + boolean fallback = false; - if (TextUtils.isEmpty(out) && Utils.settingsHelper.getBoolean(Constants.INSTADP)) { - final HttpURLConnection backup = - (HttpURLConnection) new URL("https://instadp.com/fullsize/" + userName).openConnection(); - backup.setUseCaches(false); - backup.setRequestMethod("GET"); - backup.setRequestProperty("User-Agent", Constants.A_USER_AGENT); + final int imgIndex = instadp.indexOf("preloadImg('"), lastIndex; - final String instadp = backup.getResponseCode() == HttpURLConnection.HTTP_OK ? NetworkUtils.readFromConnection(backup) : null; - backup.disconnect(); - - if (!TextUtils.isEmpty(instadp)) { - final Document doc = Jsoup.parse(instadp); - boolean fallback = false; - - final int imgIndex = instadp.indexOf("preloadImg('"), lastIndex; - - Element element = doc.selectFirst(".instadp"); - if (element != null && (element = element.selectFirst(".picture")) != null) - out = element.attr("src"); - else if ((element = doc.selectFirst(".download-btn")) != null) - out = element.attr("href"); - else if (imgIndex != -1 && (lastIndex = instadp.indexOf("')", imgIndex)) != -1) - out = instadp.substring(imgIndex + 12, lastIndex); - else { - final Elements imgs = doc.getElementsByTag("img"); - for (final Element img : imgs) { - final String imgStr = img.toString(); - if (imgStr.contains("cdninstagram.com")) out = img.attr("src"); - } + Element element = doc.selectFirst(".instadp"); + if (element != null && (element = element.selectFirst(".picture")) != null) + out = element.attr("src"); + else if ((element = doc.selectFirst(".download-btn")) != null) + out = element.attr("href"); + else if (imgIndex != -1 && (lastIndex = instadp.indexOf("')", imgIndex)) != -1) + out = instadp.substring(imgIndex + 12, lastIndex); + else { + final Elements imgs = doc.getElementsByTag("img"); + for (final Element img : imgs) { + final String imgStr = img.toString(); + if (imgStr.contains("cdninstagram.com")) out = img.attr("src"); } } } diff --git a/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java index 3617cbd2..649d28d8 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java +++ b/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java @@ -7,29 +7,34 @@ 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.GraphQLService; 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 GraphQLService graphQLService; private final ProfileModel profileModel; - private String nextCursor; - private boolean hasNextPage; + private final boolean isLoggedIn; + private String nextMaxId; + private boolean moreAvailable; - public ProfilePostFetchService(final ProfileModel profileModel) { + public ProfilePostFetchService(final ProfileModel profileModel, final boolean isLoggedIn) { this.profileModel = profileModel; - profileService = ProfileService.getInstance(); + this.isLoggedIn = isLoggedIn; + graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); + profileService = isLoggedIn ? ProfileService.getInstance() : null; } @Override public void fetch(final FetchListener> fetchListener) { - profileService.fetchPosts(profileModel, 30, nextCursor, new ServiceCallback() { + final ServiceCallback cb = new ServiceCallback() { @Override public void onSuccess(final PostsFetchResponse result) { if (result == null) return; - nextCursor = result.getNextCursor(); - hasNextPage = result.hasNextPage(); + nextMaxId = result.getNextCursor(); + moreAvailable = result.hasNextPage(); if (fetchListener != null) { fetchListener.onResult(result.getFeedModels()); } @@ -42,16 +47,18 @@ public class ProfilePostFetchService implements PostFetcher.PostFetchService { fetchListener.onFailure(t); } } - }); + }; + if (isLoggedIn) profileService.fetchPosts(profileModel.getId(), nextMaxId, cb); + else graphQLService.fetchProfilePosts(profileModel.getId(), 30, nextMaxId, cb); } @Override public void reset() { - nextCursor = null; + nextMaxId = null; } @Override public boolean hasNextPage() { - return hasNextPage; + return moreAvailable; } } diff --git a/app/src/main/java/awais/instagrabber/asyncs/QuizAction.java b/app/src/main/java/awais/instagrabber/asyncs/QuizAction.java deleted file mode 100644 index 4042b875..00000000 --- a/app/src/main/java/awais/instagrabber/asyncs/QuizAction.java +++ /dev/null @@ -1,89 +0,0 @@ -package awais.instagrabber.asyncs; - -import android.os.AsyncTask; -import android.util.Log; - -import org.json.JSONObject; - -import java.io.DataOutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.UUID; - -import awais.instagrabber.models.StoryModel; -import awais.instagrabber.models.stickers.QuizModel; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.Utils; - -import static awais.instagrabber.utils.Utils.settingsHelper; - -public class QuizAction extends AsyncTask { - private static final String TAG = "QuizAction"; - - private final StoryModel storyModel; - private final QuizModel quizModel; - private final String cookie; - private final OnTaskCompleteListener onTaskCompleteListener; - - public QuizAction(final StoryModel storyModel, - final QuizModel quizModel, - final String cookie, - final OnTaskCompleteListener onTaskCompleteListener) { - this.storyModel = storyModel; - this.quizModel = quizModel; - this.cookie = cookie; - this.onTaskCompleteListener = onTaskCompleteListener; - } - - protected Integer doInBackground(Integer... rawChoice) { - int choice = rawChoice[0]; - final String url = "https://i.instagram.com/api/v1/media/" + storyModel.getStoryMediaId().split("_")[0] + "/" + quizModel.getId() + "/story_quiz_answer/"; - HttpURLConnection urlConnection = null; - try { - JSONObject ogBody = new JSONObject("{\"client_context\":\"" + UUID.randomUUID().toString() - + "\",\"mutation_token\":\"" + UUID.randomUUID().toString() - + "\",\"_csrftoken\":\"" + cookie.split("csrftoken=")[1].split(";")[0] - + "\",\"_uid\":\"" + CookieUtils.getUserIdFromCookie(cookie) - + "\",\"__uuid\":\"" + settingsHelper.getString(Constants.DEVICE_UUID) - + "\"}"); - ogBody.put("answer", String.valueOf(choice)); - String urlParameters = Utils.sign(ogBody.toString()); - urlConnection = (HttpURLConnection) new URL(url).openConnection(); - urlConnection.setRequestMethod("POST"); - urlConnection.setUseCaches(false); - urlConnection.setRequestProperty("User-Agent", Constants.I_USER_AGENT); - urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - if (urlParameters != null) { - urlConnection.setRequestProperty("Content-Length", Integer.toString(urlParameters.getBytes().length)); - } - urlConnection.setDoOutput(true); - DataOutputStream wr = new DataOutputStream(urlConnection.getOutputStream()); - wr.writeBytes(urlParameters); - wr.flush(); - wr.close(); - Log.d(TAG, "quiz: " + url + " " + cookie + " " + urlParameters); - urlConnection.connect(); - if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) { - return choice; - } - } catch (Throwable ex) { - Log.e(TAG, "quiz: " + ex); - } finally { - if (urlConnection != null) { - urlConnection.disconnect(); - } - } - return -1; - } - - @Override - protected void onPostExecute(final Integer choice) { - if (onTaskCompleteListener == null || choice == null) return; - onTaskCompleteListener.onTaskComplete(choice); - } - - public interface OnTaskCompleteListener { - void onTaskComplete(final int choice); - } -} diff --git a/app/src/main/java/awais/instagrabber/asyncs/RespondAction.java b/app/src/main/java/awais/instagrabber/asyncs/RespondAction.java deleted file mode 100644 index 836d6b76..00000000 --- a/app/src/main/java/awais/instagrabber/asyncs/RespondAction.java +++ /dev/null @@ -1,88 +0,0 @@ -package awais.instagrabber.asyncs; - -import android.os.AsyncTask; -import android.util.Log; - -import org.json.JSONObject; - -import java.io.DataOutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.UUID; - -import awais.instagrabber.models.StoryModel; -import awais.instagrabber.models.stickers.QuestionModel; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.Utils; - -import static awais.instagrabber.utils.Utils.settingsHelper; - -public class RespondAction extends AsyncTask { - - private final StoryModel storyModel; - private final QuestionModel questionModel; - private final String cookie; - private final OnTaskCompleteListener onTaskCompleteListener; - - public RespondAction(final StoryModel storyModel, - final QuestionModel questionModel, - final String cookie, - final OnTaskCompleteListener onTaskCompleteListener) { - this.storyModel = storyModel; - this.questionModel = questionModel; - this.cookie = cookie; - this.onTaskCompleteListener = onTaskCompleteListener; - } - - protected Boolean doInBackground(String... rawChoice) { - final String url = "https://i.instagram.com/api/v1/media/" - + storyModel.getStoryMediaId().split("_")[0] + "/" + questionModel.getId() + "/story_question_response/"; - HttpURLConnection urlConnection = null; - try { - final JSONObject ogbody = new JSONObject("{\"client_context\":\"" + UUID.randomUUID().toString() - + "\",\"mutation_token\":\"" + UUID.randomUUID().toString() - + "\",\"_csrftoken\":\"" + cookie.split("csrftoken=")[1].split(";")[0] - + "\",\"_uid\":\"" + CookieUtils.getUserIdFromCookie(cookie) - + "\",\"__uuid\":\"" + settingsHelper.getString(Constants.DEVICE_UUID) - + "\"}"); - String choice = rawChoice[0].replaceAll("\"", ("\\\"")); - ogbody.put("response", choice); - String urlParameters = Utils.sign(ogbody.toString()); - urlConnection = (HttpURLConnection) new URL(url).openConnection(); - urlConnection.setRequestMethod("POST"); - urlConnection.setUseCaches(false); - urlConnection.setRequestProperty("User-Agent", Constants.I_USER_AGENT); - urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - urlConnection.setRequestProperty("Content-Length", Integer.toString(urlParameters.getBytes().length)); - urlConnection.setDoOutput(true); - DataOutputStream wr = new DataOutputStream(urlConnection.getOutputStream()); - wr.writeBytes(urlParameters); - wr.flush(); - wr.close(); - urlConnection.connect(); - if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) { - return true; - } - - } catch (Throwable ex) { - Log.e("austin_debug", "respond: " + ex); - } finally { - if (urlConnection != null) { - urlConnection.disconnect(); - } - } - return null; - } - - @Override - protected void onPostExecute(final Boolean ok) { - if (onTaskCompleteListener == null) return; - onTaskCompleteListener.onTaskComplete(ok); - - } - - public interface OnTaskCompleteListener { - void onTaskComplete(final boolean result); - } -} diff --git a/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java index 36c668d1..b13d842d 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java +++ b/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java @@ -6,34 +6,39 @@ import awais.instagrabber.customviews.helpers.PostFetcher; import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.enums.PostItemType; +import awais.instagrabber.repositories.responses.PostsFetchResponse; +import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.ProfileService; -import awais.instagrabber.webservices.ProfileService.SavedPostsFetchResponse; import awais.instagrabber.webservices.ServiceCallback; public class SavedPostFetchService implements PostFetcher.PostFetchService { private final ProfileService profileService; + private final GraphQLService graphQLService; private final String profileId; private final PostItemType type; + private final boolean isLoggedIn; private String nextMaxId; private boolean moreAvailable; - public SavedPostFetchService(final String profileId, final PostItemType type) { + public SavedPostFetchService(final String profileId, final PostItemType type, final boolean isLoggedIn) { this.profileId = profileId; this.type = type; - profileService = ProfileService.getInstance(); + this.isLoggedIn = isLoggedIn; + graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); + profileService = isLoggedIn ? ProfileService.getInstance() : null; } @Override public void fetch(final FetchListener> fetchListener) { - final ServiceCallback callback = new ServiceCallback() { + final ServiceCallback callback = new ServiceCallback() { @Override - public void onSuccess(final SavedPostsFetchResponse result) { + public void onSuccess(final PostsFetchResponse result) { if (result == null) return; - nextMaxId = result.getNextMaxId(); - moreAvailable = result.isMoreAvailable(); + nextMaxId = result.getNextCursor(); + moreAvailable = result.hasNextPage(); if (fetchListener != null) { - fetchListener.onResult(result.getItems()); + fetchListener.onResult(result.getFeedModels()); } } @@ -50,7 +55,8 @@ public class SavedPostFetchService implements PostFetcher.PostFetchService { profileService.fetchLiked(nextMaxId, callback); break; case TAGGED: - profileService.fetchTagged(profileId, nextMaxId, callback); + if (isLoggedIn) profileService.fetchTagged(profileId, nextMaxId, callback); + else graphQLService.fetchTaggedPosts(profileId, 30, nextMaxId, callback); break; case SAVED: default: diff --git a/app/src/main/java/awais/instagrabber/asyncs/VoteAction.java b/app/src/main/java/awais/instagrabber/asyncs/VoteAction.java deleted file mode 100644 index cfa91c77..00000000 --- a/app/src/main/java/awais/instagrabber/asyncs/VoteAction.java +++ /dev/null @@ -1,69 +0,0 @@ -package awais.instagrabber.asyncs; - -import android.os.AsyncTask; -import android.util.Log; - -import java.io.DataOutputStream; -import java.net.HttpURLConnection; -import java.net.URL; - -import awais.instagrabber.models.StoryModel; -import awais.instagrabber.models.stickers.PollModel; -import awais.instagrabber.utils.Constants; - -public class VoteAction extends AsyncTask { - - private static final String TAG = "VoteAction"; - - private final StoryModel storyModel; - private final PollModel pollModel; - private final String cookie; - private final OnTaskCompleteListener onTaskCompleteListener; - - public VoteAction(final StoryModel storyModel, - final PollModel pollModel, - final String cookie, - final OnTaskCompleteListener onTaskCompleteListener) { - this.storyModel = storyModel; - this.pollModel = pollModel; - this.cookie = cookie; - this.onTaskCompleteListener = onTaskCompleteListener; - } - - protected Integer doInBackground(Integer... rawChoice) { - int choice = rawChoice[0]; - final String url = "https://www.instagram.com/media/" + storyModel.getStoryMediaId().split("_")[0] + "/" + pollModel.getId() + "/story_poll_vote/"; - try { - final HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection(); - urlConnection.setRequestMethod("POST"); - urlConnection.setUseCaches(false); - urlConnection.setRequestProperty("User-Agent", Constants.USER_AGENT); - urlConnection.setRequestProperty("x-csrftoken", cookie.split("csrftoken=")[1].split(";")[0]); - urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - urlConnection.setRequestProperty("Content-Length", "6"); - urlConnection.setDoOutput(true); - DataOutputStream wr = new DataOutputStream(urlConnection.getOutputStream()); - wr.writeBytes("vote=" + choice); - wr.flush(); - wr.close(); - urlConnection.connect(); - if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) { - return choice; - } - urlConnection.disconnect(); - } catch (Exception ex) { - Log.e(TAG, "Error", ex); - } - return -1; - } - - @Override - protected void onPostExecute(final Integer result) { - if (result == null || onTaskCompleteListener == null) return; - onTaskCompleteListener.onTaskComplete(result); - } - - public interface OnTaskCompleteListener { - void onTaskComplete(final int choice); - } -} diff --git a/app/src/main/java/awais/instagrabber/asyncs/direct_messages/DirectThreadBroadcaster.java b/app/src/main/java/awais/instagrabber/asyncs/direct_messages/DirectThreadBroadcaster.java index 56f696e8..6a99a190 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/direct_messages/DirectThreadBroadcaster.java +++ b/app/src/main/java/awais/instagrabber/asyncs/direct_messages/DirectThreadBroadcaster.java @@ -122,4 +122,131 @@ // public interface OnBroadcastCompleteListener { // void onTaskComplete(DirectThreadBroadcastResponse response); // } +// +// public enum ItemType { +// TEXT("text"), +// REACTION("reaction"), +// REELSHARE("reel_share"), +// IMAGE("configure_photo"); +// +// private final String value; +// +// ItemType(final String value) { +// this.value = value; +// } +// +// public String getValue() { +// return value; +// } +// } +// +// public static abstract class BroadcastOptions { +// private final ItemType itemType; +// +// public BroadcastOptions(final ItemType itemType) { +// this.itemType = itemType; +// } +// +// public ItemType getItemType() { +// return itemType; +// } +// +// abstract Map getFormMap(); +// } +// +// public static class TextBroadcastOptions extends BroadcastOptions { +// private final String text; +// +// public TextBroadcastOptions(String text) throws UnsupportedEncodingException { +// super(ItemType.TEXT); +// this.text = URLEncoder.encode(text, "UTF-8") +// .replaceAll("\\+", "%20").replaceAll("%21", "!").replaceAll("%27", "'").replaceAll("%22", "\\\"") +// .replaceAll("%28", "(").replaceAll("%29", ")").replaceAll("%7E", "~").replaceAll("%0A", "\n"); +// } +// +// @Override +// Map getFormMap() { +// return Collections.singletonMap("text", text); +// } +// } +// +// public static class ReactionBroadcastOptions extends BroadcastOptions { +// private final String itemId; +// private final boolean delete; +// +// public ReactionBroadcastOptions(String itemId, boolean delete) { +// super(ItemType.REACTION); +// this.itemId = itemId; +// this.delete = delete; +// } +// +// @Override +// Map getFormMap() { +// final Map form = new HashMap<>(); +// form.put("item_id", itemId); +// form.put("reaction_status", delete ? "deleted" : "created"); +// form.put("reaction_type", "like"); +// return form; +// } +// } +// +// public static class StoryReplyBroadcastOptions extends BroadcastOptions { +// private final String text, mediaId, reelId; +// +// public StoryReplyBroadcastOptions(String text, String mediaId, String reelId) throws UnsupportedEncodingException { +// super(ItemType.REELSHARE); +// this.text = URLEncoder.encode(text, "UTF-8") +// .replaceAll("\\+", "%20").replaceAll("%21", "!").replaceAll("%27", "'") +// .replaceAll("%28", "(").replaceAll("%29", ")").replaceAll("%7E", "~").replaceAll("%0A", "\n"); +// this.mediaId = mediaId; +// this.reelId = reelId; // or user id, usually same +// } +// +// @Override +// Map getFormMap() { +// final Map form = new HashMap<>(); +// form.put("text", text); +// form.put("media_id", mediaId); +// form.put("reel_id", reelId); +// form.put("entry", "reel"); +// return form; +// } +// } +// +// public static class ImageBroadcastOptions extends BroadcastOptions { +// final boolean allowFullAspectRatio; +// final String uploadId; +// +// public ImageBroadcastOptions(final boolean allowFullAspectRatio, final String uploadId) { +// super(ItemType.IMAGE); +// this.allowFullAspectRatio = allowFullAspectRatio; +// this.uploadId = uploadId; +// } +// +// @Override +// Map getFormMap() { +// final Map form = new HashMap<>(); +// form.put("allow_full_aspect_ratio", String.valueOf(allowFullAspectRatio)); +// form.put("upload_id", uploadId); +// return form; +// } +// } +// +// public static class DirectThreadBroadcastResponse { +// private final int responseCode; +// private final JSONObject response; +// +// public DirectThreadBroadcastResponse(int responseCode, JSONObject response) { +// this.responseCode = responseCode; +// this.response = response; +// } +// +// public int getResponseCode() { +// return responseCode; +// } +// +// public JSONObject getResponse() { +// return response; +// } +// } // } diff --git a/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java b/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java index 53f222e0..3e0d8e14 100644 --- a/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java +++ b/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java @@ -183,11 +183,13 @@ public class PostsRecyclerView extends RecyclerView { private void initSelf() { feedViewModel = new ViewModelProvider(viewModelStoreOwner).get(FeedViewModel.class); - feedViewModel.getList().observe(lifeCycleOwner, list -> feedAdapter.submitList(list, () -> { - if (!shouldScrollToTop) return; - smoothScrollToPosition(0); - shouldScrollToTop = false; - })); + feedViewModel.getList().observe(lifeCycleOwner, list -> { + if (list.size() > 0) feedAdapter.submitList(list, () -> { + if (!shouldScrollToTop) return; + smoothScrollToPosition(0); + shouldScrollToTop = false; + }); + }); postFetcher = new PostFetcher(postFetchService, fetchListener); if (layoutPreferences.getHasGap()) { addItemDecoration(gridSpacingItemDecoration); diff --git a/app/src/main/java/awais/instagrabber/db/AppDatabase.java b/app/src/main/java/awais/instagrabber/db/AppDatabase.java index 6190575a..11547601 100644 --- a/app/src/main/java/awais/instagrabber/db/AppDatabase.java +++ b/app/src/main/java/awais/instagrabber/db/AppDatabase.java @@ -161,7 +161,7 @@ public abstract class AppDatabase extends RoomDatabase { final FavoriteType type = favoriteTypeQueryPair.first; final String query = favoriteTypeQueryPair.second; oldModels.add(new Favorite( - -1, + 0, query, type, queryDisplayExists ? cursor.getString(cursor.getColumnIndex("query_display")) diff --git a/app/src/main/java/awais/instagrabber/db/dao/FavoriteDao.java b/app/src/main/java/awais/instagrabber/db/dao/FavoriteDao.java index 50dc3129..4bf0f2ad 100644 --- a/app/src/main/java/awais/instagrabber/db/dao/FavoriteDao.java +++ b/app/src/main/java/awais/instagrabber/db/dao/FavoriteDao.java @@ -21,7 +21,7 @@ public interface FavoriteDao { @Query("SELECT * FROM favorites WHERE query_text = :query and type = :type") Favorite findFavoriteByQueryAndType(String query, FavoriteType type); - @Insert(onConflict = OnConflictStrategy.REPLACE) + @Insert List insertFavorites(Favorite... favorites); @Update diff --git a/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java index 6a468cf3..91779710 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java @@ -9,6 +9,7 @@ import android.graphics.drawable.ColorDrawable; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -19,6 +20,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentActivity; import com.facebook.drawee.backends.pipeline.Fresco; import com.facebook.drawee.controller.BaseControllerListener; @@ -30,9 +32,19 @@ import java.io.File; import awais.instagrabber.R; import awais.instagrabber.asyncs.ProfilePictureFetcher; import awais.instagrabber.databinding.DialogProfilepicBinding; +import awais.instagrabber.db.entities.Account; +import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.interfaces.FetchListener; +import awais.instagrabber.models.StoryModel; +import awais.instagrabber.repositories.responses.UserInfo; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.webservices.ProfileService; +import awais.instagrabber.webservices.ServiceCallback; + +import static awais.instagrabber.utils.Utils.settingsHelper; public class ProfilePicDialogFragment extends DialogFragment { private static final String TAG = "ProfilePicDlgFragment"; @@ -41,9 +53,15 @@ public class ProfilePicDialogFragment extends DialogFragment { private final String name; private final String fallbackUrl; + private boolean isLoggedIn; private DialogProfilepicBinding binding; private String url; + private final FetchListener fetchListener = profileUrl -> { + url = profileUrl; + setupPhoto(); + }; + public ProfilePicDialogFragment(final String id, final String name, final String fallbackUrl) { this.id = id; this.name = name; @@ -55,6 +73,8 @@ public class ProfilePicDialogFragment extends DialogFragment { final ViewGroup container, final Bundle savedInstanceState) { binding = DialogProfilepicBinding.inflate(inflater, container, false); + final String cookie = settingsHelper.getString(Constants.COOKIE); + isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != null; return binding.getRoot(); } @@ -83,7 +103,7 @@ public class ProfilePicDialogFragment extends DialogFragment { public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); init(); - fetchPhoto(); + fetchAvatar(); } private void init() { @@ -106,37 +126,55 @@ public class ProfilePicDialogFragment extends DialogFragment { } } - private void fetchPhoto() { - final FetchListener fetchListener = profileUrl -> { - url = profileUrl; - if (TextUtils.isEmpty(url)) { - url = fallbackUrl; - } - final DraweeController controller = Fresco - .newDraweeControllerBuilder() - .setUri(url) - .setOldController(binding.imageViewer.getController()) - .setControllerListener(new BaseControllerListener() { - @Override - public void onFailure(final String id, final Throwable throwable) { - super.onFailure(id, throwable); - binding.download.setVisibility(View.GONE); - binding.progressView.setVisibility(View.GONE); - } + private void fetchAvatar() { + if (isLoggedIn) { + final ProfileService profileService = ProfileService.getInstance(); + profileService.getUserInfo(id, new ServiceCallback() { + @Override + public void onSuccess(final UserInfo result) { + if (result != null) { + fetchListener.onResult(result.getHDProfilePicUrl()); + } + } - @Override - public void onFinalImageSet(final String id, - final ImageInfo imageInfo, - final Animatable animatable) { - super.onFinalImageSet(id, imageInfo, animatable); - binding.download.setVisibility(View.VISIBLE); - binding.progressView.setVisibility(View.GONE); - } - }) - .build(); - binding.imageViewer.setController(controller); - }; - new ProfilePictureFetcher(name, id, fetchListener, url, false).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + @Override + public void onFailure(final Throwable t) { + final Context context = getContext(); + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + getDialog().dismiss(); + } + }); + } + else new ProfilePictureFetcher(name, id, fetchListener, fallbackUrl, false).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void setupPhoto() { + if (TextUtils.isEmpty(url)) { + url = fallbackUrl; + } + final DraweeController controller = Fresco + .newDraweeControllerBuilder() + .setUri(url) + .setOldController(binding.imageViewer.getController()) + .setControllerListener(new BaseControllerListener() { + @Override + public void onFailure(final String id, final Throwable throwable) { + super.onFailure(id, throwable); + binding.download.setVisibility(View.GONE); + binding.progressView.setVisibility(View.GONE); + } + + @Override + public void onFinalImageSet(final String id, + final ImageInfo imageInfo, + final Animatable animatable) { + super.onFinalImageSet(id, imageInfo, animatable); + binding.download.setVisibility(View.VISIBLE); + binding.progressView.setVisibility(View.GONE); + } + }) + .build(); + binding.imageViewer.setController(controller); } private void downloadProfilePicture() { diff --git a/app/src/main/java/awais/instagrabber/fragments/CommentsViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/CommentsViewerFragment.java index c716c989..500ad96b 100644 --- a/app/src/main/java/awais/instagrabber/fragments/CommentsViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/CommentsViewerFragment.java @@ -25,6 +25,7 @@ import androidx.appcompat.widget.LinearLayoutCompat; 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; @@ -32,11 +33,18 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import awais.instagrabber.BuildConfig; import awais.instagrabber.R; import awais.instagrabber.adapters.CommentsAdapter; import awais.instagrabber.asyncs.CommentsFetcher; +import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; import awais.instagrabber.databinding.FragmentCommentsBinding; import awais.instagrabber.dialogs.ProfilePicDialogFragment; +import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.models.CommentModel; import awais.instagrabber.models.ProfileModel; import awais.instagrabber.utils.Constants; @@ -56,17 +64,48 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl private CommentsAdapter commentsAdapter; private FragmentCommentsBinding binding; - private String shortCode; - private String userId; + private LinearLayoutManager layoutManager; + private RecyclerLazyLoader lazyLoader; + private String shortCode, userId, endCursor = null; private Resources resources; private InputMethodManager imm; private AppCompatActivity fragmentActivity; private LinearLayoutCompat root; - private boolean shouldRefresh = true; + private boolean shouldRefresh = true, hasNextPage = false; private MediaService mediaService; private String postId; + private AsyncTask> currentlyRunning; private CommentsViewModel commentsViewModel; + private final FetchListener> fetchListener = new FetchListener>() { + @Override + public void doBefore() { + binding.swipeRefreshLayout.setRefreshing(true); + } + + @Override + public void onResult(final List commentModels) { + if (commentModels != null && commentModels.size() > 0) { + endCursor = commentModels.get(0).getEndCursor(); + hasNextPage = commentModels.get(0).hasNextPage(); + List list = commentsViewModel.getList().getValue(); + list = list != null ? new LinkedList<>(list) : new LinkedList<>(); + // final int oldSize = list != null ? list.size() : 0; + list.addAll(commentModels); + commentsViewModel.getList().postValue(list); + } + binding.swipeRefreshLayout.setRefreshing(false); + stopCurrentExecutor(); + } + + @Override + public void onFailure(Throwable t) { + Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); + binding.swipeRefreshLayout.setRefreshing(false); + stopCurrentExecutor(); + } + }; + private final CommentsAdapter.CommentCallback commentCallback = new CommentsAdapter.CommentCallback() { @Override public void onClick(final CommentModel comment) { @@ -181,11 +220,11 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl @Override public void onRefresh() { - binding.swipeRefreshLayout.setRefreshing(true); - new CommentsFetcher(shortCode, commentModels -> { - commentsViewModel.getList().postValue(commentModels); - binding.swipeRefreshLayout.setRefreshing(false); - }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + endCursor = null; + lazyLoader.resetState(); + commentsViewModel.getList().postValue(Collections.emptyList()); + stopCurrentExecutor(); + currentlyRunning = new CommentsFetcher(shortCode, "", fetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private void init() { @@ -198,7 +237,8 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl binding.swipeRefreshLayout.setOnRefreshListener(this); binding.swipeRefreshLayout.setRefreshing(true); commentsViewModel = new ViewModelProvider(this).get(CommentsViewModel.class); - binding.rvComments.setLayoutManager(new LinearLayoutManager(getContext())); + layoutManager = new LinearLayoutManager(getContext()); + binding.rvComments.setLayoutManager(layoutManager); commentsAdapter = new CommentsAdapter(commentCallback); binding.rvComments.setAdapter(commentsAdapter); commentsViewModel.getList().observe(getViewLifecycleOwner(), commentsAdapter::submitList); @@ -226,6 +266,13 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl }); binding.commentField.setEndIconOnClickListener(newCommentListener); } + lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { + if (hasNextPage && !TextUtils.isEmpty(endCursor)) + currentlyRunning = new CommentsFetcher(shortCode, endCursor, fetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + endCursor = null; + }); + binding.rvComments.addOnScrollListener(lazyLoader); + stopCurrentExecutor(); onRefresh(); } @@ -249,30 +296,29 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl && (userIdFromCookie.equals(commentModel.getProfileModel().getId()) || userIdFromCookie.equals(userId))) { commentDialogList = new String[]{ resources.getString(R.string.open_profile), - resources.getString(R.string.view_pfp), -// resources.getString(R.string.comment_viewer_copy_user), resources.getString(R.string.comment_viewer_copy_comment), + resources.getString(R.string.comment_viewer_see_likers), resources.getString(R.string.comment_viewer_reply_comment), commentModel.getLiked() ? resources.getString(R.string.comment_viewer_unlike_comment) : resources.getString(R.string.comment_viewer_like_comment), + resources.getString(R.string.comment_viewer_translate_comment), resources.getString(R.string.comment_viewer_delete_comment) }; } else if (!TextUtils.isEmpty(cookie)) { commentDialogList = new String[]{ resources.getString(R.string.open_profile), - resources.getString(R.string.view_pfp), -// resources.getString(R.string.comment_viewer_copy_user), resources.getString(R.string.comment_viewer_copy_comment), + resources.getString(R.string.comment_viewer_see_likers), resources.getString(R.string.comment_viewer_reply_comment), commentModel.getLiked() ? resources.getString(R.string.comment_viewer_unlike_comment) : resources.getString(R.string.comment_viewer_like_comment), + resources.getString(R.string.comment_viewer_translate_comment) }; } else { commentDialogList = new String[]{ resources.getString(R.string.open_profile), - resources.getString(R.string.view_pfp), -// resources.getString(R.string.comment_viewer_copy_user), - resources.getString(R.string.comment_viewer_copy_comment) + resources.getString(R.string.comment_viewer_copy_comment), + resources.getString(R.string.comment_viewer_see_likers) }; } final Context context = getContext(); @@ -284,25 +330,20 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl case 0: // open profile openProfile("@" + profileModel.getUsername()); break; - case 1: // view profile pic - final FragmentManager fragmentManager = getParentFragmentManager(); - final ProfilePicDialogFragment fragment = new ProfilePicDialogFragment(profileModel.getId(), - profileModel.getUsername(), - profileModel.getHdProfilePic()); - final FragmentTransaction ft = fragmentManager.beginTransaction(); - ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) - .add(fragment, "profilePicDialog") - .commit(); - break; -// case 2: // copy username -// Utils.copyText(context, profileModel.getUsername()); -// break; - case 2: // copy comment + case 1: // copy comment Utils.copyText(context, "@" + profileModel.getUsername() + ": " + commentModel.getText()); break; + case 2: // see comment likers, this is surprisingly available to anons + final NavController navController = getNavController(); + if (navController != null) { + final Bundle bundle = new Bundle(); + bundle.putString("postId", commentModel.getId()); + bundle.putBoolean("isComment", true); + navController.navigate(R.id.action_global_likesViewerFragment, bundle); + } + else Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + break; case 3: // reply to comment - // final View focus = binding.rvComments.findViewWithTag(commentModel); - // focus.setBackgroundColor(0x80888888); commentsAdapter.setSelected(commentModel); String mention = "@" + profileModel.getUsername() + " "; binding.commentText.setText(mention); @@ -326,7 +367,7 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); return; } - onRefresh(); + commentsAdapter.setLiked(commentModel, true); } @Override @@ -344,7 +385,7 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); return; } - onRefresh(); + commentsAdapter.setLiked(commentModel, false); } @Override @@ -354,7 +395,29 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl } }); break; - case 5: // delete comment + case 5: // translate comment + mediaService.translate(commentModel.getId(), "2", new ServiceCallback() { + @Override + public void onSuccess(final String result) { + if (TextUtils.isEmpty(result)) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + return; + } + new AlertDialog.Builder(context) + .setTitle(username) + .setMessage(result) + .setPositiveButton(R.string.ok, null) + .show(); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "Error translating comment", t); + Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + break; + case 6: // delete comment final String userId = CookieUtils.getUserIdFromCookie(cookie); if (userId == null) return; mediaService.deleteComment( @@ -389,4 +452,25 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl final NavDirections action = CommentsViewerFragmentDirections.actionGlobalProfileFragment(username); NavHostFragment.findNavController(this).navigate(action); } + + private void stopCurrentExecutor() { + if (currentlyRunning != null) { + try { + currentlyRunning.cancel(true); + } catch (final Exception e) { + if (BuildConfig.DEBUG) Log.e(TAG, "", e); + } + } + } + + @Nullable + private NavController getNavController() { + NavController navController = null; + try { + navController = NavHostFragment.findNavController(this); + } catch (IllegalStateException e) { + Log.e(TAG, "navigateToProfile", e); + } + return navController; + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java b/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java index 190e9a59..34c81322 100644 --- a/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java @@ -81,7 +81,6 @@ public class FavoritesFragment extends Fragment { @Override public void onSuccess(final List favorites) { favoritesViewModel.getList().postValue(favorites); - fetchMissingInfo(favorites); } @Override @@ -156,109 +155,4 @@ public class FavoritesFragment extends Fragment { binding.favoriteList.setAdapter(adapter); } - - private void fetchMissingInfo(final List allFavorites) { - final Runnable runnable = () -> { - final List updatedList = new ArrayList<>(allFavorites); - // cyclic barrier is to make the async calls synchronous - final CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> { - // Log.d(TAG, "fetchMissingInfo: barrier action"); - favoritesViewModel.getList().postValue(new ArrayList<>(updatedList)); - }); - try { - for (final Favorite model : allFavorites) { - cyclicBarrier.reset(); - // if the model has missing pic or display name (for user and location), fetch those details - switch (model.getType()) { - case LOCATION: - if (TextUtils.isEmpty(model.getDisplayName()) - || TextUtils.isEmpty(model.getPicUrl())) { - new LocationFetcher(model.getQuery(), result -> { - if (result == null) return; - final int i = updatedList.indexOf(model); - updatedList.remove(i); - final Favorite updated = new Favorite( - model.getId(), - model.getQuery(), - model.getType(), - result.getName(), - result.getSdProfilePic(), - model.getDateAdded() - ); - favoriteRepository.insertOrUpdateFavorite(updated, new RepositoryCallback() { - @Override - public void onSuccess(final Void result) { - updatedList.add(i, updated); - try { - cyclicBarrier.await(); - } catch (BrokenBarrierException | InterruptedException e) { - Log.e(TAG, "fetchMissingInfo: ", e); - } - } - - @Override - public void onDataNotAvailable() { - try { - cyclicBarrier.await(); - } catch (BrokenBarrierException | InterruptedException e) { - Log.e(TAG, "fetchMissingInfo: ", e); - } - } - }); - }).execute(); - cyclicBarrier.await(); - } - break; - case USER: - if (TextUtils.isEmpty(model.getDisplayName()) - || TextUtils.isEmpty(model.getPicUrl())) { - new ProfileFetcher(model.getQuery(), result -> { - if (result == null) return; - final int i = updatedList.indexOf(model); - updatedList.remove(i); - final Favorite updated = new Favorite( - model.getId(), - model.getQuery(), - model.getType(), - result.getName(), - result.getSdProfilePic(), - model.getDateAdded() - ); - favoriteRepository.insertOrUpdateFavorite(updated, new RepositoryCallback() { - @Override - public void onSuccess(final Void result) { - try { - cyclicBarrier.await(); - } catch (BrokenBarrierException | InterruptedException e) { - Log.e(TAG, "fetchMissingInfo: ", e); - } - } - - @Override - public void onDataNotAvailable() { - try { - cyclicBarrier.await(); - } catch (BrokenBarrierException | InterruptedException e) { - Log.e(TAG, "fetchMissingInfo: ", e); - } - } - }); - updatedList.add(i, updated); - }).execute(); - cyclicBarrier.await(); - } - break; - case HASHTAG: - default: - // hashtags don't require displayName or pic - // updatedList.add(model); - } - } - } catch (Exception e) { - Log.e(TAG, "fetchMissingInfo: ", e); - } - favoritesViewModel.getList().postValue(updatedList); - }; - new Thread(runnable).start(); - } } diff --git a/app/src/main/java/awais/instagrabber/fragments/FollowViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/FollowViewerFragment.java index f27d2c91..2512cca0 100644 --- a/app/src/main/java/awais/instagrabber/fragments/FollowViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/FollowViewerFragment.java @@ -20,29 +20,22 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.SearchView; import androidx.fragment.app.Fragment; import androidx.navigation.fragment.NavHostFragment; +import androidx.recyclerview.widget.LinearLayoutManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import java.util.ArrayList; -import java.util.Arrays; -import awais.instagrabber.BuildConfig; 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.FriendshipRepoChangeRootResponse; import awais.instagrabber.repositories.responses.FriendshipRepoListFetchResponse; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.webservices.FriendshipService; import awais.instagrabber.webservices.ServiceCallback; -import awaisomereport.LogCollector; import thoughtbot.expandableadapter.ExpandableGroup; -import static awais.instagrabber.utils.Utils.logCollector; -import static awais.instagrabber.utils.Utils.settingsHelper; - public final class FollowViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { private static final String TAG = "FollowViewerFragment"; @@ -51,9 +44,11 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh private final ArrayList followersModels = new ArrayList<>(); private final ArrayList allFollowing = new ArrayList<>(); - private boolean isFollowersList, isCompare = false, loading = false; - private String profileId, username, namePost, type; + private boolean moreAvailable = true, isFollowersList, isCompare = false, loading = false, shouldRefresh = true; + private String profileId, username, namePost, type, endCursor; private Resources resources; + private LinearLayoutManager layoutManager; + private RecyclerLazyLoader lazyLoader; private FollowModel model; private FollowAdapter adapter; private View.OnClickListener clickListener; @@ -61,9 +56,61 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh private AsyncTask currentlyExecuting; private SwipeRefreshLayout root; private FriendshipService friendshipService; - private boolean shouldRefresh = true; private AppCompatActivity fragmentActivity; + final ServiceCallback followingFetchCb = new ServiceCallback() { + @Override + public void onSuccess(final FriendshipRepoListFetchResponse result) { + if (result != null) { + followingModels.addAll(result.getItems()); + if (!isFollowersList) followModels.addAll(result.getItems()); + if (result.isMoreAvailable()) { + endCursor = result.getNextMaxId(); + friendshipService.getList(false, profileId, endCursor, this); + } else if (followersModels.size() == 0) { + if (!isFollowersList) moreAvailable = false; + friendshipService.getList(true, profileId, null, followingFetchCb); + } else { + if (!isFollowersList) moreAvailable = false; + showCompare(); + } + } else binding.swipeRefreshLayout.setRefreshing(false); + } + + @Override + public void onFailure(final Throwable t) { + binding.swipeRefreshLayout.setRefreshing(false); + Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); + Log.e(TAG, "Error fetching list (double, following)", t); + } + }; + final ServiceCallback followersFetchCb = new ServiceCallback() { + @Override + public void onSuccess(final FriendshipRepoListFetchResponse result) { + if (result != null) { + followersModels.addAll(result.getItems()); + if (isFollowersList) followModels.addAll(result.getItems()); + if (result.isMoreAvailable()) { + endCursor = result.getNextMaxId(); + friendshipService.getList(true, profileId, endCursor, this); + } else if (followingModels.size() == 0) { + if (isFollowersList) moreAvailable = false; + friendshipService.getList(false, profileId, null, followingFetchCb); + } else { + if (isFollowersList) moreAvailable = false; + showCompare(); + } + } + } + + @Override + public void onFailure(final Throwable t) { + binding.swipeRefreshLayout.setRefreshing(false); + Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); + Log.e(TAG, "Error fetching list (double, follower)", t); + } + }; + @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -141,13 +188,13 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh public void onRefresh() { if (isCompare) listCompare(); else listFollows(); + endCursor = null; + lazyLoader.resetState(); } private void listFollows() { - loading = true; type = resources.getString(isFollowersList ? R.string.followers_type_followers : R.string.followers_type_following); setSubtitle(type); - followModels.clear(); final ServiceCallback cb = new ServiceCallback() { @Override public void onSuccess(final FriendshipRepoListFetchResponse result) { @@ -156,82 +203,70 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh return; } else { + int oldSize = followModels.size() == 0 ? 0 : followModels.size() - 1; followModels.addAll(result.getItems()); if (result.isMoreAvailable()) { - friendshipService.getList(isFollowersList, profileId, result.getNextMaxId(), this); - } - else { - binding.swipeRefreshLayout.setRefreshing(false); - if (isFollowersList) followersModels.addAll(followModels); - else followingModels.addAll(followModels); - refreshAdapter(followModels, null, null, null); + 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) { binding.swipeRefreshLayout.setRefreshing(false); + Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); Log.e(TAG, "Error fetching list (single)", t); } }; - binding.swipeRefreshLayout.setRefreshing(true); - friendshipService.getList(isFollowersList, profileId, null, cb); + layoutManager = new LinearLayoutManager(getContext()); + lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { + if (!TextUtils.isEmpty(endCursor)) { + binding.swipeRefreshLayout.setRefreshing(true); + layoutManager.setStackFromEnd(true); + friendshipService.getList(isFollowersList, profileId, endCursor, cb); + } + endCursor = null; + }); + binding.rvFollow.addOnScrollListener(lazyLoader); + binding.rvFollow.setLayoutManager(layoutManager); + if (moreAvailable) { + binding.swipeRefreshLayout.setRefreshing(true); + friendshipService.getList(isFollowersList, profileId, endCursor, cb); + } + 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(); - followersModels.clear(); - followingModels.clear(); - final ServiceCallback followingFetchCb = new ServiceCallback() { - @Override - public void onSuccess(final FriendshipRepoListFetchResponse result) { - if (result != null) { - followingModels.addAll(result.getItems()); - - if (result.isMoreAvailable()) { - friendshipService.getList(false, profileId, result.getNextMaxId(), this); - } else { - showCompare(); - } - } else binding.swipeRefreshLayout.setRefreshing(false); - } - - @Override - public void onFailure(final Throwable t) { - binding.swipeRefreshLayout.setRefreshing(false); - Log.e(TAG, "Error fetching list (double, following)", t); - } - }; - final ServiceCallback followersFetchCb = new ServiceCallback() { - @Override - public void onSuccess(final FriendshipRepoListFetchResponse result) { - if (result != null) { - followersModels.addAll(result.getItems()); - if (result.isMoreAvailable()) { - friendshipService.getList(true, profileId, result.getNextMaxId(), this); - } else if (followingModels.size() == 0) { - friendshipService.getList(false, profileId, null, followingFetchCb); - } else { - showCompare(); - } - } - } - - @Override - public void onFailure(final Throwable t) { - binding.swipeRefreshLayout.setRefreshing(false); - Log.e(TAG, "Error fetching list (double, follower)", t); - } - }; - binding.swipeRefreshLayout.setRefreshing(true); - if (followersModels.size() == 0) { - friendshipService.getList(true, profileId, null, followersFetchCb); + if (moreAvailable) { + binding.swipeRefreshLayout.setRefreshing(true); + Toast.makeText(getContext(), R.string.follower_start_compare, Toast.LENGTH_LONG).show(); + friendshipService.getList(isFollowersList, + profileId, + endCursor, + isFollowersList ? followersFetchCb : followingFetchCb); } - else if (followingModels.size() == 0) { - friendshipService.getList(false, profileId, null, followingFetchCb); + else if (followersModels.size() == 0 || followingModels.size() == 0) { + binding.swipeRefreshLayout.setRefreshing(true); + Toast.makeText(getContext(), R.string.follower_start_compare, Toast.LENGTH_LONG).show(); + friendshipService.getList(!isFollowersList, + profileId, + null, + isFollowersList ? followingFetchCb : followersFetchCb); } else showCompare(); } @@ -346,12 +381,12 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh final Context context = getContext(); if (loading) Toast.makeText(context, R.string.follower_wait_to_load, Toast.LENGTH_LONG).show(); else if (isCompare) { - listFollows(); isCompare = !isCompare; + listFollows(); } else { - listCompare(); isCompare = !isCompare; + listCompare(); } return true; } @@ -371,8 +406,7 @@ public final class FollowViewerFragment extends Fragment implements SwipeRefresh if (allFollowing != null && allFollowing.size() > 0) groups.add(new ExpandableGroup(resources.getString(R.string.followers_both_following), allFollowing)); } else { - final ExpandableGroup group = new ExpandableGroup(type, followModels); - groups.add(group); + groups.add(new ExpandableGroup(type, followModels)); } adapter = new FollowAdapter(clickListener, groups); adapter.toggleGroup(0); diff --git a/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java b/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java index 26733e12..10fc0482 100644 --- a/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java @@ -54,6 +54,7 @@ import awais.instagrabber.db.entities.Favorite; import awais.instagrabber.db.repositories.FavoriteRepository; import awais.instagrabber.db.repositories.RepositoryCallback; import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; +import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.HashtagModel; import awais.instagrabber.models.PostsLayoutPreferences; @@ -363,19 +364,27 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe private void fetchHashtagModel() { stopCurrentExecutor(); binding.swipeRefreshLayout.setRefreshing(true); - currentlyExecuting = new HashtagFetcher(hashtag.substring(1), result -> { - hashtagModel = result; - binding.swipeRefreshLayout.setRefreshing(false); - final Context context = getContext(); - if (context == null) return; - if (hashtagModel == null) { - Toast.makeText(context, R.string.error_loading_profile, Toast.LENGTH_SHORT).show(); - return; + currentlyExecuting = new HashtagFetcher(hashtag.substring(1), new FetchListener() { + @Override + public void onResult(final HashtagModel result) { + hashtagModel = result; + binding.swipeRefreshLayout.setRefreshing(false); + final Context context = getContext(); + if (context == null) return; + if (hashtagModel == null) { + Toast.makeText(context, R.string.error_loading_profile, Toast.LENGTH_SHORT).show(); + return; + } + setTitle(); + setHashtagDetails(); + setupPosts(); + fetchStories(); + } + + @Override + public void onFailure(Throwable t) { + Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); } - setTitle(); - setHashtagDetails(); - setupPosts(); - fetchStories(); }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @@ -410,9 +419,12 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe hashtagDetailsBinding.btnFollowTag.setClickable(true); if (!result) { Log.e(TAG, "onSuccess: result is false"); + Snackbar.make(root, R.string.downloader_unknown_error, BaseTransientBottomBar.LENGTH_LONG) + .show(); return; } - onRefresh(); + hashtagDetailsBinding.btnFollowTag.setText(R.string.unfollow); + hashtagDetailsBinding.btnFollowTag.setChipIconResource(R.drawable.ic_outline_person_add_disabled_24); } @Override @@ -435,9 +447,12 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe hashtagDetailsBinding.btnFollowTag.setClickable(true); if (!result) { Log.e(TAG, "onSuccess: result is false"); + Snackbar.make(root, R.string.downloader_unknown_error, BaseTransientBottomBar.LENGTH_LONG) + .show(); return; } - onRefresh(); + hashtagDetailsBinding.btnFollowTag.setText(R.string.follow); + hashtagDetailsBinding.btnFollowTag.setChipIconResource(R.drawable.ic_outline_person_add_24); } @Override @@ -462,8 +477,23 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe favoriteRepository.getFavorite(hashtag.substring(1), FavoriteType.HASHTAG, new RepositoryCallback() { @Override public void onSuccess(final Favorite result) { - hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); - hashtagDetailsBinding.favChip.setText(R.string.favorite_short); + favoriteRepository.insertOrUpdateFavorite(new Favorite( + result.getId(), + hashtag.substring(1), + FavoriteType.HASHTAG, + hashtagModel.getName(), + hashtagModel.getSdProfilePic(), + result.getDateAdded() + ), new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); + hashtagDetailsBinding.favChip.setText(R.string.favorite_short); + } + + @Override + public void onDataNotAvailable() {} + }); } @Override @@ -492,18 +522,18 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe @Override public void onDataNotAvailable() { favoriteRepository.insertOrUpdateFavorite(new Favorite( - -1, + 0, hashtag.substring(1), FavoriteType.HASHTAG, hashtagModel.getName(), - null, + hashtagModel.getSdProfilePic(), new Date() ), new RepositoryCallback() { @Override public void onSuccess(final Void result) { hashtagDetailsBinding.favChip.setText(R.string.favorite_short); hashtagDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); - showSnackbar(getString(R.string.added_to_favs)); + showSnackbar(getString(R.string.added_to_favs_short)); } @Override @@ -513,7 +543,9 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe })); hashtagDetailsBinding.mainHashtagImage.setImageURI(hashtagModel.getSdProfilePic()); final String postCount = String.valueOf(hashtagModel.getPostCount()); - final SpannableStringBuilder span = new SpannableStringBuilder(getString(R.string.main_posts_count_inline, postCount)); + final SpannableStringBuilder span = new SpannableStringBuilder(getResources().getQuantityString(R.plurals.main_posts_count_inline, + hashtagModel.getPostCount() > 2000000000L ? 2000000000 : hashtagModel.getPostCount().intValue(), + postCount)); span.setSpan(new RelativeSizeSpan(1.2f), 0, postCount.length(), 0); span.setSpan(new StyleSpan(Typeface.BOLD), 0, postCount.length(), 0); hashtagDetailsBinding.mainTagPostCount.setText(span); @@ -522,7 +554,7 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe if (!hasStories) return; // show stories final NavDirections action = HashTagFragmentDirections - .actionHashtagFragmentToStoryViewerFragment(-1, null, true, false, hashtagModel.getName(), hashtagModel.getName()); + .actionHashtagFragmentToStoryViewerFragment(-1, null, true, false, hashtagModel.getName(), hashtagModel.getName(), false, false); NavHostFragment.findNavController(this).navigate(action); }); } diff --git a/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java new file mode 100644 index 00000000..157222b8 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java @@ -0,0 +1,170 @@ +package awais.instagrabber.fragments; + +import android.content.Context; +import android.content.res.Resources; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.LinearLayoutCompat; +import androidx.navigation.fragment.NavHostFragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; + +import java.util.List; + +import awais.instagrabber.R; +import awais.instagrabber.adapters.LikesAdapter; +import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; +import awais.instagrabber.databinding.FragmentLikesBinding; +import awais.instagrabber.models.ProfileModel; +import awais.instagrabber.repositories.responses.GraphQLUserListFetchResponse; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.utils.Utils; +import awais.instagrabber.webservices.GraphQLService; +import awais.instagrabber.webservices.MediaService; +import awais.instagrabber.webservices.ServiceCallback; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public final class LikesViewerFragment extends BottomSheetDialogFragment implements SwipeRefreshLayout.OnRefreshListener { + private static final String TAG = "LikesViewerFragment"; + + private final String cookie = Utils.settingsHelper.getString(Constants.COOKIE); + + private LikesAdapter likesAdapter; + private FragmentLikesBinding binding; + private LinearLayoutManager layoutManager; + private Resources resources; + private AppCompatActivity fragmentActivity; + private LinearLayoutCompat root; + private RecyclerLazyLoader lazyLoader; + private MediaService mediaService; + private GraphQLService graphQLService; + private boolean isLoggedIn; + private String postId, endCursor; + private boolean isComment; + + private final ServiceCallback> cb = new ServiceCallback>() { + @Override + public void onSuccess(final List result) { + final LikesAdapter likesAdapter = new LikesAdapter(result, v -> { + final Object tag = v.getTag(); + if (tag instanceof ProfileModel) { + ProfileModel model = (ProfileModel) tag; + final Bundle bundle = new Bundle(); + bundle.putString("username", "@" + model.getUsername()); + NavHostFragment.findNavController(LikesViewerFragment.this).navigate(R.id.action_global_profileFragment, bundle); + } + }); + binding.rvLikes.setAdapter(likesAdapter); + binding.rvLikes.setLayoutManager(new LinearLayoutManager(getContext())); + binding.swipeRefreshLayout.setRefreshing(false); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "Error", t); + try { + final Context context = getContext(); + Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); + } + catch (Exception e) {} + } + }; + + private final ServiceCallback acb = new ServiceCallback() { + @Override + public void onSuccess(final GraphQLUserListFetchResponse result) { + endCursor = result.getNextMaxId(); + final LikesAdapter likesAdapter = new LikesAdapter(result.getItems(), v -> { + final Object tag = v.getTag(); + if (tag instanceof ProfileModel) { + ProfileModel model = (ProfileModel) tag; + final Bundle bundle = new Bundle(); + bundle.putString("username", "@" + model.getUsername()); + NavHostFragment.findNavController(LikesViewerFragment.this).navigate(R.id.action_global_profileFragment, bundle); + } + }); + binding.rvLikes.setAdapter(likesAdapter); + binding.swipeRefreshLayout.setRefreshing(false); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "Error", t); + try { + final Context context = getContext(); + Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); + } + catch (Exception e) {} + } + }; + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final String cookie = settingsHelper.getString(Constants.COOKIE); + isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != null; + fragmentActivity = (AppCompatActivity) getActivity(); + mediaService = isLoggedIn ? MediaService.getInstance() : null; + graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); + // setHasOptionsMenu(true); + } + + @NonNull + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { + binding = FragmentLikesBinding.inflate(getLayoutInflater()); + binding.swipeRefreshLayout.setEnabled(false); + binding.swipeRefreshLayout.setNestedScrollingEnabled(false); + root = binding.getRoot(); + return root; + } + + @Override + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + init(); + } + + @Override + public void onRefresh() { + if (isComment && !isLoggedIn) { + lazyLoader.resetState(); + graphQLService.fetchCommentLikers(postId, null, acb); + } + else mediaService.fetchLikes(postId, isComment, cb); + } + + private void init() { + if (getArguments() == null) return; + final LikesViewerFragmentArgs fragmentArgs = LikesViewerFragmentArgs.fromBundle(getArguments()); + postId = fragmentArgs.getPostId(); + isComment = fragmentArgs.getIsComment(); + binding.swipeRefreshLayout.setOnRefreshListener(this); + binding.swipeRefreshLayout.setRefreshing(true); + resources = getResources(); + if (isComment && !isLoggedIn) { + layoutManager = new LinearLayoutManager(getContext()); + binding.rvLikes.setLayoutManager(layoutManager); + lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { + if (!TextUtils.isEmpty(endCursor)) + graphQLService.fetchCommentLikers(postId, endCursor, acb); + endCursor = null; + }); + binding.rvLikes.addOnScrollListener(lazyLoader); + } + onRefresh(); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java b/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java index acccc0b9..c48d4939 100644 --- a/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java @@ -400,8 +400,9 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR // binding.swipeRefreshLayout.setRefreshing(true); locationDetailsBinding.mainLocationImage.setImageURI(locationModel.getSdProfilePic()); final String postCount = String.valueOf(locationModel.getPostCount()); - final SpannableStringBuilder span = new SpannableStringBuilder(getString(R.string.main_posts_count_inline, - postCount)); + final SpannableStringBuilder span = new SpannableStringBuilder(getResources().getQuantityString(R.plurals.main_posts_count_inline, + locationModel.getPostCount() > 2000000000L ? 2000000000 : locationModel.getPostCount().intValue(), + postCount)); span.setSpan(new RelativeSizeSpan(1.2f), 0, postCount.length(), 0); span.setSpan(new StyleSpan(Typeface.BOLD), 0, postCount.length(), 0); locationDetailsBinding.mainLocPostCount.setText(span); @@ -455,6 +456,20 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); locationDetailsBinding.favChip.setText(R.string.favorite_short); + favoriteRepository.insertOrUpdateFavorite(new Favorite( + result.getId(), + locationId, + FavoriteType.LOCATION, + locationModel.getName(), + locationModel.getSdProfilePic(), + result.getDateAdded() + ), new RepositoryCallback() { + @Override + public void onSuccess(final Void result) {} + + @Override + public void onDataNotAvailable() {} + }); } @Override @@ -484,7 +499,7 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR @Override public void onDataNotAvailable() { favoriteRepository.insertOrUpdateFavorite(new Favorite( - -1, + 0, locationId, FavoriteType.LOCATION, locationModel.getName(), @@ -495,7 +510,7 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR public void onSuccess(final Void result) { locationDetailsBinding.favChip.setText(R.string.favorite_short); locationDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); - showSnackbar(getString(R.string.added_to_favs)); + showSnackbar(getString(R.string.added_to_favs_short)); } @Override @@ -508,7 +523,7 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR if (hasStories) { // show stories final NavDirections action = LocationFragmentDirections - .actionLocationFragmentToStoryViewerFragment(-1, null, false, true, locationId, locationModel.getName()); + .actionLocationFragmentToStoryViewerFragment(-1, null, false, true, locationId, locationModel.getName(), false, false); NavHostFragment.findNavController(this).navigate(action); } }); diff --git a/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java index f8733382..34559f0e 100644 --- a/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java @@ -18,20 +18,28 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.app.NotificationManagerCompat; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.NavDirections; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import java.util.List; + import awais.instagrabber.R; import awais.instagrabber.adapters.NotificationsAdapter; import awais.instagrabber.adapters.NotificationsAdapter.OnNotificationClickListener; import awais.instagrabber.asyncs.NotificationsFetcher; import awais.instagrabber.asyncs.PostFetcher; import awais.instagrabber.databinding.FragmentNotificationsViewerBinding; +import awais.instagrabber.dialogs.ProfilePicDialogFragment; import awais.instagrabber.fragments.settings.MorePreferencesFragmentDirections; +import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.interfaces.MentionClickListener; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.NotificationModel; import awais.instagrabber.models.enums.NotificationType; import awais.instagrabber.repositories.responses.FriendshipRepoChangeRootResponse; import awais.instagrabber.utils.Constants; @@ -40,6 +48,7 @@ import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.NotificationViewModel; import awais.instagrabber.webservices.FriendshipService; +import awais.instagrabber.webservices.MediaService; import awais.instagrabber.webservices.NewsService; import awais.instagrabber.webservices.ServiceCallback; @@ -53,96 +62,146 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe private boolean shouldRefresh = true; private NotificationViewModel notificationViewModel; private FriendshipService friendshipService; - private String userId; - private String csrfToken; + private MediaService mediaService; private NewsService newsService; + private String userId, csrfToken, type; + private Context context; - private final OnNotificationClickListener clickListener = model -> { - if (model == null) return; - final String username = model.getUsername(); - final SpannableString title = new SpannableString(username + (TextUtils.isEmpty(model.getText()) ? "" : (":\n" + model.getText()))); - title.setSpan(new RelativeSizeSpan(1.23f), 0, username.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - - String[] commentDialogList; - if (model.getShortCode() != null) { - commentDialogList = new String[]{ - getString(R.string.open_profile), - getString(R.string.view_post) - }; - } else if (model.getType() == NotificationType.REQUEST) { - commentDialogList = new String[]{ - getString(R.string.open_profile), - getString(R.string.request_approve), - getString(R.string.request_reject) - }; - } else { - commentDialogList = new String[]{getString(R.string.open_profile)}; + private final OnNotificationClickListener clickListener = new OnNotificationClickListener() { + @Override + public void onProfileClick(final String username) { + openProfile(username); } - final Context context = getContext(); - if (context == null) return; - final DialogInterface.OnClickListener profileDialogListener = (dialog, which) -> { - switch (which) { - case 0: - openProfile(model.getUsername()); - break; - case 1: - if (model.getType() == NotificationType.REQUEST) { - friendshipService.approve(userId, model.getUserId(), csrfToken, new ServiceCallback() { - @Override - public void onSuccess(final FriendshipRepoChangeRootResponse result) { - // Log.d(TAG, "onSuccess: " + result); - if (result.getStatus().equals("ok")) { - onRefresh(); - return; - } - Log.e(TAG, "approve: status was not ok!"); - } - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "approve: onFailure: ", t); - } - }); - return; - } - final AlertDialog alertDialog = new AlertDialog.Builder(context) - .setCancelable(false) - .setView(R.layout.dialog_opening_post) - .create(); - alertDialog.show(); - new PostFetcher(model.getShortCode(), feedModel -> { + @Override + public void onPreviewClick(final NotificationModel model) { + if (model.getType() == NotificationType.RESPONDED_STORY) { + final NavDirections action = NotificationsViewerFragmentDirections.actionNotificationsViewerFragmentToStoryViewerFragment( + -1, null, false, false, model.getPostId(), model.getUsername(), false, true); + NavHostFragment.findNavController(NotificationsViewerFragment.this).navigate(action); + } + else { + mediaService.fetch(model.getPostId(), new ServiceCallback() { + @Override + public void onSuccess(final FeedModel feedModel) { final PostViewV2Fragment fragment = PostViewV2Fragment .builder(feedModel) .build(); - fragment.setOnShowListener(dialog1 -> alertDialog.dismiss()); fragment.show(getChildFragmentManager(), "post_view"); - }).execute(); - break; - case 2: - friendshipService.ignore(userId, model.getUserId(), csrfToken, new ServiceCallback() { - @Override - public void onSuccess(final FriendshipRepoChangeRootResponse result) { - // Log.d(TAG, "onSuccess: " + result); - if (result.getStatus().equals("ok")) { - onRefresh(); + } + + @Override + public void onFailure(final Throwable t) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } + }); + } + } + + @Override + public void onNotificationClick(final NotificationModel model) { + if (model == null) return; + final String username = model.getUsername(); + if (model.getType() == NotificationType.FOLLOW || model.getType() == NotificationType.AYML) { + openProfile(username); + } + else { + final SpannableString title = new SpannableString(username + (TextUtils.isEmpty(model.getText()) ? "" : (":\n" + model.getText()))); + title.setSpan(new RelativeSizeSpan(1.23f), 0, username.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + + String[] commentDialogList; + if (model.getType() == NotificationType.RESPONDED_STORY) { + commentDialogList = new String[]{ + getString(R.string.open_profile), + getString(R.string.view_story) + }; + } + else if (model.getPostId() != null) { + commentDialogList = new String[]{ + getString(R.string.open_profile), + getString(R.string.view_post) + }; + } + else if (model.getType() == NotificationType.REQUEST) { + commentDialogList = new String[]{ + getString(R.string.open_profile), + getString(R.string.request_approve), + getString(R.string.request_reject) + }; + } + else commentDialogList = null; // shouldn't happen + final Context context = getContext(); + if (context == null) return; + final DialogInterface.OnClickListener profileDialogListener = (dialog, which) -> { + switch (which) { + case 0: + openProfile(username); + break; + case 1: + if (model.getType() == NotificationType.REQUEST) { + friendshipService.approve(userId, model.getUserId(), csrfToken, new ServiceCallback() { + @Override + public void onSuccess(final FriendshipRepoChangeRootResponse result) { + onRefresh(); + Log.e(TAG, "approve: status was not ok!"); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "approve: onFailure: ", t); + } + }); return; } - Log.e(TAG, "ignore: status was not ok!"); - } + else if (model.getType() == NotificationType.RESPONDED_STORY) { + final NavDirections action = NotificationsViewerFragmentDirections.actionNotificationsViewerFragmentToStoryViewerFragment( + -1, null, false, false, model.getPostId(), model.getUsername(), false, true); + NavHostFragment.findNavController(NotificationsViewerFragment.this).navigate(action); + return; + } + final AlertDialog alertDialog = new AlertDialog.Builder(context) + .setCancelable(false) + .setView(R.layout.dialog_opening_post) + .create(); + alertDialog.show(); + mediaService.fetch(model.getPostId(), new ServiceCallback() { + @Override + public void onSuccess(final FeedModel feedModel) { + final PostViewV2Fragment fragment = PostViewV2Fragment + .builder(feedModel) + .build(); + fragment.setOnShowListener(dialog1 -> alertDialog.dismiss()); + fragment.show(getChildFragmentManager(), "post_view"); + } - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "ignore: onFailure: ", t); - } - }); - break; + @Override + public void onFailure(final Throwable t) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } + }); + break; + case 2: + friendshipService.ignore(userId, model.getUserId(), csrfToken, new ServiceCallback() { + @Override + public void onSuccess(final FriendshipRepoChangeRootResponse result) { + onRefresh(); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "ignore: onFailure: ", t); + } + }); + break; + } + }; + new AlertDialog.Builder(context) + .setTitle(title) + .setItems(commentDialogList, profileDialogListener) + .setNegativeButton(R.string.cancel, null) + .show(); } - }; - new AlertDialog.Builder(context) - .setTitle(title) - .setItems(commentDialogList, profileDialogListener) - .setNegativeButton(R.string.cancel, null) - .show(); + } }; private final MentionClickListener mentionClickListener = (view, text, isHashtag, isLocation) -> { if (getContext() == null) return; @@ -158,7 +217,7 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - final Context context = getContext(); + context = getContext(); if (context == null) return; NotificationManagerCompat.from(context.getApplicationContext()).cancel(Constants.ACTIVITY_NOTIFICATION_ID); final String cookie = Utils.settingsHelper.getString(Constants.COOKIE); @@ -166,7 +225,7 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe Toast.makeText(context, R.string.activity_notloggedin, Toast.LENGTH_SHORT).show(); } friendshipService = FriendshipService.getInstance(); - newsService = NewsService.getInstance(); + mediaService = MediaService.getInstance(); userId = CookieUtils.getUserIdFromCookie(cookie); csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); } @@ -191,6 +250,8 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe } private void init() { + final NotificationsViewerFragmentArgs fragmentArgs = NotificationsViewerFragmentArgs.fromBundle(getArguments()); + type = fragmentArgs.getType(); final Context context = getContext(); CookieUtils.setupCookies(settingsHelper.getString(Constants.COOKIE)); binding.swipeRefreshLayout.setOnRefreshListener(this); @@ -205,23 +266,39 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe @Override public void onRefresh() { binding.swipeRefreshLayout.setRefreshing(true); - new NotificationsFetcher(notificationModels -> { - binding.swipeRefreshLayout.setRefreshing(false); - notificationViewModel.getList().postValue(notificationModels); - final String timestamp = String.valueOf(System.currentTimeMillis() / 1000); - newsService.markChecked(timestamp, csrfToken, new ServiceCallback() { - @Override - public void onSuccess(@NonNull final Boolean result) { - // Log.d(TAG, "onResponse: body: " + result); - if (!result) Log.e(TAG, "onSuccess: Error marking activity checked, response is false"); - } + switch (type) { + case "notif": + new NotificationsFetcher(true, new FetchListener>() { + @Override + public void onResult(final List notificationModels) { + binding.swipeRefreshLayout.setRefreshing(false); + notificationViewModel.getList().postValue(notificationModels); + } - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "onFailure: Error marking activity checked", t); - } - }); - }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + @Override + public void onFailure(Throwable t) { + binding.swipeRefreshLayout.setRefreshing(false); + Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + break; + case "ayml": + newsService = NewsService.getInstance(); + newsService.fetchSuggestions(csrfToken, new ServiceCallback>() { + @Override + public void onSuccess(final List notificationModels) { + binding.swipeRefreshLayout.setRefreshing(false); + notificationViewModel.getList().postValue(notificationModels); + } + + @Override + public void onFailure(final Throwable t) { + binding.swipeRefreshLayout.setRefreshing(false); + Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + break; + } } private void openProfile(final String username) { diff --git a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java index 4b77b6e3..da96177c 100644 --- a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java @@ -26,6 +26,7 @@ import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; +import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ScrollView; import android.widget.Toast; @@ -33,6 +34,7 @@ import android.widget.ViewSwitcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.PermissionChecker; import androidx.core.view.ViewCompat; @@ -70,6 +72,7 @@ import awais.instagrabber.customviews.VideoPlayerCallbackAdapter; import awais.instagrabber.customviews.VideoPlayerViewHelper; import awais.instagrabber.customviews.drawee.AnimatedZoomableController; import awais.instagrabber.databinding.DialogPostViewBinding; +import awais.instagrabber.fragments.main.ProfileFragment; import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.PostChild; import awais.instagrabber.models.ProfileModel; @@ -114,6 +117,7 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { private DialogInterface.OnShowListener onShowListener; private boolean isLoggedIn; private boolean hasBeenToggled = false; + private CharSequence postCaption = null; private final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener = new VerticalDragHelper.OnVerticalDragListener() { @@ -306,6 +310,7 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { @Override public void onDestroyView() { super.onDestroyView(); + if (feedModel == null) return; switch (feedModel.getItemType()) { case MEDIA_TYPE_VIDEO: if (videoPlayerViewHelper != null) { @@ -414,8 +419,7 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { private void init() { if (feedModel == null) return; - final String cookie = settingsHelper.getString(Constants.COOKIE); - isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != null; + isLoggedIn = !TextUtils.isEmpty(COOKIE) && CookieUtils.getUserIdFromCookie(COOKIE) != null; if (!wasPaused && (sharedProfilePicElement != null || sharedMainPostElement != null)) { binding.getRoot().getBackground().mutate().setAlpha(0); } @@ -534,7 +538,16 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { } }); binding.like.setOnLongClickListener(v -> { - Utils.displayToastAboveView(context, v, getString(R.string.like_without_count)); + final NavController navController = getNavController(); + if (navController != null && isLoggedIn) { + final Bundle bundle = new Bundle(); + bundle.putString("postId", feedModel.getPostId()); + bundle.putBoolean("isComment", false); + navController.navigate(R.id.action_global_likesViewerFragment, bundle); + } + else { + Utils.displayToastAboveView(context, v, getString(R.string.like_without_count)); + } return true; }); } @@ -701,13 +714,52 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { } private void setupCaption() { - final CharSequence postCaption = feedModel.getPostCaption(); + postCaption = feedModel.getPostCaption(); binding.date.setText(Utils.datetimeParser.format(new Date(feedModel.getTimestamp() * 1000L))); - if (TextUtils.isEmpty(postCaption)) { + if (!feedModel.getProfileModel().getId().equals(CookieUtils.getUserIdFromCookie(COOKIE)) && TextUtils.isEmpty(postCaption)) { binding.caption.setVisibility(View.GONE); + binding.translateTitle.setVisibility(View.GONE); binding.captionToggle.setVisibility(View.GONE); return; } + if (feedModel.getProfileModel().getId().equals(CookieUtils.getUserIdFromCookie(COOKIE))) { + binding.editCaption.setVisibility(View.VISIBLE); + binding.editCaption.setOnClickListener(v -> { + final EditText input = new EditText(context); + input.setText(postCaption); + new AlertDialog.Builder(context) + .setTitle(R.string.edit_caption) + .setView(input) + .setPositiveButton(R.string.confirm, (d, w) -> { + binding.editCaption.setVisibility(View.GONE); + mediaService.editCaption( + feedModel.getPostId(), + CookieUtils.getUserIdFromCookie(COOKIE), + input.getText().toString(), + CookieUtils.getCsrfTokenFromCookie(COOKIE), + new ServiceCallback() { + @Override + public void onSuccess(final Boolean result) { + binding.editCaption.setVisibility(View.VISIBLE); + if (result) { + feedModel.setPostCaption(input.getText().toString()); + binding.caption.setText(input.getText().toString()); + } + else Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "Error editing caption", t); + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + binding.editCaption.setVisibility(View.VISIBLE); + } + }); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + }); + } binding.caption.addOnHashtagListener(autoLinkItem -> { final NavController navController = NavHostFragment.findNavController(this); final Bundle bundle = new Bundle(); @@ -737,11 +789,33 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { binding.captionParent.getBackground().mutate().setAlpha((int) (128 + (128 * (slideOffset < 0 ? 0 : slideOffset)))); } }); - binding.caption.setOnClickListener(v -> { + binding.captionFrame.setOnClickListener(v -> { if (bottomSheetBehavior == null) return; if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) return; bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); }); + if (TextUtils.isEmpty(feedModel.getCaptionId())) + binding.translateTitle.setVisibility(View.GONE); + else binding.translateTitle.setOnClickListener(v -> { + mediaService.translate(feedModel.getCaptionId(), "1", new ServiceCallback() { + @Override + public void onSuccess(final String result) { + if (TextUtils.isEmpty(result)) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + return; + } + binding.translateTitle.setOnClickListener(null); + binding.translatedCaption.setVisibility(View.VISIBLE); + binding.translatedCaption.setText(result); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "Error translating comment", t); + Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + }); binding.captionToggle.setOnClickListener(v -> { if (bottomSheetBehavior == null) return; switch (bottomSheetBehavior.getState()) { @@ -894,6 +968,7 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { @Override public void onItemClicked(final int position) { + toggleDetails(); } @Override diff --git a/app/src/main/java/awais/instagrabber/fragments/SavedViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/SavedViewerFragment.java index fbb9f8f7..ed16f43c 100644 --- a/app/src/main/java/awais/instagrabber/fragments/SavedViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/SavedViewerFragment.java @@ -40,24 +40,26 @@ import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.PostsLayoutPreferences; import awais.instagrabber.models.enums.PostItemType; import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.DownloadUtils; +import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; import static androidx.core.content.PermissionChecker.checkSelfPermission; import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; +import static awais.instagrabber.utils.Utils.settingsHelper; public final class SavedViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { private static final int STORAGE_PERM_REQUEST_CODE = 8020; private static final int STORAGE_PERM_REQUEST_CODE_FOR_SELECTION = 8030; private FragmentSavedBinding binding; - private String username; + private String username, cookie, profileId; private ActionMode actionMode; private SwipeRefreshLayout root; private AppCompatActivity fragmentActivity; - private boolean shouldRefresh = true; + private boolean isLoggedIn, shouldRefresh = true; private PostItemType type; - private String profileId; private Set selectedFeedModels; private FeedModel downloadFeedModel; private int downloadChildPosition = -1; @@ -225,6 +227,8 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL @Override public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { + cookie = settingsHelper.getString(Constants.COOKIE); + isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != null; if (root != null) { shouldRefresh = false; return root; @@ -281,7 +285,7 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL private void setupPosts() { binding.posts.setViewModelStoreOwner(this) .setLifeCycleOwner(this) - .setPostFetchService(new SavedPostFetchService(profileId, type)) + .setPostFetchService(new SavedPostFetchService(profileId, type, isLoggedIn)) .setLayoutPreferences(layoutPreferences) .addFetchStatusChangeListener(fetching -> updateSwipeRefreshState()) .setFeedItemCallback(feedItemCallback) diff --git a/app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java new file mode 100644 index 00000000..dd851198 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/StoryListViewerFragment.java @@ -0,0 +1,220 @@ +package awais.instagrabber.fragments; + +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +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.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavDirections; +import androidx.navigation.fragment.NavHostFragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import java.util.ArrayList; +import java.util.List; + +import awais.instagrabber.R; +import awais.instagrabber.adapters.FeedStoriesListAdapter; +import awais.instagrabber.adapters.FeedStoriesListAdapter.OnFeedStoryClickListener; +import awais.instagrabber.adapters.HighlightStoriesListAdapter; +import awais.instagrabber.adapters.HighlightStoriesListAdapter.OnHighlightStoryClickListener; +import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; +import awais.instagrabber.databinding.FragmentStoryListViewerBinding; +import awais.instagrabber.fragments.main.FeedFragment; +import awais.instagrabber.fragments.settings.MorePreferencesFragmentDirections; +import awais.instagrabber.models.FeedStoryModel; +import awais.instagrabber.models.HighlightModel; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.viewmodels.FeedStoriesViewModel; +import awais.instagrabber.viewmodels.ArchivesViewModel; +import awais.instagrabber.webservices.ServiceCallback; +import awais.instagrabber.webservices.StoriesService; +import awais.instagrabber.webservices.StoriesService.ArchiveFetchResponse; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public final class StoryListViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { + private static final String TAG = "StoryListViewerFragment"; + + private AppCompatActivity fragmentActivity; + private FragmentStoryListViewerBinding binding; + private SwipeRefreshLayout root; + private boolean shouldRefresh = true, firstRefresh = true; + private FeedStoriesViewModel feedStoriesViewModel; + private ArchivesViewModel archivesViewModel; + private StoriesService storiesService; + private Context context; + private String type, endCursor = null; + private RecyclerLazyLoader lazyLoader; + private FeedStoriesListAdapter adapter; + + private final OnFeedStoryClickListener clickListener = new OnFeedStoryClickListener() { + @Override + public void onFeedStoryClick(final FeedStoryModel model, final int position) { + if (model == null) return; + final NavDirections action = StoryListViewerFragmentDirections.actionStoryListFragmentToStoryViewerFragment(position, null, false, false, null, null, false, false); + NavHostFragment.findNavController(StoryListViewerFragment.this).navigate(action); + } + + @Override + public void onProfileClick(final String username) { + openProfile(username); + } + }; + + private final OnHighlightStoryClickListener archiveClickListener = new OnHighlightStoryClickListener() { + @Override + public void onHighlightClick(final HighlightModel model, final int position) { + if (model == null) return; + final NavDirections action = StoryListViewerFragmentDirections.actionStoryListFragmentToStoryViewerFragment( + position, getString(R.string.action_archive), false, false, null, null, true, false); + NavHostFragment.findNavController(StoryListViewerFragment.this).navigate(action); + } + + @Override + public void onProfileClick(final String username) { + openProfile(username); + } + }; + + private final ServiceCallback cb = new ServiceCallback() { + @Override + public void onSuccess(final ArchiveFetchResponse result) { + endCursor = result.getNextCursor(); + final List models = archivesViewModel.getList().getValue(); + final List modelsCopy = models == null ? new ArrayList<>() : new ArrayList<>(models); + modelsCopy.addAll(result.getResult()); + archivesViewModel.getList().postValue(modelsCopy); + binding.swipeRefreshLayout.setRefreshing(false); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "Error", t); + try { + final Context context = getContext(); + Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); + } + catch (Exception e) {} + } + }; + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + fragmentActivity = (AppCompatActivity) requireActivity(); + context = getContext(); + if (context == null) return; + storiesService = StoriesService.getInstance(); + } + + @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 = FragmentStoryListViewerBinding.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; + } + + @Override + public void onResume() { + super.onResume(); + final ActionBar actionBar = fragmentActivity.getSupportActionBar(); + if (actionBar != null) actionBar.setTitle(type == "feed" ? R.string.feed_stories : R.string.action_archive); + } + + @Override + public void onDestroy() { + if (archivesViewModel != null) archivesViewModel.getList().postValue(null); + super.onDestroy(); + } + + private void init() { + final Context context = getContext(); + if (getArguments() == null) return; + final StoryListViewerFragmentArgs fragmentArgs = StoryListViewerFragmentArgs.fromBundle(getArguments()); + type = fragmentArgs.getType(); + binding.swipeRefreshLayout.setOnRefreshListener(this); + final LinearLayoutManager layoutManager = new LinearLayoutManager(context); + final ActionBar actionBar = fragmentActivity.getSupportActionBar(); + if (type == "feed") { + if (actionBar != null) actionBar.setTitle(R.string.feed_stories); + feedStoriesViewModel = new ViewModelProvider(fragmentActivity).get(FeedStoriesViewModel.class); + adapter = new FeedStoriesListAdapter(clickListener); + binding.rvStories.setLayoutManager(layoutManager); + binding.rvStories.setAdapter(adapter); + feedStoriesViewModel.getList().observe(getViewLifecycleOwner(), adapter::submitList); + } + else { + if (actionBar != null) actionBar.setTitle(R.string.action_archive); + lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { + if (!TextUtils.isEmpty(endCursor)) onRefresh(); + endCursor = null; + }); + binding.rvStories.addOnScrollListener(lazyLoader); + archivesViewModel = new ViewModelProvider(fragmentActivity).get(ArchivesViewModel.class); + final HighlightStoriesListAdapter adapter = new HighlightStoriesListAdapter(archiveClickListener); + binding.rvStories.setLayoutManager(layoutManager); + binding.rvStories.setAdapter(adapter); + archivesViewModel.getList().observe(getViewLifecycleOwner(), adapter::submitList); + } + onRefresh(); + } + + @Override + public void onRefresh() { + binding.swipeRefreshLayout.setRefreshing(true); + if (type == "feed" && firstRefresh) { + binding.swipeRefreshLayout.setRefreshing(false); + adapter.submitList(feedStoriesViewModel.getList().getValue()); + firstRefresh = false; + } + else if (type == "feed") { + final String cookie = settingsHelper.getString(Constants.COOKIE); + storiesService.getFeedStories(CookieUtils.getCsrfTokenFromCookie(cookie), new ServiceCallback>() { + @Override + public void onSuccess(final List result) { + feedStoriesViewModel.getList().postValue(result); + binding.swipeRefreshLayout.setRefreshing(false); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "failed", t); + Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } + else if (type == "archive") { + storiesService.fetchArchive(endCursor, cb); + } + } + + private void openProfile(final String username) { + final NavDirections action = MorePreferencesFragmentDirections + .actionGlobalProfileFragment("@" + username); + NavHostFragment.findNavController(this).navigate(action); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java index c0e1e119..946034a4 100644 --- a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java @@ -11,6 +11,7 @@ import android.os.Handler; import android.util.Log; import android.util.Pair; import android.view.GestureDetector; +import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -20,6 +21,9 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -54,6 +58,9 @@ import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.text.NumberFormat; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; @@ -62,10 +69,7 @@ import awais.instagrabber.BuildConfig; import awais.instagrabber.R; import awais.instagrabber.adapters.StoriesAdapter; import awais.instagrabber.asyncs.PostFetcher; -import awais.instagrabber.asyncs.QuizAction; -import awais.instagrabber.asyncs.RespondAction; import awais.instagrabber.asyncs.SeenAction; -import awais.instagrabber.asyncs.VoteAction; import awais.instagrabber.asyncs.direct_messages.CreateThreadAction; import awais.instagrabber.customviews.helpers.SwipeGestureListener; import awais.instagrabber.databinding.FragmentStoryViewerBinding; @@ -78,16 +82,27 @@ import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.models.stickers.PollModel; import awais.instagrabber.models.stickers.QuestionModel; import awais.instagrabber.models.stickers.QuizModel; +import awais.instagrabber.models.stickers.SliderModel; +import awais.instagrabber.models.stickers.SwipeUpModel; +import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions; +import awais.instagrabber.repositories.responses.StoryStickerResponse; +import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; +import awais.instagrabber.viewmodels.ArchivesViewModel; import awais.instagrabber.viewmodels.FeedStoriesViewModel; import awais.instagrabber.viewmodels.HighlightsViewModel; import awais.instagrabber.viewmodels.StoriesViewModel; +import awais.instagrabber.webservices.DirectMessagesService; import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.StoriesService; import awaisomereport.LogCollector; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; import static awais.instagrabber.customviews.helpers.SwipeGestureListener.SWIPE_THRESHOLD; import static awais.instagrabber.customviews.helpers.SwipeGestureListener.SWIPE_VELOCITY_THRESHOLD; @@ -100,8 +115,7 @@ public class StoryViewerFragment extends Fragment { private AppCompatActivity fragmentActivity; private View root; - private @NonNull - FragmentStoryViewerBinding binding; + private FragmentStoryViewerBinding binding; private String currentStoryUsername; private StoriesAdapter storiesAdapter; private SwipeEvent swipeEvent; @@ -115,26 +129,32 @@ public class StoryViewerFragment extends Fragment { private QuestionModel question; private String[] mentions; private QuizModel quiz; + private SliderModel slider; private MenuItem menuDownload; private MenuItem menuDm; private SimpleExoPlayer player; private boolean isHashtag, isLoc; private String highlight; - private boolean fetching = false; + private boolean fetching = false, sticking = false, shouldRefresh = true; private int currentFeedStoryIndex; + private double sliderValue; private StoriesViewModel storiesViewModel; - private boolean shouldRefresh = true; private StoryViewerFragmentArgs fragmentArgs; private ViewModel viewModel; - private boolean isHighlight; + private boolean isHighlight, isArchive, isNotification; + private DirectMessagesService directMessagesService; private final String cookie = settingsHelper.getString(Constants.COOKIE); + private final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); + private final String userIdFromCookie = CookieUtils.getUserIdFromCookie(cookie); + private final String deviceId = settingsHelper.getString(Constants.DEVICE_UUID); @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); fragmentActivity = (AppCompatActivity) requireActivity(); storiesService = StoriesService.getInstance(); + directMessagesService = DirectMessagesService.getInstance(csrfToken, userIdFromCookie, deviceId); setHasOptionsMenu(true); } @@ -188,24 +208,33 @@ public class StoryViewerFragment extends Fragment { new AlertDialog.Builder(context) .setTitle(R.string.reply_story) .setView(input) - .setPositiveButton(R.string.ok, (d, w) -> new CreateThreadAction(cookie, currentStory.getUserId(), threadId -> { - // try { - // final StoryReplyBroadcastOptions options = new StoryReplyBroadcastOptions( - // threadId, - // input.getText().toString(), - // currentStory.getStoryMediaId(), - // currentStory.getUserId() - // ); - // final DirectThreadBroadcaster broadcast = new DirectThreadBroadcaster(threadId); - // broadcast.setOnTaskCompleteListener(result -> Toast.makeText( - // context, - // result != null ? R.string.answered_story : R.string.downloader_unknown_error, - // Toast.LENGTH_SHORT - // ).show()); - // broadcast.execute(options); - // } catch (UnsupportedEncodingException e) { - // Log.e(TAG, "Error", e); - // } + .setPositiveButton(R.string.confirm, (d, w) -> new CreateThreadAction(cookie, currentStory.getUserId(), threadId -> { + try { + final Call request = directMessagesService + .broadcastStoryReply(BroadcastOptions.ThreadIdOrUserIds.of(threadId), + input.getText().toString(), + currentStory.getStoryMediaId(), + currentStory.getUserId()); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (!response.isSuccessful()) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + return; + } + Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + Log.e(TAG, "onFailure: ", t); + } + }); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "Error", e); + } }).execute()) .setNegativeButton(R.string.cancel, null) .show(); @@ -244,9 +273,13 @@ public class StoryViewerFragment extends Fragment { currentFeedStoryIndex = fragmentArgs.getFeedStoryIndex(); highlight = fragmentArgs.getHighlight(); isHighlight = !TextUtils.isEmpty(highlight); + isArchive = fragmentArgs.getIsArchive(); + isNotification = fragmentArgs.getIsNotification(); if (currentFeedStoryIndex >= 0) { viewModel = isHighlight - ? new ViewModelProvider(fragmentActivity).get(HighlightsViewModel.class) + ? isArchive + ? new ViewModelProvider(fragmentActivity).get(ArchivesViewModel.class) + : new ViewModelProvider(fragmentActivity).get(HighlightsViewModel.class) : new ViewModelProvider(fragmentActivity).get(FeedStoriesViewModel.class); } // feedStoryModels = feedStoriesViewModel.getList().getValue(); @@ -275,7 +308,10 @@ public class StoryViewerFragment extends Fragment { final boolean hasFeedStories; List models = null; if (currentFeedStoryIndex >= 0) { - if (isHighlight) { + if (isArchive) { + final ArchivesViewModel archivesViewModel = (ArchivesViewModel) viewModel; + models = archivesViewModel.getList().getValue(); + } else if (isHighlight) { final HighlightsViewModel highlightsViewModel = (HighlightsViewModel) viewModel; models = highlightsViewModel.getList().getValue(); // final HighlightModel model = models.get(currentFeedStoryIndex); @@ -296,15 +332,16 @@ public class StoryViewerFragment extends Fragment { swipeEvent = isRightSwipe -> { final List storyModels = storiesViewModel.getList().getValue(); final int storiesLen = storyModels == null ? 0 : storyModels.size(); + if (sticking) { + Toast.makeText(context, R.string.follower_wait_to_load, Toast.LENGTH_SHORT).show(); + return; + } if (storiesLen <= 0) return; final boolean isLeftSwipe = !isRightSwipe; final boolean endOfCurrentStories = slidePos + 1 >= storiesLen; final boolean swipingBeyondCurrentStories = (endOfCurrentStories && isLeftSwipe) || (slidePos == 0 && isRightSwipe); if (swipingBeyondCurrentStories && hasFeedStories) { final int index = currentFeedStoryIndex; - if (settingsHelper.getBoolean(MARK_AS_SEEN)) { - new SeenAction(cookie, currentStory).execute(); - } if ((isRightSwipe && index == 0) || (isLeftSwipe && index == finalModels.size() - 1)) { Toast.makeText(context, R.string.no_more_stories, Toast.LENGTH_SHORT).show(); return; @@ -312,7 +349,7 @@ public class StoryViewerFragment extends Fragment { final Object feedStoryModel = isRightSwipe ? finalModels.get(index - 1) : finalModels.size() == index + 1 ? null : finalModels.get(index + 1); - paginateStories(feedStoryModel, context, isRightSwipe, currentFeedStoryIndex == finalModels.size() - 2); + paginateStories(feedStoryModel, finalModels.get(index), context, isRightSwipe, currentFeedStoryIndex == finalModels.size() - 2); return; } if (isRightSwipe) { @@ -351,8 +388,12 @@ public class StoryViewerFragment extends Fragment { if (hasFeedStories) { binding.btnBackward.setVisibility(currentFeedStoryIndex == 0 ? View.INVISIBLE : View.VISIBLE); binding.btnForward.setVisibility(currentFeedStoryIndex == finalModels.size() - 1 ? View.INVISIBLE : View.VISIBLE); - binding.btnBackward.setOnClickListener(v -> paginateStories(finalModels.get(currentFeedStoryIndex - 1), context, true, false)); - binding.btnForward.setOnClickListener(v -> paginateStories(finalModels.get(currentFeedStoryIndex + 1), context, false, + binding.btnBackward.setOnClickListener(v -> paginateStories(finalModels.get(currentFeedStoryIndex - 1), + finalModels.get(currentFeedStoryIndex), + context, true, false)); + binding.btnForward.setOnClickListener(v -> paginateStories(finalModels.get(currentFeedStoryIndex + 1), + finalModels.get(currentFeedStoryIndex), + context, false, currentFeedStoryIndex == finalModels.size() - 2)); } @@ -365,6 +406,14 @@ public class StoryViewerFragment extends Fragment { startActivity(intent); } }); + binding.swipeUp.setOnClickListener(v -> { + final Object tag = v.getTag(); + if (tag instanceof CharSequence) { + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(tag.toString())); + startActivity(intent); + } + }); binding.viewStoryPost.setOnClickListener(v -> { final Object tag = v.getTag(); if (!(tag instanceof CharSequence)) return; @@ -406,15 +455,30 @@ public class StoryViewerFragment extends Fragment { poll.getLeftChoice() + " (" + poll.getLeftCount() + ")", poll.getRightChoice() + " (" + poll.getRightCount() + ")" }), (d, w) -> { - if (!TextUtils.isEmpty(cookie)) - new VoteAction(currentStory, poll, cookie, choice -> { - if (choice > -1) { - poll.setMyChoice(choice); - Toast.makeText(context, R.string.votef_story_poll, Toast.LENGTH_SHORT).show(); - return; - } - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - }).execute(w); + if (!TextUtils.isEmpty(cookie)) { + sticking = true; + storiesService.respondToPoll( + currentStory.getStoryMediaId().split("_")[0], + poll.getId(), + w, + userIdFromCookie, + csrfToken, + new ServiceCallback() { + @Override + public void onSuccess(final StoryStickerResponse result) { + sticking = false; + poll.setMyChoice(w); + Toast.makeText(context, R.string.votef_story_poll, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onFailure(final Throwable t) { + sticking = false; + Log.e(TAG, "Error responding", t); + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } + }); + } }) .setPositiveButton(R.string.cancel, null) .show(); @@ -426,21 +490,36 @@ public class StoryViewerFragment extends Fragment { new AlertDialog.Builder(context) .setTitle(question.getQuestion()) .setView(input) - .setPositiveButton(R.string.ok, (d, w) -> new RespondAction(currentStory, question, cookie, result -> { - if (result) { - Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); - } else - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - }).execute(input.getText().toString())) + .setPositiveButton(R.string.confirm, (d, w) -> { + sticking = true; + storiesService.respondToQuestion( + currentStory.getStoryMediaId().split("_")[0], + question.getId(), + input.getText().toString(), + userIdFromCookie, + csrfToken, + new ServiceCallback() { + @Override + public void onSuccess(final StoryStickerResponse result) { + sticking = false; + Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onFailure(final Throwable t) { + sticking = false; + Log.e(TAG, "Error responding", t); + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } + }); + }) .setNegativeButton(R.string.cancel, null) .show(); } else if (tag instanceof String[]) { mentions = (String[]) tag; new AlertDialog.Builder(context) .setTitle(R.string.story_mentions) - .setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, mentions), (d, w) -> { - openProfile(mentions[w]); - }) + .setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, mentions), (d, w) -> openProfile(mentions[w])) .setPositiveButton(R.string.cancel, null) .show(); } else if (tag instanceof QuizModel) { @@ -451,27 +530,122 @@ public class StoryViewerFragment extends Fragment { new AlertDialog.Builder(context) .setTitle(quiz.getMyChoice() > -1 ? getString(R.string.story_quizzed) : quiz.getQuestion()) .setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, choices), (d, w) -> { - if (quiz.getMyChoice() == -1 && !TextUtils.isEmpty(cookie)) - new QuizAction(currentStory, quiz, cookie, choice -> { - if (choice > -1) { - quiz.setMyChoice(choice); - Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); - return; - } - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - }).execute(w); + if (quiz.getMyChoice() == -1 && !TextUtils.isEmpty(cookie)) { + sticking = true; + storiesService.respondToQuiz( + currentStory.getStoryMediaId().split("_")[0], + quiz.getId(), + w, + userIdFromCookie, + csrfToken, + new ServiceCallback() { + @Override + public void onSuccess(final StoryStickerResponse result) { + sticking = false; + quiz.setMyChoice(w); + Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onFailure(final Throwable t) { + sticking = false; + Log.e(TAG, "Error responding", t); + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } + }); + } }) .setPositiveButton(R.string.cancel, null) .show(); + } else if (tag instanceof SliderModel) { + slider = (SliderModel) tag; + NumberFormat percentage = NumberFormat.getPercentInstance(); + percentage.setMaximumFractionDigits(2); + LinearLayout sliderView = new LinearLayout(context); + sliderView.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + sliderView.setOrientation(LinearLayout.VERTICAL); + TextView tv = new TextView(context); + tv.setGravity(Gravity.CENTER_HORIZONTAL); + final SeekBar input = new SeekBar(context); + double avg = slider.getAverage() * 100; + input.setProgress((int) avg); + sliderView.addView(input); + sliderView.addView(tv); + if (slider.getMyChoice().isNaN() && slider.canVote()) { + input.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + sliderValue = progress / 100.0; + tv.setText(percentage.format(sliderValue)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + new AlertDialog.Builder(context) + .setTitle(TextUtils.isEmpty(slider.getQuestion()) ? slider.getEmoji() : slider.getQuestion()) + .setMessage(getResources().getQuantityString(R.plurals.slider_info, + slider.getVoteCount(), + slider.getVoteCount(), + percentage.format(slider.getAverage()))) + .setView(sliderView) + .setPositiveButton(R.string.confirm, (d, w) -> { + sticking = true; + storiesService.respondToSlider( + currentStory.getStoryMediaId().split("_")[0], + slider.getId(), + sliderValue, + userIdFromCookie, + csrfToken, + new ServiceCallback() { + @Override + public void onSuccess(final StoryStickerResponse result) { + sticking = false; + slider.setMyChoice(sliderValue); + Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onFailure(final Throwable t) { + sticking = false; + Log.e(TAG, "Error responding", t); + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } + }); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + } else { + input.setEnabled(false); + tv.setText(getString(R.string.slider_answer, percentage.format(slider.getMyChoice()))); + new AlertDialog.Builder(context) + .setTitle(TextUtils.isEmpty(slider.getQuestion()) ? slider.getEmoji() : slider.getQuestion()) + .setMessage(getResources().getQuantityString(R.plurals.slider_info, + slider.getVoteCount(), + slider.getVoteCount(), + percentage.format(slider.getAverage()))) + .setView(sliderView) + .setPositiveButton(R.string.ok, null) + .show(); + } } }; binding.poll.setOnClickListener(storyActionListener); binding.answer.setOnClickListener(storyActionListener); binding.mention.setOnClickListener(storyActionListener); binding.quiz.setOnClickListener(storyActionListener); + binding.slider.setOnClickListener(storyActionListener); } private void resetView() { + final Context context = getContext(); slidePos = 0; lastSlidePos = 0; if (menuDownload != null) menuDownload.setVisible(false); @@ -480,10 +654,23 @@ public class StoryViewerFragment extends Fragment { releasePlayer(); String currentStoryMediaId = null; if (currentFeedStoryIndex >= 0) { - if (isHighlight) { + if (isArchive) { + final ArchivesViewModel archivesViewModel = (ArchivesViewModel) viewModel; + final List models = archivesViewModel.getList().getValue(); + if (models == null || models.isEmpty() || currentFeedStoryIndex >= models.size()) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + return; + } + final HighlightModel model = models.get(currentFeedStoryIndex); + currentStoryMediaId = model.getId(); + currentStoryUsername = model.getTitle(); + } else if (isHighlight) { final HighlightsViewModel highlightsViewModel = (HighlightsViewModel) viewModel; final List models = highlightsViewModel.getList().getValue(); - if (models == null || models.isEmpty() || currentFeedStoryIndex >= models.size()) return; + if (models == null || models.isEmpty() || currentFeedStoryIndex >= models.size()) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + return; + } final HighlightModel model = models.get(currentFeedStoryIndex); currentStoryMediaId = model.getId(); currentStoryUsername = model.getTitle(); @@ -502,7 +689,12 @@ public class StoryViewerFragment extends Fragment { isHashtag = fragmentArgs.getIsHashtag(); isLoc = fragmentArgs.getIsLoc(); final boolean hasUsername = !TextUtils.isEmpty(currentStoryUsername); - if (hasUsername) { + if (isHighlight) { + final ActionBar actionBar = fragmentActivity.getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(highlight); + } + } else if (hasUsername) { currentStoryUsername = currentStoryUsername.replace("@", ""); final ActionBar actionBar = fragmentActivity.getSupportActionBar(); if (actionBar != null) { @@ -511,6 +703,31 @@ public class StoryViewerFragment extends Fragment { } storiesViewModel.getList().setValue(Collections.emptyList()); if (currentStoryMediaId == null) return; + if (isNotification) { + storiesService.fetch(currentStoryMediaId, new ServiceCallback() { + @Override + public void onSuccess(final StoryModel storyModel) { + fetching = false; + binding.storiesList.setVisibility(View.GONE); + if (storyModel == null) { + storiesViewModel.getList().setValue(Collections.emptyList()); + currentStory = null; + return; + } + storiesViewModel.getList().setValue(Collections.singletonList(storyModel)); + currentStory = storyModel; + refreshStory(); + } + + @Override + public void onFailure(final Throwable t) { + final Context context = getContext(); + Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); + Log.e(TAG, "Error", t); + } + }); + return; + } final ServiceCallback> storyCallback = new ServiceCallback>() { @Override public void onSuccess(final List storyModels) { @@ -521,7 +738,9 @@ public class StoryViewerFragment extends Fragment { binding.storiesList.setVisibility(View.GONE); return; } - binding.storiesList.setVisibility(View.VISIBLE); + binding.storiesList.setVisibility((storyModels.size() == 1 && currentFeedStoryIndex == -1) ? View.GONE : View.VISIBLE); + binding.btnBackward.setVisibility(currentFeedStoryIndex == -1 ? View.GONE : View.VISIBLE); + binding.btnForward.setVisibility(currentFeedStoryIndex == -1 ? View.GONE : View.VISIBLE); storiesViewModel.getList().setValue(storyModels); currentStory = storyModels.get(0); refreshStory(); @@ -529,6 +748,8 @@ public class StoryViewerFragment extends Fragment { @Override public void onFailure(final Throwable t) { + final Context context = getContext(); + Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); Log.e(TAG, "Error", t); } }; @@ -543,7 +764,7 @@ public class StoryViewerFragment extends Fragment { private void refreshStory() { if (binding.storiesList.getVisibility() == View.VISIBLE) { final List storyModels = storiesViewModel.getList().getValue(); - if (storyModels != null) { + if (storyModels != null && storyModels.size() > 0) { StoryModel item = storyModels.get(lastSlidePos); if (item != null) { item.setCurrentSlide(false); @@ -587,6 +808,17 @@ public class StoryViewerFragment extends Fragment { binding.quiz.setVisibility(quiz != null ? View.VISIBLE : View.GONE); binding.quiz.setTag(quiz); + slider = currentStory.getSlider(); + binding.slider.setVisibility(slider != null ? View.VISIBLE : View.GONE); + binding.slider.setTag(slider); + + final SwipeUpModel swipeUp = currentStory.getSwipeUp(); + if (swipeUp != null) { + binding.swipeUp.setVisibility(View.VISIBLE); + binding.swipeUp.setText(swipeUp.getText()); + binding.swipeUp.setTag(swipeUp.getUrl()); + } + releasePlayer(); if (isHashtag || isLoc) { final ActionBar actionBar = fragmentActivity.getSupportActionBar(); @@ -670,8 +902,8 @@ public class StoryViewerFragment extends Fragment { @Override public void onLoadCompleted(final int windowIndex, @Nullable final MediaSource.MediaPeriodId mediaPeriodId, - final LoadEventInfo loadEventInfo, - final MediaLoadData mediaLoadData) { + @NonNull final LoadEventInfo loadEventInfo, + @NonNull final MediaLoadData mediaLoadData) { if (menuDownload != null) menuDownload.setVisible(true); if (currentStory.canReply() && menuDm != null && !TextUtils.isEmpty(cookie)) menuDm.setVisible(true); @@ -681,8 +913,8 @@ public class StoryViewerFragment extends Fragment { @Override public void onLoadStarted(final int windowIndex, @Nullable final MediaSource.MediaPeriodId mediaPeriodId, - final LoadEventInfo loadEventInfo, - final MediaLoadData mediaLoadData) { + @NonNull final LoadEventInfo loadEventInfo, + @NonNull final MediaLoadData mediaLoadData) { if (menuDownload != null) menuDownload.setVisible(true); if (currentStory.canReply() && menuDm != null && !TextUtils.isEmpty(cookie)) menuDm.setVisible(true); @@ -692,17 +924,17 @@ public class StoryViewerFragment extends Fragment { @Override public void onLoadCanceled(final int windowIndex, @Nullable final MediaSource.MediaPeriodId mediaPeriodId, - final LoadEventInfo loadEventInfo, - final MediaLoadData mediaLoadData) { + @NonNull final LoadEventInfo loadEventInfo, + @NonNull final MediaLoadData mediaLoadData) { binding.progressView.setVisibility(View.GONE); } @Override public void onLoadError(final int windowIndex, @Nullable final MediaSource.MediaPeriodId mediaPeriodId, - final LoadEventInfo loadEventInfo, - final MediaLoadData mediaLoadData, - final IOException error, + @NonNull final LoadEventInfo loadEventInfo, + @NonNull final MediaLoadData mediaLoadData, + @NonNull final IOException error, final boolean wasCanceled) { if (menuDownload != null) menuDownload.setVisible(false); if (menuDm != null) menuDm.setVisible(false); @@ -745,12 +977,25 @@ public class StoryViewerFragment extends Fragment { player = null; } - private void paginateStories(Object feedStory, Context context, boolean backward, boolean last) { - if (feedStory != null) { + private void paginateStories(Object newFeedStory, Object oldFeedStory, Context context, boolean backward, boolean last) { + if (newFeedStory != null) { if (fetching) { Toast.makeText(context, R.string.be_patient, Toast.LENGTH_SHORT).show(); return; } + if (settingsHelper.getBoolean(MARK_AS_SEEN) + && oldFeedStory instanceof FeedStoryModel + && viewModel instanceof FeedStoriesViewModel) { + final FeedStoriesViewModel feedStoriesViewModel = (FeedStoriesViewModel) viewModel; + final FeedStoryModel oldFeedStoryModel = (FeedStoryModel) oldFeedStory; + if (!oldFeedStoryModel.isFullyRead()) { + oldFeedStoryModel.setFullyRead(true); + final List models = feedStoriesViewModel.getList().getValue(); + final List modelsCopy = models == null ? new ArrayList<>() : new ArrayList<>(models); + modelsCopy.set(currentFeedStoryIndex, oldFeedStoryModel); + feedStoriesViewModel.getList().postValue(models); + } + } fetching = true; binding.btnBackward.setVisibility(currentFeedStoryIndex == 1 && backward ? View.INVISIBLE : View.VISIBLE); binding.btnForward.setVisibility(last ? View.INVISIBLE : View.VISIBLE); diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java index 6b6cfadf..55ea42eb 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java @@ -18,7 +18,9 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.AppCompatButton; import androidx.appcompat.widget.AppCompatImageView; import androidx.fragment.app.Fragment; @@ -42,6 +44,7 @@ import awais.instagrabber.utils.Utils; public class DirectMessageSettingsFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { private static final String TAG = "DirectMsgsSettingsFrag"; + private AppCompatActivity fragmentActivity; private RecyclerView userList; private RecyclerView leftUserList; private EditText titleText; @@ -82,6 +85,7 @@ public class DirectMessageSettingsFragment extends Fragment implements SwipeRefr @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); + fragmentActivity = (AppCompatActivity) requireActivity(); basicClickListener = v -> { final Object tag = v.getTag(); if (tag instanceof ProfileModel) { @@ -147,6 +151,11 @@ public class DirectMessageSettingsFragment extends Fragment implements SwipeRefr threadTitle = DirectMessageSettingsFragmentArgs.fromBundle(getArguments()).getTitle(); binding.swipeRefreshLayout.setEnabled(false); + final ActionBar actionBar = fragmentActivity.getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(threadTitle); + } + userList = binding.userList; userList.setHasFixedSize(true); userList.setLayoutManager(layoutManager); @@ -209,7 +218,7 @@ public class DirectMessageSettingsFragment extends Fragment implements SwipeRefr class ChangeSettings extends AsyncTask { String action, argument; boolean ok = false; - private String text; + private final String text; public ChangeSettings(final String text) { this.text = text; diff --git a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java index afc14848..832977c8 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java @@ -12,6 +12,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.activity.OnBackPressedDispatcher; @@ -47,6 +48,7 @@ import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.FeedStoryModel; import awais.instagrabber.models.PostsLayoutPreferences; import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.FeedStoriesViewModel; @@ -55,6 +57,7 @@ import awais.instagrabber.webservices.StoriesService; import static androidx.core.content.PermissionChecker.checkSelfPermission; import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; +import static awais.instagrabber.utils.Utils.settingsHelper; public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { private static final String TAG = "FeedFragment"; @@ -74,6 +77,7 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre private int downloadChildPosition = -1; private PostsLayoutPreferences layoutPreferences = Utils.getPostsLayoutPreferences(Constants.PREF_POSTS_LAYOUT); private RecyclerView storiesRecyclerView; + private MenuItem storyListMenu; private final FeedAdapterV2.FeedItemCallback feedItemCallback = new FeedAdapterV2.FeedItemCallback() { @Override @@ -272,26 +276,23 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre @Override public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { inflater.inflate(R.menu.feed_menu, menu); + storyListMenu = menu.findItem(R.id.storyList); + storyListMenu.setVisible(!storiesFetching); } @Override public boolean onOptionsItemSelected(@NonNull final MenuItem item) { - if (item.getItemId() == R.id.layout) { + if (item.getItemId() == R.id.storyList) { + final NavDirections action = FeedFragmentDirections.actionGlobalStoryListViewerFragment("feed"); + NavHostFragment.findNavController(FeedFragment.this).navigate(action); + } + else if (item.getItemId() == R.id.layout) { showPostsLayoutPreferences(); return true; } return super.onOptionsItemSelected(item); } - @Override - public void onResume() { - super.onResume(); - updateSwipeRefreshState(); - // if (videoAwareRecyclerScroller != null && shouldAutoPlay) { - // videoAwareRecyclerScroller.startPlaying(); - // } - } - @Override public void onRefresh() { binding.feedRecyclerView.refresh(); @@ -309,9 +310,13 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre @Override public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); - final boolean granted = grantResults[0] == PackageManager.PERMISSION_GRANTED; + final boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED; final Context context = getContext(); if (context == null) return; + if (!granted) { + Toast.makeText(context, R.string.download_permission, Toast.LENGTH_SHORT).show(); + return; + } if (requestCode == STORAGE_PERM_REQUEST_CODE && granted) { if (downloadFeedModel == null) return; DownloadUtils.showDownloadDialog(context, downloadFeedModel, downloadChildPosition); @@ -346,11 +351,22 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre } private void setupFeedStories() { + if (storyListMenu != null) storyListMenu.setVisible(false); feedStoriesViewModel = new ViewModelProvider(fragmentActivity).get(FeedStoriesViewModel.class); - final FeedStoriesAdapter feedStoriesAdapter = new FeedStoriesAdapter((model, position) -> { - final NavDirections action = FeedFragmentDirections.actionFeedFragmentToStoryViewerFragment(position, null, false, false, null, null); - NavHostFragment.findNavController(this).navigate(action); - }); + final FeedStoriesAdapter feedStoriesAdapter = new FeedStoriesAdapter( + new FeedStoriesAdapter.OnFeedStoryClickListener() { + @Override + public void onFeedStoryClick(FeedStoryModel model, int position) { + final NavDirections action = FeedFragmentDirections.actionFeedFragmentToStoryViewerFragment(position, null, false, false, null, null, false, false); + NavHostFragment.findNavController(FeedFragment.this).navigate(action); + } + + @Override + public void onFeedStoryLongClick(FeedStoryModel model, int position) { + navigateToProfile("@" + model.getProfileModel().getUsername()); + } + } + ); final Context context = getContext(); if (context == null) return; storiesRecyclerView = new RecyclerView(context); @@ -362,18 +378,20 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre storiesRecyclerView.setLayoutManager(new LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)); storiesRecyclerView.setAdapter(feedStoriesAdapter); fragmentActivity.setCollapsingView(storiesRecyclerView); - feedStoriesViewModel.getList().observe(fragmentActivity, feedStoriesAdapter::submitList); + feedStoriesViewModel.getList().observe(getViewLifecycleOwner(), feedStoriesAdapter::submitList); fetchStories(); } private void fetchStories() { + final String cookie = settingsHelper.getString(Constants.COOKIE); storiesFetching = true; updateSwipeRefreshState(); - storiesService.getFeedStories(new ServiceCallback>() { + storiesService.getFeedStories(CookieUtils.getCsrfTokenFromCookie(cookie), new ServiceCallback>() { @Override public void onSuccess(final List result) { feedStoriesViewModel.getList().postValue(result); storiesFetching = false; + if (storyListMenu != null) storyListMenu.setVisible(true); updateSwipeRefreshState(); } diff --git a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java index 4c12b4e3..d5ab821d 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -54,7 +54,6 @@ import awais.instagrabber.R; import awais.instagrabber.activities.MainActivity; import awais.instagrabber.adapters.FeedAdapterV2; import awais.instagrabber.adapters.HighlightsAdapter; -import awais.instagrabber.asyncs.HighlightsFetcher; import awais.instagrabber.asyncs.ProfileFetcher; import awais.instagrabber.asyncs.ProfilePostFetchService; import awais.instagrabber.asyncs.UsernameFetcher; @@ -75,6 +74,7 @@ import awais.instagrabber.dialogs.ProfilePicDialogFragment; import awais.instagrabber.fragments.PostViewV2Fragment; import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.HighlightModel; import awais.instagrabber.models.PostsLayoutPreferences; import awais.instagrabber.models.ProfileModel; import awais.instagrabber.models.StoryModel; @@ -89,6 +89,7 @@ import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.HighlightsViewModel; import awais.instagrabber.webservices.FriendshipService; +import awais.instagrabber.webservices.MediaService; import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.StoriesService; @@ -113,6 +114,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe private Handler usernameSettingHandler; private FriendshipService friendshipService; private StoriesService storiesService; + private MediaService mediaService; private boolean shouldRefresh = true; private boolean hasStories = false; private HighlightsAdapter highlightsAdapter; @@ -298,6 +300,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe fragmentActivity = (MainActivity) requireActivity(); friendshipService = FriendshipService.getInstance(); storiesService = StoriesService.getInstance(); + mediaService = MediaService.getInstance(); accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(getContext())); favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(getContext())); setHasOptionsMenu(true); @@ -370,10 +373,10 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe } if (item.getItemId() == R.id.restrict) { if (!isLoggedIn) return false; - final String action = profileModel.getRestricted() ? "Unrestrict" : "Restrict"; + final String action = profileModel.isRestricted() ? "Unrestrict" : "Restrict"; friendshipService.toggleRestrict( profileModel.getId(), - !profileModel.getRestricted(), + !profileModel.isRestricted(), CookieUtils.getCsrfTokenFromCookie(cookie), new ServiceCallback() { @Override @@ -392,7 +395,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe if (item.getItemId() == R.id.block) { final String userIdFromCookie = CookieUtils.getUserIdFromCookie(cookie); if (!isLoggedIn) return false; - if (profileModel.getBlocked()) { + if (profileModel.isBlocked()) { friendshipService.unblock( userIdFromCookie, profileModel.getId(), @@ -434,6 +437,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe @Override public void onRefresh() { + profileDetailsBinding.mainProfileImage.setVisibility(View.INVISIBLE); fetchProfileDetails(); } @@ -571,44 +575,99 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe fetchStoryAndHighlights(profileId); } setupButtons(profileId, myId); - if (!profileId.equals(myId)) { - profileDetailsBinding.favCb.setVisibility(View.VISIBLE); - favoriteRepository.getFavorite(username.substring(1), FavoriteType.USER, new RepositoryCallback() { - @Override - public void onSuccess(final Favorite result) { - profileDetailsBinding.favCb.setChecked(true); - profileDetailsBinding.favCb.setButtonDrawable(R.drawable.ic_star_check_24); - } + profileDetailsBinding.favChip.setVisibility(View.VISIBLE); + final FavoriteRepository favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(getContext())); + favoriteRepository.getFavorite(profileModel.getUsername(), FavoriteType.USER, new RepositoryCallback() { + @Override + public void onSuccess(final Favorite result) { + profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); + profileDetailsBinding.favChip.setText(R.string.added_to_favs_short); + favoriteRepository.insertOrUpdateFavorite(new Favorite( + result.getId(), + profileModel.getUsername(), + FavoriteType.USER, + profileModel.getName(), + profileModel.getSdProfilePic(), + result.getDateAdded() + ), new RepositoryCallback() { + @Override + public void onSuccess(final Void result) {} - @Override - public void onDataNotAvailable() { - profileDetailsBinding.favCb.setChecked(false); - profileDetailsBinding.favCb.setButtonDrawable(R.drawable.ic_outline_star_plus_24); - } - }); - } else { - profileDetailsBinding.favCb.setVisibility(View.GONE); - } + @Override + public void onDataNotAvailable() {} + }); + } + + @Override + public void onDataNotAvailable() { + profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); + profileDetailsBinding.favChip.setText(R.string.add_to_favorites); + } + }); + profileDetailsBinding.favChip.setOnClickListener( + v -> favoriteRepository.getFavorite(profileModel.getUsername(), FavoriteType.USER, new RepositoryCallback() { + @Override + public void onSuccess(final Favorite result) { + favoriteRepository.deleteFavorite(profileModel.getUsername(), FavoriteType.USER, new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + profileDetailsBinding.favChip.setText(R.string.add_to_favorites); + profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); + showSnackbar(getString(R.string.removed_from_favs)); + } + + @Override + public void onDataNotAvailable() {} + }); + } + + @Override + public void onDataNotAvailable() { + favoriteRepository.insertOrUpdateFavorite(new Favorite( + 0, + profileModel.getUsername(), + FavoriteType.USER, + profileModel.getName(), + profileModel.getSdProfilePic(), + new Date() + ), new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + profileDetailsBinding.favChip.setText(R.string.added_to_favs); + profileDetailsBinding.favChip.setChipIconResource(R.drawable.ic_star_check_24); + showSnackbar(getString(R.string.added_to_favs)); + } + + @Override + public void onDataNotAvailable() {} + }); + } + })); profileDetailsBinding.mainProfileImage.setImageURI(profileModel.getHdProfilePic()); + profileDetailsBinding.mainProfileImage.setVisibility(View.VISIBLE); - final long followersCount = profileModel.getFollowersCount(); - final long followingCount = profileModel.getFollowingCount(); + final Long followersCount = profileModel.getFollowersCount(); + final Long followingCount = profileModel.getFollowingCount(); final String postCount = String.valueOf(profileModel.getPostCount()); - SpannableStringBuilder span = new SpannableStringBuilder(getString(R.string.main_posts_count, - postCount)); + SpannableStringBuilder span = new SpannableStringBuilder(getResources().getQuantityString(R.plurals.main_posts_count_inline, + profileModel.getPostCount() > 2000000000L ? 2000000000 : profileModel.getPostCount().intValue(), + postCount)); span.setSpan(new RelativeSizeSpan(1.2f), 0, postCount.length(), 0); span.setSpan(new StyleSpan(Typeface.BOLD), 0, postCount.length(), 0); profileDetailsBinding.mainPostCount.setText(span); + profileDetailsBinding.mainPostCount.setVisibility(View.VISIBLE); final String followersCountStr = String.valueOf(followersCount); final int followersCountStrLen = followersCountStr.length(); - span = new SpannableStringBuilder(getString(R.string.main_posts_followers, - followersCountStr)); + span = new SpannableStringBuilder(getResources().getQuantityString(R.plurals.main_posts_followers, + followersCount > 2000000000L ? 2000000000 : followersCount.intValue(), + followersCountStr)); span.setSpan(new RelativeSizeSpan(1.2f), 0, followersCountStrLen, 0); span.setSpan(new StyleSpan(Typeface.BOLD), 0, followersCountStrLen, 0); profileDetailsBinding.mainFollowers.setText(span); + profileDetailsBinding.mainFollowers.setVisibility(View.VISIBLE); final String followingCountStr = String.valueOf(followingCount); final int followingCountStrLen = followingCountStr.length(); @@ -617,6 +676,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe span.setSpan(new RelativeSizeSpan(1.2f), 0, followingCountStrLen, 0); span.setSpan(new StyleSpan(Typeface.BOLD), 0, followingCountStrLen, 0); profileDetailsBinding.mainFollowing.setText(span); + profileDetailsBinding.mainFollowing.setVisibility(View.VISIBLE); profileDetailsBinding.mainFullName.setText(TextUtils.isEmpty(profileModel.getName()) ? profileModel.getUsername() : profileModel.getName()); @@ -640,6 +700,51 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe .trim())); profileDetailsBinding.mainBiography .addOnURLClickListener(autoLinkItem -> Utils.openURL(getContext(), autoLinkItem.getOriginalText().trim())); + profileDetailsBinding.mainBiography.setOnClickListener(v -> { + String[] commentDialogList; + if (!TextUtils.isEmpty(cookie)) { + commentDialogList = new String[]{ + getResources().getString(R.string.bio_copy), + getResources().getString(R.string.bio_translate) + }; + } else { + commentDialogList = new String[]{ + getResources().getString(R.string.bio_copy) + }; + } + new AlertDialog.Builder(context) + .setItems(commentDialogList, (d,w) -> { + switch (w) { + case 0: + Utils.copyText(context, biography); + break; + case 1: + mediaService.translate(profileModel.getId(), "3", new ServiceCallback() { + @Override + public void onSuccess(final String result) { + if (TextUtils.isEmpty(result)) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + return; + } + new AlertDialog.Builder(context) + .setTitle(profileModel.getUsername()) + .setMessage(result) + .setPositiveButton(R.string.ok, null) + .show(); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "Error translating bio", t); + Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + break; + } + }) + .setNegativeButton(R.string.cancel, null) + .show(); + }); profileDetailsBinding.mainBiography.setOnLongClickListener(v -> { Utils.copyText(context, biography); return true; @@ -687,6 +792,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe } private void setupButtons(final String profileId, final String myId) { + profileDetailsBinding.btnTagged.setVisibility(profileModel.isReallyPrivate() ? View.GONE : View.VISIBLE); if (isLoggedIn) { if (profileId.equals(myId)) { profileDetailsBinding.btnTagged.setVisibility(View.VISIBLE); @@ -696,15 +802,29 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe profileDetailsBinding.btnSaved.setText(R.string.saved); return; } - profileDetailsBinding.btnTagged.setVisibility(View.GONE); profileDetailsBinding.btnSaved.setVisibility(View.GONE); profileDetailsBinding.btnLiked.setVisibility(View.GONE); profileDetailsBinding.btnDM.setVisibility(View.VISIBLE); // maybe there is a judgment mechanism? profileDetailsBinding.btnFollow.setVisibility(View.VISIBLE); - if (profileModel.getFollowing()) { + if (profileModel.isFollowing() || profileModel.isFollower()) { + profileDetailsBinding.mainStatus.setVisibility(View.VISIBLE); + if (!profileModel.isFollowing()) { + profileDetailsBinding.mainStatus.setChipBackgroundColor(getResources().getColorStateList(R.color.blue_800)); + profileDetailsBinding.mainStatus.setText(R.string.status_follower); + } + else if (!profileModel.isFollower()) { + profileDetailsBinding.mainStatus.setChipBackgroundColor(getResources().getColorStateList(R.color.deep_orange_800)); + profileDetailsBinding.mainStatus.setText(R.string.status_following); + } + else { + profileDetailsBinding.mainStatus.setChipBackgroundColor(getResources().getColorStateList(R.color.green_800)); + profileDetailsBinding.mainStatus.setText(R.string.status_mutual); + } + } + if (profileModel.isFollowing()) { profileDetailsBinding.btnFollow.setText(R.string.unfollow); profileDetailsBinding.btnFollow.setIconResource(R.drawable.ic_outline_person_add_disabled_24); - } else if (profileModel.getRequested()) { + } else if (profileModel.isRequested()) { profileDetailsBinding.btnFollow.setText(R.string.cancel); profileDetailsBinding.btnFollow.setIconResource(R.drawable.ic_outline_person_add_disabled_24); } else { @@ -713,16 +833,15 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe } if (restrictMenuItem != null) { restrictMenuItem.setVisible(true); - if (profileModel.getRestricted()) { + if (profileModel.isRestricted()) { restrictMenuItem.setTitle(R.string.unrestrict); } else { restrictMenuItem.setTitle(R.string.restrict); } } - profileDetailsBinding.btnTagged.setVisibility(profileModel.isReallyPrivate() ? View.GONE : View.VISIBLE); if (blockMenuItem != null) { blockMenuItem.setVisible(true); - if (profileModel.getBlocked()) { + if (profileModel.isBlocked()) { blockMenuItem.setTitle(R.string.unblock); } else { blockMenuItem.setTitle(R.string.block); @@ -732,7 +851,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe } if (!profileModel.isReallyPrivate() && restrictMenuItem != null) { restrictMenuItem.setVisible(true); - if (profileModel.getRestricted()) { + if (profileModel.isRestricted()) { restrictMenuItem.setTitle(R.string.unrestrict); } else { restrictMenuItem.setTitle(R.string.restrict); @@ -760,20 +879,55 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe Log.e(TAG, "Error", t); } }); - new HighlightsFetcher(profileId, - result -> { - highlightsFetching = false; - if (result != null) { - profileDetailsBinding.highlightsList.setVisibility(View.VISIBLE); - highlightsViewModel.getList().postValue(result); - } else profileDetailsBinding.highlightsList.setVisibility(View.GONE); - }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + storiesService.fetchHighlights(profileId, + new ServiceCallback>() { + @Override + public void onSuccess(final List result) { + highlightsFetching = false; + if (result != null) { + profileDetailsBinding.highlightsList.setVisibility(View.VISIBLE); + highlightsViewModel.getList().postValue(result); + } + else profileDetailsBinding.highlightsList.setVisibility(View.GONE); + } + + @Override + public void onFailure(final Throwable t) { + profileDetailsBinding.highlightsList.setVisibility(View.GONE); + Log.e(TAG, "Error", t); + } + }); } private void setupCommonListeners() { + final Context context = getContext(); final String userIdFromCookie = CookieUtils.getUserIdFromCookie(cookie); profileDetailsBinding.btnFollow.setOnClickListener(v -> { - if (profileModel.getFollowing() || profileModel.getRequested()) { + if (profileModel.isFollowing() && profileModel.isPrivate()) { + new AlertDialog.Builder(context) + .setTitle(R.string.priv_acc) + .setMessage(R.string.priv_acc_confirm) + .setPositiveButton(R.string.confirm, (d, w) -> + friendshipService.unfollow( + userIdFromCookie, + profileModel.getId(), + CookieUtils.getCsrfTokenFromCookie(cookie), + new ServiceCallback() { + @Override + public void onSuccess(final FriendshipRepoChangeRootResponse result) { + // Log.d(TAG, "Unfollow success: " + result); + onRefresh(); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "Error unfollowing", t); + } + })) + .setNegativeButton(R.string.cancel, null) + .show(); + } + else if (profileModel.isFollowing() || profileModel.isRequested()) { friendshipService.unfollow( userIdFromCookie, profileModel.getId(), @@ -854,72 +1008,18 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe if (which == 1) { // show stories final NavDirections action = ProfileFragmentDirections - .actionProfileFragmentToStoryViewerFragment(-1, null, false, false, profileModel.getId(), username); + .actionProfileFragmentToStoryViewerFragment(-1, null, false, false, profileModel.getId(), username, false, false); NavHostFragment.findNavController(this).navigate(action); return; } showProfilePicDialog(); }; - final Context context = getContext(); if (context == null) return; new AlertDialog.Builder(context) .setItems(options, profileDialogListener) .setNegativeButton(R.string.cancel, null) .show(); }); - profileDetailsBinding.favCb.setOnCheckedChangeListener((buttonView, isChecked) -> { - // do not do anything if state matches the db, as listener is set before profile details are set - final Context context = getContext(); - if (context == null) return; - final String finalUsername = username.startsWith("@") ? username.substring(1) : username; - favoriteRepository.getFavorite(finalUsername, FavoriteType.USER, new RepositoryCallback() { - @Override - public void onSuccess(final Favorite result) { - if (isChecked) return; // already a fav - buttonView.setVisibility(View.GONE); - profileDetailsBinding.favProgress.setVisibility(View.VISIBLE); - favoriteRepository.deleteFavorite(finalUsername, FavoriteType.USER, new RepositoryCallback() { - @Override - public void onSuccess(final Void result) { - profileDetailsBinding.favCb.setButtonDrawable(R.drawable.ic_outline_star_plus_24); - profileDetailsBinding.favProgress.setVisibility(View.GONE); - profileDetailsBinding.favCb.setVisibility(View.VISIBLE); - showSnackbar(getString(R.string.removed_from_favs)); - } - - @Override - public void onDataNotAvailable() {} - }); - } - - @Override - public void onDataNotAvailable() { - if (!isChecked) return; // not in fav already - buttonView.setVisibility(View.GONE); - profileDetailsBinding.favProgress.setVisibility(View.VISIBLE); - final Favorite model = new Favorite( - -1, - finalUsername, - FavoriteType.USER, - profileModel.getName(), - profileModel.getSdProfilePic(), - new Date() - ); - favoriteRepository.insertOrUpdateFavorite(model, new RepositoryCallback() { - @Override - public void onSuccess(final Void result) { - profileDetailsBinding.favCb.setButtonDrawable(R.drawable.ic_star_check_24); - profileDetailsBinding.favProgress.setVisibility(View.GONE); - profileDetailsBinding.favCb.setVisibility(View.VISIBLE); - showSnackbar(getString(R.string.added_to_favs)); - } - - @Override - public void onDataNotAvailable() {} - }); - } - }); - }); } private void showSnackbar(final String message) { @@ -951,7 +1051,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe private void setupPosts() { binding.postsRecyclerView.setViewModelStoreOwner(this) .setLifeCycleOwner(this) - .setPostFetchService(new ProfilePostFetchService(profileModel)) + .setPostFetchService(new ProfilePostFetchService(profileModel, isLoggedIn)) .setLayoutPreferences(layoutPreferences) .addFetchStatusChangeListener(fetching -> updateSwipeRefreshState()) .setFeedItemCallback(feedItemCallback) @@ -969,7 +1069,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe highlightsViewModel = new ViewModelProvider(fragmentActivity).get(HighlightsViewModel.class); highlightsAdapter = new HighlightsAdapter((model, position) -> { final NavDirections action = ProfileFragmentDirections - .actionProfileFragmentToStoryViewerFragment(position, model.getTitle(), false, false, null, null); + .actionProfileFragmentToStoryViewerFragment(position, model.getTitle(), false, false, null, null, false, false); NavHostFragment.findNavController(this).navigate(action); }); final Context context = getContext(); diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/AboutFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/AboutFragment.java index 682de804..51158a3b 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/AboutFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/AboutFragment.java @@ -88,9 +88,10 @@ public class AboutFragment extends BasePreferencesFragment { preference.setSummary(R.string.about_feedback_summary); preference.setIconSpaceReserved(false); preference.setOnPreferenceClickListener(p -> { - final Intent intent = new Intent(Intent.ACTION_SENDTO); - intent.setData(Uri.parse(getString(R.string.about_feedback_summary) - + "?body=Please%20note%20that%20your%20email%20address%20and%20the%20entire%20content%20will%20be%20published%20onto%20GitHub%20issues.%20If%20you%20do%20not%20wish%20to%20do%20that%2C%20use%20other%20contact%20methods%20instead.")); + final Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("message/rfc822") + .putExtra(Intent.EXTRA_EMAIL, getString(R.string.about_feedback_summary)) + .putExtra(Intent.EXTRA_TEXT, "Please note that your email address and the entire content will be published onto GitHub issues. If you do not wish to do that, use other contact methods instead."); if (intent.resolveActivity(context.getPackageManager()) != null) startActivity(intent); return true; }); diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/BackupPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/BackupPreferencesFragment.java index 62d59258..0214eabc 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/BackupPreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/BackupPreferencesFragment.java @@ -26,10 +26,30 @@ public class BackupPreferencesFragment extends BasePreferencesFragment { if (context == null) { return; } + screen.addPreference(getAboutPreference(context)); + screen.addPreference(getWarningPreference(context)); screen.addPreference(getCreatePreference(context)); screen.addPreference(getRestorePreference(context)); } + private Preference getAboutPreference(@NonNull final Context context) { + final Preference preference = new Preference(context); + preference.setSummary(R.string.backup_summary); + preference.setEnabled(false); + preference.setIcon(R.drawable.ic_outline_info_24); + preference.setIconSpaceReserved(true); + return preference; + } + + private Preference getWarningPreference(@NonNull final Context context) { + final Preference preference = new Preference(context); + preference.setSummary(R.string.backup_warning); + preference.setEnabled(false); + preference.setIcon(R.drawable.ic_warning); + preference.setIconSpaceReserved(true); + return preference; + } + private Preference getCreatePreference(@NonNull final Context context) { final Preference preference = new Preference(context); preference.setTitle(R.string.create_backup); diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java index 5dcdde7a..6e2b88b8 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java @@ -134,7 +134,18 @@ public class MorePreferencesFragment extends BasePreferencesFragment { screen.addPreference(getDivider(context)); if (isLoggedIn) { screen.addPreference(getPreference(R.string.action_notif, R.drawable.ic_not_liked, preference -> { - NavHostFragment.findNavController(this).navigate(R.id.action_global_notificationsViewerFragment); + final NavDirections navDirections = MorePreferencesFragmentDirections.actionGlobalNotificationsViewerFragment("notif"); + NavHostFragment.findNavController(this).navigate(navDirections); + return true; + })); + screen.addPreference(getPreference(R.string.action_ayml, R.drawable.ic_suggested_users, preference -> { + final NavDirections navDirections = MorePreferencesFragmentDirections.actionGlobalNotificationsViewerFragment("ayml"); + NavHostFragment.findNavController(this).navigate(navDirections); + return true; + })); + screen.addPreference(getPreference(R.string.action_archive, R.drawable.ic_archive, preference -> { + final NavDirections navDirections = MorePreferencesFragmentDirections.actionGlobalStoryListViewerFragment("archive"); + NavHostFragment.findNavController(this).navigate(navDirections); return true; })); } diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/SettingsPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/SettingsPreferencesFragment.java index 0f567583..24e935cf 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/SettingsPreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/SettingsPreferencesFragment.java @@ -79,6 +79,7 @@ public class SettingsPreferencesFragment extends BasePreferencesFragment { screen.addPreference(loggedInUsersPreferenceCategory); loggedInUsersPreferenceCategory.setIconSpaceReserved(false); loggedInUsersPreferenceCategory.setTitle(R.string.login_settings); + loggedInUsersPreferenceCategory.addPreference(getStorySortPreference()); loggedInUsersPreferenceCategory.addPreference(getMarkStoriesSeenPreference()); loggedInUsersPreferenceCategory.addPreference(getMarkDMSeenPreference()); loggedInUsersPreferenceCategory.addPreference(getEnableActivityNotificationsPreference()); @@ -204,6 +205,25 @@ public class SettingsPreferencesFragment extends BasePreferencesFragment { return preference; } + private Preference getStorySortPreference() { + final Context context = getContext(); + if (context == null) return null; + final ListPreference preference = new ListPreference(context); + preference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()); + final int length = getResources().getStringArray(R.array.story_sorts).length; + final String[] values = new String[length]; + for (int i = 0; i < length; i++) { + values[i] = String.valueOf(i); + } + preference.setKey(Constants.STORY_SORT); + preference.setTitle(R.string.story_sort_setting); + preference.setDialogTitle(R.string.story_sort_setting); + preference.setEntries(R.array.story_sorts); + preference.setIconSpaceReserved(false); + preference.setEntryValues(values); + return preference; + } + private Preference getMarkStoriesSeenPreference() { final Context context = getContext(); if (context == null) return null; diff --git a/app/src/main/java/awais/instagrabber/models/BasePostModel.java b/app/src/main/java/awais/instagrabber/models/BasePostModel.java index 38f5f8fd..4f8486d3 100755 --- a/app/src/main/java/awais/instagrabber/models/BasePostModel.java +++ b/app/src/main/java/awais/instagrabber/models/BasePostModel.java @@ -11,16 +11,12 @@ import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.utils.Utils; public abstract class BasePostModel implements Serializable, Selectable { - protected String postId; - protected String displayUrl; - protected String shortCode; + protected String postId, displayUrl, shortCode, captionId; protected CharSequence postCaption; protected MediaItemType itemType; - protected boolean isSelected; - protected boolean isDownloaded; + protected boolean isSelected, isDownloaded; protected long timestamp; - boolean liked; - boolean saved; + boolean liked, saved; public boolean getLike() { return liked; @@ -46,6 +42,10 @@ public abstract class BasePostModel implements Serializable, Selectable { return postCaption; } + public final String getCaptionId() { + return captionId; + } + public final String getShortCode() { return shortCode; } diff --git a/app/src/main/java/awais/instagrabber/models/CommentModel.java b/app/src/main/java/awais/instagrabber/models/CommentModel.java index 9ce02bb0..c66f6916 100755 --- a/app/src/main/java/awais/instagrabber/models/CommentModel.java +++ b/app/src/main/java/awais/instagrabber/models/CommentModel.java @@ -11,11 +11,10 @@ public class CommentModel { private final ProfileModel profileModel; private final String id; private final String text; - private final long likes; + private long likes; private final long timestamp; private List childCommentModels; - private final boolean liked; - private boolean hasNextPage; + private boolean liked, hasNextPage; private String endCursor; public CommentModel(final String id, @@ -53,6 +52,11 @@ public class CommentModel { return liked; } + public void setLiked(boolean liked) { + this.likes = liked ? likes + 1 : likes - 1; + this.liked = liked; + } + public ProfileModel getProfileModel() { return profileModel; } diff --git a/app/src/main/java/awais/instagrabber/models/FeedModel.java b/app/src/main/java/awais/instagrabber/models/FeedModel.java index d9178640..f4e51664 100755 --- a/app/src/main/java/awais/instagrabber/models/FeedModel.java +++ b/app/src/main/java/awais/instagrabber/models/FeedModel.java @@ -24,7 +24,7 @@ public final class FeedModel extends PostModel { private String displayUrl; private String thumbnailUrl; private String shortCode; - private String postCaption; + private String postCaption, captionId; private long commentsCount; private long timestamp; private boolean liked; @@ -76,6 +76,11 @@ public final class FeedModel extends PostModel { return this; } + public Builder setCaptionId(final String captionId) { + this.captionId = captionId; + return this; + } + public Builder setCommentsCount(final long commentsCount) { this.commentsCount = commentsCount; return this; @@ -127,8 +132,8 @@ public final class FeedModel extends PostModel { } public FeedModel build() { - return new FeedModel(profileModel, itemType, viewCount, postId, displayUrl, thumbnailUrl, shortCode, postCaption, commentsCount, - timestamp, liked, bookmarked, likesCount, locationName, locationId, sliderItems, imageHeight, imageWidth); + return new FeedModel(profileModel, itemType, viewCount, postId, displayUrl, thumbnailUrl, shortCode, postCaption, captionId, + commentsCount, timestamp, liked, bookmarked, likesCount, locationName, locationId, sliderItems, imageHeight, imageWidth); } } @@ -140,6 +145,7 @@ public final class FeedModel extends PostModel { final String thumbnailUrl, final String shortCode, final String postCaption, + final String captionId, final long commentsCount, final long timestamp, final boolean liked, @@ -150,7 +156,7 @@ public final class FeedModel extends PostModel { final List sliderItems, final int imageHeight, final int imageWidth) { - super(itemType, postId, displayUrl, thumbnailUrl, shortCode, postCaption, timestamp, liked, bookmarked); + super(itemType, postId, displayUrl, thumbnailUrl, shortCode, postCaption, captionId, timestamp, liked, bookmarked); this.profileModel = profileModel; this.commentsCount = commentsCount; this.likesCount = likesCount; diff --git a/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java b/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java index e2d9fc77..01ffac76 100755 --- a/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java +++ b/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java @@ -1,36 +1,66 @@ package awais.instagrabber.models; +import android.util.Log; + +import androidx.annotation.NonNull; + import java.io.Serializable; +import java.util.Date; + +import awais.instagrabber.utils.Utils; public final class FeedStoryModel implements Serializable { private final String storyMediaId; private final ProfileModel profileModel; - private StoryModel[] storyModels; - private boolean fullyRead; + private final StoryModel firstStoryModel; + private Boolean fullyRead; + private final long timestamp; + private final int mediaCount; - public FeedStoryModel(final String storyMediaId, final ProfileModel profileModel, final boolean fullyRead) { + public FeedStoryModel(final String storyMediaId, final ProfileModel profileModel, final boolean fullyRead, + final long timestamp, final StoryModel firstStoryModel, final int mediaCount) { this.storyMediaId = storyMediaId; this.profileModel = profileModel; this.fullyRead = fullyRead; + this.timestamp = timestamp; + this.firstStoryModel = firstStoryModel; + this.mediaCount = mediaCount; } public String getStoryMediaId() { return storyMediaId; } + public long getTimestamp() { + return timestamp; + } + + @NonNull + public String getDateTime() { + return Utils.datetimeParser.format(new Date(timestamp * 1000L)); + } + + public int getMediaCount() { + return mediaCount; + } + public ProfileModel getProfileModel() { return profileModel; } - public void setStoryModels(final StoryModel[] storyModels) { - this.storyModels = storyModels; +// public void setFirstStoryModel(final StoryModel firstStoryModel) { +// this.firstStoryModel = firstStoryModel; +// } + + public StoryModel getFirstStoryModel() { + return firstStoryModel; } - public StoryModel[] getStoryModels() { - return storyModels; - } - - public boolean getFullyRead() { + public Boolean isFullyRead() { return fullyRead; } + + public void setFullyRead(final boolean fullyRead) { + this.fullyRead = fullyRead; + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/HashtagModel.java b/app/src/main/java/awais/instagrabber/models/HashtagModel.java index fbcac9db..58ff7932 100755 --- a/app/src/main/java/awais/instagrabber/models/HashtagModel.java +++ b/app/src/main/java/awais/instagrabber/models/HashtagModel.java @@ -29,7 +29,7 @@ public final class HashtagModel implements Serializable { return sdProfilePic; } - public long getPostCount() { return postCount; } + public Long getPostCount() { return postCount; } public boolean getFollowing() { return following; } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/HighlightModel.java b/app/src/main/java/awais/instagrabber/models/HighlightModel.java index 70e4032a..86d0327d 100755 --- a/app/src/main/java/awais/instagrabber/models/HighlightModel.java +++ b/app/src/main/java/awais/instagrabber/models/HighlightModel.java @@ -1,16 +1,28 @@ package awais.instagrabber.models; +import androidx.annotation.NonNull; + +import java.util.Date; + +import awais.instagrabber.utils.Utils; + public final class HighlightModel { private final String title; private final String id; private final String thumbnailUrl; + private final long timestamp; + private final int mediaCount; public HighlightModel(final String title, final String id, - final String thumbnailUrl) { + final String thumbnailUrl, + final long timestamp, + final int mediaCount) { this.title = title; this.id = id; this.thumbnailUrl = thumbnailUrl; + this.timestamp = timestamp; + this.mediaCount = mediaCount; } public String getTitle() { @@ -24,4 +36,17 @@ public final class HighlightModel { public String getThumbnailUrl() { return thumbnailUrl; } + + public long getTimestamp() { + return timestamp; + } + + @NonNull + public String getDateTime() { + return Utils.datetimeParser.format(new Date(timestamp * 1000L)); + } + + public int getMediaCount() { + return mediaCount; + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/LocationModel.java b/app/src/main/java/awais/instagrabber/models/LocationModel.java index fedaa773..b237d4e4 100755 --- a/app/src/main/java/awais/instagrabber/models/LocationModel.java +++ b/app/src/main/java/awais/instagrabber/models/LocationModel.java @@ -59,5 +59,5 @@ public final class LocationModel implements Serializable { return sdProfilePic; } - public long getPostCount() { return postCount; } + public Long getPostCount() { return postCount; } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/NotificationModel.java b/app/src/main/java/awais/instagrabber/models/NotificationModel.java index d461a033..a7b78b47 100755 --- a/app/src/main/java/awais/instagrabber/models/NotificationModel.java +++ b/app/src/main/java/awais/instagrabber/models/NotificationModel.java @@ -13,7 +13,7 @@ public final class NotificationModel { private final String userId; private final String username; private final String profilePicUrl; - private final String shortCode; + private final String postId; private final String previewUrl; private final NotificationType type; private final CharSequence text; @@ -25,7 +25,7 @@ public final class NotificationModel { final String userId, final String username, final String profilePicUrl, - final String shortCode, + final String postId, final String previewUrl, final NotificationType type) { this.id = id; @@ -34,7 +34,7 @@ public final class NotificationModel { this.userId = userId; this.username = username; this.profilePicUrl = profilePicUrl; - this.shortCode = shortCode; + this.postId = postId; this.previewUrl = previewUrl; this.type = type; } @@ -47,6 +47,10 @@ public final class NotificationModel { return text; } + public long getTimestamp() { + return timestamp; + } + @NonNull public String getDateTime() { return Utils.datetimeParser.format(new Date(timestamp * 1000L)); @@ -64,8 +68,8 @@ public final class NotificationModel { return profilePicUrl; } - public String getShortCode() { - return shortCode; + public String getPostId() { + return postId; } public String getPreviewPic() { diff --git a/app/src/main/java/awais/instagrabber/models/PostModel.java b/app/src/main/java/awais/instagrabber/models/PostModel.java index e74924d7..75bb6753 100755 --- a/app/src/main/java/awais/instagrabber/models/PostModel.java +++ b/app/src/main/java/awais/instagrabber/models/PostModel.java @@ -19,6 +19,7 @@ public class PostModel extends BasePostModel { final String thumbnailUrl, final String shortCode, final CharSequence postCaption, + final String captionId, long timestamp, boolean liked, boolean bookmarked) { @@ -28,6 +29,7 @@ public class PostModel extends BasePostModel { this.thumbnailUrl = thumbnailUrl; this.shortCode = shortCode; this.postCaption = postCaption; + this.captionId = captionId; this.timestamp = timestamp; this.liked = liked; this.saved = bookmarked; diff --git a/app/src/main/java/awais/instagrabber/models/ProfileModel.java b/app/src/main/java/awais/instagrabber/models/ProfileModel.java index e6367ef6..ffed64f7 100755 --- a/app/src/main/java/awais/instagrabber/models/ProfileModel.java +++ b/app/src/main/java/awais/instagrabber/models/ProfileModel.java @@ -3,15 +3,15 @@ package awais.instagrabber.models; import java.io.Serializable; public final class ProfileModel implements Serializable { - private final boolean isPrivate, reallyPrivate, isVerified, following, restricted, blocked, requested; + private final boolean isPrivate, reallyPrivate, isVerified, following, follower, restricted, blocked, requested; private final long postCount, followersCount, followingCount; private final String id, username, name, biography, url, sdProfilePic, hdProfilePic; public ProfileModel(final boolean isPrivate, final boolean reallyPrivate, final boolean isVerified, final String id, final String username, final String name, final String biography, final String url, final String sdProfilePic, final String hdProfilePic, final long postCount, - final long followersCount, final long followingCount, final boolean following, final boolean restricted, - final boolean blocked, final boolean requested) { + final long followersCount, final long followingCount, final boolean following, final boolean follower, + final boolean restricted, final boolean blocked, final boolean requested) { this.isPrivate = isPrivate; this.reallyPrivate = reallyPrivate; this.isVerified = isVerified; @@ -26,21 +26,22 @@ public final class ProfileModel implements Serializable { this.followersCount = followersCount; this.followingCount = followingCount; this.following = following; + this.follower = follower; this.restricted = restricted; this.blocked = blocked; this.requested = requested; } public static ProfileModel getDefaultProfileModel() { - return new ProfileModel(false, false, false, null, null, null, null, null, null, null, 0, 0, 0, false, false, false, false); + return new ProfileModel(false, false, false, null, null, null, null, null, null, null, 0, 0, 0, false, false, false, false, false); } public static ProfileModel getDefaultProfileModel(final String userId) { - return new ProfileModel(false, false, false, userId, null, null, null, null, null, null, 0, 0, 0, false, false, false, false); + return new ProfileModel(false, false, false, userId, null, null, null, null, null, null, 0, 0, 0, false, false, false, false, false); } public static ProfileModel getDefaultProfileModel(final String userId, final String username) { - return new ProfileModel(false, false, false, userId, username, null, null, null, null, null, 0, 0, 0, false, false, false, false); + return new ProfileModel(false, false, false, userId, username, null, null, null, null, null, 0, 0, 0, false, false, false, false, false); } public boolean isPrivate() { @@ -83,31 +84,35 @@ public final class ProfileModel implements Serializable { return hdProfilePic; } - public long getPostCount() { + public Long getPostCount() { return postCount; } - public long getFollowersCount() { + public Long getFollowersCount() { return followersCount; } - public long getFollowingCount() { + public Long getFollowingCount() { return followingCount; } - public boolean getFollowing() { + public boolean isFollowing() { return following; } - public boolean getRestricted() { + public boolean isFollower() { + return follower; + } + + public boolean isRestricted() { return restricted; } - public boolean getBlocked() { + public boolean isBlocked() { return blocked; } - public boolean getRequested() { + public boolean isRequested() { return requested; } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/StoryModel.java b/app/src/main/java/awais/instagrabber/models/StoryModel.java index 9d3959c8..80b451c8 100755 --- a/app/src/main/java/awais/instagrabber/models/StoryModel.java +++ b/app/src/main/java/awais/instagrabber/models/StoryModel.java @@ -6,11 +6,13 @@ import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.models.stickers.PollModel; import awais.instagrabber.models.stickers.QuestionModel; import awais.instagrabber.models.stickers.QuizModel; +import awais.instagrabber.models.stickers.SliderModel; import awais.instagrabber.models.stickers.SwipeUpModel; public final class StoryModel implements Serializable { private final String storyMediaId; private final String storyUrl; + private String thumbnail; private final String username; private final String userId; private final MediaItemType itemType; @@ -21,6 +23,7 @@ public final class StoryModel implements Serializable { private String spotify; private PollModel poll; private QuestionModel question; + private SliderModel slider; private QuizModel quiz; private SwipeUpModel swipeUp; private String[] mentions; @@ -28,10 +31,11 @@ public final class StoryModel implements Serializable { private boolean isCurrentSlide = false; private final boolean canReply; - public StoryModel(final String storyMediaId, final String storyUrl, final MediaItemType itemType, + public StoryModel(final String storyMediaId, final String storyUrl, final String thumbnail, final MediaItemType itemType, final long timestamp, final String username, final String userId, final boolean canReply) { this.storyMediaId = storyMediaId; this.storyUrl = storyUrl; + this.thumbnail = thumbnail; this.itemType = itemType; this.timestamp = timestamp; this.username = username; @@ -43,6 +47,10 @@ public final class StoryModel implements Serializable { return storyUrl; } + public String getThumbnail() { + return thumbnail; + } + public String getStoryMediaId() { return storyMediaId; } @@ -71,6 +79,10 @@ public final class StoryModel implements Serializable { return question; } + public SliderModel getSlider() { + return slider; + } + public QuizModel getQuiz() { return quiz; } @@ -85,6 +97,10 @@ public final class StoryModel implements Serializable { return position; } + public void setThumbnail(final String thumbnail) { + this.thumbnail = thumbnail; + } + public void setVideoUrl(final String videoUrl) { this.videoUrl = videoUrl; } @@ -109,6 +125,10 @@ public final class StoryModel implements Serializable { this.question = question; } + public void setSlider(final SliderModel slider) { + this.slider = slider; + } + public void setQuiz(final QuizModel quiz) { this.quiz = quiz; } diff --git a/app/src/main/java/awais/instagrabber/models/enums/NotificationType.java b/app/src/main/java/awais/instagrabber/models/enums/NotificationType.java index acdadfbd..7e39443b 100755 --- a/app/src/main/java/awais/instagrabber/models/enums/NotificationType.java +++ b/app/src/main/java/awais/instagrabber/models/enums/NotificationType.java @@ -5,12 +5,20 @@ import java.util.HashMap; import java.util.Map; public enum NotificationType implements Serializable { + // web LIKE("GraphLikeAggregatedStory"), FOLLOW("GraphFollowAggregatedStory"), COMMENT("GraphCommentMediaStory"), MENTION("GraphMentionStory"), TAGGED("GraphUserTaggedStory"), - REQUEST("REQUEST"); + // app story_type + COMMENT_LIKE("13"), + TAGGED_COMMENT("14"), + RESPONDED_STORY("213"), + // efr - random value + REQUEST("REQUEST"), + // ayml - random value + AYML("AYML"); private final String itemType; private static final Map map = new HashMap<>(); diff --git a/app/src/main/java/awais/instagrabber/models/stickers/SliderModel.java b/app/src/main/java/awais/instagrabber/models/stickers/SliderModel.java new file mode 100755 index 00000000..ff5c0f20 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/stickers/SliderModel.java @@ -0,0 +1,53 @@ +package awais.instagrabber.models.stickers; + +import java.io.Serializable; + +public final class SliderModel implements Serializable { + private final int voteCount; + private final Double average; + private Double myChoice; + private final boolean canVote; + private final String id, question, emoji; + + public SliderModel(final String id, final String question, final String emoji, final boolean canVote, + final Double average, final int voteCount, final Double myChoice) { + this.id = id; + this.question = question; + this.emoji = emoji; + this.canVote = canVote; + this.average = average; + this.voteCount = voteCount; + this.myChoice = myChoice; + } + + public String getId() { + return id; + } + + public String getQuestion() { + return question; + } + + public String getEmoji() { + return emoji; + } + + public boolean canVote() { + return canVote; + } + + public int getVoteCount() { + return voteCount; + } + + public Double getAverage() { + return average; + } + + public Double getMyChoice() { return myChoice; } + + public Double setMyChoice(final Double choice) { + this.myChoice = choice; + return choice; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/repositories/FeedRepository.java b/app/src/main/java/awais/instagrabber/repositories/FeedRepository.java index 3672fb90..24ac5e01 100644 --- a/app/src/main/java/awais/instagrabber/repositories/FeedRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/FeedRepository.java @@ -3,10 +3,12 @@ package awais.instagrabber.repositories; import java.util.Map; import retrofit2.Call; -import retrofit2.http.GET; -import retrofit2.http.QueryMap; +import retrofit2.http.FieldMap; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.POST; public interface FeedRepository { - @GET("/graphql/query/") - Call fetch(@QueryMap(encoded = true) Map queryParams); + @FormUrlEncoded + @POST("/api/v1/feed/timeline/") + Call fetch(@FieldMap final Map signedForm); } diff --git a/app/src/main/java/awais/instagrabber/repositories/GraphQLRepository.java b/app/src/main/java/awais/instagrabber/repositories/GraphQLRepository.java new file mode 100644 index 00000000..23020035 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/GraphQLRepository.java @@ -0,0 +1,12 @@ +package awais.instagrabber.repositories; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.QueryMap; + +public interface GraphQLRepository { + @GET("/graphql/query/") + Call fetch(@QueryMap(encoded = true) Map queryParams); +} diff --git a/app/src/main/java/awais/instagrabber/repositories/LocationRepository.java b/app/src/main/java/awais/instagrabber/repositories/LocationRepository.java index 2237254d..7cb5ca41 100644 --- a/app/src/main/java/awais/instagrabber/repositories/LocationRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/LocationRepository.java @@ -12,7 +12,4 @@ public interface LocationRepository { @GET("/api/v1/feed/location/{location}/") Call fetchPosts(@Path("location") final String locationId, @QueryMap Map queryParams); - - @GET("/graphql/query/") - Call fetchGraphQLPosts(@QueryMap(encoded = true) Map queryParams); } diff --git a/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java b/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java index 73633b24..cff9cac3 100644 --- a/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java @@ -5,48 +5,57 @@ import java.util.Map; import retrofit2.Call; import retrofit2.http.FieldMap; import retrofit2.http.FormUrlEncoded; +import retrofit2.http.GET; import retrofit2.http.Header; import retrofit2.http.POST; import retrofit2.http.Path; import retrofit2.http.QueryMap; public interface MediaRepository { + @GET("/api/v1/media/{mediaId}/info/") + Call fetch(@Path("mediaId") final String mediaId); + + @GET("/api/v1/media/{mediaId}/{action}/") + Call fetchLikes(@Path("mediaId") final String mediaId, + @Path("action") final String action); // one of "likers" or "comment_likers" @FormUrlEncoded @POST("/api/v1/media/{mediaId}/{action}/") - Call action(@Header("User-Agent") final String userAgent, - @Path("action") final String action, + Call action(@Path("action") final String action, @Path("mediaId") final String mediaId, @FieldMap final Map signedForm); @FormUrlEncoded @POST("/api/v1/media/{mediaId}/comment/") - Call comment(@Header("User-Agent") final String userAgent, - @Path("mediaId") final String mediaId, + Call comment(@Path("mediaId") final String mediaId, @FieldMap final Map signedForm); @FormUrlEncoded @POST("/api/v1/media/{mediaId}/comment/bulk_delete/") - Call commentsBulkDelete(@Header("User-Agent") final String userAgent, - @Path("mediaId") final String mediaId, + Call commentsBulkDelete(@Path("mediaId") final String mediaId, @FieldMap final Map signedForm); @FormUrlEncoded @POST("/api/v1/media/{commentId}/comment_like/") - Call commentLike(@Header("User-Agent") final String userAgent, - @Path("commentId") final String commentId, + Call commentLike(@Path("commentId") final String commentId, @FieldMap final Map signedForm); @FormUrlEncoded @POST("/api/v1/media/{commentId}/comment_unlike/") - Call commentUnlike(@Header("User-Agent") final String userAgent, - @Path("commentId") final String commentId, + Call commentUnlike(@Path("commentId") final String commentId, @FieldMap final Map signedForm); + @FormUrlEncoded + @POST("/api/v1/media/{mediaId}/edit_media/") + Call editCaption(@Path("mediaId") final String mediaId, + @FieldMap final Map signedForm); + + @GET("/api/v1/language/translate/") + Call translate(@QueryMap final Map form); + @FormUrlEncoded @POST("/api/v1/media/upload_finish/") - Call uploadFinish(// @Header("User-Agent") final String userAgent, - @Header("retry_context") final String retryContext, + Call uploadFinish(@Header("retry_context") final String retryContext, @QueryMap Map queryParams, @FieldMap final Map signedForm); } diff --git a/app/src/main/java/awais/instagrabber/repositories/NewsRepository.java b/app/src/main/java/awais/instagrabber/repositories/NewsRepository.java index e8e71116..20b50934 100644 --- a/app/src/main/java/awais/instagrabber/repositories/NewsRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/NewsRepository.java @@ -6,16 +6,24 @@ import awais.instagrabber.utils.Constants; import retrofit2.Call; import retrofit2.http.FieldMap; import retrofit2.http.FormUrlEncoded; +import retrofit2.http.GET; import retrofit2.http.Header; import retrofit2.http.Headers; import retrofit2.http.POST; +import retrofit2.http.Query; public interface NewsRepository { - Call inbox(); + @Headers("User-Agent: " + Constants.USER_AGENT) + @GET("https://www.instagram.com/accounts/activity/?__a=1") + Call webInbox(); + + @Headers("User-Agent: " + Constants.I_USER_AGENT) + @GET("/api/v1/news/inbox/") + Call appInbox(@Query(value = "mark_as_seen", encoded = true) boolean markAsSeen); @FormUrlEncoded - @Headers("User-Agent: " + Constants.USER_AGENT) - @POST("https://www.instagram.com/web/activity/mark_checked/") - Call markChecked(@Header("x-csrftoken") String csrfToken, @FieldMap Map map); + @Headers("User-Agent: " + Constants.I_USER_AGENT) + @POST("/api/v1/discover/ayml/") + Call getAyml(@FieldMap final Map form); } diff --git a/app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java b/app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java index 8643b682..916dd094 100644 --- a/app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java @@ -9,11 +9,11 @@ import retrofit2.http.QueryMap; public interface ProfileRepository { - @GET("api/v1/users/{uid}/info/") + @GET("/api/v1/users/{uid}/info/") Call getUserInfo(@Path("uid") final String uid); - @GET("/graphql/query/") - Call fetch(@QueryMap Map queryMap); + @GET("/api/v1/feed/user/{uid}/") + Call fetch(@Path("uid") final String uid, @QueryMap Map queryParams); @GET("/api/v1/feed/saved/") Call fetchSaved(@QueryMap Map queryParams); diff --git a/app/src/main/java/awais/instagrabber/repositories/StoriesRepository.java b/app/src/main/java/awais/instagrabber/repositories/StoriesRepository.java index 42f262d6..139ba352 100644 --- a/app/src/main/java/awais/instagrabber/repositories/StoriesRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/StoriesRepository.java @@ -2,17 +2,40 @@ package awais.instagrabber.repositories; import java.util.Map; +import awais.instagrabber.repositories.responses.StoryStickerResponse; import retrofit2.Call; +import retrofit2.http.FieldMap; +import retrofit2.http.FormUrlEncoded; import retrofit2.http.GET; import retrofit2.http.Header; +import retrofit2.http.Path; +import retrofit2.http.POST; import retrofit2.http.QueryMap; import retrofit2.http.Url; public interface StoriesRepository { + @GET("/api/v1/media/{mediaId}/info/") + Call fetch(@Path("mediaId") final String mediaId); + // this one is the same as MediaRepository.fetch BUT you need to make sure it's a story - @GET("graphql/query/") - Call getStories(@QueryMap(encoded = true) Map variables); + @GET("/api/v1/feed/reels_tray/") + Call getFeedStories(); + + @GET("/api/v1/highlights/{uid}/highlights_tray/") + Call fetchHighlights(@Path("uid") final String uid); + + @GET("/api/v1/archive/reel/day_shells/") + Call fetchArchive(@QueryMap Map queryParams); @GET Call getUserStory(@Header("User-Agent") String userAgent, @Url String url); + + @FormUrlEncoded + @POST("/api/v1/media/{storyId}/{stickerId}/{action}/") + Call respondToSticker(@Header("User-Agent") String userAgent, + @Path("storyId") String storyId, + @Path("stickerId") String stickerId, + @Path("action") String action, + // story_poll_vote, story_question_response, story_slider_vote, story_quiz_answer + @FieldMap Map form); } diff --git a/app/src/main/java/awais/instagrabber/repositories/TagsRepository.java b/app/src/main/java/awais/instagrabber/repositories/TagsRepository.java index b646a8a3..4db3efbd 100644 --- a/app/src/main/java/awais/instagrabber/repositories/TagsRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/TagsRepository.java @@ -24,7 +24,4 @@ public interface TagsRepository { @GET("/api/v1/feed/tag/{tag}/") Call fetchPosts(@Path("tag") final String tag, @QueryMap Map queryParams); - - @GET("/graphql/query/") - Call fetchGraphQLPosts(@QueryMap(encoded = true) Map queryParams); } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/GraphQLUserListFetchResponse.java b/app/src/main/java/awais/instagrabber/repositories/responses/GraphQLUserListFetchResponse.java new file mode 100644 index 00000000..ebcd93b6 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/GraphQLUserListFetchResponse.java @@ -0,0 +1,79 @@ +package awais.instagrabber.repositories.responses; + +import androidx.annotation.NonNull; + +import java.util.List; +import java.util.Objects; + +import awais.instagrabber.models.ProfileModel; +import awais.instagrabber.utils.TextUtils; + +public class GraphQLUserListFetchResponse { + private String nextMaxId; + private String status; + private List items; + + public GraphQLUserListFetchResponse(final String nextMaxId, + final String status, + final List items) { + this.nextMaxId = nextMaxId; + this.status = status; + this.items = items; + } + + public boolean isMoreAvailable() { + return !TextUtils.isEmpty(nextMaxId); + } + + public String getNextMaxId() { + return nextMaxId; + } + + public GraphQLUserListFetchResponse setNextMaxId(final String nextMaxId) { + this.nextMaxId = nextMaxId; + return this; + } + + public String getStatus() { + return status; + } + + public GraphQLUserListFetchResponse setStatus(final String status) { + this.status = status; + return this; + } + + public List getItems() { + return items; + } + + public GraphQLUserListFetchResponse setItems(final List items) { + this.items = items; + return this; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final GraphQLUserListFetchResponse that = (GraphQLUserListFetchResponse) o; + return Objects.equals(nextMaxId, that.nextMaxId) && + Objects.equals(status, that.status) && + Objects.equals(items, that.items); + } + + @Override + public int hashCode() { + return Objects.hash(nextMaxId, status, items); + } + + @NonNull + @Override + public String toString() { + return "GraphQLUserListFetchResponse{" + + "nextMaxId='" + nextMaxId + '\'' + + ", status='" + status + '\'' + + ", items=" + items + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/StoryStickerResponse.java b/app/src/main/java/awais/instagrabber/repositories/responses/StoryStickerResponse.java new file mode 100644 index 00000000..8ac2f4a9 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/StoryStickerResponse.java @@ -0,0 +1,20 @@ +package awais.instagrabber.repositories.responses; + +public class StoryStickerResponse { + private final String status; + + public StoryStickerResponse(final String status) { + this.status = status; + } + + public String getStatus() { + return status; + } + + @Override + public String toString() { + return "StoryStickerResponse{" + + "status='" + status + '\'' + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/UserInfo.java b/app/src/main/java/awais/instagrabber/repositories/responses/UserInfo.java index 4cca9b69..995beab9 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/UserInfo.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/UserInfo.java @@ -2,15 +2,18 @@ package awais.instagrabber.repositories.responses; public class UserInfo { private final String pk; - private final String username; - private final String fullName; - private final String profilePicUrl; + private final String username, fullName, profilePicUrl, hdProfilePicUrl; - public UserInfo(final String pk, final String username, final String fullName, final String profilePicUrl) { + public UserInfo(final String pk, + final String username, + final String fullName, + final String profilePicUrl, + final String hdProfilePicUrl) { this.pk = pk; this.username = username; this.fullName = fullName; this.profilePicUrl = profilePicUrl; + this.hdProfilePicUrl = hdProfilePicUrl; } public String getPk() { @@ -29,6 +32,10 @@ public class UserInfo { return profilePicUrl; } + public String getHDProfilePicUrl() { + return hdProfilePicUrl; + } + @Override public String toString() { return "UserInfo{" + @@ -36,6 +43,7 @@ public class UserInfo { ", username='" + username + '\'' + ", fullName='" + fullName + '\'' + ", profilePicUrl='" + profilePicUrl + '\'' + + ", hdProfilePicUrl='" + hdProfilePicUrl + '\'' + '}'; } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectUser.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectUser.java index 07d4dde5..d9da1cf3 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectUser.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectUser.java @@ -99,11 +99,11 @@ public class DirectUser { profileModel.getSdProfilePic(), null, new DirectUserFriendshipStatus( - profileModel.getFollowing(), - profileModel.getBlocked(), + profileModel.isFollowing(), + profileModel.isBlocked(), profileModel.isPrivate(), false, - profileModel.getRequested(), + profileModel.isRequested(), false ), profileModel.isVerified(), diff --git a/app/src/main/java/awais/instagrabber/utils/Constants.java b/app/src/main/java/awais/instagrabber/utils/Constants.java index fc098c68..ac93abba 100644 --- a/app/src/main/java/awais/instagrabber/utils/Constants.java +++ b/app/src/main/java/awais/instagrabber/utils/Constants.java @@ -8,6 +8,7 @@ public final class Constants { public static final String CUSTOM_DATE_TIME_FORMAT = "date_time_custom_format"; public static final String APP_THEME = "app_theme_v19"; public static final String APP_LANGUAGE = "app_language_v19"; + public static final String STORY_SORT = "story_sort"; // int prefs public static final String PREV_INSTALL_VERSION = "prevVersion"; // boolean prefs @@ -56,9 +57,9 @@ public final class Constants { // spoof public static final String USER_AGENT = "Mozilla/5.0 (Linux; Android 8.1.0; motorola one Build/OPKS28.63-18-3; wv) " + "AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/70.0.3538.80 Mobile Safari/537.36 " + - "Instagram 166.1.0.42.245 Android (27/8.1.0; 320dpi; 720x1362; motorola; motorola one; deen_sprout; qcom; pt_BR; 256099205)"; + "Instagram 169.1.0.29.135 Android (27/8.1.0; 320dpi; 720x1362; motorola; motorola one; deen_sprout; qcom; pt_BR; 262886998)"; public static final String I_USER_AGENT = - "Instagram 166.1.0.42.245 Android (27/8.1.0; 320dpi; 720x1362; motorola; motorola one; deen_sprout; qcom; pt_BR; 256099205)"; + "Instagram 169.1.0.29.135 Android (27/8.1.0; 320dpi; 720x1362; motorola; motorola one; deen_sprout; qcom; pt_BR; 262886998)"; public static final String A_USER_AGENT = "https://Barinsta.AustinHuang.me / mailto:Barinsta@AustinHuang.me"; // see https://github.com/dilame/instagram-private-api/blob/master/src/core/constants.ts public static final String SUPPORTED_CAPABILITIES = "[ { \"name\": \"SUPPORTED_SDK_VERSIONS\", \"value\":" + diff --git a/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java b/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java index dab09b03..03003ce0 100755 --- a/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java @@ -152,7 +152,7 @@ public final class ExportImportUtils { continue; } final Favorite favorite = new Favorite( - -1, + 0, query, favoriteType, favsObject.optString("s"), diff --git a/app/src/main/java/awais/instagrabber/utils/LocaleUtils.java b/app/src/main/java/awais/instagrabber/utils/LocaleUtils.java index a0a40151..0ea69a55 100755 --- a/app/src/main/java/awais/instagrabber/utils/LocaleUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/LocaleUtils.java @@ -73,6 +73,10 @@ public final class LocaleUtils { if (appLanguageIndex == 14) return "zh_TW"; if (appLanguageIndex == 15) return "ca"; if (appLanguageIndex == 16) return "ru"; + if (appLanguageIndex == 17) return "hi"; + if (appLanguageIndex == 18) return "nl"; + if (appLanguageIndex == 19) return "sk"; + if (appLanguageIndex == 20) return "ja"; return null; } diff --git a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java index d608dcdd..e526ae99 100644 --- a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java @@ -20,11 +20,17 @@ import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.PostChild; import awais.instagrabber.models.ProfileModel; import awais.instagrabber.models.enums.MediaItemType; +import awais.instagrabber.models.StoryModel; import awais.instagrabber.repositories.responses.directmessages.DirectItem; import awais.instagrabber.repositories.responses.directmessages.DirectThreadDirectStory; import awais.instagrabber.repositories.responses.directmessages.DirectThreadLastSeenAt; import awais.instagrabber.repositories.responses.directmessages.ImageVersions2; import awais.instagrabber.repositories.responses.directmessages.MediaCandidate; +import awais.instagrabber.models.stickers.PollModel; +import awais.instagrabber.models.stickers.QuestionModel; +import awais.instagrabber.models.stickers.QuizModel; +import awais.instagrabber.models.stickers.SliderModel; +import awais.instagrabber.models.stickers.SwipeUpModel; import awaisomereport.LogCollector; public final class ResponseBodyUtils { @@ -186,8 +192,8 @@ public final class ResponseBodyUtils { return null; } - // public static DirectItemMediaModel getDirectMediaModel(final JSONObject mediaObj) throws Exception { - // final DirectItemMediaModel mediaModel; + // public static DirectItemModel.DirectItemMediaModel getDirectMediaModel(final JSONObject mediaObj) throws Exception { + // final DirectItemModel.DirectItemMediaModel mediaModel; // if (mediaObj == null) mediaModel = null; // else { // final JSONObject userObj = mediaObj.optJSONObject("user"); @@ -203,7 +209,7 @@ public final class ResponseBodyUtils { // userObj.getString("full_name"), // null, null, // userObj.getString("profile_pic_url"), - // null, 0, 0, 0, false, false, false, false); + // null, 0, 0, 0, false, false, false, false, false); // } // // final MediaItemType mediaType = getMediaItemType(mediaObj.optInt("media_type", -1)); @@ -212,7 +218,7 @@ public final class ResponseBodyUtils { // if (TextUtils.isEmpty(id)) id = null; // // final ThumbnailDetails thumbnailDetails = getThumbnailUrl(mediaObj, mediaType); - // mediaModel = new DirectItemMediaModel( + // mediaModel = new DirectItemModel.DirectItemMediaModel( // mediaType, // mediaObj.optLong("expiring_at"), // mediaObj.optLong("pk"), @@ -226,7 +232,7 @@ public final class ResponseBodyUtils { // } // return mediaModel; // } - + // // private static DirectItemType getDirectItemType(final String itemType) { // if ("placeholder".equals(itemType)) return DirectItemType.PLACEHOLDER; // if ("media".equals(itemType)) return DirectItemType.MEDIA; @@ -245,81 +251,99 @@ public final class ResponseBodyUtils { // if ("felix_share".equals(itemType)) return DirectItemType.FELIX_SHARE; // return DirectItemType.TEXT; // } - + // // @NonNull // public static InboxThreadModel createInboxThreadModel(@NonNull final JSONObject data, final boolean inThreadView) throws Exception { // final InboxReadState readState = data.getInt("read_state") == 0 ? InboxReadState.STATE_READ : InboxReadState.STATE_UNREAD; + // final String threadType = data.getString("thread_type"); // they're all "private", group is identified by boolean "is_group" // - // final JSONArray leftUsersJsonArray = data.optJSONArray("left_users"); + // final String threadId = data.getString("thread_id"); + // final String threadV2Id = data.getString("thread_v2_id"); + // final String threadTitle = data.getString("thread_title"); // - // final JSONArray usersJsonArray = data.getJSONArray("users"); - // final List users = new ArrayList<>(); - // for (int j = 0; j < usersJsonArray.length(); ++j) { - // final JSONObject userObject = usersJsonArray.getJSONObject(j); - // users.add(new ProfileModel(userObject.optBoolean("is_private"), - // false, - // userObject.optBoolean("is_verified"), - // String.valueOf(userObject.get("pk")), - // userObject.optString("username"), // optional cuz fb users - // userObject.getString("full_name"), - // null, - // null, - // userObject.getString("profile_pic_url"), - // null, - // 0, - // 0, - // 0, - // false, - // false, - // false, - // false)); - // } + // final String threadNewestCursor = data.optString("newest_cursor"); + // final String threadOldestCursor = data.optString("oldest_cursor"); + // final String threadNextCursor = data.has("next_cursor") ? data.getString("next_cursor") : null; + // final String threadPrevCursor = data.has("prev_cursor") ? data.getString("prev_cursor") : null; // - // final List leftUsers = new ArrayList<>(); - // for (int j = 0; j < leftUsersJsonArray.length(); ++j) { - // final JSONObject userObject = leftUsersJsonArray.getJSONObject(j); - // leftUsers.add(new ProfileModel(userObject.getBoolean("is_private"), - // false, - // userObject.optBoolean("is_verified"), - // String.valueOf(userObject.get("pk")), - // userObject.getString("username"), - // userObject.getString("full_name"), - // null, - // null, - // userObject.getString("profile_pic_url"), - // null, - // 0, - // 0, - // 0, - // false, - // false, - // false, - // false)); - // } + // final boolean threadHasOlder = data.getBoolean("has_older"); + // final long unreadCount = data.optLong("read_state", 0); // + // final long lastActivityAt = data.optLong("last_activity_at"); + // final boolean named = data.optBoolean("named"); + // final boolean muted = data.optBoolean("muted"); + // final boolean isPin = data.optBoolean("is_pin"); + // final boolean isSpam = data.optBoolean("is_spam"); + // final boolean isGroup = data.optBoolean("is_group"); + // final boolean pending = data.optBoolean("pending"); + // final boolean archived = data.optBoolean("archived"); + // final boolean canonical = data.optBoolean("canonical"); + // + // final JSONArray users = data.getJSONArray("users"); + // final int usersLen = users.length(); + // final JSONArray leftusers = data.optJSONArray("left_users"); + // final int leftusersLen = leftusers == null ? 0 : leftusers.length(); // final JSONArray admins = data.getJSONArray("admin_user_ids"); - // final List adminIDs = new ArrayList<>(); - // for (int j = 0; j < admins.length(); ++j) { - // adminIDs.add(admins.getLong(j)); + // final int adminsLen = admins.length(); + // + // final ProfileModel[] userModels = new ProfileModel[usersLen]; + // for (int j = 0; j < usersLen; ++j) { + // final JSONObject userObject = users.getJSONObject(j); + // userModels[j] = new ProfileModel(userObject.optBoolean("is_private"), + // false, + // userObject.optBoolean("is_verified"), + // String.valueOf(userObject.get("pk")), + // userObject.optString("username"), // optional cuz fb users + // userObject.getString("full_name"), + // null, null, + // userObject.getString("profile_pic_url"), + // null, 0, 0, 0, false, false, false, false, false); + // } + // + // final ProfileModel[] leftuserModels = new ProfileModel[leftusersLen]; + // for (int j = 0; j < leftusersLen; ++j) { + // final JSONObject userObject = leftusers.getJSONObject(j); + // leftuserModels[j] = new ProfileModel(userObject.getBoolean("is_private"), + // false, + // userObject.optBoolean("is_verified"), + // String.valueOf(userObject.get("pk")), + // userObject.getString("username"), + // userObject.getString("full_name"), + // null, null, + // userObject.getString("profile_pic_url"), + // null, 0, 0, 0, false, false, false, false, false); + // } + // + // final Long[] adminIDs = new Long[adminsLen]; + // for (int j = 0; j < adminsLen; ++j) { + // adminIDs[j] = admins.getLong(j); // } // // final JSONArray items = data.getJSONArray("items"); // final int itemsLen = items.length(); // - // final List itemModels = new ArrayList<>(itemsLen); + // final ArrayList itemModels = new ArrayList<>(itemsLen); // for (int i = 0; i < itemsLen; ++i) { // final JSONObject itemObject = items.getJSONObject(i); // // CharSequence text = null; // ProfileModel profileModel = null; - // DMItemMediaModel mediaModel = null; + // DirectItemModel.DirectItemLinkModel linkModel = null; + // DirectItemModel.DirectItemMediaModel directMedia = null; + // DirectItemModel.DirectItemReelShareModel reelShareModel = null; + // DirectItemModel.DirectItemActionLogModel actionLogModel = null; + // DirectItemModel.DirectItemAnimatedMediaModel animatedMediaModel = null; + // DirectItemModel.DirectItemVoiceMediaModel voiceMediaModel = null; + // DirectItemModel.DirectItemRavenMediaModel ravenMediaModel = null; + // DirectItemModel.DirectItemVideoCallEventModel videoCallEventModel = null; // // final DirectItemType itemType = getDirectItemType(itemObject.getString("item_type")); // switch (itemType) { // case ANIMATED_MEDIA: { // final JSONObject animatedMedia = itemObject.getJSONObject("animated_media"); // final JSONObject stickerImage = animatedMedia.getJSONObject("images").getJSONObject("fixed_height"); - // mediaModel = new DirectItemAnimatedMediaModel( + // + // animatedMediaModel = new DirectItemModel.DirectItemAnimatedMediaModel( // animatedMedia.getBoolean("is_random"), // animatedMedia.getBoolean("is_sticker"), // animatedMedia.getString("id"), @@ -330,9 +354,11 @@ public final class ResponseBodyUtils { // stickerImage.getInt("width")); // } // break; + // // case VOICE_MEDIA: { // final JSONObject voiceMedia = itemObject.getJSONObject("voice_media").getJSONObject("media"); // final JSONObject audio = voiceMedia.getJSONObject("audio"); + // // int[] waveformData = null; // final JSONArray waveformDataArray = audio.optJSONArray("waveform_data"); // if (waveformDataArray != null) { @@ -343,35 +369,40 @@ public final class ResponseBodyUtils { // waveformData[j] = (int) (waveformDataArray.optDouble(j) * 10); // } // } - // mediaModel = new DirectItemVoiceMediaModel( + // + // voiceMediaModel = new DirectItemModel.DirectItemVoiceMediaModel( // voiceMedia.getString("id"), // audio.getString("audio_src"), // audio.getLong("duration"), // waveformData); // } // break; + // // case LINK: { // final JSONObject linkObj = itemObject.getJSONObject("link"); - // DirectItemLinkContext itemLinkContext = null; + // + // DirectItemModel.DirectItemLinkContext itemLinkContext = null; // final JSONObject linkContext = linkObj.optJSONObject("link_context"); // if (linkContext != null) { - // itemLinkContext = new DirectItemLinkContext( + // itemLinkContext = new DirectItemModel.DirectItemLinkContext( // linkContext.getString("link_url"), // linkContext.optString("link_title"), // linkContext.optString("link_summary"), // linkContext.optString("link_image_url") // ); // } - // mediaModel = new DirectItemLinkModel( + // + // linkModel = new DirectItemModel.DirectItemLinkModel( // linkObj.getString("text"), // linkObj.getString("client_context"), // linkObj.optString("mutation_token"), // itemLinkContext); // } // break; + // // case REEL_SHARE: { // final JSONObject reelShare = itemObject.getJSONObject("reel_share"); - // mediaModel = new DirectItemReelShareModel( + // reelShareModel = new DirectItemModel.DirectItemReelShareModel( // reelShare.optBoolean("is_reel_persisted"), // reelShare.getLong("reel_owner_id"), // reelShare.getJSONObject("media").getJSONObject("user").getString("username"), @@ -383,34 +414,39 @@ public final class ResponseBodyUtils { // getDirectMediaModel(reelShare.optJSONObject("media"))); // } // break; + // // case RAVEN_MEDIA: { // final JSONObject visualMedia = itemObject.getJSONObject("visual_media"); + // // final JSONArray seenUserIdsArray = visualMedia.getJSONArray("seen_user_ids"); // final int seenUsersLen = seenUserIdsArray.length(); // final String[] seenUserIds = new String[seenUsersLen]; // for (int j = 0; j < seenUsersLen; j++) // seenUserIds[j] = seenUserIdsArray.getString(j); - // RavenExpiringMediaActionSummary expiringSummaryModel = null; + // + // DirectItemModel.RavenExpiringMediaActionSummaryModel expiringSummaryModel = null; // final JSONObject actionSummary = visualMedia.optJSONObject("expiring_media_action_summary"); // if (actionSummary != null) - // expiringSummaryModel = new RavenExpiringMediaActionSummary( + // expiringSummaryModel = new DirectItemModel.RavenExpiringMediaActionSummaryModel( // actionSummary.getLong("timestamp"), actionSummary.getInt("count"), // getExpiringMediaType(actionSummary.getString("type"))); - // final RavenMediaViewMode viewType; + // + // final RavenMediaViewType viewType; // final String viewMode = visualMedia.getString("view_mode"); // switch (viewMode) { // case "replayable": - // viewType = RavenMediaViewMode.REPLAYABLE; + // viewType = RavenMediaViewType.REPLAYABLE; // break; // case "permanent": - // viewType = RavenMediaViewMode.PERMANENT; + // viewType = RavenMediaViewType.PERMANENT; // break; // case "once": // default: - // viewType = RavenMediaViewMode.ONCE; + // viewType = RavenMediaViewType.ONCE; // } - // mediaModel = new DirectItemRavenMediaModel( - // visualMedia.optLong(viewType == RavenMediaViewMode.PERMANENT ? "url_expire_at_secs" : "replay_expiring_at_us"), + // + // ravenMediaModel = new DirectItemModel.DirectItemRavenMediaModel( + // visualMedia.optLong(viewType == RavenMediaViewType.PERMANENT ? "url_expire_at_secs" : "replay_expiring_at_us"), // visualMedia.optInt("playback_duration_secs"), // visualMedia.getInt("seen_count"), // seenUserIds, @@ -420,14 +456,16 @@ public final class ResponseBodyUtils { // // } // break; + // // case VIDEO_CALL_EVENT: { // final JSONObject videoCallEvent = itemObject.getJSONObject("video_call_event"); - // mediaModel = new DirectItemVideoCallEventModel(videoCallEvent.getLong("vc_id"), - // videoCallEvent.optBoolean("thread_has_audio_only_call"), - // videoCallEvent.getString("action"), - // videoCallEvent.getString("description")); + // videoCallEventModel = new DirectItemModel.DirectItemVideoCallEventModel(videoCallEvent.optLong("vc_id"), + // videoCallEvent.optBoolean("thread_has_audio_only_call"), + // videoCallEvent.getString("action"), + // videoCallEvent.getString("description")); // } // break; + // // case PROFILE: { // final JSONObject profile = itemObject.getJSONObject("profile"); // profileModel = new ProfileModel(profile.getBoolean("is_private"), @@ -438,13 +476,15 @@ public final class ResponseBodyUtils { // profile.getString("full_name"), // null, null, // profile.getString("profile_pic_url"), - // null, 0, 0, 0, false, false, false, false); + // null, 0, 0, 0, false, false, false, false, false); // } // break; + // // case PLACEHOLDER: // final JSONObject placeholder = itemObject.getJSONObject("placeholder"); // text = placeholder.getString("title") + "
" + placeholder.getString("message") + ""; // break; + // // case ACTION_LOG: // if (inThreadView && itemObject.optInt("hide_in_thread", 0) != 0) // continue; @@ -457,29 +497,35 @@ public final class ResponseBodyUtils { // + desc.substring(boldItem.getInt("start") + q * 7, boldItem.getInt("end") + q * 7) // + "" + desc.substring(boldItem.getInt("end") + q * 7); // } - // mediaModel = new DirectItemActionLogModel(desc); + // actionLogModel = new DirectItemModel.DirectItemActionLogModel(desc); // break; + // // case MEDIA_SHARE: - // mediaModel = getDirectMediaModel(itemObject.getJSONObject("media_share")); + // directMedia = getDirectMediaModel(itemObject.getJSONObject("media_share")); // break; + // // case CLIP: - // mediaModel = getDirectMediaModel(itemObject.getJSONObject("clip").getJSONObject("clip")); + // directMedia = getDirectMediaModel(itemObject.getJSONObject("clip").getJSONObject("clip")); // break; + // // case FELIX_SHARE: - // mediaModel = getDirectMediaModel(itemObject.getJSONObject("felix_share").getJSONObject("video")); + // directMedia = getDirectMediaModel(itemObject.getJSONObject("felix_share").getJSONObject("video")); // break; + // // case MEDIA: - // mediaModel = getDirectMediaModel(itemObject.optJSONObject("media")); + // directMedia = getDirectMediaModel(itemObject.optJSONObject("media")); // break; + // // case LIKE: // text = itemObject.getString("like"); // break; + // // case STORY_SHARE: // final JSONObject storyShare = itemObject.getJSONObject("story_share"); // if (!storyShare.has("media")) // text = "" + storyShare.optString("message") + ""; // else { - // mediaModel = new DirectItemReelShareModel( + // reelShareModel = new DirectItemModel.DirectItemReelShareModel( // storyShare.optBoolean("is_reel_persisted"), // storyShare.getJSONObject("media").getJSONObject("user").getLong("pk"), // storyShare.getJSONObject("media").getJSONObject("user").getString("username"), @@ -491,6 +537,7 @@ public final class ResponseBodyUtils { // getDirectMediaModel(storyShare.optJSONObject("media"))); // } // break; + // // case TEXT: // if (!itemObject.has("text")) // Log.d("AWAISKING_APP", "itemObject: " + itemObject); // todo @@ -514,49 +561,40 @@ public final class ResponseBodyUtils { // liked, // itemType, // text, + // linkModel, // profileModel, - // mediaModel)); + // reelShareModel, + // directMedia, + // actionLogModel, + // voiceMediaModel, + // ravenMediaModel, + // videoCallEventModel, + // animatedMediaModel)); // } // - // return new InboxThreadModel(readState, - // data.getString("thread_id"), - // data.getString("thread_v2_id"), - // data.getString("thread_type"), - // data.getString("thread_title"), - // data.optString("newest_cursor"), - // data.optString("oldest_cursor"), - // data.has("next_cursor") ? data.getString("next_cursor") : null, - // data.has("prev_cursor") ? data.getString("prev_cursor") : null, + // itemModels.trimToSize(); + // + // return new InboxThreadModel(readState, threadId, threadV2Id, threadType, threadTitle, + // threadNewestCursor, threadOldestCursor, threadNextCursor, threadPrevCursor, // null, // todo - // users, - // leftUsers, - // adminIDs, - // itemModels, - // data.optBoolean("muted"), - // data.optBoolean("is_pin"), - // data.optBoolean("named"), - // data.optBoolean("canonical"), - // data.optBoolean("pending"), - // data.getBoolean("has_older"), - // data.optLong("read_state", 0), - // data.optBoolean("is_spam"), - // data.optBoolean("is_group"), - // data.optBoolean("archived"), - // data.optLong("last_activity_at")); + // userModels, leftuserModels, adminIDs, + // itemModels.toArray(new DirectItemModel[0]), + // muted, isPin, named, canonical, + // pending, threadHasOlder, unreadCount, isSpam, isGroup, archived, lastActivityAt); // } - - // private static ActionType getExpiringMediaType(final String type) { - // if ("raven_sent".equals(type)) return ActionType.SENT; - // if ("raven_opened".equals(type)) return ActionType.OPENED; - // if ("raven_blocked".equals(type)) return ActionType.BLOCKED; - // if ("raven_sending".equals(type)) return ActionType.SENDING; - // if ("raven_replayed".equals(type)) return ActionType.REPLAYED; - // if ("raven_delivered".equals(type)) return ActionType.DELIVERED; - // if ("raven_suggested".equals(type)) return ActionType.SUGGESTED; - // if ("raven_screenshot".equals(type)) return ActionType.SCREENSHOT; - // if ("raven_cannot_deliver".equals(type)) return ActionType.CANNOT_DELIVER; + // + // private static RavenExpiringMediaType getExpiringMediaType(final String type) { + // if ("raven_sent".equals(type)) return RavenExpiringMediaType.RAVEN_SENT; + // if ("raven_opened".equals(type)) return RavenExpiringMediaType.RAVEN_OPENED; + // if ("raven_blocked".equals(type)) return RavenExpiringMediaType.RAVEN_BLOCKED; + // if ("raven_sending".equals(type)) return RavenExpiringMediaType.RAVEN_SENDING; + // if ("raven_replayed".equals(type)) return RavenExpiringMediaType.RAVEN_REPLAYED; + // if ("raven_delivered".equals(type)) return RavenExpiringMediaType.RAVEN_DELIVERED; + // if ("raven_suggested".equals(type)) return RavenExpiringMediaType.RAVEN_SUGGESTED; + // if ("raven_screenshot".equals(type)) return RavenExpiringMediaType.RAVEN_SCREENSHOT; + // if ("raven_cannot_deliver".equals(type)) return RavenExpiringMediaType.RAVEN_CANNOT_DELIVER; // //if ("raven_unknown".equals(type)) [default?] - // return ActionType.UNKNOWN; + // return RavenExpiringMediaType.RAVEN_UNKNOWN; // } public static FeedModel parseItem(final JSONObject itemJson) throws JSONException { @@ -590,6 +628,7 @@ public final class ResponseBodyUtils { 0, 0, following, + false, restricted, false, requested); @@ -606,7 +645,8 @@ public final class ResponseBodyUtils { .setPostId(itemJson.getString(Constants.EXTRAS_ID)) .setThumbnailUrl(mediaType != MediaItemType.MEDIA_TYPE_SLIDER ? ResponseBodyUtils.getLowQualityImage(itemJson) : null) .setShortCode(itemJson.getString("code")) - .setPostCaption(captionJson != null ? captionJson.optString("text") : null) + .setPostCaption(captionJson != null ? captionJson.optString("text") : "") + .setCaptionId(captionJson != null ? captionJson.optString("pk") : null) .setCommentsCount(itemJson.optInt("comment_count")) .setTimestamp(itemJson.optLong("taken_at", -1)) .setLiked(itemJson.optBoolean("has_liked")) @@ -627,7 +667,9 @@ public final class ResponseBodyUtils { break; case MEDIA_TYPE_SLIDER: final List childPosts = getChildPosts(itemJson); - feedModelBuilder.setSliderItems(childPosts); + feedModelBuilder.setSliderItems(childPosts) + .setImageHeight(childPosts.get(0).getHeight()) + .setImageWidth(childPosts.get(0).getWidth()); break; } return feedModelBuilder.build(); @@ -673,6 +715,7 @@ public final class ResponseBodyUtils { false, false, false, + false, false); } JSONObject tempJsonObject = feedItem.optJSONObject("edge_media_preview_comment"); @@ -743,7 +786,9 @@ public final class ResponseBodyUtils { final JSONArray children = sidecar.optJSONArray("edges"); if (children != null) { final List sliderItems = getSliderItems(children); - feedModelBuilder.setSliderItems(sliderItems); + feedModelBuilder.setSliderItems(sliderItems) + .setImageHeight(sliderItems.get(0).getHeight()) + .setImageWidth(sliderItems.get(0).getWidth()); } } } @@ -832,6 +877,134 @@ public final class ResponseBodyUtils { return sliderItems; } + public static StoryModel parseStoryItem(final JSONObject data, + final boolean isLoc, + final boolean isHashtag, + final String localUsername) throws JSONException { + final boolean isVideo = data.has("video_duration"); + final StoryModel model = new StoryModel(data.getString("id"), + data.getJSONObject("image_versions2").getJSONArray("candidates").getJSONObject(0) + .getString("url"), null, + isVideo ? MediaItemType.MEDIA_TYPE_VIDEO : MediaItemType.MEDIA_TYPE_IMAGE, + data.optLong("taken_at", 0), + (isLoc || isHashtag) + ? data.getJSONObject("user").getString("username") + : localUsername, + data.getJSONObject("user").getString("pk"), + data.optBoolean("can_reply")); + + if (data.getJSONObject("image_versions2").getJSONArray("candidates").length() > 1) { + model.setThumbnail(data.getJSONObject("image_versions2").getJSONArray("candidates").getJSONObject(1) + .getString("url")); + } + + final JSONArray videoResources = data.optJSONArray("video_versions"); + if (isVideo && videoResources != null) + model.setVideoUrl(ResponseBodyUtils.getHighQualityPost(videoResources, true, true, false)); + + if (data.has("story_feed_media")) { + model.setTappableShortCode(data.getJSONArray("story_feed_media").getJSONObject(0).optString("media_code")); + } + + // TODO: this may not be limited to spotify + if (!data.isNull("story_app_attribution")) + model.setSpotify(data.getJSONObject("story_app_attribution").optString("content_url").split("\\?")[0]); + + if (data.has("story_polls")) { + final JSONArray storyPolls = data.optJSONArray("story_polls"); + JSONObject tappableObject = null; + if (storyPolls != null) { + tappableObject = storyPolls.getJSONObject(0).optJSONObject("poll_sticker"); + } + if (tappableObject != null) model.setPoll(new PollModel( + String.valueOf(tappableObject.getLong("poll_id")), + tappableObject.getString("question"), + tappableObject.getJSONArray("tallies").getJSONObject(0).getString("text"), + tappableObject.getJSONArray("tallies").getJSONObject(0).getInt("count"), + tappableObject.getJSONArray("tallies").getJSONObject(1).getString("text"), + tappableObject.getJSONArray("tallies").getJSONObject(1).getInt("count"), + tappableObject.optInt("viewer_vote", -1) + )); + } + if (data.has("story_questions")) { + final JSONObject tappableObject = data.getJSONArray("story_questions").getJSONObject(0) + .optJSONObject("question_sticker"); + if (tappableObject != null && !tappableObject.getString("question_type").equals("music")) + model.setQuestion(new QuestionModel( + String.valueOf(tappableObject.getLong("question_id")), + tappableObject.getString("question") + )); + } + if (data.has("story_quizs")) { + JSONObject tappableObject = data.getJSONArray("story_quizs").getJSONObject(0).optJSONObject("quiz_sticker"); + if (tappableObject != null) { + String[] choices = new String[tappableObject.getJSONArray("tallies").length()]; + Long[] counts = new Long[choices.length]; + for (int q = 0; q < choices.length; ++q) { + JSONObject tempchoice = tappableObject.getJSONArray("tallies").getJSONObject(q); + choices[q] = (q == tappableObject.getInt("correct_answer") ? "*** " : "") + + tempchoice.getString("text"); + counts[q] = tempchoice.getLong("count"); + } + model.setQuiz(new QuizModel( + String.valueOf(tappableObject.getLong("quiz_id")), + tappableObject.getString("question"), + choices, + counts, + tappableObject.optInt("viewer_answer", -1) + )); + } + } + if (data.has("story_cta") && data.has("link_text")) { + JSONObject tappableObject = data.getJSONArray("story_cta").getJSONObject(0).getJSONArray("links").getJSONObject(0); + String swipeUpUrl = tappableObject.getString("webUri"); + if (swipeUpUrl.startsWith("http")) { + model.setSwipeUp(new SwipeUpModel(swipeUpUrl, data.getString("link_text"))); + } + } + if (data.has("story_sliders")) { + final JSONObject tappableObject = data.getJSONArray("story_sliders").getJSONObject(0) + .optJSONObject("slider_sticker"); + if (tappableObject != null) + model.setSlider(new SliderModel( + String.valueOf(tappableObject.getLong("slider_id")), + tappableObject.getString("question"), + tappableObject.getString("emoji"), + tappableObject.getBoolean("viewer_can_vote"), + tappableObject.getDouble("slider_vote_average"), + tappableObject.getInt("slider_vote_count"), + tappableObject.optDouble("viewer_vote") + )); + } + JSONArray hashtags = data.optJSONArray("story_hashtags"); + JSONArray locations = data.optJSONArray("story_locations"); + JSONArray atmarks = data.optJSONArray("reel_mentions"); + String[] mentions = new String[(hashtags == null ? 0 : hashtags.length()) + + (atmarks == null ? 0 : atmarks.length()) + + (locations == null ? 0 : locations.length())]; + if (hashtags != null) { + for (int h = 0; h < hashtags.length(); ++h) { + mentions[h] = "#" + hashtags.getJSONObject(h).getJSONObject("hashtag").getString("name"); + } + } + if (atmarks != null) { + for (int h = 0; h < atmarks.length(); ++h) { + mentions[h + (hashtags == null ? 0 : hashtags.length())] = + "@" + atmarks.getJSONObject(h).getJSONObject("user").getString("username"); + } + } + if (locations != null) { + for (int h = 0; h < locations.length(); ++h) { + mentions[h + (hashtags == null ? 0 : hashtags.length()) + (atmarks == null ? 0 : atmarks.length())] = + locations.getJSONObject(h).getJSONObject("location").getString("short_name") + + " (" + locations.getJSONObject(h).getJSONObject("location").getLong("pk") + ")"; + } + } + if (mentions.length != 0) model.setMentions(mentions); + + return model; + } + public static String getThumbUrl(final ImageVersions2 imageVersions2) { if (imageVersions2 == null) return null; final List candidates = imageVersions2.getCandidates(); diff --git a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java index 4e380af9..eb43d3b2 100755 --- a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java +++ b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java @@ -41,6 +41,7 @@ import static awais.instagrabber.utils.Constants.PREF_TOPIC_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; +import static awais.instagrabber.utils.Constants.STORY_SORT; import static awais.instagrabber.utils.Constants.SWAP_DATE_TIME_FORMAT_ENABLED; public final class SettingsHelper { @@ -123,7 +124,7 @@ public final class SettingsHelper { {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, PREF_PROFILE_POSTS_LAYOUT, PREF_TOPIC_POSTS_LAYOUT, PREF_HASHTAG_POSTS_LAYOUT, PREF_LOCATION_POSTS_LAYOUT, - PREF_LIKED_POSTS_LAYOUT, PREF_TAGGED_POSTS_LAYOUT, PREF_SAVED_POSTS_LAYOUT, PREF_EMOJI_VARIANTS}) + PREF_LIKED_POSTS_LAYOUT, PREF_TAGGED_POSTS_LAYOUT, PREF_SAVED_POSTS_LAYOUT, STORY_SORT, PREF_EMOJI_VARIANTS}) public @interface StringSettings {} @StringDef({DOWNLOAD_USER_FOLDER, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, SHOW_QUICK_ACCESS_DIALOG, MUTED_VIDEOS, diff --git a/app/src/main/java/awais/instagrabber/utils/Utils.java b/app/src/main/java/awais/instagrabber/utils/Utils.java index f6327430..ddcd5d6f 100644 --- a/app/src/main/java/awais/instagrabber/utils/Utils.java +++ b/app/src/main/java/awais/instagrabber/utils/Utils.java @@ -94,12 +94,9 @@ public final class Utils { if (signed == null) { return null; } - final String[] parts = signed.split("&"); final Map map = new HashMap<>(); - for (final String part : parts) { - final String[] partSplit = part.split("="); - map.put(partSplit[0], partSplit[1]); - } + map.put("ig_sig_key_version", Constants.SIGNATURE_VERSION); + map.put("signed_body", signed.split("&signed_body=")[1]); return map; } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/ArchivesViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/ArchivesViewModel.java new file mode 100644 index 00000000..18802826 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/viewmodels/ArchivesViewModel.java @@ -0,0 +1,19 @@ +package awais.instagrabber.viewmodels; + +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import java.util.List; + +import awais.instagrabber.models.HighlightModel; + +public class ArchivesViewModel extends ViewModel { + private MutableLiveData> list; + + public MutableLiveData> getList() { + if (list == null) { + list = new MutableLiveData<>(); + } + return list; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/viewmodels/BasePostViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/BasePostViewModel.java index b0f59e64..eb4dc8d2 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/BasePostViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/BasePostViewModel.java @@ -16,4 +16,12 @@ public class BasePostViewModel extends ViewModel { } return list; } + + public void edit(int index, T post) { + + } + + public void delete(int index) { + + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/viewmodels/FeedStoriesViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/FeedStoriesViewModel.java index fd61557d..f4bd3b41 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/FeedStoriesViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/FeedStoriesViewModel.java @@ -3,6 +3,7 @@ package awais.instagrabber.viewmodels; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; +import java.util.ArrayList; import java.util.List; import awais.instagrabber.models.FeedStoryModel; diff --git a/app/src/main/java/awais/instagrabber/viewmodels/FollowViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/FollowViewModel.java new file mode 100644 index 00000000..dcd65d4a --- /dev/null +++ b/app/src/main/java/awais/instagrabber/viewmodels/FollowViewModel.java @@ -0,0 +1,19 @@ +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; + + public MutableLiveData> getList() { + if (list == null) { + list = new MutableLiveData<>(); + } + return list; + } +} diff --git a/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java b/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java index 88ee24a9..9a67f8ed 100644 --- a/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java +++ b/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java @@ -6,16 +6,19 @@ import com.google.common.collect.ImmutableMap; import org.json.JSONArray; +import java.io.UnsupportedEncodingException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.UUID; import awais.instagrabber.repositories.DirectMessagesRepository; import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions; import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions.ThreadIdOrUserIds; import awais.instagrabber.repositories.requests.directmessages.LinkBroadcastOptions; import awais.instagrabber.repositories.requests.directmessages.PhotoBroadcastOptions; +import awais.instagrabber.repositories.requests.directmessages.StoryReplyBroadcastOptions; import awais.instagrabber.repositories.requests.directmessages.TextBroadcastOptions; import awais.instagrabber.repositories.requests.directmessages.VideoBroadcastOptions; import awais.instagrabber.repositories.requests.directmessages.VoiceBroadcastOptions; @@ -146,6 +149,12 @@ public class DirectMessagesService extends BaseService { return broadcast(new VoiceBroadcastOptions(clientContext, threadIdOrUserIds, uploadId, waveform, samplingFreq)); } + public Call broadcastStoryReply(final ThreadIdOrUserIds threadIdOrUserIds, + final String text, + final String mediaId, final String reelId) throws UnsupportedEncodingException { + return broadcast(new StoryReplyBroadcastOptions(UUID.randomUUID().toString(), threadIdOrUserIds, text, mediaId, reelId)); + } + private Call broadcast(@NonNull final BroadcastOptions broadcastOptions) { if (TextUtils.isEmpty(broadcastOptions.getClientContext())) { throw new IllegalArgumentException("Broadcast requires a valid client context value"); diff --git a/app/src/main/java/awais/instagrabber/webservices/DiscoverService.java b/app/src/main/java/awais/instagrabber/webservices/DiscoverService.java index 1f542694..84d7f305 100644 --- a/app/src/main/java/awais/instagrabber/webservices/DiscoverService.java +++ b/app/src/main/java/awais/instagrabber/webservices/DiscoverService.java @@ -165,6 +165,7 @@ public class DiscoverService extends BaseService { false, false, false, + false, false); } final String resourceUrl = ResponseBodyUtils.getHighQualityImage(coverMediaJson); diff --git a/app/src/main/java/awais/instagrabber/webservices/FeedService.java b/app/src/main/java/awais/instagrabber/webservices/FeedService.java index d4e84e35..584d5e14 100644 --- a/app/src/main/java/awais/instagrabber/webservices/FeedService.java +++ b/app/src/main/java/awais/instagrabber/webservices/FeedService.java @@ -19,6 +19,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.TimeZone; +import java.util.UUID; import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.PostChild; @@ -36,7 +38,6 @@ import retrofit2.Retrofit; public class FeedService extends BaseService { private static final String TAG = "FeedService"; - private static final boolean loadFromMock = false; private final FeedRepository repository; @@ -44,7 +45,7 @@ public class FeedService extends BaseService { private FeedService() { final Retrofit retrofit = getRetrofitBuilder() - .baseUrl("https://www.instagram.com") + .baseUrl("https://i.instagram.com") .build(); repository = retrofit.create(FeedRepository.class); } @@ -56,41 +57,26 @@ public class FeedService extends BaseService { return instance; } - public void fetch(final int maxItemsToLoad, + public void fetch(final String csrfToken, final String cursor, final ServiceCallback callback) { - if (loadFromMock) { - final Handler handler = new Handler(); - handler.postDelayed(() -> { - final ClassLoader classLoader = getClass().getClassLoader(); - if (classLoader == null) { - Log.e(TAG, "fetch: classLoader is null!"); - return; - } - try (InputStream resourceAsStream = classLoader.getResourceAsStream("feed_response.json"); - Reader in = new InputStreamReader(resourceAsStream, StandardCharsets.UTF_8)) { - final int bufferSize = 1024; - final char[] buffer = new char[bufferSize]; - final StringBuilder out = new StringBuilder(); - int charsRead; - while ((charsRead = in.read(buffer, 0, buffer.length)) > 0) { - out.append(buffer, 0, charsRead); - } - callback.onSuccess(parseResponseBody(out.toString())); - } catch (IOException | JSONException e) { - Log.e(TAG, "fetch: ", e); - } - }, 1000); - return; + final Map form = new HashMap<>(); + form.put("_uuid", UUID.randomUUID().toString()); + form.put("_csrftoken", csrfToken); + form.put("phone_id", UUID.randomUUID().toString()); + form.put("device_id", UUID.randomUUID().toString()); + form.put("client_session_id", UUID.randomUUID().toString()); + form.put("is_prefetch", "0"); + form.put("timezone_offset", String.valueOf(TimeZone.getDefault().getRawOffset() / 1000)); + if (!TextUtils.isEmpty(cursor)) { + form.put("max_id", cursor); + form.put("reason", "pagination"); } - final Map queryMap = new HashMap<>(); - queryMap.put("query_hash", "c699b185975935ae2a457f24075de8c7"); - queryMap.put("variables", "{" + - "\"fetch_media_item_count\":" + maxItemsToLoad + "," + - "\"fetch_like\":3,\"has_stories\":false,\"has_stories\":false,\"has_threaded_comments\":true," + - "\"fetch_media_item_cursor\":\"" + (cursor == null ? "" : cursor) + "\"" + - "}"); - final Call request = repository.fetch(queryMap); + else { + form.put("is_pull_to_refresh", "1"); + form.put("reason", "pull_to_refresh"); + } + final Call request = repository.fetch(form); request.enqueue(new Callback() { @Override public void onResponse(@NonNull final Call call, @NonNull final Response response) { @@ -130,35 +116,45 @@ public class FeedService extends BaseService { @NonNull private PostsFetchResponse parseResponseBody(@NonNull final String body) throws JSONException { + final JSONObject root = new JSONObject(body); + final boolean moreAvailable = root.optBoolean("more_available"); + String nextMaxId = root.optString("next_max_id"); + final boolean needNewMaxId = nextMaxId.equals("feed_recs_head_load"); + final JSONArray feedItems = root.optJSONArray("items"); final List feedModels = new ArrayList<>(); - final JSONObject timelineFeed = new JSONObject(body) - .getJSONObject("data") - .getJSONObject(Constants.EXTRAS_USER) - .getJSONObject("edge_web_feed_timeline"); - final String endCursor; - final boolean hasNextPage; - - final JSONObject pageInfo = timelineFeed.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 feedItems = timelineFeed.getJSONArray("edges"); - for (int i = 0; i < feedItems.length(); ++i) { final JSONObject itemJson = feedItems.optJSONObject(i); - if (itemJson == null) { + if (itemJson == null || itemJson.has("injected")) { continue; } - final FeedModel feedModel = ResponseBodyUtils.parseGraphQLItem(itemJson); - if (feedModel != null) { - feedModels.add(feedModel); + else if (itemJson.has("end_of_feed_demarcator") && needNewMaxId) { + final JSONArray groups = itemJson.getJSONObject("end_of_feed_demarcator").getJSONObject("group_set").getJSONArray("groups"); + for (int j = 0; j < groups.length(); ++j) { + final JSONObject groupJson = groups.optJSONObject(j); + if (groupJson.getString("id").equals("past_posts")) { + nextMaxId = groupJson.optString("next_max_id"); + final JSONArray miniFeedItems = groupJson.optJSONArray("feed_items"); + for (int k = 0; k < miniFeedItems.length(); ++k) { + final JSONObject miniItemJson = miniFeedItems.optJSONObject(k); + if (miniItemJson == null || miniItemJson.has("injected")) { + continue; + } + final FeedModel feedModel = ResponseBodyUtils.parseItem(miniItemJson); + if (feedModel != null) { + feedModels.add(feedModel); + } + } + } + else continue; + } + } + else { + final FeedModel feedModel = ResponseBodyUtils.parseItem(itemJson); + if (feedModel != null) { + feedModels.add(feedModel); + } } } - return new PostsFetchResponse(feedModels, hasNextPage, endCursor); + return new PostsFetchResponse(feedModels, moreAvailable, nextMaxId); } -} +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/webservices/FriendshipService.java b/app/src/main/java/awais/instagrabber/webservices/FriendshipService.java index a370b7df..a31eb3b6 100644 --- a/app/src/main/java/awais/instagrabber/webservices/FriendshipService.java +++ b/app/src/main/java/awais/instagrabber/webservices/FriendshipService.java @@ -153,13 +153,12 @@ public class FriendshipService extends BaseService { }); } - // log in required public void getList(final boolean follower, final String targetUserId, final String maxId, final ServiceCallback callback) { final Map queryMap = new HashMap<>(); - queryMap.put("max_id", maxId == null ? "" : maxId); + if (maxId != null) queryMap.put("max_id", maxId); final Call request = repository.getList(Constants.I_USER_AGENT, targetUserId, follower ? "followers" : "following", @@ -173,7 +172,6 @@ public class FriendshipService extends BaseService { } final String body = response.body(); if (TextUtils.isEmpty(body)) { - callback.onSuccess(null); return; } diff --git a/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java b/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java new file mode 100644 index 00000000..3a9c9606 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java @@ -0,0 +1,243 @@ +package awais.instagrabber.webservices; + +import android.os.Handler; +import android.util.Log; + +import androidx.annotation.NonNull; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +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.ProfileModel; +import awais.instagrabber.repositories.GraphQLRepository; +import awais.instagrabber.repositories.responses.GraphQLUserListFetchResponse; +import awais.instagrabber.repositories.responses.PostsFetchResponse; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.ResponseBodyUtils; +import awais.instagrabber.utils.TextUtils; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; + +public class GraphQLService extends BaseService { + private static final String TAG = "GraphQLService"; + private static final boolean loadFromMock = false; + + private final GraphQLRepository repository; + + private static GraphQLService instance; + + private GraphQLService() { + final Retrofit retrofit = getRetrofitBuilder() + .baseUrl("https://www.instagram.com") + .build(); + repository = retrofit.create(GraphQLRepository.class); + } + + public static GraphQLService getInstance() { + if (instance == null) { + instance = new GraphQLService(); + } + return instance; + } + + private void fetch(final String queryHash, + final String variables, + final String arg1, + final String arg2, + final ServiceCallback callback) { + final Map queryMap = new HashMap<>(); + queryMap.put("query_hash", queryHash); + queryMap.put("variables", variables); + final Call request = repository.fetch(queryMap); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + try { + // Log.d(TAG, "onResponse: body: " + response.body()); + final PostsFetchResponse postsFetchResponse = parsePostResponse(response, arg1, arg2); + 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 call, @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + } + + public void fetchLocationPosts(@NonNull final String locationId, + final String maxId, + final ServiceCallback callback) { + fetch("36bd0f2bf5911908de389b8ceaa3be6d", + "{\"id\":\"" + locationId + "\"," + + "\"first\":25," + + "\"after\":\"" + (maxId == null ? "" : maxId) + "\"}", + Constants.EXTRAS_LOCATION, + "edge_location_to_media", + callback); + } + + public void fetchHashtagPosts(@NonNull final String tag, + final String maxId, + final ServiceCallback callback) { + fetch("9b498c08113f1e09617a1703c22b2f32", + "{\"tag_name\":\"" + tag + "\"," + + "\"first\":25," + + "\"after\":\"" + (maxId == null ? "" : maxId) + "\"}", + Constants.EXTRAS_HASHTAG, + "edge_hashtag_to_media", + callback); + } + + public void fetchProfilePosts(@NonNull final String profileId, + final int postsPerPage, + final String maxId, + final ServiceCallback callback) { + fetch("18a7b935ab438c4514b1f742d8fa07a7", + "{\"id\":\"" + profileId + "\"," + + "\"first\":" + postsPerPage + "," + + "\"after\":\"" + (maxId == null ? "" : maxId) + "\"}", + Constants.EXTRAS_USER, + "edge_owner_to_timeline_media", + callback); + } + + public void fetchTaggedPosts(@NonNull final String profileId, + final int postsPerPage, + final String maxId, + final ServiceCallback callback) { + fetch("31fe64d9463cbbe58319dced405c6206", + "{\"id\":\"" + profileId + "\"," + + "\"first\":" + postsPerPage + "," + + "\"after\":\"" + (maxId == null ? "" : maxId) + "\"}", + Constants.EXTRAS_USER, + "edge_user_to_photos_of_you", + callback); + } + + @NonNull + private PostsFetchResponse parsePostResponse(@NonNull final Response response, @NonNull final String arg1, @NonNull final String arg2) 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(response.body(), arg1, arg2); + } + + @NonNull + private PostsFetchResponse parseResponseBody(@NonNull final String body, @NonNull final String arg1, @NonNull final String arg2) + throws JSONException { + final List feedModels = new ArrayList<>(); + final JSONObject timelineFeed = new JSONObject(body) + .getJSONObject("data") + .getJSONObject(arg1) + .getJSONObject(arg2); + final String endCursor; + final boolean hasNextPage; + + final JSONObject pageInfo = timelineFeed.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 feedItems = timelineFeed.getJSONArray("edges"); + + for (int i = 0; i < feedItems.length(); ++i) { + final JSONObject itemJson = feedItems.optJSONObject(i); + if (itemJson == null) { + continue; + } + final FeedModel feedModel = ResponseBodyUtils.parseGraphQLItem(itemJson); + if (feedModel != null) { + feedModels.add(feedModel); + } + } + return new PostsFetchResponse(feedModels, hasNextPage, endCursor); + } + + public void fetchCommentLikers(final String commentId, + final String endCursor, + final ServiceCallback callback) { + final Map queryMap = new HashMap<>(); + queryMap.put("query_hash", "5f0b1f6281e72053cbc07909c8d154ae"); + queryMap.put("variables", "{\"comment_id\":\"" + commentId + "\"," + + "\"first\":30," + + "\"after\":\"" + (endCursor == null ? "" : endCursor) + "\"}"); + final Call request = repository.fetch(queryMap); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + final String rawBody = response.body(); + if (rawBody == null) { + Log.e(TAG, "Error occurred while fetching gql comment likes of "+commentId); + callback.onSuccess(null); + return; + } + try { + final JSONObject body = new JSONObject(rawBody); + final String status = body.getString("status"); + final JSONObject data = body.getJSONObject("data").getJSONObject("comment").getJSONObject("edge_liked_by"); + final JSONObject pageInfo = data.getJSONObject("page_info"); + final String endCursor = pageInfo.getBoolean("has_next_page") ? pageInfo.getString("end_cursor") : null; + final JSONArray users = data.getJSONArray("edges"); + final int usersLen = users.length(); + final List userModels = new ArrayList<>(); + for (int j = 0; j < usersLen; ++j) { + final JSONObject userObject = users.getJSONObject(j).getJSONObject("node"); + userModels.add(new ProfileModel(userObject.optBoolean("is_private"), + false, + userObject.optBoolean("is_verified"), + userObject.getString("id"), + userObject.getString("username"), + userObject.optString("full_name"), + null, null, + userObject.getString("profile_pic_url"), + null, 0, 0, 0, false, false, false, false, false)); + } + callback.onSuccess(new GraphQLUserListFetchResponse(endCursor, status, userModels)); + } catch (JSONException e) { + Log.e(TAG, "onResponse", e); + if (callback != null) { + callback.onFailure(e); + } + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + } +} diff --git a/app/src/main/java/awais/instagrabber/webservices/LocationService.java b/app/src/main/java/awais/instagrabber/webservices/LocationService.java index 241453be..a9b40981 100644 --- a/app/src/main/java/awais/instagrabber/webservices/LocationService.java +++ b/app/src/main/java/awais/instagrabber/webservices/LocationService.java @@ -19,6 +19,7 @@ import java.util.Objects; import awais.instagrabber.models.FeedModel; import awais.instagrabber.repositories.LocationRepository; +import awais.instagrabber.repositories.responses.PostsFetchResponse; import awais.instagrabber.utils.ResponseBodyUtils; import awais.instagrabber.utils.TextUtils; import retrofit2.Call; @@ -29,7 +30,7 @@ import retrofit2.Retrofit; public class LocationService extends BaseService { private static final String TAG = "LocationService"; - private final LocationRepository repository, webRepository; + private final LocationRepository repository; private static LocationService instance; @@ -38,10 +39,6 @@ public class LocationService extends BaseService { .baseUrl("https://i.instagram.com") .build(); repository = retrofit.create(LocationRepository.class); - final Retrofit webRetrofit = getRetrofitBuilder() - .baseUrl("https://www.instagram.com") - .build(); - webRepository = webRetrofit.create(LocationRepository.class); } public static LocationService getInstance() { @@ -53,7 +50,7 @@ public class LocationService extends BaseService { public void fetchPosts(@NonNull final String locationId, final String maxId, - final ServiceCallback callback) { + final ServiceCallback callback) { final ImmutableMap.Builder builder = ImmutableMap.builder(); if (!TextUtils.isEmpty(maxId)) { builder.put("max_id", maxId); @@ -71,7 +68,7 @@ public class LocationService extends BaseService { callback.onSuccess(null); return; } - final LocationPostsFetchResponse tagPostsFetchResponse = parseResponse(body); + final PostsFetchResponse tagPostsFetchResponse = parseResponse(body); callback.onSuccess(tagPostsFetchResponse); } catch (JSONException e) { Log.e(TAG, "onResponse", e); @@ -88,20 +85,16 @@ public class LocationService extends BaseService { }); } - private LocationPostsFetchResponse parseResponse(@NonNull final String body) throws JSONException { + private PostsFetchResponse parseResponse(@NonNull final String body) throws JSONException { final JSONObject root = new JSONObject(body); final boolean moreAvailable = root.optBoolean("more_available"); final String nextMaxId = root.optString("next_max_id"); - final int numResults = root.optInt("num_results"); - final String status = root.optString("status"); final JSONArray itemsJson = root.optJSONArray("items"); final List items = parseItems(itemsJson); - return new LocationPostsFetchResponse( + return new PostsFetchResponse( + items, moreAvailable, - nextMaxId, - numResults, - status, - items + nextMaxId ); } @@ -122,174 +115,4 @@ public class LocationService extends BaseService { } return feedModels; } - - public void fetchGraphQLPosts(@NonNull final String locationId, - final String maxId, - final ServiceCallback callback) { - final Map queryMap = new HashMap<>(); - queryMap.put("query_hash", "36bd0f2bf5911908de389b8ceaa3be6d"); - queryMap.put("variables", "{" + - "\"id\":\"" + locationId + "\"," + - "\"first\":25," + - "\"after\":\"" + (maxId == null ? "" : maxId) + "\"" + - "}"); - final Call request = webRepository.fetchGraphQLPosts(queryMap); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - try { - if (callback == null) { - return; - } - final String body = response.body(); - if (TextUtils.isEmpty(body)) { - callback.onSuccess(null); - return; - } - final LocationPostsFetchResponse tagPostsFetchResponse = parseGraphQLResponse(body); - callback.onSuccess(tagPostsFetchResponse); - } catch (JSONException e) { - Log.e(TAG, "onResponse", e); - callback.onFailure(e); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - private LocationPostsFetchResponse parseGraphQLResponse(@NonNull final String body) throws JSONException { - final JSONObject rootroot = new JSONObject(body); - final JSONObject root = rootroot.getJSONObject("data").getJSONObject("location").getJSONObject("edge_location_to_media"); - final boolean moreAvailable = root.getJSONObject("page_info").optBoolean("has_next_page"); - final String nextMaxId = root.getJSONObject("page_info").optString("end_cursor"); - final int numResults = root.optInt("count"); - final String status = rootroot.optString("status"); - final JSONArray itemsJson = root.optJSONArray("edges"); - final List items = parseGraphQLItems(itemsJson); - return new LocationPostsFetchResponse( - moreAvailable, - nextMaxId, - numResults, - status, - items - ); - } - - private List parseGraphQLItems(final JSONArray items) throws JSONException { - if (items == null) { - return Collections.emptyList(); - } - final List feedModels = new ArrayList<>(); - for (int i = 0; i < items.length(); i++) { - final JSONObject itemJson = items.optJSONObject(i); - if (itemJson == null) { - continue; - } - final FeedModel feedModel = ResponseBodyUtils.parseGraphQLItem(itemJson); - if (feedModel != null) { - feedModels.add(feedModel); - } - } - return feedModels; - } - - public static class LocationPostsFetchResponse { - private boolean moreAvailable; - private String nextMaxId; - private int numResults; - private String status; - private List items; - - public LocationPostsFetchResponse(final boolean moreAvailable, - final String nextMaxId, - final int numResults, - final String status, - final List items) { - this.moreAvailable = moreAvailable; - this.nextMaxId = nextMaxId; - this.numResults = numResults; - this.status = status; - this.items = items; - } - - public boolean isMoreAvailable() { - return moreAvailable; - } - - public LocationPostsFetchResponse setMoreAvailable(final boolean moreAvailable) { - this.moreAvailable = moreAvailable; - return this; - } - - public String getNextMaxId() { - return nextMaxId; - } - - public LocationPostsFetchResponse setNextMaxId(final String nextMaxId) { - this.nextMaxId = nextMaxId; - return this; - } - - public int getNumResults() { - return numResults; - } - - public LocationPostsFetchResponse setNumResults(final int numResults) { - this.numResults = numResults; - return this; - } - - public String getStatus() { - return status; - } - - public LocationPostsFetchResponse setStatus(final String status) { - this.status = status; - return this; - } - - public List getItems() { - return items; - } - - public LocationPostsFetchResponse setItems(final List items) { - this.items = items; - return this; - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final LocationPostsFetchResponse that = (LocationPostsFetchResponse) o; - return moreAvailable == that.moreAvailable && - numResults == that.numResults && - Objects.equals(nextMaxId, that.nextMaxId) && - Objects.equals(status, that.status) && - Objects.equals(items, that.items); - } - - @Override - public int hashCode() { - return Objects.hash(moreAvailable, nextMaxId, numResults, status, items); - } - - @NonNull - @Override - public String toString() { - return "LocationPostsFetchResponse{" + - "moreAvailable=" + moreAvailable + - ", nextMaxId='" + nextMaxId + '\'' + - ", numResults=" + numResults + - ", status='" + status + '\'' + - ", items=" + items + - '}'; - } - } } diff --git a/app/src/main/java/awais/instagrabber/webservices/MediaService.java b/app/src/main/java/awais/instagrabber/webservices/MediaService.java index 035938f9..236113ec 100644 --- a/app/src/main/java/awais/instagrabber/webservices/MediaService.java +++ b/app/src/main/java/awais/instagrabber/webservices/MediaService.java @@ -7,20 +7,25 @@ import androidx.annotation.NonNull; import com.google.common.collect.ImmutableMap; +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 java.util.UUID; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.ProfileModel; import awais.instagrabber.repositories.MediaRepository; import awais.instagrabber.repositories.requests.UploadFinishOptions; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.DateUtils; import awais.instagrabber.utils.MediaUploadHelper; +import awais.instagrabber.utils.ResponseBodyUtils; import awais.instagrabber.utils.Utils; import retrofit2.Call; import retrofit2.Callback; @@ -48,6 +53,37 @@ public class MediaService extends BaseService { return instance; } + public void fetch(final String mediaId, + final ServiceCallback callback) { + final Call request = repository.fetch(mediaId); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (callback == null) return; + final String body = response.body(); + if (body == null) { + callback.onSuccess(null); + return; + } + try { + final JSONObject itemJson = new JSONObject(body).getJSONArray("items").getJSONObject(0); + callback.onSuccess(ResponseBodyUtils.parseItem(itemJson)); + } catch (JSONException e) { + callback.onFailure(e); + } + } + + @Override + public void onFailure(@NonNull final Call call, + @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + } + public void like(final String mediaId, final String userId, final String csrfToken, @@ -88,7 +124,7 @@ public class MediaService extends BaseService { form.put("_uuid", UUID.randomUUID().toString()); // form.put("radio_type", "wifi-none"); final Map signedForm = Utils.sign(form); - final Call request = repository.action(Constants.I_USER_AGENT, action, mediaId, signedForm); + final Call request = repository.action(action, mediaId, signedForm); request.enqueue(new Callback() { @Override public void onResponse(@NonNull final Call call, @@ -137,7 +173,7 @@ public class MediaService extends BaseService { form.put("replied_to_comment_id", replyToCommentId); } final Map signedForm = Utils.sign(form); - final Call commentRequest = repository.comment(Constants.I_USER_AGENT, mediaId, signedForm); + final Call commentRequest = repository.comment(mediaId, signedForm); commentRequest.enqueue(new Callback() { @Override public void onResponse(@NonNull final Call call, @NonNull final Response response) { @@ -183,7 +219,7 @@ public class MediaService extends BaseService { form.put("_uid", userId); form.put("_uuid", UUID.randomUUID().toString()); final Map signedForm = Utils.sign(form); - final Call bulkDeleteRequest = repository.commentsBulkDelete(Constants.USER_AGENT, mediaId, signedForm); + final Call bulkDeleteRequest = repository.commentsBulkDelete(mediaId, signedForm); bulkDeleteRequest.enqueue(new Callback() { @Override public void onResponse(@NonNull final Call call, @NonNull final Response response) { @@ -219,7 +255,7 @@ public class MediaService extends BaseService { // form.put("_uid", userId); // form.put("_uuid", UUID.randomUUID().toString()); final Map signedForm = Utils.sign(form); - final Call commentLikeRequest = repository.commentLike(Constants.USER_AGENT, commentId, signedForm); + final Call commentLikeRequest = repository.commentLike(commentId, signedForm); commentLikeRequest.enqueue(new Callback() { @Override public void onResponse(@NonNull final Call call, @NonNull final Response response) { @@ -255,7 +291,7 @@ public class MediaService extends BaseService { // form.put("_uid", userId); // form.put("_uuid", UUID.randomUUID().toString()); final Map signedForm = Utils.sign(form); - final Call commentUnlikeRequest = repository.commentUnlike(Constants.USER_AGENT, commentId, signedForm); + final Call commentUnlikeRequest = repository.commentUnlike(commentId, signedForm); commentUnlikeRequest.enqueue(new Callback() { @Override public void onResponse(@NonNull final Call call, @NonNull final Response response) { @@ -283,6 +319,126 @@ public class MediaService extends BaseService { }); } + public void editCaption(final String postId, + final String userId, + final String newCaption, + @NonNull final String csrfToken, + @NonNull final ServiceCallback callback) { + final Map form = new HashMap<>(); + form.put("_csrftoken", csrfToken); + form.put("_uid", userId); + form.put("_uuid", UUID.randomUUID().toString()); + form.put("igtv_feed_preview", "false"); + form.put("media_id", postId); + form.put("caption_text", newCaption); + final Map signedForm = Utils.sign(form); + final Call request = repository.editCaption(postId, signedForm); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + final String body = response.body(); + if (body == null) { + Log.e(TAG, "Error occurred while editing caption"); + callback.onSuccess(false); + return; + } + try { + final JSONObject jsonObject = new JSONObject(body); + final String status = jsonObject.optString("status"); + callback.onSuccess(status.equals("ok")); + } catch (JSONException e) { + // Log.e(TAG, "Error parsing body", e); + callback.onFailure(e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "Error editing caption", t); + callback.onFailure(t); + } + }); + } + + public void fetchLikes(final String mediaId, + final boolean isComment, + @NonNull final ServiceCallback> callback) { + final Call likesRequest = repository.fetchLikes(mediaId, isComment ? "comment_likers" : "likers"); + likesRequest.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + final String body = response.body(); + if (body == null) { + Log.e(TAG, "Error occurred while fetching likes of "+mediaId); + callback.onSuccess(null); + return; + } + try { + final JSONObject data = new JSONObject(body); + final JSONArray users = data.getJSONArray("users"); + final int usersLen = users.length(); + final List userModels = new ArrayList<>(); + for (int j = 0; j < usersLen; ++j) { + final JSONObject userObject = users.getJSONObject(j); + userModels.add(new ProfileModel(userObject.optBoolean("is_private"), + false, + userObject.optBoolean("is_verified"), + String.valueOf(userObject.get("pk")), + userObject.getString("username"), + userObject.optString("full_name"), + null, null, + userObject.getString("profile_pic_url"), + null, 0, 0, 0, false, false, false, false, false)); + } + callback.onSuccess(userModels); + } catch (JSONException e) { + // Log.e(TAG, "Error parsing body", e); + callback.onFailure(e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "Error getting likes", t); + callback.onFailure(t); + } + }); + } + + public void translate(final String id, + final String type, // 1 caption 2 comment 3 bio + @NonNull final ServiceCallback callback) { + final Map form = new HashMap<>(); + form.put("id", id); + form.put("type", type); + final Call request = repository.translate(form); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + final String body = response.body(); + if (body == null) { + Log.e(TAG, "Error occurred while translating"); + callback.onSuccess(null); + return; + } + try { + final JSONObject jsonObject = new JSONObject(body); + final String translation = jsonObject.optString("translation"); + callback.onSuccess(translation); + } catch (JSONException e) { + // Log.e(TAG, "Error parsing body", e); + callback.onFailure(e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "Error translating", t); + callback.onFailure(t); + } + }); + } + public Call uploadFinish(final long userId, @NonNull final String csrfToken, @NonNull final UploadFinishOptions options) { diff --git a/app/src/main/java/awais/instagrabber/webservices/NewsService.java b/app/src/main/java/awais/instagrabber/webservices/NewsService.java index af56f058..3e2a37fa 100644 --- a/app/src/main/java/awais/instagrabber/webservices/NewsService.java +++ b/app/src/main/java/awais/instagrabber/webservices/NewsService.java @@ -1,14 +1,26 @@ package awais.instagrabber.webservices; +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.HashMap; +import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import awais.instagrabber.BuildConfig; +import awais.instagrabber.models.NotificationModel; +import awais.instagrabber.models.enums.NotificationType; import awais.instagrabber.repositories.NewsRepository; +import awais.instagrabber.utils.Constants; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -35,25 +47,34 @@ public class NewsService extends BaseService { return instance; } - public void markChecked(final String timestamp, - final String csrfToken, - final ServiceCallback callback) { - final Map map = new HashMap<>(); - map.put("timestamp", timestamp); - final Call request = repository.markChecked(csrfToken, map); + public void fetchAppInbox(final boolean markAsSeen, + final ServiceCallback> callback) { + final List result = new ArrayList<>(); + final Call request = repository.appInbox(markAsSeen); request.enqueue(new Callback() { @Override public void onResponse(@NonNull final Call call, @NonNull final Response response) { final String body = response.body(); if (body == null) { - callback.onSuccess(false); + callback.onSuccess(null); return; } try { final JSONObject jsonObject = new JSONObject(body); - final String status = jsonObject.optString("status"); - callback.onSuccess(status.equals("ok")); + final JSONArray oldStories = jsonObject.getJSONArray("old_stories"), + newStories = jsonObject.getJSONArray("new_stories"); + for (int j = 0; j < newStories.length(); ++j) { + final NotificationModel newsItem = parseNewsItem(newStories.getJSONObject(j)); + if (newsItem != null) result.add(newsItem); + } + + for (int i = 0; i < oldStories.length(); ++i) { + final NotificationModel newsItem = parseNewsItem(oldStories.getJSONObject(i)); + if (newsItem != null) result.add(newsItem); + } + + callback.onSuccess(result); } catch (JSONException e) { callback.onFailure(e); } @@ -66,4 +87,175 @@ public class NewsService extends BaseService { } }); } + + public void fetchWebInbox(final boolean markAsSeen, + final ServiceCallback> callback) { + final Call request = repository.webInbox(); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + final String body = response.body(); + if (body == null) { + callback.onSuccess(null); + return; + } + try { + final List result = new ArrayList<>(); + final JSONObject page = new JSONObject(body) + .getJSONObject("graphql") + .getJSONObject("user"); + final JSONObject ewaf = page.getJSONObject("activity_feed") + .optJSONObject("edge_web_activity_feed"); + final JSONObject efr = page.optJSONObject("edge_follow_requests"); + JSONObject data; + JSONArray media; + if (ewaf != null + && (media = ewaf.optJSONArray("edges")) != null + && media.length() > 0 + && media.optJSONObject(0).optJSONObject("node") != null) { + for (int i = 0; i < media.length(); ++i) { + data = media.optJSONObject(i).optJSONObject("node"); + if (data == null) continue; + final String type = data.getString("__typename"); + final NotificationType notificationType = NotificationType.valueOfType(type); + if (notificationType == null) continue; + final JSONObject user = data.getJSONObject("user"); + result.add(new NotificationModel( + data.getString(Constants.EXTRAS_ID), + data.optString("text"), // comments or mentions + data.getLong("timestamp"), + user.getString("id"), + user.getString("username"), + user.getString("profile_pic_url"), + !data.isNull("media") ? data.getJSONObject("media").getString("id") : null, + !data.isNull("media") ? data.getJSONObject("media").getString("thumbnail_src") : null, notificationType)); + } + } + + if (efr != null + && (media = efr.optJSONArray("edges")) != null + && media.length() > 0 + && media.optJSONObject(0).optJSONObject("node") != null) { + for (int i = 0; i < media.length(); ++i) { + data = media.optJSONObject(i).optJSONObject("node"); + if (data == null) continue; + result.add(new NotificationModel( + data.getString(Constants.EXTRAS_ID), + data.optString("full_name"), + 0L, + data.getString(Constants.EXTRAS_ID), + data.getString("username"), + data.getString("profile_pic_url"), + null, + null, NotificationType.REQUEST)); + } + } + callback.onSuccess(result); + } catch (JSONException e) { + callback.onFailure(e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + callback.onFailure(t); + // Log.e(TAG, "onFailure: ", t); + } + }); + } + + private NotificationModel parseNewsItem(final JSONObject itemJson) throws JSONException { + if (itemJson == null) return null; + final String type = itemJson.getString("story_type"); + final NotificationType notificationType = NotificationType.valueOfType(type); + if (notificationType == null) { + if (BuildConfig.DEBUG) Log.d("austin_debug", "unhandled news type: "+itemJson); + return null; + } + final JSONObject data = itemJson.getJSONObject("args"); + return new NotificationModel( + data.getString("tuuid"), + data.has("text") ? data.getString("text") : cleanRichText(data.optString("rich_text", "")), + data.getLong("timestamp"), + data.getString("profile_id"), + data.getString("profile_name"), + data.getString("profile_image"), + !data.isNull("media") ? data.getJSONArray("media").getJSONObject(0).getString("id") : null, + !data.isNull("media") ? data.getJSONArray("media").getJSONObject(0).getString("image") : null, + notificationType); + } + + private String cleanRichText(final String raw) { + final Matcher matcher = Pattern.compile("\\{[\\p{L}\\d._]+\\|000000\\|1\\|user\\?id=\\d+\\}").matcher(raw); + String result = raw; + while (matcher.find()) { + final String richObject = raw.substring(matcher.start(), matcher.end()); + final String username = richObject.split("\\|")[0].substring(1); + result = result.replace(richObject, username); + } + return result; + } + + public void fetchSuggestions(final String csrfToken, + final ServiceCallback> callback) { + final Map form = new HashMap<>(); + form.put("_uuid", UUID.randomUUID().toString()); + form.put("_csrftoken", csrfToken); + form.put("phone_id", UUID.randomUUID().toString()); + form.put("device_id", UUID.randomUUID().toString()); + form.put("module", "discover_people"); + form.put("paginate", "false"); + final Call request = repository.getAyml(form); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + final String body = response.body(); + if (body == null) { + callback.onSuccess(null); + return; + } + try { + final List result = new ArrayList<>(); + final JSONObject jsonObject = new JSONObject(body); + final JSONArray oldStories = jsonObject.getJSONObject("suggested_users").getJSONArray("suggestions"), + newStories = jsonObject.getJSONObject("new_suggested_users").getJSONArray("suggestions"); + + for (int j = 0; j < newStories.length(); ++j) { + final NotificationModel newsItem = parseAymlItem(newStories.getJSONObject(j)); + if (newsItem != null) result.add(newsItem); + } + + for (int i = 0; i < oldStories.length(); ++i) { + final NotificationModel newsItem = parseAymlItem(oldStories.getJSONObject(i)); + if (newsItem != null) result.add(newsItem); + } + + callback.onSuccess(result); + } catch (JSONException e) { + callback.onFailure(e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + callback.onFailure(t); + // Log.e(TAG, "onFailure: ", t); + } + }); + } + + private NotificationModel parseAymlItem(final JSONObject itemJson) throws JSONException { + if (itemJson == null) return null; + final JSONObject data = itemJson.getJSONObject("user"); + return new NotificationModel( + itemJson.getString("uuid"), + itemJson.getString("social_context"), + 0L, + data.getString("pk"), + data.getString("username"), + data.getString("profile_pic_url"), + data.getString("full_name"), // just borrowing this field + null, + NotificationType.AYML); + } } diff --git a/app/src/main/java/awais/instagrabber/webservices/ProfileService.java b/app/src/main/java/awais/instagrabber/webservices/ProfileService.java index 47766743..495cd06c 100644 --- a/app/src/main/java/awais/instagrabber/webservices/ProfileService.java +++ b/app/src/main/java/awais/instagrabber/webservices/ProfileService.java @@ -12,15 +12,9 @@ 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 java.util.Objects; 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; @@ -36,7 +30,6 @@ public class ProfileService extends BaseService { private static final String TAG = "ProfileService"; private final ProfileRepository repository; - private final ProfileRepository wwwRepository; private static ProfileService instance; @@ -44,11 +37,7 @@ 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() { @@ -74,7 +63,9 @@ public class ProfileService extends BaseService { uid, user.getString(Constants.EXTRAS_USERNAME), user.optString("full_name"), - user.optString("profile_pic_url") + user.optString("profile_pic_url"), + user.has("hd_profile_pic_url_info") + ? user.getJSONObject("hd_profile_pic_url_info").optString("url") : null ); callback.onSuccess(userInfo); } catch (JSONException e) { @@ -89,33 +80,31 @@ public class ProfileService extends BaseService { }); } - - public void fetchPosts(final ProfileModel profileModel, - final int postsPerPage, - final String cursor, + public void fetchPosts(final String userId, + final String maxId, final ServiceCallback callback) { - final Map queryMap = new HashMap<>(); - queryMap.put("query_hash", "18a7b935ab438c4514b1f742d8fa07a7"); - queryMap.put("variables", "{" + - "\"id\":\"" + profileModel.getId() + "\"," + - "\"first\":" + postsPerPage + "," + - "\"after\":\"" + (cursor == null ? "" : cursor) + "\"" + - "}"); - final Call request = wwwRepository.fetch(queryMap); + final ImmutableMap.Builder builder = ImmutableMap.builder(); + if (!TextUtils.isEmpty(maxId)) { + builder.put("max_id", maxId); + } + final Call request = repository.fetch(userId, builder.build()); request.enqueue(new Callback() { @Override public void onResponse(@NonNull final Call call, @NonNull final Response response) { try { - // Log.d(TAG, "onResponse: body: " + response.body()); - final PostsFetchResponse postsFetchResponse = parseResponse(profileModel, response); - if (callback != null) { - callback.onSuccess(postsFetchResponse); + if (callback == null) { + return; } + final String body = response.body(); + if (TextUtils.isEmpty(body)) { + callback.onSuccess(null); + return; + } + final PostsFetchResponse postsFetchResponse = parseProfilePostsResponse(body, false); + callback.onSuccess(postsFetchResponse); } catch (JSONException e) { Log.e(TAG, "onResponse", e); - if (callback != null) { - callback.onFailure(e); - } + callback.onFailure(e); } } @@ -128,165 +117,8 @@ public class ProfileService extends BaseService { }); } - private PostsFetchResponse parseResponse(final ProfileModel profileModel, final Response 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 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_to_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 sliderItems = getSliderItems(children); - builder.setSliderItems(sliderItems); - } - } - } - final FeedModel feedModel = builder.build(); - feedModels.add(feedModel); - } - return new PostsFetchResponse(feedModels, hasNextPage, endCursor); - } - - @NonNull - private List getSliderItems(final JSONArray children) throws JSONException { - final List 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; - } - public void fetchSaved(final String maxId, - final ServiceCallback callback) { + final ServiceCallback callback) { final ImmutableMap.Builder builder = ImmutableMap.builder(); if (!TextUtils.isEmpty(maxId)) { builder.put("max_id", maxId); @@ -304,8 +136,8 @@ public class ProfileService extends BaseService { callback.onSuccess(null); return; } - final SavedPostsFetchResponse savedPostsFetchResponse = parseSavedPostsResponse(body, true); - callback.onSuccess(savedPostsFetchResponse); + final PostsFetchResponse PostsFetchResponse = parseSavedPostsResponse(body, true); + callback.onSuccess(PostsFetchResponse); } catch (JSONException e) { Log.e(TAG, "onResponse", e); callback.onFailure(e); @@ -322,7 +154,7 @@ public class ProfileService extends BaseService { } public void fetchLiked(final String maxId, - final ServiceCallback callback) { + final ServiceCallback callback) { final ImmutableMap.Builder builder = ImmutableMap.builder(); if (!TextUtils.isEmpty(maxId)) { builder.put("max_id", maxId); @@ -340,8 +172,8 @@ public class ProfileService extends BaseService { callback.onSuccess(null); return; } - final SavedPostsFetchResponse savedPostsFetchResponse = parseSavedPostsResponse(body, false); - callback.onSuccess(savedPostsFetchResponse); + final PostsFetchResponse PostsFetchResponse = parseSavedPostsResponse(body, false); + callback.onSuccess(PostsFetchResponse); } catch (JSONException e) { Log.e(TAG, "onResponse", e); callback.onFailure(e); @@ -359,7 +191,7 @@ public class ProfileService extends BaseService { public void fetchTagged(final String profileId, final String maxId, - final ServiceCallback callback) { + final ServiceCallback callback) { final ImmutableMap.Builder builder = ImmutableMap.builder(); if (!TextUtils.isEmpty(maxId)) { builder.put("max_id", maxId); @@ -377,8 +209,8 @@ public class ProfileService extends BaseService { callback.onSuccess(null); return; } - final SavedPostsFetchResponse savedPostsFetchResponse = parseSavedPostsResponse(body, false); - callback.onSuccess(savedPostsFetchResponse); + final PostsFetchResponse PostsFetchResponse = parseSavedPostsResponse(body, false); + callback.onSuccess(PostsFetchResponse); } catch (JSONException e) { Log.e(TAG, "onResponse", e); callback.onFailure(e); @@ -394,7 +226,20 @@ public class ProfileService extends BaseService { }); } - private SavedPostsFetchResponse parseSavedPostsResponse(final String body, final boolean isInMedia) throws JSONException { + private PostsFetchResponse parseProfilePostsResponse(final String body, final boolean isInMedia) throws JSONException { + final JSONObject root = new JSONObject(body); + final boolean moreAvailable = root.optBoolean("more_available"); + final String nextMaxId = root.optString("next_max_id"); + final JSONArray itemsJson = root.optJSONArray("items"); + final List items = parseItems(itemsJson, isInMedia); + return new PostsFetchResponse( + items, + moreAvailable, + nextMaxId + ); + } + + private PostsFetchResponse parseSavedPostsResponse(final String body, final boolean isInMedia) throws JSONException { final JSONObject root = new JSONObject(body); final boolean moreAvailable = root.optBoolean("more_available"); final String nextMaxId = root.optString("next_max_id"); @@ -402,12 +247,10 @@ public class ProfileService extends BaseService { final String status = root.optString("status"); final JSONArray itemsJson = root.optJSONArray("items"); final List items = parseItems(itemsJson, isInMedia); - return new SavedPostsFetchResponse( + return new PostsFetchResponse( + items, moreAvailable, - nextMaxId, - numResults, - status, - items + nextMaxId ); } @@ -428,98 +271,4 @@ public class ProfileService extends BaseService { } return feedModels; } - - public static class SavedPostsFetchResponse { - private boolean moreAvailable; - private String nextMaxId; - private int numResults; - private String status; - private List items; - - public SavedPostsFetchResponse(final boolean moreAvailable, - final String nextMaxId, - final int numResults, - final String status, - final List items) { - this.moreAvailable = moreAvailable; - this.nextMaxId = nextMaxId; - this.numResults = numResults; - this.status = status; - this.items = items; - } - - public boolean isMoreAvailable() { - return moreAvailable; - } - - public SavedPostsFetchResponse setMoreAvailable(final boolean moreAvailable) { - this.moreAvailable = moreAvailable; - return this; - } - - public String getNextMaxId() { - return nextMaxId; - } - - public SavedPostsFetchResponse setNextMaxId(final String nextMaxId) { - this.nextMaxId = nextMaxId; - return this; - } - - public int getNumResults() { - return numResults; - } - - public SavedPostsFetchResponse setNumResults(final int numResults) { - this.numResults = numResults; - return this; - } - - public String getStatus() { - return status; - } - - public SavedPostsFetchResponse setStatus(final String status) { - this.status = status; - return this; - } - - public List getItems() { - return items; - } - - public SavedPostsFetchResponse setItems(final List items) { - this.items = items; - return this; - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final SavedPostsFetchResponse that = (SavedPostsFetchResponse) o; - return moreAvailable == that.moreAvailable && - numResults == that.numResults && - Objects.equals(nextMaxId, that.nextMaxId) && - Objects.equals(status, that.status) && - Objects.equals(items, that.items); - } - - @Override - public int hashCode() { - return Objects.hash(moreAvailable, nextMaxId, numResults, status, items); - } - - @NonNull - @Override - public String toString() { - return "SavedPostsFetchResponse{" + - "moreAvailable=" + moreAvailable + - ", nextMaxId='" + nextMaxId + '\'' + - ", numResults=" + numResults + - ", status='" + status + '\'' + - ", items=" + items + - '}'; - } - } } diff --git a/app/src/main/java/awais/instagrabber/webservices/StoriesService.java b/app/src/main/java/awais/instagrabber/webservices/StoriesService.java index ecc657d1..7ea3b5c9 100644 --- a/app/src/main/java/awais/instagrabber/webservices/StoriesService.java +++ b/app/src/main/java/awais/instagrabber/webservices/StoriesService.java @@ -1,6 +1,5 @@ package awais.instagrabber.webservices; -import android.os.Handler; import android.util.Log; import androidx.annotation.NonNull; @@ -9,26 +8,24 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.TimeZone; +import java.util.UUID; import awais.instagrabber.models.FeedStoryModel; +import awais.instagrabber.models.HighlightModel; import awais.instagrabber.models.ProfileModel; import awais.instagrabber.models.StoryModel; -import awais.instagrabber.models.enums.MediaItemType; -import awais.instagrabber.models.stickers.PollModel; -import awais.instagrabber.models.stickers.QuestionModel; -import awais.instagrabber.models.stickers.QuizModel; import awais.instagrabber.repositories.StoriesRepository; +import awais.instagrabber.repositories.responses.StoryStickerResponse; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.ResponseBodyUtils; +import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -44,7 +41,7 @@ public class StoriesService extends BaseService { private StoriesService() { final Retrofit retrofit = getRetrofitBuilder() - .baseUrl("https://www.instagram.com") + .baseUrl("https://i.instagram.com") .build(); repository = retrofit.create(StoriesRepository.class); } @@ -56,35 +53,39 @@ public class StoriesService extends BaseService { return instance; } - public void getFeedStories(final ServiceCallback> callback) { - if (loadFromMock) { - final Handler handler = new Handler(); - handler.postDelayed(() -> { - final ClassLoader classLoader = getClass().getClassLoader(); - if (classLoader == null) { - Log.e(TAG, "getFeedStories: classLoader is null!"); + public void fetch(final String mediaId, + final ServiceCallback callback) { + final Call request = repository.fetch(mediaId); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (callback == null) return; + final String body = response.body(); + if (body == null) { + callback.onSuccess(null); return; } - try (InputStream resourceAsStream = classLoader.getResourceAsStream("stories_response.json"); - Reader in = new InputStreamReader(resourceAsStream, StandardCharsets.UTF_8)) { - final int bufferSize = 1024; - final char[] buffer = new char[bufferSize]; - final StringBuilder out = new StringBuilder(); - int charsRead; - while ((charsRead = in.read(buffer, 0, buffer.length)) > 0) { - out.append(buffer, 0, charsRead); - } - parseStoriesBody(out.toString(), callback); - } catch (IOException e) { - Log.e(TAG, "getFeedStories: ", e); + try { + final JSONObject itemJson = new JSONObject(body).getJSONArray("items").getJSONObject(0); + callback.onSuccess(ResponseBodyUtils.parseStoryItem(itemJson, false, false, null)); + } catch (JSONException e) { + callback.onFailure(e); } - }, 1000); - return; - } - final Map queryMap = new HashMap<>(); - queryMap.put("query_hash", "b7b84d884400bc5aa7cfe12ae843a091"); - queryMap.put("variables", "{\"only_stories\":true,\"stories_prefetch\":false,\"stories_video_dash_manifest\":false}"); - final Call response = repository.getStories(queryMap); + } + + @Override + public void onFailure(@NonNull final Call call, + @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + } + + public void getFeedStories(final String csrfToken, final ServiceCallback> callback) { + final Call response = repository.getFeedStories(); response.enqueue(new Callback() { @Override public void onResponse(@NonNull final Call call, @NonNull final Response response) { @@ -106,31 +107,138 @@ public class StoriesService extends BaseService { private void parseStoriesBody(final String body, final ServiceCallback> callback) { try { final List feedStoryModels = new ArrayList<>(); - final JSONArray feedStoriesReel = new JSONObject(body) - .getJSONObject("data") - .getJSONObject(Constants.EXTRAS_USER) - .getJSONObject("feed_reels_tray") - .getJSONObject("edge_reels_tray_to_reel") - .getJSONArray("edges"); + final JSONArray feedStoriesReel = new JSONObject(body).getJSONArray("tray"); for (int i = 0; i < feedStoriesReel.length(); ++i) { - final JSONObject node = feedStoriesReel.getJSONObject(i).getJSONObject("node"); + final JSONObject node = feedStoriesReel.getJSONObject(i); final JSONObject user = node.getJSONObject(node.has("user") ? "user" : "owner"); final ProfileModel profileModel = new ProfileModel(false, false, false, - user.getString("id"), + user.getString("pk"), user.getString("username"), null, null, null, user.getString("profile_pic_url"), - null, 0, 0, 0, false, false, false, false); + null, 0, 0, 0, false, false, false, false, false); final String id = node.getString("id"); - final boolean fullyRead = !node.isNull("seen") && node.getLong("seen") == node.getLong("latest_reel_media"); - feedStoryModels.add(new FeedStoryModel(id, profileModel, fullyRead)); + final long timestamp = node.getLong("latest_reel_media"); + final int mediaCount = node.getInt("media_count"); + final boolean fullyRead = !node.isNull("seen") && node.getLong("seen") == timestamp; + final JSONObject itemJson = node.has("items") ? node.getJSONArray("items").getJSONObject(0) : null; + StoryModel firstStoryModel = null; + if (itemJson != null) { + firstStoryModel = ResponseBodyUtils.parseStoryItem(itemJson, false, false, null); + } + feedStoryModels.add(new FeedStoryModel(id, profileModel, fullyRead, timestamp, firstStoryModel, mediaCount)); } - callback.onSuccess(feedStoryModels); + callback.onSuccess(sort(feedStoryModels)); } catch (JSONException e) { Log.e(TAG, "Error parsing json", e); } } + public void fetchHighlights(final String profileId, + final ServiceCallback> callback) { + final Call request = repository.fetchHighlights(profileId); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + try { + if (callback == null) { + return; + } + final String body = response.body(); + if (TextUtils.isEmpty(body)) { + callback.onSuccess(null); + return; + } + final JSONArray highlightsReel = new JSONObject(body).getJSONArray("tray"); + + final int length = highlightsReel.length(); + final List highlightModels = new ArrayList<>(); + + for (int i = 0; i < length; ++i) { + final JSONObject highlightNode = highlightsReel.getJSONObject(i); + highlightModels.add(new HighlightModel( + highlightNode.getString("title"), + highlightNode.getString(Constants.EXTRAS_ID), + highlightNode.getJSONObject("cover_media") + .getJSONObject("cropped_image_version") + .getString("url"), + highlightNode.getLong("latest_reel_media"), + highlightNode.getInt("media_count") + )); + } + callback.onSuccess(highlightModels); + } catch (JSONException e) { + Log.e(TAG, "onResponse", e); + callback.onFailure(e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + } + + public void fetchArchive(final String maxId, + final ServiceCallback callback) { + final Map form = new HashMap<>(); + form.put("include_suggested_highlights", "false"); + form.put("is_in_archive_home", "true"); + form.put("include_cover", "1"); + form.put("timezone_offset", String.valueOf(TimeZone.getDefault().getRawOffset() / 1000)); + if (!TextUtils.isEmpty(maxId)) { + form.put("max_id", maxId); // NOT TESTED + } + final Call request = repository.fetchArchive(form); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + try { + if (callback == null) { + return; + } + final String body = response.body(); + if (TextUtils.isEmpty(body)) { + callback.onSuccess(null); + return; + } + final JSONObject data = new JSONObject(body); + final JSONArray highlightsReel = data.getJSONArray("items"); + + final int length = highlightsReel.length(); + final List highlightModels = new ArrayList<>(); + + for (int i = 0; i < length; ++i) { + final JSONObject highlightNode = highlightsReel.getJSONObject(i); + highlightModels.add(new HighlightModel( + null, + highlightNode.getString(Constants.EXTRAS_ID), + highlightNode.getJSONObject("cover_image_version").getString("url"), + highlightNode.getLong("latest_reel_media"), + highlightNode.getInt("media_count") + )); + } + callback.onSuccess(new ArchiveFetchResponse(highlightModels, + data.getBoolean("more_available"), + data.getString("max_id"))); + } catch (JSONException e) { + Log.e(TAG, "onResponse", e); + callback.onFailure(e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + } + public void getUserStory(final String id, final String username, final boolean isLoc, @@ -138,6 +246,7 @@ public class StoriesService extends BaseService { final boolean highlight, final ServiceCallback> callback) { final String url = buildUrl(id, isLoc, isHashtag, highlight); + Log.d("austin_debug", url); final Call userStoryCall = repository.getUserStory(Constants.I_USER_AGENT, url); userStoryCall.enqueue(new Callback() { @Override @@ -171,101 +280,7 @@ public class StoriesService extends BaseService { final List models = new ArrayList<>(); for (int i = 0; i < mediaLen; ++i) { data = media.getJSONObject(i); - final boolean isVideo = data.has("video_duration"); - final StoryModel model = new StoryModel(data.getString("id"), - data.getJSONObject("image_versions2").getJSONArray("candidates").getJSONObject(0) - .getString("url"), - isVideo ? MediaItemType.MEDIA_TYPE_VIDEO : MediaItemType.MEDIA_TYPE_IMAGE, - data.optLong("taken_at", 0), - (isLoc || isHashtag) - ? data.getJSONObject("user").getString("username") - : localUsername, - data.getJSONObject("user").getString("pk"), - data.getBoolean("can_reply")); - - final JSONArray videoResources = data.optJSONArray("video_versions"); - if (isVideo && videoResources != null) - model.setVideoUrl(ResponseBodyUtils.getHighQualityPost(videoResources, true, true, false)); - - if (data.has("story_feed_media")) { - model.setTappableShortCode(data.getJSONArray("story_feed_media").getJSONObject(0).optString("media_code")); - } - - // TODO: this may not be limited to spotify - if (!data.isNull("story_app_attribution")) - model.setSpotify(data.getJSONObject("story_app_attribution").optString("content_url").split("\\?")[0]); - - if (data.has("story_polls")) { - final JSONArray storyPolls = data.optJSONArray("story_polls"); - JSONObject tappableObject = null; - if (storyPolls != null) { - tappableObject = storyPolls.getJSONObject(0).optJSONObject("poll_sticker"); - } - if (tappableObject != null) model.setPoll(new PollModel( - String.valueOf(tappableObject.getLong("poll_id")), - tappableObject.getString("question"), - tappableObject.getJSONArray("tallies").getJSONObject(0).getString("text"), - tappableObject.getJSONArray("tallies").getJSONObject(0).getInt("count"), - tappableObject.getJSONArray("tallies").getJSONObject(1).getString("text"), - tappableObject.getJSONArray("tallies").getJSONObject(1).getInt("count"), - tappableObject.optInt("viewer_vote", -1) - )); - } - if (data.has("story_questions")) { - final JSONObject tappableObject = data.getJSONArray("story_questions").getJSONObject(0) - .optJSONObject("question_sticker"); - if (tappableObject != null && !tappableObject.getString("question_type").equals("music")) - model.setQuestion(new QuestionModel( - String.valueOf(tappableObject.getLong("question_id")), - tappableObject.getString("question") - )); - } - if (data.has("story_quizs")) { - JSONObject tappableObject = data.getJSONArray("story_quizs").getJSONObject(0).optJSONObject("quiz_sticker"); - if (tappableObject != null) { - String[] choices = new String[tappableObject.getJSONArray("tallies").length()]; - Long[] counts = new Long[choices.length]; - for (int q = 0; q < choices.length; ++q) { - JSONObject tempchoice = tappableObject.getJSONArray("tallies").getJSONObject(q); - choices[q] = (q == tappableObject.getInt("correct_answer") ? "*** " : "") - + tempchoice.getString("text"); - counts[q] = tempchoice.getLong("count"); - } - model.setQuiz(new QuizModel( - String.valueOf(tappableObject.getLong("quiz_id")), - tappableObject.getString("question"), - choices, - counts, - tappableObject.optInt("viewer_answer", -1) - )); - } - } - JSONArray hashtags = data.optJSONArray("story_hashtags"); - JSONArray locations = data.optJSONArray("story_locations"); - JSONArray atmarks = data.optJSONArray("reel_mentions"); - String[] mentions = new String[(hashtags == null ? 0 : hashtags.length()) - + (atmarks == null ? 0 : atmarks.length()) - + (locations == null ? 0 : locations.length())]; - if (hashtags != null) { - for (int h = 0; h < hashtags.length(); ++h) { - mentions[h] = "#" + hashtags.getJSONObject(h).getJSONObject("hashtag").getString("name"); - } - } - if (atmarks != null) { - for (int h = 0; h < atmarks.length(); ++h) { - mentions[h + (hashtags == null ? 0 : hashtags.length())] = - "@" + atmarks.getJSONObject(h).getJSONObject("user").getString("username"); - } - } - if (locations != null) { - for (int h = 0; h < locations.length(); ++h) { - mentions[h + (hashtags == null ? 0 : hashtags.length()) + (atmarks == null ? 0 : atmarks.length())] = - locations.getJSONObject(h).getJSONObject("location").getString("short_name") - + " (" + locations.getJSONObject(h).getJSONObject("location").getLong("pk") + ")"; - } - } - if (mentions.length != 0) model.setMentions(mentions); - models.add(model); + models.add(ResponseBodyUtils.parseStoryItem(data, isLoc, isHashtag, localUsername)); } callback.onSuccess(models); } else { @@ -283,6 +298,83 @@ public class StoriesService extends BaseService { }); } + private void respondToSticker(final String storyId, + final String stickerId, + final String action, + final String arg1, + final String arg2, + final String userId, + final String csrfToken, + final ServiceCallback callback) { + final Map form = new HashMap<>(); + form.put("_csrftoken", csrfToken); + form.put("_uid", userId); + form.put("_uuid", UUID.randomUUID().toString()); + form.put("mutation_token", UUID.randomUUID().toString()); + form.put("client_context", UUID.randomUUID().toString()); + form.put("radio_type", "wifi-none"); + form.put(arg1, arg2); + final Map signedForm = Utils.sign(form); + final Call request = + repository.respondToSticker(Constants.I_USER_AGENT, storyId, stickerId, action, signedForm); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (callback != null) { + callback.onSuccess(response.body()); + } + } + + @Override + public void onFailure(@NonNull final Call call, + @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + } + + // RespondAction.java + public void respondToQuestion(final String storyId, + final String stickerId, + final String answer, + final String userId, + final String csrfToken, + final ServiceCallback callback) { + respondToSticker(storyId, stickerId, "story_question_response", "response", answer, userId, csrfToken, callback); + } + + // QuizAction.java + public void respondToQuiz(final String storyId, + final String stickerId, + final int answer, + final String userId, + final String csrfToken, + final ServiceCallback callback) { + respondToSticker(storyId, stickerId, "story_quiz_answer", "answer", String.valueOf(answer), userId, csrfToken, callback); + } + + // VoteAction.java + public void respondToPoll(final String storyId, + final String stickerId, + final int answer, + final String userId, + final String csrfToken, + final ServiceCallback callback) { + respondToSticker(storyId, stickerId, "story_poll_vote", "vote", String.valueOf(answer), userId, csrfToken, callback); + } + + public void respondToSlider(final String storyId, + final String stickerId, + final double answer, + final String userId, + final String csrfToken, + final ServiceCallback callback) { + respondToSticker(storyId, stickerId, "story_slider_vote", "vote", String.valueOf(answer), userId, csrfToken, callback); + } + private String buildUrl(final String id, final boolean isLoc, final boolean isHashtag, final boolean highlight) { final String userId = id.replace(":", "%3A"); final StringBuilder builder = new StringBuilder(); @@ -302,4 +394,47 @@ public class StoriesService extends BaseService { } return builder.toString(); } + + private List sort(final List list) { + final List listCopy = new ArrayList<>(list); + Collections.sort(listCopy, (o1, o2) -> { + int result; + switch (Utils.settingsHelper.getString(Constants.STORY_SORT)) { + case "1": + result = o1.getTimestamp() > o2.getTimestamp() ? -1 : (o1.getTimestamp() == o2.getTimestamp() ? 0 : 1); + break; + case "2": + result = o1.getTimestamp() > o2.getTimestamp() ? 1 : (o1.getTimestamp() == o2.getTimestamp() ? 0 : -1); + break; + default: + result = 0; + } + return result; + }); + return listCopy; + } + + public class ArchiveFetchResponse { + private final List archives; + private final boolean hasNextPage; + private final String nextCursor; + + public ArchiveFetchResponse(final List archives, final boolean hasNextPage, final String nextCursor) { + this.archives = archives; + this.hasNextPage = hasNextPage; + this.nextCursor = nextCursor; + } + + public List getResult() { + return archives; + } + + public boolean hasNextPage() { + return hasNextPage; + } + + public String getNextCursor() { + return nextCursor; + } + } } diff --git a/app/src/main/java/awais/instagrabber/webservices/TagsService.java b/app/src/main/java/awais/instagrabber/webservices/TagsService.java index 5aabaee1..7f3dc490 100644 --- a/app/src/main/java/awais/instagrabber/webservices/TagsService.java +++ b/app/src/main/java/awais/instagrabber/webservices/TagsService.java @@ -19,6 +19,7 @@ import java.util.Objects; import awais.instagrabber.models.FeedModel; import awais.instagrabber.repositories.TagsRepository; +import awais.instagrabber.repositories.responses.PostsFetchResponse; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.ResponseBodyUtils; import awais.instagrabber.utils.TextUtils; @@ -118,8 +119,8 @@ public class TagsService extends BaseService { public void fetchPosts(@NonNull final String tag, final String maxId, - final ServiceCallback callback) { - final ImmutableMap.Builder builder = ImmutableMap.builder(); + final ServiceCallback callback) { + final ImmutableMap.Builder builder = ImmutableMap.builder(); if (!TextUtils.isEmpty(maxId)) { builder.put("max_id", maxId); } @@ -136,7 +137,7 @@ public class TagsService extends BaseService { callback.onSuccess(null); return; } - final TagPostsFetchResponse tagPostsFetchResponse = parseResponse(body); + final PostsFetchResponse tagPostsFetchResponse = parseResponse(body); callback.onSuccess(tagPostsFetchResponse); } catch (JSONException e) { Log.e(TAG, "onResponse", e); @@ -153,20 +154,16 @@ public class TagsService extends BaseService { }); } - private TagPostsFetchResponse parseResponse(@NonNull final String body) throws JSONException { + private PostsFetchResponse parseResponse(@NonNull final String body) throws JSONException { final JSONObject root = new JSONObject(body); final boolean moreAvailable = root.optBoolean("more_available"); final String nextMaxId = root.optString("next_max_id"); - final int numResults = root.optInt("num_results"); - final String status = root.optString("status"); final JSONArray itemsJson = root.optJSONArray("items"); final List items = parseItems(itemsJson); - return new TagPostsFetchResponse( + return new PostsFetchResponse( + items, moreAvailable, - nextMaxId, - numResults, - status, - items + nextMaxId ); } @@ -187,174 +184,4 @@ public class TagsService extends BaseService { } return feedModels; } - - public void fetchGraphQLPosts(@NonNull final String tag, - final String maxId, - final ServiceCallback callback) { - final Map queryMap = new HashMap<>(); - queryMap.put("query_hash", "9b498c08113f1e09617a1703c22b2f32"); - queryMap.put("variables", "{" + - "\"tag_name\":\"" + tag + "\"," + - "\"first\":25," + - "\"after\":\"" + (maxId == null ? "" : maxId) + "\"" + - "}"); - final Call request = webRepository.fetchGraphQLPosts(queryMap); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - try { - if (callback == null) { - return; - } - final String body = response.body(); - if (TextUtils.isEmpty(body)) { - callback.onSuccess(null); - return; - } - final TagPostsFetchResponse tagPostsFetchResponse = parseGraphQLResponse(body); - callback.onSuccess(tagPostsFetchResponse); - } catch (JSONException e) { - Log.e(TAG, "onResponse", e); - callback.onFailure(e); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - if (callback != null) { - callback.onFailure(t); - } - } - }); - } - - private TagPostsFetchResponse parseGraphQLResponse(@NonNull final String body) throws JSONException { - final JSONObject rootroot = new JSONObject(body); - final JSONObject root = rootroot.getJSONObject("data").getJSONObject("hashtag").getJSONObject("edge_hashtag_to_media"); - final boolean moreAvailable = root.getJSONObject("page_info").optBoolean("has_next_page"); - final String nextMaxId = root.getJSONObject("page_info").optString("end_cursor"); - final int numResults = root.optInt("count"); - final String status = rootroot.optString("status"); - final JSONArray itemsJson = root.optJSONArray("edges"); - final List items = parseGraphQLItems(itemsJson); - return new TagPostsFetchResponse( - moreAvailable, - nextMaxId, - numResults, - status, - items - ); - } - - private List parseGraphQLItems(final JSONArray items) throws JSONException { - if (items == null) { - return Collections.emptyList(); - } - final List feedModels = new ArrayList<>(); - for (int i = 0; i < items.length(); i++) { - final JSONObject itemJson = items.optJSONObject(i); - if (itemJson == null) { - continue; - } - final FeedModel feedModel = ResponseBodyUtils.parseGraphQLItem(itemJson); - if (feedModel != null) { - feedModels.add(feedModel); - } - } - return feedModels; - } - - public static class TagPostsFetchResponse { - private boolean moreAvailable; - private String nextMaxId; - private int numResults; - private String status; - private List items; - - public TagPostsFetchResponse(final boolean moreAvailable, - final String nextMaxId, - final int numResults, - final String status, - final List items) { - this.moreAvailable = moreAvailable; - this.nextMaxId = nextMaxId; - this.numResults = numResults; - this.status = status; - this.items = items; - } - - public boolean isMoreAvailable() { - return moreAvailable; - } - - public TagPostsFetchResponse setMoreAvailable(final boolean moreAvailable) { - this.moreAvailable = moreAvailable; - return this; - } - - public String getNextMaxId() { - return nextMaxId; - } - - public TagPostsFetchResponse setNextMaxId(final String nextMaxId) { - this.nextMaxId = nextMaxId; - return this; - } - - public int getNumResults() { - return numResults; - } - - public TagPostsFetchResponse setNumResults(final int numResults) { - this.numResults = numResults; - return this; - } - - public String getStatus() { - return status; - } - - public TagPostsFetchResponse setStatus(final String status) { - this.status = status; - return this; - } - - public List getItems() { - return items; - } - - public TagPostsFetchResponse setItems(final List items) { - this.items = items; - return this; - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final TagPostsFetchResponse that = (TagPostsFetchResponse) o; - return moreAvailable == that.moreAvailable && - numResults == that.numResults && - Objects.equals(nextMaxId, that.nextMaxId) && - Objects.equals(status, that.status) && - Objects.equals(items, that.items); - } - - @Override - public int hashCode() { - return Objects.hash(moreAvailable, nextMaxId, numResults, status, items); - } - - @NonNull - @Override - public String toString() { - return "TagPostsFetchResponse{" + - "moreAvailable=" + moreAvailable + - ", nextMaxId='" + nextMaxId + '\'' + - ", numResults=" + numResults + - ", status='" + status + '\'' + - ", items=" + items + - '}'; - } - } } diff --git a/app/src/main/java/awaisomereport/CrashReporter.java b/app/src/main/java/awaisomereport/CrashReporter.java index 93e5b0f0..d538bb4c 100755 --- a/app/src/main/java/awaisomereport/CrashReporter.java +++ b/app/src/main/java/awaisomereport/CrashReporter.java @@ -54,7 +54,7 @@ public final class CrashReporter implements Thread.UncaughtExceptionHandler { public void uncaughtException(@NonNull final Thread t, @NonNull final Throwable exception) { final StringBuilder reportBuilder = new StringBuilder(); reportBuilder.append("IMPORTANT: If sending by email, your email address and the entire content will be made public on GitHub issues."); - reportBuilder.append("IMPORTANT: When possible, please describe the steps leading to this crash. Thank you for your cooperation."); + reportBuilder.append("\r\nIMPORTANT: When possible, please describe the steps leading to this crash. Thank you for your cooperation."); reportBuilder.append("\r\n\r\nError report collected on: ").append(new Date().toString()); reportBuilder diff --git a/app/src/main/res/drawable/ic_archive.xml b/app/src/main/res/drawable/ic_archive.xml new file mode 100644 index 00000000..65001884 --- /dev/null +++ b/app/src/main/res/drawable/ic_archive.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_story_list.xml b/app/src/main/res/drawable/ic_story_list.xml new file mode 100644 index 00000000..54589b26 --- /dev/null +++ b/app/src/main/res/drawable/ic_story_list.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_suggested_users.xml b/app/src/main/res/drawable/ic_suggested_users.xml new file mode 100644 index 00000000..709fe2d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_suggested_users.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/launch.xml b/app/src/main/res/drawable/launch.xml new file mode 100644 index 00000000..c34eafaa --- /dev/null +++ b/app/src/main/res/drawable/launch.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/launch_dark.xml b/app/src/main/res/drawable/launch_dark.xml new file mode 100644 index 00000000..d436940f --- /dev/null +++ b/app/src/main/res/drawable/launch_dark.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_post_layout_preferences.xml b/app/src/main/res/layout/dialog_post_layout_preferences.xml index 7bf0eefc..938efd8c 100644 --- a/app/src/main/res/layout/dialog_post_layout_preferences.xml +++ b/app/src/main/res/layout/dialog_post_layout_preferences.xml @@ -17,15 +17,20 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" - app:layout_constraintGuide_percent="0.4" /> + app:layout_constraintGuide_percent="0.32" /> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/location_nav_graph.xml b/app/src/main/res/navigation/location_nav_graph.xml index b787f9eb..eea03d9c 100644 --- a/app/src/main/res/navigation/location_nav_graph.xml +++ b/app/src/main/res/navigation/location_nav_graph.xml @@ -24,6 +24,21 @@ app:nullable="false" /> + + + + + + + @@ -90,5 +105,13 @@ android:name="username" app:argType="string" app:nullable="true" /> + + \ No newline at end of file diff --git a/app/src/main/res/navigation/more_nav_graph.xml b/app/src/main/res/navigation/more_nav_graph.xml index 50d9cf54..8980e73a 100644 --- a/app/src/main/res/navigation/more_nav_graph.xml +++ b/app/src/main/res/navigation/more_nav_graph.xml @@ -8,7 +8,9 @@ + + + + + + + app:destination="@id/notification_viewer_nav_graph"> + + + tools:layout="@layout/fragment_notifications_viewer"> + + + + app:destination="@id/notificationsViewer"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/profile_nav_graph.xml b/app/src/main/res/navigation/profile_nav_graph.xml index 58b2d1d4..bf78ac76 100644 --- a/app/src/main/res/navigation/profile_nav_graph.xml +++ b/app/src/main/res/navigation/profile_nav_graph.xml @@ -24,6 +24,21 @@ app:nullable="false" /> + + + + + + + + app:destination="@id/notification_viewer_nav_graph"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ca/arrays.xml b/app/src/main/res/values-ca/arrays.xml index 597031bc..af7ee708 100644 --- a/app/src/main/res/values-ca/arrays.xml +++ b/app/src/main/res/values-ca/arrays.xml @@ -2,41 +2,26 @@ Predeterminat del sistema - Anglès - Francès [Gràcies a @kernoeb] - Castellà [Gràcies a @sguinetti] - Chinès si - Indonès [Gràcies a @Galang23] - Italià [Gràcies a @RAR_Ramar] - Alemany [Gràcies a @peterge1998] - Polonès [Gràcies a @Lego8486] - Turc [Gràcies a @faydin90] - Portuguès brasiler [Gràcies a @wagnim] - Persa [Gràcies a @farzadx] - Macedoni [Gràcies a @snajdovski] - Vietnamita [Gràcies a @fouze555] - Xinès tradicional [Gràcies a @Still34] - Catalan [Thanks to @retiolus] - Russian [Thanks to @rikishi0071] - - - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 + English + Français [Merci à @kernoeb et @PierreM0] + Español [Gracias a @sguinetti, @akrai y @retiolus] + 简体中文 + Bahasa Indonesia [Terima kasih @Galang23] + Italiano [Grazie a @RAR_Ramar e GiorgioHerbie] + Deutsch [Danke an @peterge1998] + Polski [Podziękowania dla @Lego8486] + Türkçe [@faydin90 tarafından] + Português (Brasil) [Obrigado @wagnim, @RickyM7 e @cizordj] + پارسی [ با سپاس از farzadx@ ] + Македонски [Благодарност до @snajdovski] + Tiếng Việt [bởi Yato Fouze] + 繁體中文 [感謝 @Still34] + Català [Gràcies a @retiolus] + Русский [Спасибо @rikishi0071] + हिन्दी + Nederlands [Met dank aan Lesley Natrop] + Slovenčina [Vďaka @CrafterSvK] + 日本語 [協力 ysakamoto] Automàtic / Predeterminat del sistema @@ -44,11 +29,10 @@ Fosc Clar - - 0 - 1 - 2 - 3 + + Predeterminat d\'Instagram (no llegits i després llegits) + De més recent a més antic + De més antic a més recent Ningún @@ -58,53 +42,6 @@ \| - - - S\'està carregant... - - - dd-MM-yyyy - dd/MM/yyyy - dd.MM.yyyy - dd-MM-yy - dd/MM/yy - dd.MM.yy - dd-MMM-yyyy - dd/MMM/yyyy - dd.MMM.yyyy - dd-MMM-yy - dd/MMM/yy - dd.MMM.yy - MM-dd-yyyy - MM/dd/yyyy - MM.dd.yyyy - yyyy-MM-dd - yyyy/MM/dd - yyyy.MM.dd - MM-dd-yy - MM/dd/yy - MM.dd.yy - yy-MM-dd - yy/MM/dd - yy.MM.dd - MMM-dd-yyyy - MMM/dd/yyyy - MMM.dd.yyyy - yyyy-MMM-dd - yyyy/MMM/dd - yyyy.MMM.dd - MMM-dd-yy - MMM/dd/yy - MMM.dd.yy - yy-MMM-dd - yy/MMM/dd - yy.MMM.dd - - - hh:mm:ss a - h:mm:ss a - HH:mm:ss - H:mm:ss - @string/title_dm @string/feed diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index b56aaf81..d73c710c 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -23,7 +23,7 @@ Preferits Descobrir Comentaris - Notificacions + Activitat Destaca: %s Cerca actualitzacions a l\'inici Descarrega les publicacions a carpetes de nom d\'usuari @@ -32,6 +32,7 @@ Marca els missatges com a vists després de visualitzar-los Altres membres sabran que l\'has vist Habilitar les notificacions d\'activitat + Ordre de les històries S\'ha produït un error en carregar el perfil!\nTorna a iniciar sessió i torna a cercar. S\'ha produït un error en crear la(es) carpeta(es) de descàrrega. Desa a una carpeta personalitzada @@ -42,10 +43,15 @@ Utilitza Instadp per a les imatges del perfil d\'alta definició Importar/Exportar Llengua - %s\nPublicacions - %s Publicacions - %s\n Seguidors - %s\n Seguint + + %s Publicació + %s Publicacions + + + %s Seguidor + %s Seguidors + + %s Seguint Reproduir vídeos automàticament Silenciar sempre els vídeos Seleccionar el que s\'ha de descarregar @@ -63,9 +69,15 @@ Contestar Resposta… Has contestat amb èxit! + + %d mitjana de resposta %s + %d mitjana de respostes %s + + La teva resposta: %s Respon a la història Resposta… Qüestionari + Desplaçador Ja has contestat! Mencions Aquest compte és privat @@ -98,6 +110,11 @@ Desbloquejar Restringir Deixar de restringir + Copiar bio + Traduir bio + Mutu + Seguint + Seguidor Mapa Exportar Importar @@ -126,6 +143,7 @@ No es pot eliminar el compte actualment en ús Esteu segur que vols suprimir \"%s\"? Obre el perfil + Veure història Mostra la imatge del perfil Has compartit un enllaç @@ -175,9 +193,11 @@ Només pots descarregar 100 publicacions alhora. No siguis massa cobdiciós! Copiar el nom d\'usuari Copiar el comentari + Veure m\'agrades del comentari Respondre al comentari Donar \"m\'agrada\" al comentari Treure \"m\'agrada\" al comentari + Traduir comentari Esborrar comentari No als comentaris buits! Vols cercar el nom d\'usuari? @@ -208,6 +228,8 @@ L\'aplicació ha fallat Vaja... L\'aplicació ha fallat, però no et preocupis, pots enviar un informe d\'error al desenvolupador per ajudar-lo a solucionar el problema. (: Activitat + Arxiu de històries + Suggested users Seleccionar imatge S\'està pujant… Tens: @@ -239,7 +261,7 @@ Visita la nostra pàgina web Obtén suport, parla, trobat amb altres i diverteix-te! Veure el nostre codi font a GitHub - Informa d\'errors, contribueix i diverteix-te (una altra vegada)! + Inspecta, segueix, informa d\'errors, contribueix i diverteix-te (una altra vegada)! Enviar comentaris per correu electrònic Reconeixements de tercers S\'utilitzen les següents biblioteques de codi obert: @@ -252,21 +274,26 @@ Barista Material Dark S\'ha afegit als preferits - Afegir als preferits + S\'ha agregat als favorits + Favorit Comptes Etiquetes Ubicacions Desconegut Eliminat dels preferits - Còpia de seguretat & Restauració - Crear - Restaurar + Còpia de seguretat & Restaurar els paràmetres de l\'usuari + Fer una còpia de seguretat de la configuració de l\'aplicació, la informació d\'inici de sessió del compte, i/o dades preferides a un text net o arxiu de còpia de seguretat xifrat per a la restauració posterior. + Si estàs fent una còpia de seguretat de la informació d\'inici de sessió, tracta l\'arxiu com a confidencial: Mantén-lo en algun lloc segur! + Crear un nou fitxer de còpia de seguretat + Restaurar des d\'un fitxer de còpia de seguretat existent Arxiu: Introduir la contrasenya Selecciona un fitxer de còpia de seguretat (.zaai/.backup) Aplicar Desar Llegenda + Editar llegenda... + Traduir la llegenda... Línia de temps del reproductor de vídeo Donant m\'agrada… El \"m\'agrada\" ha fallat @@ -282,6 +309,7 @@ Eliminar Comentar Disposició + Fil d\'històries Obrint publicació... Compartir Estil de disposició @@ -295,7 +323,9 @@ Mostrar la separació de la graella Desactivar l\'animació Espera primer que la tasca actual s\'hagi completat! - Post not found! + Depenent del nombre d\'usuaris, això pot trigar una estona a carregar-se. Si us plau, tingues paciència. + Publicació no trobada! + No s\'ha trobat cap aplicació que obri l\'enllaç %d m\'agrada %d m\'agrades @@ -304,4 +334,9 @@ %d comentari %d comentaris + + %s història + %s històries + + No s\'ha concedit el permís d\'emmagatzematge! diff --git a/app/src/main/res/values-cs/arrays.xml b/app/src/main/res/values-cs/arrays.xml index 141aef55..b8616a13 100644 --- a/app/src/main/res/values-cs/arrays.xml +++ b/app/src/main/res/values-cs/arrays.xml @@ -1,110 +1,47 @@ - System Default - English - French [Thanks to @kernoeb] - Spanish [Thanks to @sguinetti] - Chinese Simplified - Indonesian [Thanks to @Galang23] - Italian [Thanks to @RAR_Ramar] - German [Thanks to @peterge1998] - Polish [Thanks to @Lego8486] - Turkish [Thanks to @faydin90] - Brazilian Portuguese [Thanks to @wagnim] - Persian [Thanks to @farzadx] - Macedonian [Thanks to @snajdovski] - Vietnamese [Thanks to @fouze555] - Chinese Traditional [Thanks to @Still34] - Catalan [Thanks to @retiolus] - Russian [Thanks to @rikishi0071] - - - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 + Podle nastavení systému + English + Français [Merci à @kernoeb et @PierreM0] + Español [Gracias a @sguinetti, @akrai y @retiolus] + 简体中文 + Bahasa Indonesia [Terima kasih @Galang23] + Italiano [Grazie a @RAR_Ramar e GiorgioHerbie] + Deutsch [Danke an @peterge1998] + Polski [Podziękowania dla @Lego8486] + Türkçe [@faydin90 tarafından] + Português (Brasil) [Obrigado @wagnim, @RickyM7 e @cizordj] + پارسی [ با سپاس از farzadx@ ] + Македонски [Благодарност до @snajdovski] + Tiếng Việt [bởi Yato Fouze] + 繁體中文 [感謝 @Still34] + Català [Gràcies a @retiolus] + Русский [Спасибо @rikishi0071] + हिन्दी + Nederlands [Met dank aan Lesley Natrop] + Slovenčina [Vďaka @CrafterSvK] + 日本語 [協力 ysakamoto] - Auto / Follow System - Auto / Follow Battery - Dark - Light + Automaticky / Podle systému + Automaticky / Podle baterie + Tmavý + Světlý - - 0 - 1 - 2 - 3 + + Instagram default (Unread then read) + Od nejnovějších po nejstarší + Od nejstarších po nejnovější - None + Žádný \@ at on \| - - - Loading... - - - dd-MM-yyyy - dd/MM/yyyy - dd.MM.yyyy - dd-MM-yy - dd/MM/yy - dd.MM.yy - dd-MMM-yyyy - dd/MMM/yyyy - dd.MMM.yyyy - dd-MMM-yy - dd/MMM/yy - dd.MMM.yy - MM-dd-yyyy - MM/dd/yyyy - MM.dd.yyyy - yyyy-MM-dd - yyyy/MM/dd - yyyy.MM.dd - MM-dd-yy - MM/dd/yy - MM.dd.yy - yy-MM-dd - yy/MM/dd - yy.MM.dd - MMM-dd-yyyy - MMM/dd/yyyy - MMM.dd.yyyy - yyyy-MMM-dd - yyyy/MMM/dd - yyyy.MMM.dd - MMM-dd-yy - MMM/dd/yy - MMM.dd.yy - yy-MMM-dd - yy/MMM/dd - yy.MMM.dd - - - hh:mm:ss a - h:mm:ss a - HH:mm:ss - H:mm:ss - @string/title_dm @string/feed diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 743529ee..e9413f87 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -23,7 +23,7 @@ Oblíbené Objevit Komentáře - Upozornění + Activity Zvýraznit: %s Zkontrolovat aktualizace při spuštění Stáhnout příspěvky do složek s uživatelským jménem @@ -32,112 +32,136 @@ Označovat přímou zprávu po zobrazení jako zobrazenou Ostatní členové uvidí, že jste si ji ji zobrazili Enable activity notifications + Feed stories sort Error loading profile!\nTry logging in and search again. Error creating Download folder(s). - Save to custom folder - Select folder - Theme + Uložit do vlastní složky + Vybrat adresář + Motiv Only affects logged-in users: Only affects anonymous users: Use Instadp for high definition profile pictures - Import/Export - Language - %s\nPosts - %s Posts - %s\nFollowers - %s\nFollowing - Autoplay videos - Always mute videos - Select what to download + Importovat/exportovat + Jazyk + + %s příspěvek + %s příspěvky + %s příspěvků + %s příspěvků + + + %s sledující + %s sledující + %s sledujících + %s sledujících + + %s Following + Videa spouštět automaticky + Vždy ztlumit videa + Vyberte, co stáhnout Current Whole Album - Show stories - No more stories! - Be patient! - View Post - View Post + Zobrazit příběhy + Žádné další příběhy! + Buďte trpěliví! + Zobrazit příspěvek + Zobrazit příspěvek Spotify - Vote - Vote successful! - You have already voted! - Respond + Hlasovat + Hlasování úspěšné! + Už jste hlasoval! + Odpovědět Answer… Answer successful! + + %d response averaging %s + %d responses averaging %s + %d responses averaging %s + %d responses averaging %s + + Your answer: %s Reply to story Reply… - Quiz + Kvíz + Slider You have already answered! - Mentions - This Account is Private + Zmínky + Tento účet je soukromý You won\'t be able to access posts after unfollowing! Are you sure? You can log in via More -> Account on the bottom-right corner or you can view public accounts without login! You can swipe left/right for explore/feed, or search something below! - This Account has No Posts + Tento účet nezveřejnil žádné příspěvky No Such Posts! Current version: v%s read more… - Login - Logout + Přihlásit se + Odhlásit se Browse Instagram anonymously - Remove all accounts - This will remove all added accounts from the app!\nTo remove just one account, long tap the account from the account switcher dialog.\nDo you want to continue? + Odstranit všechny účty + Tato akce odstraní všechny přidané účty z aplikace!\nChcete-li odstranit jen jeden účet, dlouze ho podržte v přepínači účtů.\nChcete stále pokračovat? Date format Liked Saved Tagged Message - Like - Unlike + To se mi líbí + Už se mi to nelíbí Bookmark Unbookmark - Follow + Sledovat Unfollow Favorite Unfavorite - Block - Unblock - Restrict - Unrestrict - Map - Export - Import + Zablokovat + Odblokovat + Omezit + Zrušit omezení + Copy bio + Translate bio + Mutual + Following + Follower + Mapa + Exportovat + Importovat Export Logins - Accounts - Settings - Favorites - Import settings + Účty + Nastavení + Oblíbené + Importovat nastavení Import Logins - Import accounts - Import favorites - Successfully imported! - Failed to import! - Successfully exported! - Failed to export! - Password is empty! - Refresh - Get cookies + Importovat účty + Importovat oblíbené + Úspěšně importováno! + Import se nezdařil! + Úspěšně exportováno! + Export se nezdařil! + Heslo nebylo vyplněno! + Načíst znovu + Získat cookies Desktop Mode Use custom format Separator - Time Format - Date Format - Preview - Swap Time and Date positions - Cannot delete currently in use account - Are you sure you want to delete \'%s\'? - Open profile - View profile picture - You + Formát času + Formát datumu + Náhled + Prohodit pozice času a datumu + Nemůžete smazat aktuálně používaný účet + Opravdu chcete smazat \'%s\'? + Otevřít profil + Zobrazit příběh + Zobrazit profilovou fotku + Vy Shared a link Shared a media Shared a timed message Replied to a story Reacted on a story Mentioned in a story - Unsupported message type - Open link - Copy text - Download attachment + Nepodporovaný typ zprávy + Otevřít odkaz + Zkopírovat text + Stáhnout přílohu Like message Unlike message Unsend message @@ -146,18 +170,18 @@ Unknown media type Media expired! Delivered - Sent - Opened - Replayed - Sending… - Blocked + Odesláno + Otevřeno + Znovu přehráno + Odesílám… + Blokován Suggested Screenshotted Cannot deliver Great success! - Leave - Leave this chat? - Kick + Opustit + Chcete opustit tento chat? + Vyhodit Left users Download directly Downloads posts directly to the phone! @@ -175,10 +199,12 @@ You can only download 100 posts at a time. Don\'t be too greedy! Copy username Copy comment + View comment likers Reply to comment Like comment Unlike comment - Delete comment + Přeložit komentář + Odstranit komentář No empty comments! Do you want to search the username? Do you want to search the hashtag? @@ -188,9 +214,9 @@ Both following each other not following %s %s is not following - Error loading cookies - Write a new comment… - Write a new message… + Chyba při načítání souborů cookies + Napsat nový komentář… + Napsat novou zprávu… Liked your post Commented on your post: Started following you @@ -205,9 +231,11 @@ An update is available! (%s) Reminder: If you downloaded from F-Droid, you must update from it! Same applies for GitHub. Thank you for updating Barinsta! - App crashed - Oops.. the app crashed, but don\'t worry you can send error report to the developer to help him fix the issue. (: + Aplikace spadla + Oops.. aplikace se zhroutila, ale nebojte se, můžete poslat chybové hlášení vývojáři, abyste mu pomohli vyřešit tuto chybu. (: Activity + Story archive + Suggested users Select Picture Uploading… You have: @@ -223,18 +251,18 @@ DM %d selected Successfully logged out! - Info + Informace Mark as seen Do not show again until next update - Version + Verze Start screen - General - Theme - Downloads - Locale - Account - Current login not working? Simply add the account again. - Add account + Obecné + Motiv + Stažené + Místní prostředí + Účet + Nejde se vám přihlásit? Prostě přidejte účet znovu. + Přidat účet License (English only) Visit our website Get support, discuss, meet others, and have fun! @@ -252,21 +280,26 @@ Barista Material Dark Added to Favorites - Add to favorites - Accounts - Hashtags - Locations + Favorited + Favorite + Účty + Hashtagy + Místa Unknown Removed from Favourites - Backup & Restore - Create - Restore + Backup & Restore User Settings + Back up app settings, account login information, and/or favorites data to a plain text or encrypted backup file for later restoration. + If you\'re backing up login info, treat the file as confidential: Keep them somewhere safe! + Create new backup file + Restore from existing backup file File: - Enter password + Zadejte heslo Select a backup file (.zaai/.backup) - Apply - Save + Použít + Uložit Caption + Edit caption... + Translate caption... Video player timeline Liking… Like unsuccessful @@ -282,20 +315,23 @@ Delete Comment Layout + Feed stories Opening post... - Share - Layout style - Column count + Sdílet + Styl rozložení + Počet sloupců 2 3 - Show names - Show avatars - Avatar size - Corners - Show grid gap - Disable animation - Please wait for the current task to complete first! + Zobrazit jména + Zobrazit avatary + Velikost avataru + Rohy + Zobrazit mezery v mřížce + Zakázat animace + Počkejte prosím nejdříve na dokončení aktuální úlohy! + Depending on user counts, this can take a while to load. Please be patient. Post not found! + No app found which opens urls %d like %d likes @@ -308,4 +344,11 @@ %d comments %d comments + + %s story + %s stories + %s stories + %s stories + + Přístup k úložišti nebyl udělen! diff --git a/app/src/main/res/values-de/arrays.xml b/app/src/main/res/values-de/arrays.xml index 3a96e398..6e04749e 100644 --- a/app/src/main/res/values-de/arrays.xml +++ b/app/src/main/res/values-de/arrays.xml @@ -2,41 +2,26 @@ Systemstandard - Englisch - Französisch [Danke an @kernoeb] - Spanisch [Danke an @sguinetti] - Chinesisch (Vereinfacht) - Indonesisch [Danke an @Galang23] - Italienisch [Danke an @RAR_Ramar] - Deutsch [Danke an @peterge1998] - Polnisch [Danke an @Lego8486] - Türkisch [Danke an @faydin90] - Brasilianisches Portugiesisch [Danke an @wagnim] - Persisch [Danke an @farzadx] - Mazedonisch [Danke an @snajdovski] - Vietnamesisch [Danke an @fouze555] - Chinesische Traditionelle Sprache [Danke an @Still34] - Catalan [Thanks to @retiolus] - Russian [Thanks to @rikishi0071] - - - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 + English + Français [Merci à @kernoeb et @PierreM0] + Español [Gracias a @sguinetti, @akrai y @retiolus] + 简体中文 + Bahasa Indonesia [Terima kasih @Galang23] + Italiano [Grazie a @RAR_Ramar e GiorgioHerbie] + Deutsch [Danke an @peterge1998] + Polski [Podziękowania dla @Lego8486] + Türkçe [@faydin90 tarafından] + Português (Brasil) [Obrigado @wagnim, @RickyM7 e @cizordj] + پارسی [ با سپاس از farzadx@ ] + Македонски [Благодарност до @snajdovski] + Tiếng Việt [bởi Yato Fouze] + 繁體中文 [感謝 @Still34] + Català [Gràcies a @retiolus] + Русский [Спасибо @rikishi0071] + हिन्दी + Nederlands [Met dank aan Lesley Natrop] + Slovenčina [Vďaka @CrafterSvK] + 日本語 [協力 ysakamoto] Auto / Systemstandard @@ -44,11 +29,10 @@ Dunkel Hell - - 0 - 1 - 2 - 3 + + Instagram default (Unread then read) + From newest to oldest + From oldest to newest Kein(e) @@ -58,53 +42,6 @@ \| - - - Lädt... - - - dd-MM-yyyy - dd/MM/yyyy - dd.MM.yyyy - dd-MM-yy - dd/MM/yy - dd.MM.yy - dd-MMM-yyyy - dd/MMM/yyyy - dd.MMM.yyyy - dd-MMM-yy - dd/MMM/yy - dd.MMM.yy - MM-dd-yyyy - MM/dd/yyyy - MM.dd.yyyy - yyyy-MM-dd - yyyy/MM/dd - yyyy.MM.dd - MM-dd-yy - MM/dd/yy - MM.dd.yy - yy-MM-dd - yy/MM/dd - yy.MM.dd - MMM-dd-yyyy - MMM/dd/yyyy - MMM.dd.yyyy - yyyy-MMM-dd - yyyy/MMM/dd - yyyy.MMM.dd - MMM-dd-yy - MMM/dd/yy - MMM.dd.yy - yy-MMM-dd - yy/MMM/dd - yy.MMM.dd - - - hh:mm:ss a - h:mm:ss a - HH:mm:ss - H:mm:ss - @string/title_dm @string/feed diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4a088279..a5bf0edc 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -23,7 +23,7 @@ Favoriten Entdecken Kommentare - Benachrichtigungen + Aktivität Hervorheben: %s Beim Start auf Aktualisierungen prüfen Beiträge in Benutzernamen-Ordner herunterladen @@ -32,6 +32,7 @@ Direktnachrichten nach dem Ansehen als gesehen markieren Andere Mitglieder werden wissen, dass du sie gesehen hast Aktivitätsbenachrichtigungen aktivieren + Feed stories sort Fehler beim Laden des Profils!\nVersuche dich einzuloggen und erneut zu suchen. Fehler beim Erstellen von Download-Ordner(n). In benutzerdefiniertem Ordner speichern @@ -42,10 +43,15 @@ Instadp für hochauflösende Profilbilder verwenden Importieren/Exportieren Sprache - %s\nBeiträge - %s Beiträge - %s\nAbonnenten - %s\nAbonniert + + %s Beitrag + %s Beiträge + + + %s Abonnent + %s Abonnenten + + %s Abonniert Videos automatisch abspielen Videos immer stummschalten Datei zum Download auswählen @@ -61,11 +67,17 @@ Abstimmung erfolgreich! Du hast bereits abgestimmt! Antworten - Answer… + Antworten… Antwort erfolgreich! + + %d Antwort im Durchschnitt %s + %d Antworten im Durchschnitt %s + + Deine Antwort: %s Auf Story antworten - Reply… + Antworten… Quiz + Schieberegler Du hast bereits geantwortet! Erwähnungen Dieser Account ist privat @@ -86,8 +98,8 @@ Gespeichert Markiert Nachricht - Like - Unlike + Gefällt mir + Gefällt mir nicht mehr Lesezeichen Lesezeichen entfernen Folgen @@ -98,6 +110,11 @@ Nicht mehr blockieren Einschränken Nicht mehr einschränken + Steckbrief kopieren + Steckbrief übersetzen + Mutual + Abonniert + Abonnent Karte Exportieren Importieren @@ -126,6 +143,7 @@ Derzeit verwendetes Konto kann nicht gelöscht werden Bist du sicher, dass du \'%s \' löschen möchtest? Profil öffnen + Story ansehen Profilbild ansehen Du Einen Link geteilt @@ -157,7 +175,7 @@ Großer Erfolg! Verlassen Chat verlassen? - Kick + Entfernen Left users Direkt herunterladen Lade Posts direkt auf das Handy herunter! @@ -165,7 +183,7 @@ Bitte erteile die Berechtigung und versuche das Herunterladen erneut! Download gestartet Download abgeschlossen - Downloading post… + Beitrag wird heruntergeladen… Medien herunterladen Profilbild wird heruntergeladen Datei im Download-Verzeichnis heruntergeladen! @@ -175,9 +193,11 @@ Du kannst nur 100 Beiträge gleichzeitig herunterladen. Sei nicht zu gierig! Benutzername kopieren Kommentar kopieren + View comment likers Auf Kommentar antworten Kommentar gefällt mir Kommentar gefällt mir nicht mehr + Kommentar übersetzen Kommentar löschen Keine leeren Kommentare! Möchtest du nach dem Benutzernamen suchen? @@ -189,27 +209,29 @@ Hat %s nicht abonniert Nicht von %s abonniert Fehler beim Laden der Cookies - Write a new comment… - Write a new message… + Schreibe einen neuen Kommentar… + Schreibe eine neue Nachricht… gefällt dein Foto. Hat deinen Beitrag kommentiert: folgt dir jetzt. Hat dich erwähnt: - Tagged you in a post + Hat dich in einem Beitrag getaggt Angefordert dir zu folgen Anfrage annehmen Anfrage ablehnen - Share this public post to… + Diesen öffentlichen Beitrag teilen mit… Dies ist ein privater Beitrag! Teile ihn mit denen, die ihn sehen können! - This category is somehow empty… + Diese Kategorie ist leer… Ein Update ist verfügbar! (%s) Erinnerung: Wenn du die App von F-Droid heruntergeladen hast, musst du sie dort aktualisieren! Das gilt auch für GitHub. Danke für die Aktualisierung von Barinsta! App abgestürzt Hoppla.. die App ist abgestürzt, aber keine Sorge — du kannst Fehlerberichte an den Entwickler senden, um ihm zu helfen, das Problem zu beheben. (: Aktivität + Story archive + Suggested users Bild auswählen - Uploading… + Hochladen… Du hast: %d Abonnenten %d Kommentare @@ -252,56 +274,69 @@ Barista Material Dark Zu Favoriten hinzugefügt - Zu Favoriten hinzufügen + Favorisiert + Favorisieren Konten Hashtags Standorte Unbekannt Aus Favoriten entfernt - Sichern & Wiederherstellen - Erstellen - Wiederherstellen + Backup & Restore User Settings + Back up app settings, account login information, and/or favorites data to a plain text or encrypted backup file for later restoration. + If you\'re backing up login info, treat the file as confidential: Keep them somewhere safe! + Create new backup file + Restore from existing backup file Datei: Passwort eingeben Wähle eine Sicherungsdatei (.zaai/.backup) Anwenden Speichern - Caption - Video player timeline - Liking… - Like unsuccessful - Unlike unsuccessful - Unliking… - Controls - Saving… - Removing… - Save unsuccessful - Remove unsuccessful - Downloading… - Download item %d of %d - Delete - Comment + Bildunterschrift + Beschriftung bearbeiten... + Bildunterschrift übersetzen... + Zeitleiste des Videoplayers + Gefällt… + Hinzufügen fehlgeschlagen + Entfernen fehlgeschlagen + …entfernen + Steuerung + Speichern… + Wird entfernt… + Speichern fehlgeschlagen + Entfernen fehlgeschlagen + Herunterladen… + Element %d von %d herunterladen + Löschen + Kommentar Layout - Opening post... - Share + Feed stories + Öffne Beitrag... + Teilen Layoutstil Spaltenanzahl 2 3 Namen anzeigen Avatare anzeigen - Avatar size - Corners + Avatargröße + Ecken Rasterabstand anzeigen - Disable animation - Please wait for the current task to complete first! - Post not found! + Animationen deaktivieren + Bitte warte bis die aktuelle Aufgabe abgeschlossen ist! + Abhängig von der Anzahl der Benutzer kann dies eine Weile dauern. Bitte haben Sie etwas Geduld. + Beitrag nicht gefunden! + Keine App gefunden, die URLs öffnet - %d like - %d likes + %d Person gefällt das + %d Personen gefällt das - %d comment - %d comments + %d Kommentar + %d Kommentare + + %s story + %s stories + + Speicher-Zugriff nicht erteilt! diff --git a/app/src/main/res/values-es/arrays.xml b/app/src/main/res/values-es/arrays.xml index 6d597809..5542fae4 100755 --- a/app/src/main/res/values-es/arrays.xml +++ b/app/src/main/res/values-es/arrays.xml @@ -2,41 +2,26 @@ Predeterminado del sistema - Inglés - Francés [Gracias a @kernoeb] - Español [Gracias a @akrai] - Chino Simplificado - Indonesio [Gracias a @Galang23] - Italiano [Gracias a @RAR_Ramar] - Alemán [Gracias a @peterge1998] - Polaco [Gracias a @Lego8486] - Turco [Gracias a @faydin90] - Portugués brasileño [Gracias a @wagnim] - Persa [Gracias a @farzadx] - Macedonio [Gracias a @snajdovski] - Vietnamita [Gracias a @fouze555] - Chino tradicional [Gracias a @Still34] - Catalan [Thanks to @retiolus] - Russian [Thanks to @rikishi0071] - - - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 + English + Français [Merci à @kernoeb et @PierreM0] + Español [Gracias a @sguinetti, @akrai y @retiolus] + 简体中文 + Bahasa Indonesia [Terima kasih @Galang23] + Italiano [Grazie a @RAR_Ramar e GiorgioHerbie] + Deutsch [Danke an @peterge1998] + Polski [Podziękowania dla @Lego8486] + Türkçe [@faydin90 tarafından] + Português (Brasil) [Obrigado @wagnim, @RickyM7 e @cizordj] + پارسی [ با سپاس از farzadx@ ] + Македонски [Благодарност до @snajdovski] + Tiếng Việt [bởi Yato Fouze] + 繁體中文 [感謝 @Still34] + Català [Gràcies a @retiolus] + Русский [Спасибо @rikishi0071] + हिन्दी + Nederlands [Met dank aan Lesley Natrop] + Slovenčina [Vďaka @CrafterSvK] + 日本語 [協力 ysakamoto] Auto / Seguir al sistema @@ -44,11 +29,10 @@ Oscuro Claro - - 0 - 1 - 2 - 3 + + Predeterminado de Instagram (no leídos, después leídos) + Del más reciente al más antiguo + Del más antiguo al más reciente Ninguno @@ -58,53 +42,6 @@ \| - - - Cargando... - - - dd-MM-yyyy - dd/MM/yyyy - dd.MM.yyyy - dd-MM-yy - dd/MM/yy - dd.MM.yy - dd-MMM-yyyy - dd/MMM/yyyy - dd.MMM.yyyy - dd-MMM-yy - dd/MMM/yy - dd.MMM.yy - MM-dd-yyyy - MM/dd/yyyy - MM.dd.yyyy - yyyy-MM-dd - yyyy/MM/dd - yyyy.MM.dd - MM-dd-yy - MM/dd/yy - MM.dd.yy - yy-MM-dd - yy/MM/dd - yy.MM.dd - MMM-dd-yyyy - MMM/dd/yyyy - MMM.dd.yyyy - yyyy-MMM-dd - yyyy/MMM/dd - yyyy.MMM.dd - MMM-dd-yy - MMM/dd/yy - MMM.dd.yy - yy-MMM-dd - yy/MMM/dd - yy.MMM.dd - - - hh:mm:ss a - h:mm:ss a - HH:mm:ss - H:mm:ss - @string/title_dm @string/feed diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 9dee9c34..a18642a9 100755 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -23,15 +23,16 @@ Favoritos Explorar Comentarios - Notificaciones + Actividad Resaltar: %s Buscar actualizaciones al inicio - Descargar publicaciones en carpetas por nombre de usuario + Usar carpetas con el nombre de usuario Marcar historias como vistas después de verlas El autor de la historia sabrá que lo has visto Marcar Mensaje Directo como visto después de verlo Otros miembros sabrán que lo has visto Activar notificaciones de actividad + Orden de las historias ¡Error al cargar el perfil!\nPrueba iniciando sesión y buscando de nuevo. Error creando carpeta(s) de descarga. Guardar en carpeta personalizada @@ -42,10 +43,15 @@ Usar Instadp para imágenes de perfil de alta definición Importar/Exportar Idioma - %s\nPublicaciones - %s Publicaciones - %s\nSeguidores - %s\nSiguiendo + + %s publicación + %s publicaciones + + + %s seguidor + %s seguidores + + %s seguidos Autorreproducir vídeos Siempre silenciar vídeos Seleccionar qué descargar @@ -63,9 +69,15 @@ Comentar Responder… ¡Has respondido con éxito! + + %d respuesta promediando un %s + %d respuestas promediando un %s + + Tu respuesta: %s Responder a la historia Responder… Cuestionario + Deslizador ¡Ya has respondido! Menciones Esta cuenta es privada @@ -98,6 +110,11 @@ Desbloquear Restringir Desrestringir + Copiar biografía + Traducir biografía + Mutuo + Siguiendo + Seguidor Mapa Exportar Importar @@ -126,6 +143,7 @@ No se puede borrar la cuenta actualmente en uso ¿Estás seguro de que quieres borrar \'%s\'? Abrir perfil + Ver historia Ver foto de perfil Compartió un enlace @@ -175,16 +193,18 @@ Sólo puedes descargar 100 publicaciones a la vez. ¡No seas abaricioso! Copiar nombre de usuario Copiar comentario + Ver me gustas del comentario Responder a comentario Dar me gusta a comentario Quitar me gusta a comentario + Traducir comentario Eliminar comentario ¡No hay comentarios vacíos! ¿Quieres buscar el nombre de usuario? ¿Quieres buscar el hashtag? Seguidores Siguiendo - Comparando seguidores & siguiendo + Comparando seguidores & seguidos Ambos se siguen mutuamente no está siguiendo %s %s no está siguiendo @@ -208,6 +228,8 @@ La aplicación se ha bloqueado Vaya.. La aplicación dejó de funcionar, pero no te preocupes, puedes enviar un reporte de error al desarrollador para ayudarle a arreglar el problema. (: Actividad + Archivo de historias + Usuarios sugeridos Seleccionar imagen Subiendo… Tienes: @@ -251,22 +273,27 @@ Tema oscuro Barista Material oscuro - Añadido a favoritos - Agregar a favoritos + En favoritos + En favoritos + Agregar a favoritos Cuentas Hashtags Ubicaciones Desconocido Eliminado de favoritos - Respaldar & Restaurar - Crear - Restaurar + Respaldar & restaurar ajustes del usuario + Respalda los ajustes de la aplicación, inicio de sesión de cuentas y/o cuentas favoritas a un archivo de texto plano o un archivo cifrado para su posterior restauración. + Si está haciendo un respaldo de los inicios de sesión de cuentas, trate el archivo como confidencial: ¡Manténgalos en algún lugar seguro! + Crear un nuevo archivo del respaldo + Restaurar desde un archivo de respaldo existente Archivo: Introduzca contraseña Seleccione un archivo de respaldo (.zaai/.backup) Aplicar Guardar Leyenda + Editar leyenda... + Traducir leyenda... Línea de tiempo del reproductor de vídeo Gustando… Me gusta fallido @@ -282,6 +309,7 @@ Borrar Comentario Disposición + Muro de historias Abriendo publicación... Compartir Estilo de disposición @@ -295,7 +323,9 @@ Mostrar espaciado de cuadrícula Desactivar animaciones ¡Por favor, espera a que la tarea actual se complete primero! - Post not found! + Dependiendo de la cuenta de usuario, esto puede tardar un tiempo. Por favor, sea paciente. + ¡Publicación no encontrada! + No se encontró ninguna aplicación que abra los enlaces %d me gusta %d me gustas @@ -304,4 +334,9 @@ %d comentario %d comentarios + + %s historia + %s historias + + ¡Permiso de almacenamiento no concecido! diff --git a/app/src/main/res/values-fa/arrays.xml b/app/src/main/res/values-fa/arrays.xml index 9583530a..fdbbe30f 100644 --- a/app/src/main/res/values-fa/arrays.xml +++ b/app/src/main/res/values-fa/arrays.xml @@ -2,41 +2,26 @@ پیشگزیده سامانه - انگلیسی - فرانسوی [ با سپاس از kernoeb@ ] - اسپانیایی [ با سپاس از sguinetti@ ] - چینی ساده شده - اندونزیایی [ با سپاس از Galang23@ ] - ایتالیایی [ با سپاس از RAR_Ramar@ ] - آلمانی [ با سپاس از peterge1998@ ] - لهستانی [ با سپاس از Lego8486@ ] - ترکی [ با سپاس از faydin90@ ] - برزیلی پرتغالی [ با سپاس از wagnim@ ] - پارسی [ با سپاس از Frzking@ ] - مقدونی [ با تشکر از snajdovski@ ] - ویتنامی [ با سپاس از fouze555@ ] - Chinese Traditional [Thanks to @Still34] - Catalan [Thanks to @retiolus] - Russian [Thanks to @rikishi0071] - - - ۰ - ۱ - ۲ - ۳ - ۴ - ۵ - ۶ - ۷ - ۸ - ۹ - ۱۰ - 11 - 12 - 13 - 14 - 15 - 16 + English + Français [Merci à @kernoeb et @PierreM0] + Español [Gracias a @sguinetti, @akrai y @retiolus] + 简体中文 + Bahasa Indonesia [Terima kasih @Galang23] + Italiano [Grazie a @RAR_Ramar e GiorgioHerbie] + Deutsch [Danke an @peterge1998] + Polski [Podziękowania dla @Lego8486] + Türkçe [@faydin90 tarafından] + Português (Brasil) [Obrigado @wagnim, @RickyM7 e @cizordj] + پارسی [ با سپاس از farzadx@ ] + Македонски [Благодарност до @snajdovski] + Tiếng Việt [bởi Yato Fouze] + 繁體中文 [感謝 @Still34] + Català [Gràcies a @retiolus] + Русский [Спасибо @rikishi0071] + हिन्दी + Nederlands [Met dank aan Lesley Natrop] + Slovenčina [Vďaka @CrafterSvK] + 日本語 [協力 ysakamoto] خودکار / پیروی از سامانه @@ -44,11 +29,10 @@ تیره روشن - - ۰ - ۱ - ۲ - ۳ + + Instagram default (Unread then read) + From newest to oldest + From oldest to newest هیچکدام @@ -58,53 +42,6 @@ \| - - - در بارگیری... - - - dd-MM-yyyy - dd/MM/yyyy - dd.MM.yyyy - dd-MM-yy - dd/MM/yy - dd.MM.yy - dd-MMM-yyyy - dd/MMM/yyyy - dd.MMM.yyyy - dd-MMM-yy - dd/MMM/yy - dd.MMM.yy - MM-dd-yyyy - MM/dd/yyyy - MM.dd.yyyy - yyyy-MM-dd - yyyy/MM/dd - yyyy.MM.dd - MM-dd-yy - MM/dd/yy - MM.dd.yy - yy-MM-dd - yy/MM/dd - yy.MM.dd - MMM-dd-yyyy - MMM/dd/yyyy - MMM.dd.yyyy - yyyy-MMM-dd - yyyy/MMM/dd - yyyy.MMM.dd - MMM-dd-yy - MMM/dd/yy - MMM.dd.yy - yy-MMM-dd - yy/MMM/dd - yy.MMM.dd - - - hh:mm:ss a - h:mm:ss a - HH:mm:ss - H:mm:ss - @string/title_dm @string/feed diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index daeb67c6..cfb7e714 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -23,7 +23,7 @@ علاقه مندی ها کاوش دیدگاه‌ها - آگهداد + فعالیت درخشندگی: %s بررسی بروزرسانی هنگام آغاز برنامه بارگیری پست ها در پوشه های به نام کاربر @@ -32,6 +32,7 @@ نشان کردن پیام خصوصی بعنوان دیده شده بعد از دیدن کاربران دیگر خواهند فهمید شما این را دیده اید روشن کردن آگهداد فعالیت ها + Feed stories sort خطا در نمایش نمایه!\n دوباره وارد شوید و جستجو کنید. خطا در ساخت پوشه(ها). ذخیره در پوشه سفارشی @@ -42,10 +43,15 @@ استفاده از Instadp برای بالا بردن وضوح عکس‌های نمایه وارد کردن/خروجی گرفتن زبان - %s\n پست‌ها - %s پست ها - %s\nدنبال کننده ها - %s\nدنبال کننده ها + + %s Post + %s Posts + + + %s Follower + %s Followers + + %s Following پخش خودکار فیلم ها همیشه فیلم هارو بی صدا کن انتخاب کن چی دانلود کنی @@ -63,9 +69,15 @@ پاسخ پاسخ… جواب درست است! + + %d response averaging %s + %d responses averaging %s + + Your answer: %s پاسخ به استوری پاسخ… نظرسنجی + Slider در حال حاظر شما رای داده اید! یادآوری ها این اکانت شخصی است @@ -99,6 +111,11 @@ رفع مسدودیت محدود کردن حذف محدودیت + Copy bio + Translate bio + Mutual + Following + Follower نقشه صادر کردن وارد کردن @@ -127,6 +144,7 @@ نمیتواند حذف شود این اکانت در حال استفاده است آیا مطمن هستید میخواهید حذف کنید \'%s\'؟ باز کردن پروفایل + View story دیدن عکس پروفایل شما یک لینک به اشتراک گذاشته شد @@ -176,9 +194,11 @@ شما فقط 100 پست در یک زمان میتوانید دانلود کنید. خیلی حریص نباشید! کپی نام کاربری کپی دیدگاه + View comment likers پاسخ به دیدگاه پسندیدن دیدگاه نپسندیدن دیدگاه + Translate comment هذف دیدگاه بدون دیدگاه تهی! ایا میخواهید نام کاربری جستوجو کنید ؟ @@ -209,6 +229,8 @@ برنامه خراب شد اوپسس.. برنامه خراب است، اما نگران نباشید شما میتوانید گزارش خطا را برای توسعه دهنده بفرستید برای کمک به او در درست کردن مشکل. (: فعالیت + Story archive + Suggested users انتخاب تصویر Uploading… شما باید: @@ -253,21 +275,26 @@ باریستا متریال تیره به علاقه مندی ها افزوده شد - افزودن به علاقه ‌مندی ‌ها + Favorited + Favorite هساب‌ها هشتگ ها جاها ناشناخته از علاقه مندی ها حذف شده - پشتیبان گیری & بازیابی - ساخت - بازیابی + Backup & Restore User Settings + Back up app settings, account login information, and/or favorites data to a plain text or encrypted backup file for later restoration. + If you\'re backing up login info, treat the file as confidential: Keep them somewhere safe! + Create new backup file + Restore from existing backup file پرونده: رمز عبور را وارد کنید یک فایل پشتیبان گیری انتخاب کنید (.zaai/.backup) گماشتن Save Caption + Edit caption... + Translate caption... Video player timeline پسندیدن… Like unsuccessful @@ -283,6 +310,7 @@ هذف دیدگاه Layout + Feed stories باز کردن پیک... همرسانی Layout style @@ -296,7 +324,9 @@ Show grid gap Disable animation Please wait for the current task to complete first! + Depending on user counts, this can take a while to load. Please be patient. Post not found! + No app found which opens urls %d پسند %d پسند @@ -305,4 +335,9 @@ %d دیدگاه %d دیدگاه + + %s story + %s stories + + Storage permission not granted! diff --git a/app/src/main/res/values-fr/arrays.xml b/app/src/main/res/values-fr/arrays.xml index ad7fe84b..b55f2763 100755 --- a/app/src/main/res/values-fr/arrays.xml +++ b/app/src/main/res/values-fr/arrays.xml @@ -2,41 +2,26 @@ Par défaut (Système) - Anglais - Français [Merci à @kernoeb] - Espagnol [Merci à @sguinetti] - Chinois Simplifié - Indonésien [Merci à @Galang23] - Italien [Merci à @RAR_Ramar] - Allemand [Merci à @peterge1998] - Polonais [Merci à @Lego8486] - Turc [Merci à @faydin90] - Portugais brésilien [Merci à @wagnim] - Perse [Merci à @farzadx] - Macédonien [merci à @snajdovski] - Vietnamien [merci à @fouze555] - Chinois traditionnel [Merci à @Still34] - Catalan [Merci à @retiolus] - Russe [Merci à @rikishi0071] - - - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 + English + Français [Merci à @kernoeb et @PierreM0] + Español [Gracias a @sguinetti, @akrai y @retiolus] + 简体中文 + Bahasa Indonesia [Terima kasih @Galang23] + Italiano [Grazie a @RAR_Ramar e GiorgioHerbie] + Deutsch [Danke an @peterge1998] + Polski [Podziękowania dla @Lego8486] + Türkçe [@faydin90 tarafından] + Português (Brasil) [Obrigado @wagnim, @RickyM7 e @cizordj] + پارسی [ با سپاس از farzadx@ ] + Македонски [Благодарност до @snajdovski] + Tiếng Việt [bởi Yato Fouze] + 繁體中文 [感謝 @Still34] + Català [Gràcies a @retiolus] + Русский [Спасибо @rikishi0071] + हिन्दी + Nederlands [Met dank aan Lesley Natrop] + Slovenčina [Vďaka @CrafterSvK] + 日本語 [協力 ysakamoto] Automatique (Système) @@ -44,11 +29,10 @@ Sombre Clair - - 0 - 1 - 2 - 3 + + Instagram par défaut (Non lu puis lu) + Du plus récent au plus ancien + Du plus ancien au plus récent Aucun @@ -58,53 +42,6 @@ \| - - - Chargement ... - - - dd-MM-yyyy - dd/MM/yyyy - dd.MM.yyyy - dd-MM-yy - dd/MM/yy - dd.MM.yy - dd-MMM-yyyy - dd/MMM/yyyy - dd.MMM.yyyy - dd-MMM-yy - dd/MMM/yy - dd.MMM.yy - MM-dd-yyyy - MM/dd/yyyy - MM.dd.yyyy - yyyy-MM-dd - yyyy/MM/dd - yyyy.MM.dd - MM-dd-yy - MM/dd/yy - MM.dd.yy - yy-MM-dd - yy/MM/dd - yy.MM.dd - MMM-dd-yyyy - MMM/dd/yyyy - MMM.dd.yyyy - yyyy-MMM-dd - yyyy/MMM/dd - yyyy.MMM.dd - MMM-dd-yy - MMM/dd/yy - MMM.dd.yy - yy-MMM-dd - yy/MMM/dd - yy.MMM.dd - - - hh:mm:ss a - h:mm:ss a - HH:mm:ss - H:mm:ss - @string/title_dm @string/feed diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 99aa7aa4..bcc4529b 100755 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -23,7 +23,7 @@ Favoris Découvrir Commentaires - Notifications + Activité Temps forts: %s Rechercher les mises à jours au démarrage Télécharger les messages dans les dossiers des noms d\'utilisateurs @@ -32,6 +32,7 @@ Marquer les messages privés comme vus après consultation Les autres utilisateurs saurons que vous l\'avez vu Activer les notifications d\'activités + Tri du flux de stories Erreur de chargement du profil!\nVeuillez vous reconnecter et réessayez. Erreur de création de dossier(s) de téléchargement. Enregistrer dans un dossier personnalisé @@ -42,10 +43,15 @@ Utiliser Instadp pour les photos de profil haute définition Importer/Exporter Langues - %s\nPosts - %s Publications - %s\nAbonnés - %s\nAbonnements + + %s Publication + %s Publications + + + %s Abonné + %s Abonnés + + %s Abonnements Lecture automatique des vidéos Toujours couper le son des vidéos Sélectionnez ce que vous souhaitez télécharger @@ -63,9 +69,15 @@ Répondre Réponse… Réponse envoyée ! + + %d réponse en moyenne %s + %d réponses en moyenne %s + + Votre réponse : %s Répondre à la story Réponse… Quiz + Curseur Vous avez déjà répondu ! Mentions Compte privé @@ -98,6 +110,11 @@ Débloquer Restreindre Retirer des restrictions + Copier la bio + Traduire la bio + Mutuel + Abonnements + Abonné Carte Exporter Importer @@ -126,6 +143,7 @@ Impossible de supprimer le compte actuellement utilisé Êtes-vous sûr de vouloir supprimer \'%s\' ? Ouvrir le profil + Voir la story Voir la photo de profil Vous A partagé un lien @@ -175,9 +193,11 @@ Vous ne pouvez télécharger que 100 publications à la fois. Ne soyez pas trop gourmands ! Copier le nom d\'utilisateur Copier le commentaire + Voir les \"j\'aime\" du commentaire Répondre au commentaire Aimer le commentaire Retirer le j\'aime du commentaire + Traduire le commentaire Supprimer le commentaire Aucun commentaire vide ! Souhaitez-vous faire une recherche sur le nom d\'utilisateur ? @@ -208,6 +228,8 @@ L\'application a planté Oups.. l\'application a planté, mais ne vous inquiétez pas, vous pouvez envoyer un rapport d\'erreur au développeur pour l\'aider à résoudre le problème :) Activité + Archive du story + Utilisateurs suggérés Sélectionnez une image Envoi en cours… Vous avez : @@ -252,21 +274,26 @@ Barista Material Dark Ajouté aux favoris - Ajouter aux favoris + Ajouté aux favoris + Ajouter aux favoris Comptes Hashtags Lieux Inconnu Supprimé des favoris - Sauvegarder & Rétablir - Créer - Rétablir + Paramètres de sauvegarde et de restauration + Sauvegardez les paramètres de l\'application, les informations de connexion du compte et/ou les données favorites sur un fichier de sauvegarde en texte brut ou chiffré pour une restauration ultérieure. + Si vous sauvegardez vos informations de connexion, traitez le fichier comme confidentiel : Gardez-les quelque part en sécurité ! + Créer un nouveau fichier de sauvegarde + Restaurer à partir du fichier de sauvegarde existant Fichier : Saisissez le mot de passe Sélectionnez un fichier de sauvegarde (.zaai/.backup) Appliquer Sauvegarder Légende + Modifier la légende... + Traduire la légende... Chronologie du lecteur vidéo Entrain d\'aimer… Erreur lors du \"j\'aime\" @@ -282,6 +309,7 @@ Effacer Commentaire Disposition + Flux de stories Entrain d\'ouvrir la publication... Partager Style du modèle @@ -295,7 +323,9 @@ Afficher les lacunes de la grille Désactiver les animations Veuillez d\'abord attendre que la tâche en cours soit terminée ! - Post not found! + Selon le nombre d\'utilisateurs, cela peut prendre un certain temps à charger. Veuillez patienter. + Publication non trouvée! + Aucune application trouvée qui ouvre les urls %d j\'aime %d j\'aimes @@ -304,4 +334,9 @@ %d commentaire %d commentaires + + %s story + %s stories + + L\'autorisation d\'accès au stockage a été refusée ! diff --git a/app/src/main/res/values-hi/arrays.xml b/app/src/main/res/values-hi/arrays.xml index d31549be..f3ebd40e 100644 --- a/app/src/main/res/values-hi/arrays.xml +++ b/app/src/main/res/values-hi/arrays.xml @@ -2,41 +2,26 @@ सिस्टम निर्धारित - अंग्रेजी - फ़्रान्सीसी [@kernoeb के द्वारा] - स्पेनी भाषा [@sguinetti के द्बारा] - चीनी (सरलीकृत) - इंडोनेशियाई भाषा [@Galang23 के द्बारा] - इटालवी [@RAR_Ramar के द्बारा] - जर्मन [@peterge1998 के द्बारा] - पोलिश भाषा [@Lego8486 के द्बारा] - तुर्की भाषा [@faydin90 के द्बारा] - ब्राजीलियाई पुर्तगाली [@wagnim के द्बारा] - फ़ारसी [@farzadx के द्बारा] - मेसिडोनियन [@snajdovski के द्बारा] - वियतनामी [@fouze555 के द्बारा] - Chinese Traditional [Thanks to @Still34] - Catalan [Thanks to @retiolus] - Russian [Thanks to @rikishi0071] - - - 0 - - - - - - - - - - १० - ११ - १२ - १३ - 14 - 15 - 16 + English + Français [Merci à @kernoeb et @PierreM0] + Español [Gracias a @sguinetti, @akrai y @retiolus] + 简体中文 + Bahasa Indonesia [Terima kasih @Galang23] + Italiano [Grazie a @RAR_Ramar e GiorgioHerbie] + Deutsch [Danke an @peterge1998] + Polski [Podziękowania dla @Lego8486] + Türkçe [@faydin90 tarafından] + Português (Brasil) [Obrigado @wagnim, @RickyM7 e @cizordj] + پارسی [ با سپاس از farzadx@ ] + Македонски [Благодарност до @snajdovski] + Tiếng Việt [bởi Yato Fouze] + 繁體中文 [感謝 @Still34] + Català [Gràcies a @retiolus] + Русский [Спасибо @rikishi0071] + हिन्दी + Nederlands [Met dank aan Lesley Natrop] + Slovenčina [Vďaka @CrafterSvK] + 日本語 [協力 ysakamoto] सिस्टम के अनुसार @@ -44,11 +29,10 @@ काला हल्का - - - - - + + Instagram default (Unread then read) + From newest to oldest + From oldest to newest कुछ नहीं @@ -58,53 +42,6 @@ \| - - - खुल रहा है... - - - dd-MM-yyyy - dd/MM/yyyy - dd.MM.yyyy - dd-MM-yy - dd/MM/yy - dd.MM.yy - dd-MMM-yyyy - dd/MMM/yyyy - dd.MMM.yyyy - dd-MMM-yy - dd/MMM/yy - dd.MMM.yy - MM-dd-yyyy - MM/dd/yyyy - MM.dd.yyyy - yyyy-MM-dd - yyyy/MM/dd - yyyy.MM.dd - MM-dd-yy - MM/dd/yy - MM.dd.yy - yy-MM-dd - yy/MM/dd - yy.MM.dd - MMM-dd-yyyy - MMM/dd/yyyy - MMM.dd.yyyy - yyyy-MMM-dd - yyyy/MMM/dd - yyyy.MMM.dd - MMM-dd-yy - MMM/dd/yy - MMM.dd.yy - yy-MMM-dd - yy/MMM/dd - yy.MMM.dd - - - hh:mm:ss a - h:mm:ss a - HH:mm:ss - H:mm:ss - @string/title_dm @string/feed diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 5a598487..60071328 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -23,7 +23,7 @@ पसंदीदा खोजिए टिप्पणियाँ - सूचनाएँ + कार्यकलाप विशेषताएँ: %s खुलने पर अपडेट के लिए जाँच करें पोस्ट को ब्यबहारकारी के नाम पर किये फोल्डरस में रखें @@ -32,6 +32,7 @@ तुम देखने के बाद सीधा संदेश को \"दिखागया\" लिखा जाएगा अलग सदस्य जानेंगे कि तुम देखे हो क्रियाकलाप सूचनाएँ दिखायें + Feed stories sort रूपरेखा लोड नहिं हो रहा है!\n लग इन करके पुनः खोज करें डाउनलोड के फोल्डर(स) बनाने में त्रुटि आ रहा है। खुद के द्बारा निर्धारित फोल्डर पे रखें @@ -42,10 +43,15 @@ अधिक स्पष्ट रूपरेखा के लिये instadp कि ब्यबहार करें। आयात/निर्यात भाषा - %s\n पोस्टस - %s पोस्टस - %s\n अनुगामीयाॅ - %s\n फलो कर रहें है + + %s Post + %s Posts + + + %s Follower + %s Followers + + %s Following वीडियो ऑटोप्ले करें सर्बदा वीडियो को शब्दहिन रखें डाउनलोड करने के लिए चयन करें @@ -63,9 +69,15 @@ जवाब उत्तर… सही उत्तर + + %d response averaging %s + %d responses averaging %s + + Your answer: %s स्टोरि पे प्रतिक्रिया करें जवाब… प्रश्नोत्तरी + Slider आप पहले से ही उत्तर दिये हो! उल्लेख यह एकाउॅट निजी है @@ -99,6 +111,11 @@ अनब्ल़ॉक करें प्रतिबंध लगाए प्रतिबंध न लगाए + Copy bio + Translate bio + Mutual + Following + Follower नक्शा निर्यात करें आयात करें @@ -127,6 +144,7 @@ बर्तमीन ब्यबह्रत एकाउँट को मिटा नहिं सकते क्या आप सचमुच %s मिटाना चाहते हैं? प्रोफाइल खुलें + View story प्रोफ़ाइल चित्र देखें आप लिंक साझा किया @@ -176,9 +194,11 @@ आप सिर्फ १०० पोस्टस एक बार में डाउनलोड कर सकते हैं. ज्यादा लोभी बनना हानिकारक हैं! उपयोगकर्ता नाम की प्रतिलिपि बनाएँ टिप्पणी की प्रतिलिपि बनाएँ + View comment likers टिप्पणी का जवाब दें टिप्पणी को पसन्द करें टिप्पणी को पसन्द न करें + Translate comment टिप्पणी हटा दें रिक्त टिप्पणी न करें! ब्यबहारकारी के नाम ढूंढने चाहते हैं? @@ -209,6 +229,8 @@ एप में त्रुटि हुयी। Oops.. the app crashed, but don\'t worry you can send error report to the developer to help him fix the issue. (: कार्यकलाप + Story archive + Suggested users चित्र का चयन करें अपलोड हो रहा है... आपके पास है: @@ -253,21 +275,26 @@ Barista Material Dark Added to Favorites - Add to favorites + Favorited + Favorite Accounts Hashtags Locations Unknown Removed from Favourites - Backup & Restore - Create - Restore + Backup & Restore User Settings + Back up app settings, account login information, and/or favorites data to a plain text or encrypted backup file for later restoration. + If you\'re backing up login info, treat the file as confidential: Keep them somewhere safe! + Create new backup file + Restore from existing backup file File: Enter password Select a backup file (.zaai/.backup) Apply Save Caption + Edit caption... + Translate caption... Video player timeline Liking… Like unsuccessful @@ -283,6 +310,7 @@ Delete Comment Layout + Feed stories Opening post... Share Layout style @@ -296,7 +324,9 @@ Show grid gap Disable animation Please wait for the current task to complete first! + Depending on user counts, this can take a while to load. Please be patient. Post not found! + No app found which opens urls %d like %d likes @@ -305,4 +335,9 @@ %d comment %d comments + + %s story + %s stories + + Storage permission not granted! diff --git a/app/src/main/res/values-in/arrays.xml b/app/src/main/res/values-in/arrays.xml index 97192bb2..32f0cd3a 100644 --- a/app/src/main/res/values-in/arrays.xml +++ b/app/src/main/res/values-in/arrays.xml @@ -2,41 +2,26 @@ Bawaan Sistem - Inggris - Prancis [Terima kasih @kernoeb] - Spanyol [Terima kasih @sguinetti] - Tiongkok Sederhana - Bahasa Indonesia [Terima kasih @Galang23] - Italia [Terima kasih @RAR_Ramar] - Jerman [Teirma kasih @peterge1998] - Polandia [Terima kasih @Lego8486] - Turki [Terima kasih @faydin90] - Portugis Brazil [Terima kasih @wagnim] - Persia [Terima kasih @farzadx] - Makedonia [Terima kasih @snajdovski] - Vietnam - Tiongkok Tradisional [Terima kasih @Still34] - Catalan [Thanks to @retiolus] - Russian [Thanks to @rikishi0071] - - - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 + English + Français [Merci à @kernoeb et @PierreM0] + Español [Gracias a @sguinetti, @akrai y @retiolus] + 简体中文 + Bahasa Indonesia [Terima kasih @Galang23] + Italiano [Grazie a @RAR_Ramar e GiorgioHerbie] + Deutsch [Danke an @peterge1998] + Polski [Podziękowania dla @Lego8486] + Türkçe [@faydin90 tarafından] + Português (Brasil) [Obrigado @wagnim, @RickyM7 e @cizordj] + پارسی [ با سپاس از farzadx@ ] + Македонски [Благодарност до @snajdovski] + Tiếng Việt [bởi Yato Fouze] + 繁體中文 [感謝 @Still34] + Català [Gràcies a @retiolus] + Русский [Спасибо @rikishi0071] + हिन्दी + Nederlands [Met dank aan Lesley Natrop] + Slovenčina [Vďaka @CrafterSvK] + 日本語 [協力 ysakamoto] Otomatis / Ikuti Sistem @@ -44,11 +29,10 @@ Gelap Terang - - 0 - 1 - 2 - 3 + + Instagram default (Unread then read) + From newest to oldest + From oldest to newest Tidak ada @@ -58,53 +42,6 @@ tanggal - - - Memuat... - - - dd-MM-yyyy - dd/MM/yyyy - dd.MM.yyyy - dd-MM-yy - dd/MM/yy - dd.MM.yy - dd-MMM-yyyy - dd/MMM/yyyy - dd.MMM.yyyy - dd-MMM-yy - dd/MMM/yy - dd.MMM.yy - MM-dd-yyyy - MM/dd/yyyy - MM.dd.yyyy - yyyy-MM-dd - yyyy/MM/dd - yyyy.MM.dd - MM-dd-yy - MM/dd/yy - MM.dd.yy - yy-MM-dd - yy/MM/dd - yy.MM.dd - MMM-dd-yyyy - MMM/dd/yyyy - MMM.dd.yyyy - yyyy-MMM-dd - yyyy/MMM/dd - yyyy.MMM.dd - MMM-dd-yy - MMM/dd/yy - MMM.dd.yy - yy-MMM-dd - yy/MMM/dd - yy.MMM.dd - - - hh:mm:ss a - h:mm:ss a - HH:mm:ss - H:mm:ss - @string/title_dm @string/feed diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 6102db09..508c1537 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -23,7 +23,7 @@ Favorit Temukan Komentar - Pemberitahuan + Aktivitas Sorotan: %s Cek pembaruan saat memulai Unduh kiriman ke folder nama pengguna @@ -32,6 +32,7 @@ Tandai DM dibaca setelah melihat Peserta lain akan tahu Anda melihatnya Nyalakan pemberitahuan aktivitas + Feed stories sort Galat saat memuat profil!\nCoba masuk dan cari lagi. Galat saat membuat folder unduhan. Simpan ke folder khusus @@ -42,10 +43,13 @@ Gunakan Instadp untuk foto profil kualitas tinggi Ekspor/impor Pengaturan Bahasa - %s\nkiriman - %s Kiriman - %s\npengikut - %s\ndiikuti + + %s Posts + + + %s Followers + + %s Following Otomatis putar video Selalu bisukan video Pilih apa yang akan diunduh @@ -63,9 +67,14 @@ Tanggapi Balasan… Berhasil menjawab! + + %d responses averaging %s + + Your answer: %s Balas cerita Balas… Kuis + Slider Anda sudah menjawab! Sebutan Akun Ini Bersifat Pribadi @@ -98,6 +107,11 @@ Batalkan Blokir Batasi Hilangkan Batasan + Copy bio + Translate bio + Mutual + Following + Follower Peta Ekspor Impor @@ -126,6 +140,7 @@ Tidak dapat menghapus akun yang sedang digunakan Anda yakin ingin menghapus \'%s\'? Buka profil + View story Lihat foto profil Anda Membagikan alamat web @@ -175,9 +190,11 @@ Anda hanya dapat mengunduh 100 kiriman saat bersamaan. Jangan serakah-serakah! Salin nama pengguna Salin komentar + View comment likers Balas komentar Suka komentar Batal suka komentar + Translate comment Hapus komentar Ups, komentar kosong! Apakah anda ingin mencari nama pengguna ini? @@ -208,6 +225,8 @@ Aplikasi berhenti bekerja… Ups! Aplikasi berhenti bekerja! Jangan khawatir, anda dapat mengirimkan laporan galat/kesalahan kepada Pengembang untuk membantunya memperbaiki masalah ini. (: Aktivitas + Story archive + Suggested users Pilih Gambar Mengunggah… Anda memiliki: @@ -252,21 +271,26 @@ Barista Material Dark Ditambahkan ke Kesukaan - Tambahkan ke Kesukaan + Favorited + Favorite Akun Tagar Lokasi Tak diketahui Dihapus dari Kesukaan - Cadangkan & Pulihkan - Buat - Pulihkan + Backup & Restore User Settings + Back up app settings, account login information, and/or favorites data to a plain text or encrypted backup file for later restoration. + If you\'re backing up login info, treat the file as confidential: Keep them somewhere safe! + Create new backup file + Restore from existing backup file Berkas: Masukkan sandi Pilih berkas cadangan (.zaai/.backup) Terapkan Simpan Keterangan + Edit caption... + Translate caption... Bilah waktu pemutar video Menyukai… Gagal menyukai @@ -282,6 +306,7 @@ Hapus Komentar Tata letak + Feed stories Membuka kiriman... Bagikan Gaya tata letak @@ -295,11 +320,17 @@ Tampilkan batas kisi Matikan animasi Harap tunggu hingga tugas saat ini selesai! + Depending on user counts, this can take a while to load. Please be patient. Post not found! + No app found which opens urls %d suka %d komentar + + %s stories + + Storage permission not granted! diff --git a/app/src/main/res/values-it/arrays.xml b/app/src/main/res/values-it/arrays.xml index d602168d..56db53d5 100755 --- a/app/src/main/res/values-it/arrays.xml +++ b/app/src/main/res/values-it/arrays.xml @@ -2,41 +2,26 @@ Predefinito di Sistema - Inglese - Francese [Grazie a @kernoeb] - Spagnolo [Grazie a @sguinetti] - Cinese Semplificato - Indonesiano [Grazie a @Galang23] - Italiano [Grazie a @RAR_Ramar e Giorgio Arbato] - Tedesco [Grazie a @peterge1998] - Polacco [Grazie a @Lego8486] - Turco [Grazie a @faydin90] - Portoghese Brasiliano [Grazie a @wagnim] - Persiano [Grazie a @farzadx] - Macedone [Grazie a @snajdovski] - Vietnamita [Grazie a @fouze555] - Cinese Tradizionale [Grazie a @Still34] - Catalano [Grazie a @retiolus] - Russo [Grazie a @rikishi0071] - - - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 + English + Français [Merci à @kernoeb et @PierreM0] + Español [Gracias a @sguinetti, @akrai y @retiolus] + 简体中文 + Bahasa Indonesia [Terima kasih @Galang23] + Italiano [Grazie a @RAR_Ramar e GiorgioHerbie] + Deutsch [Danke an @peterge1998] + Polski [Podziękowania dla @Lego8486] + Türkçe [@faydin90 tarafından] + Português (Brasil) [Obrigado @wagnim, @RickyM7 e @cizordj] + پارسی [ با سپاس از farzadx@ ] + Македонски [Благодарност до @snajdovski] + Tiếng Việt [bởi Yato Fouze] + 繁體中文 [感謝 @Still34] + Català [Gràcies a @retiolus] + Русский [Спасибо @rikishi0071] + हिन्दी + Nederlands [Met dank aan Lesley Natrop] + Slovenčina [Vďaka @CrafterSvK] + 日本語 [協力 ysakamoto] Automatico / Basato sul Sistema @@ -44,11 +29,10 @@ Scuro Chiaro - - 0 - 1 - 2 - 3 + + Instagram default (Unread then read) + From newest to oldest + From oldest to newest Nessuno @@ -58,53 +42,6 @@ \| - - - Caricamento... - - - dd-MM-yyyy - dd/MM/yyyy - dd.MM.yyyy - dd-MM-yy - dd/MM/yy - dd.MM.yy - dd-MMM-yyyy - dd/MMM/yyyy - dd.MMM.yyyy - dd-MMM-yy - dd/MMM/yy - dd.MMM.yy - MM-dd-yyyy - MM/dd/yyyy - MM.dd.yyyy - yyyy-MM-dd - yyyy/MM/dd - yyyy.MM.dd - MM-dd-yy - MM/dd/yy - MM.dd.yy - yy-MM-dd - yy/MM/dd - yy.MM.dd - MMM-dd-yyyy - MMM/dd/yyyy - MMM.dd.yyyy - yyyy-MMM-dd - yyyy/MMM/dd - yyyy.MMM.dd - MMM-dd-yy - MMM/dd/yy - MMM.dd.yy - yy-MMM-dd - yy/MMM/dd - yy.MMM.dd - - - hh:mm:ss a - h:mm:ss a - HH:mm:ss - H:mm:ss - @string/title_dm @string/feed diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 632cd150..54b45f0e 100755 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -23,7 +23,7 @@ Preferiti Scopri Commenti - Notifiche + Attività In Evidenza: %s Verifica per aggiornamenti all\'avvio Scarica i post nelle cartelle del nome utente @@ -32,6 +32,7 @@ Segna il DM come visto dopo la visualizzazione Altri membri sapranno che lo hai visualizzato Abilita notifiche attività + Feed stories sort Errore caricando il profilo!\nProva ad accedere e cercare ancora. Errore creando le cartelle di Download. Salva alla cartella personalizzata @@ -42,10 +43,15 @@ Usa Instadp per immagini del profilo ad alta definizione Importa/Esporta Lingua - %s\nPost - %s Post - %s\nSeguaci - %s\nSeguiti + + %s Post + %s Post + + + %s Seguace + %s Seguaci + + %s Seguiti Riproduzione automatica video Silenzia sempre i video Seleziona cosa scaricare @@ -63,9 +69,15 @@ Rispondi Rispondi… Risposta corretta! + + %d risposta media %s + %d risposte medie %s + + La tua risposta: %s Rispondi alla storia Rispondi… Quiz + Scorrimento Hai già risposto! Menzioni Questo Profilo è Privato @@ -98,6 +110,11 @@ Sblocca Limita Rimuovi limitazione + Copia bio + Traduci bio + Interesse reciproco + Seguendo + Seguace Mappa Esporta Importa @@ -126,6 +143,7 @@ Impossibile eliminare il profilo correntemente in uso Sei sicuro di voler eliminare \'%s\'? Apri profilo + Visualizza storia Vedi immagine del profilo Tu Ha condiviso un link @@ -175,9 +193,11 @@ Puoi scaricare solo 100 post per volta. Non essere troppo avido! Copia nome utente Copia commento + Visualizza i mi piace del commento Rispondi al commento Mi Piace il commento Il commento Non mi Piace Più + Traduci commento Elimina commento Nessun commento vuoto! Vuoi cercare il nome utente? @@ -208,6 +228,8 @@ L\'app è crashata Ops... l\'app è crashata, ma non preoccuparti, puoi inviare una segnalazione dell\'errore allo sviluppatore per aiutarlo a risolvere il problema. (: Attività + Story archive + Suggested users Seleziona Immagine Caricamento… Hai: @@ -252,21 +274,26 @@ Barista Materiale Scuro Aggiunto ai Preferiti - Aggiungi ai preferiti + Favorito + Favorisci Profili Hashtag Luoghi Sconosciuto Rimosso dai Preferiti - Backup e Ripristino - Crea - Ripristina + Backup & Restore User Settings + Back up app settings, account login information, and/or favorites data to a plain text or encrypted backup file for later restoration. + If you\'re backing up login info, treat the file as confidential: Keep them somewhere safe! + Create new backup file + Restore from existing backup file File: Inserisci password Selezionare un file di backup (.zaai/.backup) Applica Salva Didascalia + Modifica didascalia... + Traduci didascalia... Barra temporale del lettore video Mettendo Mi Piace… Impossibile mettere Mi Piace @@ -282,6 +309,7 @@ Elimina Commento Disposizione + Feed stories Aprendo il post... Condividi Stile disposizione @@ -295,7 +323,9 @@ Mostra divario griglia Disabilita animazione Prima, sei pregato di attendere che l\'attività corrente sia completata! + A seconda del numero di utenti, questo può richiedere un po\' di tempo. Si prega di essere pazienti. Post non trovato! + Nessuna applicazione trovata che apre gli urls %d Mi piace %d Mi piace @@ -304,4 +334,9 @@ %d commento %d commenti + + %s story + %s stories + + Permesso di archiviazione non garantito! diff --git a/app/src/main/res/values-ja/arrays.xml b/app/src/main/res/values-ja/arrays.xml new file mode 100644 index 00000000..29533a71 --- /dev/null +++ b/app/src/main/res/values-ja/arrays.xml @@ -0,0 +1,70 @@ + + + + システムのデフォルト + English + Français [Merci à @kernoeb et @PierreM0] + Español [Gracias a @sguinetti, @akrai y @retiolus] + 简体中文 + Bahasa Indonesia [Terima kasih @Galang23] + Italiano [Grazie a @RAR_Ramar e GiorgioHerbie] + Deutsch [Danke an @peterge1998] + Polski [Podziękowania dla @Lego8486] + Türkçe [@faydin90 tarafından] + Português (Brasil) [Obrigado @wagnim, @RickyM7 e @cizordj] + پارسی [ با سپاس از farzadx@ ] + Македонски [Благодарност до @snajdovski] + Tiếng Việt [bởi Yato Fouze] + 繁體中文 [感謝 @Still34] + Català [Gràcies a @retiolus] + Русский [Спасибо @rikishi0071] + हिन्दी + Nederlands [Met dank aan Lesley Natrop] + Slovenčina [Vďaka @CrafterSvK] + 日本語 [協力 ysakamoto] + + + 自動/システム設定に従う + 自動 / バッテリー設定に従う + ダーク + ライト + + + Instagramのデフォルト(未読を上位に) + 最新を上位に + 古いものを上位に + + + なし + \@ + at + on + \| + - + + + @string/title_dm + @string/feed + @string/profile + @string/title_discover + @string/more + + + @string/light_white_theme + @string/light_barinsta_theme + @string/light_bibliogram_theme + + + @style/AppTheme.Light.White + @style/AppTheme.Light.Barinsta + @style/AppTheme.Light.Bibliogram + + + @string/dark_black_theme + @string/dark_material_dark_theme + + + @style/AppTheme.Dark.Black + @style/AppTheme.Dark.MaterialDark + + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml new file mode 100644 index 00000000..79cea4f1 --- /dev/null +++ b/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,336 @@ + + + このアプリについて + ダイレクトメッセージ + 設定 + ダウンロード + ユーザー名を検索… + 比較する + テキストのコピーに失敗しました + クリップボードにコピーしました! + 報告する + パスワード (最大32文字) + パスワードを設定してください (最大32文字) + パスワード + OK + はい + キャンセル + いいえ + 確認 + Up + 次回から表示しない + カレントディレクトリ + お気に入り + 発見 + コメント + アクティビティ + ハイライト: %s + 起動時にアップデートを確認 + ユーザ名のフォルダに投稿をダウンロード + ストーリーを表示後に既読としてマーク + ストーリーの作成者は、あなたが閲覧したことを知ることができます。 + DMを表示後に既読としてマーク + ほかのメンバーは、あなたが閲覧したことを知ることができます。 + アクティビティの通知を有効化 + フィードのストーリーの並び順 + プロファイルの読み込みエラー!\nログインしてもう一度検索してください。 + ダウンロードフォルダの作成中にエラーが発生しました。 + カスタムフォルダーに保存 + フォルダーを選択 + テーマ + ログインユーザーにのみ影響します: + 匿名ユーザーにのみ影響します: + 高解像度のプロフィール画像にInstadpを使用する + インポート/エクスポート + 言語 + + %s 件 + + + %s フォロワー + + %s フォロー中 + 動画を自動再生する + 動画を常にミュートする + ダウンロード対象を選択 + この画像 + アルバム全体 + ストーリーズを表示 + これ以上のストーリーズはありません! + お待ちください + 投稿を表示 + 投稿を表示 + Spotify + 投票 + 投票しました! + すでに投票済みです! + 返信する + 回答… + 回答しました! + + %d responses averaging %s + + あなたの回答: %s + ストーリーズに返信 + 返信… + 質問 + Slider + すでに回答済みです! + メンション + このアカウントは非公開です + フォローを解除すると、投稿にアクセスできなくなります。よろしいですか? + 右下にある 詳細 -> アカウント でログインすることができます。または、ログインせずに公開アカウントを表示することができます! + 左/右にスワイプして、発見/フィードを表示したり、下から検索したりできます。 + このアカウントには投稿がありません + 投稿はありません! + 現在のバージョン: v%s + 続きを読む… + ログイン + ログアウト + Instagramを匿名で閲覧する + すべてのアカウントを削除 + 追加された全てのアカウントがアプリから削除されます!\nアカウントを1つだけ削除するには、アカウント選択ダイアログからアカウントを長押ししてください \n\n続けますか? + 日付の形式 + いいね! + 保存 + タグ付き + メッセージ + いいね! + いいね!を取り消す + ブックマーク + ブックマークを解除する + フォローする + フォローを解除 + お気に入り + お気に入りを解除 + ブロック + ブロックを解除 + 制限する + 制限を解除 + プロフィールをコピー + プロフィールを翻訳する + 相互フォロー + フォロー中 + フォロワー + Map + エクスポート + インポート + ログイン情報をエクスポート + アカウント + 設定 + お気に入り + 設定をインポート + ログイン情報をインポート + アカウントをインポート + お気に入りをインポート + インポートに成功しました! + インポートに失敗しました! + エクスポートに成功しました! + エクスポートに失敗しました! + パスワードが未入力です! + 更新 + Cookie を取得する + デスクトップモード + カスタム形式を使用する + 区切り記号 + 時刻形式 + 日付形式 + プレビュー + 時刻と日付の位置を入れ替え + 現在使用中のアカウントは削除できません + \'%s\' を削除してもよろしいですか? + プロフィールを開く + ストーリーズを表示 + プロフィール画像を表示 + あなた + リンクを共有 + メディアを共有 + Shared a timed message + Replied to a story + Reacted on a story + Mentioned in a story + サポートされていないメッセージ形式です + リンクを開く + テキストをコピー + 添付ファイルをダウンロード + メッセージにいいね! + いいね!を取り消す + メッセージの送信を取り消す + 投稿者プロフィールを表示 + %s から共有された投稿 + 不明なメディアタイプ + メディアの有効期限が切れました! + 送信済み + 送信済み + 既読 + 再生済み + 送信中… + ブロック済 + Suggested + Screenshotted + 配信できません + 成功! + 退出する + チャットを終了しますか? + キックする + Left users + 直接ダウンロードする + 投稿を直接ダウンロード! + 投稿を取得中 + 権限を許可して、もう一度ダウンロードしてください! + ダウンロード開始 + ダウンロードが完了しました + 投稿をダウンロード中… + メディアをダウンロード中 + プロフィール画像をダウンロード中 + ダウンロードフォルダにファイルがダウンロードされました! + 不明なエラーが発生しました! + フォルダの作成中にエラーが発生しました! + ダウンロード中にエラーが発生しました + 一度に100個までの投稿しかダウンロードできません。取り過ぎ注意! + ユーザー名をコピー + コメントをコピー + コメントにいいね!したユーザーを表示 + コメントに返信 + コメントに いいね! + コメントのいいね!を取り消す + コメントを翻訳 + コメントを削除 + No empty comments! + ユーザー名を検索しますか? + ハッシュタグを検索しますか? + フォロワー + フォロー中 + フォロワーとフォロー中を比較しています + お互いをフォローしています + %s をフォローしていません + %s はあなたをフォローしていません + Cookieの読み込みエラー + 新しいコメントを書く… + 新しいメッセージを作成… + あなたの投稿にいいね!しました + あなたの投稿にコメント: + あなたをフォローしました + あなたへのメンション: + 投稿にあなたをタグ付けしました。 + フォローをリクエストしました + リクエストを承認 + リクエストを拒否 + シェア先… + これはプライベートな投稿です!閲覧できる人に共有してください! + このカテゴリは空です… + アップデートが利用可能です! (%s) + 注意: F-Droidからダウンロードした場合は、そちらからアップデートする必要があります。GitHubでも同様です。 + Barinstaをアップデートしていただきありがとうございます! + アプリがクラッシュしました + アプリがクラッシュしましたが、エラーレポートを開発者に送信して問題の解決を手助けすることができます。(: + アクティビティ + ストーリーのアーカイブ + おすすめのユーザー + 画像を選択 + アップロード中… + You have: + %d 人のフォロワー + %d コメント + %d 個のコメントへのいいね! + %d 個のユーザータグ + %d いいね! + この通知をクリックする前にログアウトしましたか? + フィード + ユーザー + 詳細 + DM + %d 件選択済み + ログアウトに成功しました! + 情報 + 既読にする + 次回の更新まで再表示しない + バージョン + 起動画面 + 全般 + テーマ + ダウンロード + 言語 + アカウント + 現在のログインが動作しませんか?アカウントを再度追加してください + アカウントを追加 + ライセンス(英語のみ) + ウェブサイトを開く + サポートを受けて、議論して、他の人に出会って、楽しんでください! + GitHubでソースコードを見る + 監査して、スターを付けて、バグ報告して、貢献して、楽しんでください(二回目)! + メールでフィードバックを送信 + サードパーティ製アプリ + 以下のサードパーティー製のオープンソースライブラリが使用されています: + Reminder + このアプリは自己責任で使用してください。ダウンロードした画像は、適用される法律で許可されている目的の範囲内でのみ使用することができます。 + ホワイト + ブラック + ライトテーマ + ダークテーマ + Barista + マテリアルダーク + お気に入りに追加しました + お気に入り + お気に入り + アカウント + ハッシュタグ + 場所 + 不明 + お気に入りから削除しました + ユーザー設定のバックアップと復元 + アプリの設定、アカウントのログイン情報、お気に入りのデータを、プレーンテキストまたは暗号化されたバックアップファイルにバックアップし、後で復元します。 + ログイン情報をバックアップする場合は、ファイルを機密扱いとして安全な場所に保管してください。 + 新しいバックアップファイルを作成 + 既存のバックアップファイルから復元 + ファイル: + パスワードを入力してください + バックアップ ファイルを選択してください (.zaai/.backup) + 適用する + 保存 + キャプション + キャプションを編集... + キャプションを翻訳... + ビデオプレーヤーのタイムライン + Liking… + いいね!に失敗しました + いいね!の取消しに失敗しました + いいね!を取り消しています… + Controls + 保存中… + 削除中... + 保存に失敗しました + 削除に失敗しました + ダウンロード中… + %d 個中の %d 個目をダウンロード + 削除 + コメント + レイアウト + フィードのストーリー + 投稿を開いています... + 共有 + レイアウトスタイル + 列数 + 2 + 3 + 名前を表示 + アバターを表示 + アバターのサイズ + コーナー + グリッドの隙間 + アニメーションを無効化 + 現在のタスクが完了するまでお待ちください! + ユーザー数に応じて、読み込みに時間がかかる場合があります。しばらくお待ちください。 + 投稿が見つかりません! + URLを開くアプリが見つかりません + + %d いいね! + + + %d コメント + + + %s ストーリー + + ストレージへのアクセス権限がありません + diff --git a/app/src/main/res/values-kn/arrays.xml b/app/src/main/res/values-kn/arrays.xml new file mode 100644 index 00000000..6c8f5836 --- /dev/null +++ b/app/src/main/res/values-kn/arrays.xml @@ -0,0 +1,70 @@ + + + + System Default + English + Français [Merci à @kernoeb et @PierreM0] + Español [Gracias a @sguinetti, @akrai y @retiolus] + 简体中文 + Bahasa Indonesia [Terima kasih @Galang23] + Italiano [Grazie a @RAR_Ramar e GiorgioHerbie] + Deutsch [Danke an @peterge1998] + Polski [Podziękowania dla @Lego8486] + Türkçe [@faydin90 tarafından] + Português (Brasil) [Obrigado @wagnim, @RickyM7 e @cizordj] + پارسی [ با سپاس از farzadx@ ] + Македонски [Благодарност до @snajdovski] + Tiếng Việt [bởi Yato Fouze] + 繁體中文 [感謝 @Still34] + Català [Gràcies a @retiolus] + Русский [Спасибо @rikishi0071] + हिन्दी + Nederlands [Met dank aan Lesley Natrop] + Slovenčina [Vďaka @CrafterSvK] + 日本語 [協力 ysakamoto] + + + Auto / Follow System + Auto / Follow Battery + Dark + Light + + + Instagram default (Unread then read) + From newest to oldest + From oldest to newest + + + None + \@ + at + on + \| + - + + + @string/title_dm + @string/feed + @string/profile + @string/title_discover + @string/more + + + @string/light_white_theme + @string/light_barinsta_theme + @string/light_bibliogram_theme + + + @style/AppTheme.Light.White + @style/AppTheme.Light.Barinsta + @style/AppTheme.Light.Bibliogram + + + @string/dark_black_theme + @string/dark_material_dark_theme + + + @style/AppTheme.Dark.Black + @style/AppTheme.Dark.MaterialDark + + diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml new file mode 100644 index 00000000..fa9471c4 --- /dev/null +++ b/app/src/main/res/values-kn/strings.xml @@ -0,0 +1,342 @@ + + + ಬಗ್ಗೆ + ನೇರ ಸಂದೇಶಗಳು + ಸಿದ್ಧತೆಗಳು + ಡೌನ್‌ಲೋಡ್ + ಬಳಕೆದಾರರನ್ನು ಹುಡುಕಿ… + Compare + Error copying text + Copied to clipboard! + Report + Password (Max 32 chars) + Set a password (max 32 chars) + Password + OK + Yes + Cancel + No + Confirm + Up + Don\'t Show Again + Current directory + Favorites + Discover + Comments + Activity + Highlight: %s + Check for updates at startup + Download posts to username folders + Mark stories as seen after viewing + Story author will know you viewed it + Mark DM as seen after viewing + Other members will know you viewed it + Enable activity notifications + Feed stories sort + Error loading profile!\nTry logging in and search again. + Error creating Download folder(s). + Save to custom folder + Select folder + Theme + Only affects logged-in users: + Only affects anonymous users: + Use Instadp for high definition profile pictures + Import/Export + Language + + %s Post + %s Posts + + + %s Follower + %s Followers + + %s Following + Autoplay videos + Always mute videos + Select what to download + Current + Whole Album + Show stories + No more stories! + Be patient! + View Post + View Post + Spotify + Vote + Vote successful! + You have already voted! + Respond + Answer… + Answer successful! + + %d response averaging %s + %d responses averaging %s + + Your answer: %s + Reply to story + Reply… + Quiz + Slider + You have already answered! + Mentions + This Account is Private + You won\'t be able to access posts after unfollowing! Are you sure? + You can log in via More -> Account on the bottom-right corner or you can view public accounts without login! + You can swipe left/right for explore/feed, or search something below! + This Account has No Posts + No Such Posts! + Current version: v%s + read more… + Login + Logout + Browse Instagram anonymously + Remove all accounts + This will remove all added accounts from the app!\nTo remove just one account, long tap the account from the account switcher dialog.\nDo you want to continue? + Date format + Liked + Saved + Tagged + Message + Like + Unlike + Bookmark + Unbookmark + Follow + Unfollow + Favorite + Unfavorite + Block + Unblock + Restrict + Unrestrict + Copy bio + Translate bio + Mutual + Following + Follower + Map + Export + Import + Export Logins + Accounts + Settings + Favorites + Import settings + Import Logins + Import accounts + Import favorites + Successfully imported! + Failed to import! + Successfully exported! + Failed to export! + Password is empty! + Refresh + Get cookies + Desktop Mode + Use custom format + Separator + Time Format + Date Format + Preview + Swap Time and Date positions + Cannot delete currently in use account + Are you sure you want to delete \'%s\'? + Open profile + View story + View profile picture + You + Shared a link + Shared a media + Shared a timed message + Replied to a story + Reacted on a story + Mentioned in a story + Unsupported message type + Open link + Copy text + Download attachment + Like message + Unlike message + Unsend message + View author profile + Post shared from %s + Unknown media type + Media expired! + Delivered + Sent + Opened + Replayed + Sending… + Blocked + Suggested + Screenshotted + Cannot deliver + Great success! + Leave + Leave this chat? + Kick + Left users + Download directly + Downloads posts directly to the phone! + Fetching post(s) + Please grant permissions and try downloading again! + Download started + Download completed + Downloading post… + Downloading media + Downloading profile picture + File downloaded in Downloads folder! + Unknown error occurred!!! + Error creating folder! + Error downloading file + You can only download 100 posts at a time. Don\'t be too greedy! + Copy username + Copy comment + View comment likers + Reply to comment + Like comment + Unlike comment + Translate comment + Delete comment + No empty comments! + Do you want to search the username? + Do you want to search the hashtag? + Followers + Following + Comparing followers & following + Both following each other + not following %s + %s is not following + Error loading cookies + Write a new comment… + Write a new message… + Liked your post + Commented on your post: + Started following you + Mentioned you: + Tagged you in a post + Requested following you + Approve request + Reject request + Share this public post to… + This is a private post! Share to those who can view them! + This category is somehow empty… + An update is available! (%s) + Reminder: If you downloaded from F-Droid, you must update from it! Same applies for GitHub. + Thank you for updating Barinsta! + App crashed + Oops.. the app crashed, but don\'t worry you can send error report to the developer to help him fix the issue. (: + Activity + Story archive + Suggested users + Select Picture + Uploading… + You have: + %d follows + %d comments + %d comment likes + %d usertags + %d likes + You logged out before clicking this notification?! + Feed + Profile + More + DM + %d selected + Successfully logged out! + Info + Mark as seen + Do not show again until next update + Version + Start screen + General + Theme + Downloads + Locale + Account + Current login not working? Simply add the account again. + Add account + License (English only) + Visit our website + Get support, discuss, meet others, and have fun! + See our source code on GitHub + Audit, star, report bugs, contribute, and have fun (again)! + Send feedback by email + Third-Party Attributions + The following third-party open-source libraries are used: + Reminder + Please use this app responsibly. Downloaded images should only be used for purposes allowed by applicable laws. + White + Black + Light theme + Dark theme + Barista + Material Dark + Added to Favorites + Favorited + Favorite + Accounts + Hashtags + Locations + Unknown + Removed from Favourites + Backup & Restore User Settings + Back up app settings, account login information, and/or favorites data to a plain text or encrypted backup file for later restoration. + If you\'re backing up login info, treat the file as confidential: Keep them somewhere safe! + Create new backup file + Restore from existing backup file + File: + Enter password + Select a backup file (.zaai/.backup) + Apply + Save + Caption + Edit caption... + Translate caption... + Video player timeline + Liking… + Like unsuccessful + Unlike unsuccessful + Unliking… + Controls + Saving… + Removing… + Save unsuccessful + Remove unsuccessful + Downloading… + Download item %d of %d + Delete + Comment + Layout + Feed stories + Opening post... + Share + Layout style + Column count + 2 + 3 + Show names + Show avatars + Avatar size + Corners + Show grid gap + Disable animation + Please wait for the current task to complete first! + Depending on user counts, this can take a while to load. Please be patient. + Post not found! + No app found which opens urls + + %d like + %d likes + + + %d comment + %d comments + + + %s story + %s stories + + Storage permission not granted! + diff --git a/app/src/main/res/values-mk/arrays.xml b/app/src/main/res/values-mk/arrays.xml index eeec44ac..88e5e46a 100644 --- a/app/src/main/res/values-mk/arrays.xml +++ b/app/src/main/res/values-mk/arrays.xml @@ -2,41 +2,26 @@ Системски одбрано - Англиски - Француски [Благодарност до @kernoeb] - Шпански [Благодарност до @sguinetti] - Кинески - Индонезиски [Благодарност до @Galang23] - Италијански [Благодарност до @RAR_Ramar] - Германски [Благодарност до @peterge1998] - Полски [Благодарност до @Lego8486] - Турски [Благодарност до @faydin90] - Бразилски Португалски [Благодарност до @wagnim] - Персиски [Благодарност до @farzadx] - Македонски [Благодарност до @snajdovski] - Виетнамски[Благодарност до @fouze555] - Chinese Traditional [Thanks to @Still34] - Catalan [Thanks to @retiolus] - Russian [Thanks to @rikishi0071] - - - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 + English + Français [Merci à @kernoeb et @PierreM0] + Español [Gracias a @sguinetti, @akrai y @retiolus] + 简体中文 + Bahasa Indonesia [Terima kasih @Galang23] + Italiano [Grazie a @RAR_Ramar e GiorgioHerbie] + Deutsch [Danke an @peterge1998] + Polski [Podziękowania dla @Lego8486] + Türkçe [@faydin90 tarafından] + Português (Brasil) [Obrigado @wagnim, @RickyM7 e @cizordj] + پارسی [ با سپاس از farzadx@ ] + Македонски [Благодарност до @snajdovski] + Tiếng Việt [bởi Yato Fouze] + 繁體中文 [感謝 @Still34] + Català [Gràcies a @retiolus] + Русский [Спасибо @rikishi0071] + हिन्दी + Nederlands [Met dank aan Lesley Natrop] + Slovenčina [Vďaka @CrafterSvK] + 日本語 [協力 ysakamoto] Автоматска тема @@ -44,11 +29,10 @@ Темно Светло - - 0 - 1 - 2 - 3 + + Стандардно (Од непрочитано па прочитано) + Од најнови до најстари + Од најнови до најстари Ништо @@ -58,53 +42,6 @@ \| - - - Вчитување... - - - dd-MM-yyyy - dd/MM/yyyy - dd.MM.yyyy - dd-MM-yy - dd/MM/yy - dd.MM.yy - dd-MMM-yyyy - dd/MMM/yyyy - dd.MMM.yyyy - dd-MMM-yy - dd/MMM/yy - dd.MMM.yy - MM-dd-yyyy - MM/dd/yyyy - MM.dd.yyyy - yyyy-MM-dd - yyyy/MM/dd - yyyy.MM.dd - MM-dd-yy - MM/dd/yy - MM.dd.yy - yy-MM-dd - yy/MM/dd - yy.MM.dd - MMM-dd-yyyy - MMM/dd/yyyy - MMM.dd.yyyy - yyyy-MMM-dd - yyyy/MMM/dd - yyyy.MMM.dd - MMM-dd-yy - MMM/dd/yy - MMM.dd.yy - yy-MMM-dd - yy/MMM/dd - yy.MMM.dd - - - hh:mm:ss a - h:mm:ss a - HH:mm:ss - H:mm:ss - @string/title_dm @string/feed diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 50e213ba..c322a500 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -23,7 +23,7 @@ Омилени Откриј Коментари - Нотификации + Активности Важни Приказни: %s Провери за надоградба (ажурирање) при старт на апликацијата. Превземи постови во фолдерот со кориснички имиња @@ -32,20 +32,26 @@ Означи порака како видена при отварање. Другите членови ќе знаат дека си ја видел содржината на чатот. Овозможи нотификации + Сортирање на објави Фатална Грека при отварање на профилот!\nПробајте повторно да се логирате и да пребарате. Фатална Грешка при креирање на фолдер(и). Зачувај во сопствена папка Одбери фолдер Тема Влијае само на корисниците кои се логирани: - Влијае на анинимните корисници: + Влијае на анонимните корисници: Користи Instadp за профили со висока резолуција - Увоз/Извоз + Експорт/Импорт Јазик - %s\nПостови - %s Постови - %s\nСледбеници - %s\nСледбеници + + %s Објава + %s Објави + + + %s Следач + %s Следачи + + %sСледбеници Autoplay на видеа Секогаш гледај видеа без звук Селектирај што сакаш да превземеш @@ -61,11 +67,17 @@ Гласањето беше успешно! Вие веќе гласавте! Одговори - Answer… + Одговор… Одговарњето беше успешно! + + %d одговор со просек од %s + %d одговори со просек %s + + Твојот одговор: %s Одговори на приказната - Reply… + Одговори… Квиз + Лизгач Вие веќе гласавте/одговоривте! Спомнувања Овoј корисник има приватен профил @@ -86,8 +98,8 @@ Зачувано Такнато Порака - Like - Unlike + Лајк + Дислајк Обележи Одобележи Следи @@ -98,6 +110,11 @@ Одблокни Ограничи Одограничи + Копирај био + Преведи био + Заеднички + Оние кои ги следиш + Следач Мапа Експорт Импорт @@ -126,6 +143,7 @@ Неможе да избришите акаунт во корист Дали сте сигурни дека сакате да избришите \'%s\'? Отвори Профил + Погледни приказна Прегладај профилната слика Ти Сподели линк @@ -165,7 +183,7 @@ Ве молиме одобрете ги дозволите и пробајте повторно да превземите! Превзмањето започна Превземањето е готово - Downloading post… + Превземање на Објава… Се превзема медиум Се превзема профилната слика Фајлот беше превземен во фолдерот Downloads! @@ -175,9 +193,11 @@ Можете само 100 постови наеднаш да превземите. Не бидете алчни! Копирај username Копирај Коментар + Прегледај лајкови на коментар Одговори на коментар Лајкни коментар Одлајкни коментар + Преведи коментар Избриши коментар Не смее празни коментари! Дали сакате да го пребарате ова корисничко име? @@ -189,27 +209,29 @@ Не се следат %s %s не следи Грешка при вчитување колачиња - Write a new comment… - Write a new message… + Напиши нов коментар… + Напишете нова порака… Го лајкна твојот пост Коментира на твојот пост: Започна да те следи Те спомна: - Tagged you in a post + Те тагна во објава Пуште барање за следење Одобри барање Не одобрувај барање - Share this public post to… + Сподели ја оваа јавна објава до… Овој пост е приватен! Споделте го на оние кои можат да го видат! - This category is somehow empty… + Оваа категорија е празна… Нова надоградба е присутна! (%s) Потсетник: Ако го имате превземено од F-Droid, морате од таму да надоградите! Истото важи и за верзиите од GitHub. Ви благодариме за надоградбата на Barinsta! Апликацијата крашна аааагхххх.. апликацијаа крашна, ама не се грижете, можете да пратите листа на фатални грешки кај програмерите за да ви помогнат да се поправи проблемот. (: Активности + Story archive + Suggested users Селектирај слика - Uploading… + Се Прикачува… Вие имате: %d следачи %d коментари @@ -252,56 +274,69 @@ Barista Материјално Црна Додадено во Омилени - Додади во омилени + Омилени + Омилено Акаунти Хаштагови Локации Непознато Отстрането од омилени - Направете Резервна Копија & Враќање - Креирај - Врати + Зачувај & Врати Кориснички Опции + Зачувај опции од апликацијата, кориснички профил, и/или информации од твоите омилени профили во обичен текст или со енкрипција за подоцна да можеш да ги вратиш. + Ако зачувуваш информации за најавување, третирај го генералниот фајл како многу важен, чувај го на безбедно место! + Креирај новa резервна копија + Поврати информации од резервна копија Фајл: Внесете лозинка Одбери backup фајл (.zaai/.backup) - Apply - Save - Caption - Video player timeline - Liking… - Like unsuccessful - Unlike unsuccessful - Unliking… - Controls - Saving… - Removing… - Save unsuccessful - Remove unsuccessful - Downloading… - Download item %d of %d - Delete - Comment - Layout - Opening post... - Share - Layout style - Column count + Примени + Зачувај + Наслов + Промени наслов... + Преведи наслов... + Лента на Видеоплеер + Се лајкнува… + Лајкнувањето беше неуспешно + Одлајкнувањето беше неуспешно + Се одлајкнува… + Контроли + Се зачувува… + Се отстранува… + Зачувувањето неуспешно + Отстранувањето е неуспешно + Се превзема… + Превземање податок %d од %d + Избриши + Коментирај + Изглед + Feed stories + Отварање на Објава... + Сподели + Стил на изглед + Број на колони 2 3 - Show names - Show avatars - Avatar size - Corners - Show grid gap - Disable animation - Please wait for the current task to complete first! - Post not found! + Прикажи имиња + Прикажи Аватар + Големина на Аватар + Ќошеви + Прикажи растојание меѓу слики + Исклучи анимации + Ве молиме почекајте сегашната задача да заврши прво! + Во зависност од бројот на корисници, ова може да потрае, Ве молиме бидете трпеливи. + Објавата не беше пронајдена! + Но беше пронајдена апликација за отварање линкови - %d like - %d likes + %d лајк + %d лајкови - %d comment - %d comments + %d коментар + %d коментари + + %s story + %s stories + + Нема дозвола за пристап на меморија! diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index e5fbb438..aa7d71de 100755 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -1,5 +1,10 @@ + + --> + + --> diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index edd43633..188c046c 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -123,8 +123,8 @@