From bb5244665b286bf6b43dc53e2b5d7c86af9bceba Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Mon, 5 Jul 2021 20:11:58 -0400 Subject: [PATCH] story viewmodel (wip) hiding the storylist doesn't work yet but everything else should be good --- .../instagrabber/adapters/StoriesAdapter.java | 12 + .../fragments/StoryViewerFragment.java | 1307 ----------------- .../fragments/StoryViewerFragment.kt | 902 ++++++++++++ .../fragments/main/ProfileFragment.kt | 4 +- .../models/enums/StoryPaginationType.kt | 7 + .../repositories/StoriesService.kt | 2 +- .../responses/stories/StoryMedia.kt | 4 +- .../viewmodels/ProfileFragmentViewModel.kt | 9 +- .../viewmodels/StoryFragmentViewModel.kt | 458 ++++++ .../webservices/StoriesRepository.kt | 26 +- .../main/res/drawable/ic_story_sticker.xml | 10 + .../res/drawable/ic_story_viewer_list.xml | 10 + .../main/res/layout/fragment_story_viewer.xml | 220 +-- app/src/main/res/menu/story_menu.xml | 12 - .../navigation/direct_messages_nav_graph.xml | 1 - .../main/res/navigation/feed_nav_graph.xml | 1 - .../main/res/navigation/hashtag_nav_graph.xml | 1 - .../res/navigation/location_nav_graph.xml | 1 - .../notification_viewer_nav_graph.xml | 1 - .../main/res/navigation/profile_nav_graph.xml | 1 - .../res/navigation/story_list_nav_graph.xml | 1 - app/src/main/res/values/ids.xml | 9 + app/src/main/res/values/strings.xml | 14 +- 23 files changed, 1555 insertions(+), 1458 deletions(-) delete mode 100644 app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.kt create mode 100644 app/src/main/java/awais/instagrabber/models/enums/StoryPaginationType.kt create mode 100644 app/src/main/java/awais/instagrabber/viewmodels/StoryFragmentViewModel.kt create mode 100644 app/src/main/res/drawable/ic_story_sticker.xml create mode 100644 app/src/main/res/drawable/ic_story_viewer_list.xml diff --git a/app/src/main/java/awais/instagrabber/adapters/StoriesAdapter.java b/app/src/main/java/awais/instagrabber/adapters/StoriesAdapter.java index 9a502f01..21a5b621 100755 --- a/app/src/main/java/awais/instagrabber/adapters/StoriesAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/StoriesAdapter.java @@ -1,5 +1,7 @@ package awais.instagrabber.adapters; +import java.util.List; + import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -72,6 +74,16 @@ public final class StoriesAdapter extends ListAdapter list = getCurrentList(); + for (int i = 0; i < list.size(); i++) { + final StoryMedia item = list.get(i); + if (!item.isCurrentSlide() && i != newIndex) continue; + item.setCurrentSlide(i == newIndex); + notifyItemChanged(i, item); + } + } + public interface OnItemClickListener { void onItemClick(StoryMedia storyModel, int position); } diff --git a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java deleted file mode 100644 index d6400c7e..00000000 --- a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java +++ /dev/null @@ -1,1307 +0,0 @@ -package awais.instagrabber.fragments; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.graphics.drawable.Animatable; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.text.Editable; -import android.text.TextWatcher; -import android.util.Log; -import android.view.GestureDetector; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -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; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.view.GestureDetectorCompat; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.ViewModel; -import androidx.lifecycle.ViewModelProvider; -import androidx.navigation.NavController; -import androidx.navigation.NavDirections; -import androidx.navigation.fragment.NavHostFragment; -import androidx.recyclerview.widget.LinearLayoutManager; - -import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.drawee.controller.BaseControllerListener; -import com.facebook.drawee.interfaces.DraweeController; -import com.facebook.imagepipeline.image.ImageInfo; -import com.facebook.imagepipeline.request.ImageRequest; -import com.facebook.imagepipeline.request.ImageRequestBuilder; -import com.google.android.exoplayer2.MediaItem; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.source.LoadEventInfo; -import com.google.android.exoplayer2.source.MediaLoadData; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSourceEventListener; -import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; - -import java.io.IOException; -import java.text.NumberFormat; -import java.util.Arrays; -import java.util.ArrayList; -import java.util.Collections; -import java.util.stream.Collectors; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import awais.instagrabber.BuildConfig; -import awais.instagrabber.R; -import awais.instagrabber.adapters.StoriesAdapter; -import awais.instagrabber.customviews.helpers.SwipeGestureListener; -import awais.instagrabber.databinding.FragmentStoryViewerBinding; -import awais.instagrabber.fragments.main.ProfileFragmentDirections; -import awais.instagrabber.fragments.settings.PreferenceKeys; -import awais.instagrabber.interfaces.SwipeEvent; -import awais.instagrabber.models.enums.MediaItemType; -import awais.instagrabber.repositories.requests.StoryViewerOptions; -import awais.instagrabber.repositories.requests.StoryViewerOptions.Type; -import awais.instagrabber.repositories.requests.directmessages.ThreadIdsOrUserIds; -import awais.instagrabber.repositories.responses.stories.*; -import awais.instagrabber.utils.AppExecutors; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.CoroutineUtilsKt; -import awais.instagrabber.utils.DownloadUtils; -import awais.instagrabber.utils.ResponseBodyUtils; -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.DirectMessagesRepository; -import awais.instagrabber.webservices.MediaRepository; -import awais.instagrabber.webservices.ServiceCallback; -import awais.instagrabber.webservices.StoriesRepository; -import kotlinx.coroutines.Dispatchers; - -import static awais.instagrabber.customviews.helpers.SwipeGestureListener.SWIPE_THRESHOLD; -import static awais.instagrabber.customviews.helpers.SwipeGestureListener.SWIPE_VELOCITY_THRESHOLD; -import static awais.instagrabber.fragments.settings.PreferenceKeys.MARK_AS_SEEN; -import static awais.instagrabber.utils.Utils.settingsHelper; - -public class StoryViewerFragment extends Fragment { - private static final String TAG = "StoryViewerFragment"; - - private final String cookie = settingsHelper.getString(Constants.COOKIE); - - private AppCompatActivity fragmentActivity; - private View root; - private FragmentStoryViewerBinding binding; - private String currentStoryUsername; - private String highlightTitle; - private StoriesAdapter storiesAdapter; - private SwipeEvent swipeEvent; - private GestureDetectorCompat gestureDetector; - private StoriesRepository storiesRepository; - private MediaRepository mediaRepository; - private StoryMedia currentStory; - private Broadcast live; - private int slidePos; - private int lastSlidePos; - private String url; - private PollSticker poll; - private QuestionSticker question; - private List mentions = new ArrayList(); - private QuizSticker quiz; - private SliderSticker slider; - private MenuItem menuDownload, menuDm, menuProfile; - private SimpleExoPlayer player; - // private boolean isHashtag; - // private boolean isLoc; - // private String highlight; - private String actionBarTitle, actionBarSubtitle; - private boolean fetching = false, sticking = false, shouldRefresh = true; - private boolean downloadVisible = false, dmVisible = false, profileVisible = true; - private int currentFeedStoryIndex; - private double sliderValue; - private StoriesViewModel storiesViewModel; - private ViewModel viewModel; - // private boolean isHighlight; - // private boolean isArchive; - // private boolean isNotification; - private DirectMessagesRepository directMessagesRepository; - private StoryViewerOptions options; - private String csrfToken; - private String deviceId; - private long userId; - - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); - if (csrfToken == null) return; - userId = CookieUtils.getUserIdFromCookie(cookie); - deviceId = settingsHelper.getString(Constants.DEVICE_UUID); - fragmentActivity = (AppCompatActivity) requireActivity(); - storiesRepository = StoriesRepository.Companion.getInstance(); - mediaRepository = MediaRepository.Companion.getInstance(); - directMessagesRepository = DirectMessagesRepository.Companion.getInstance(); - setHasOptionsMenu(true); - } - - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { - if (root != null) { - shouldRefresh = false; - return root; - } - binding = FragmentStoryViewerBinding.inflate(inflater, container, false); - 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 onCreateOptionsMenu(@NonNull final Menu menu, final MenuInflater menuInflater) { - menuInflater.inflate(R.menu.story_menu, menu); - menuDownload = menu.findItem(R.id.action_download); - menuDm = menu.findItem(R.id.action_dms); - menuProfile = menu.findItem(R.id.action_profile); - menuDownload.setVisible(downloadVisible); - menuDm.setVisible(dmVisible); - menuProfile.setVisible(profileVisible); - } - - @Override - public void onPrepareOptionsMenu(@NonNull final Menu menu) { - // hide menu items from activity - } - - @Override - public boolean onOptionsItemSelected(@NonNull final MenuItem item) { - final Context context = getContext(); - if (context == null) return false; - int itemId = item.getItemId(); - if (itemId == R.id.action_download) { - downloadStory(); - return true; - } - if (itemId == R.id.action_dms) { - final EditText input = new EditText(context); - input.setHint(R.string.reply_hint); - final AlertDialog ad = new AlertDialog.Builder(context) - .setTitle(R.string.reply_story) - .setView(input) - .setPositiveButton(R.string.confirm, (d, w) -> directMessagesRepository.broadcastStoryReply( - csrfToken, - userId, - deviceId, - ThreadIdsOrUserIds.Companion.ofOneUser(String.valueOf(currentStory.getUser().getPk())), - input.getText().toString(), - currentStory.getId(), - String.valueOf(currentStory.getUser().getPk()), - CoroutineUtilsKt.getContinuation( - (directThreadBroadcastResponse, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { - if (throwable1 != null) { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - Log.e(TAG, "onFailure: ", throwable1); - return; - } - Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); - }), Dispatchers.getIO() - ) - )) - .setNegativeButton(R.string.cancel, null) - .show(); - ad.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - input.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {} - - @Override - public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { - ad.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(!TextUtils.isEmpty(s)); - } - - @Override - public void afterTextChanged(final Editable s) {} - }); - return true; - } - if (itemId == R.id.action_profile) { - openProfile("@" + currentStory.getUser().getPk()); - } - return false; - } - - @Override - public void onPause() { - super.onPause(); - if (player != null) { - player.pause(); - } - } - - @Override - public void onResume() { - super.onResume(); - final ActionBar actionBar = fragmentActivity.getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(actionBarTitle); - actionBar.setSubtitle(actionBarSubtitle); - } - setHasOptionsMenu(true); - } - - @Override - public void onDestroy() { - releasePlayer(); - // reset subtitle - final ActionBar actionBar = fragmentActivity.getSupportActionBar(); - if (actionBar != null) { - actionBar.setSubtitle(null); - } - super.onDestroy(); - } - - private void init() { - if (getArguments() == null) return; - final StoryViewerFragmentArgs fragmentArgs = StoryViewerFragmentArgs.fromBundle(getArguments()); - options = fragmentArgs.getOptions(); - currentFeedStoryIndex = options.getCurrentFeedStoryIndex(); - // highlight = fragmentArgs.getHighlight(); - // isHighlight = !TextUtils.isEmpty(highlight); - // isArchive = fragmentArgs.getIsArchive(); - // isNotification = fragmentArgs.getIsNotification(); - final Type type = options.getType(); - if (currentFeedStoryIndex >= 0) { - switch (type) { - case HIGHLIGHT: - viewModel = new ViewModelProvider(fragmentActivity).get(HighlightsViewModel.class); - break; - case STORY_ARCHIVE: - viewModel = new ViewModelProvider(fragmentActivity).get(ArchivesViewModel.class); - break; - default: - case FEED_STORY_POSITION: - viewModel = new ViewModelProvider(fragmentActivity).get(FeedStoriesViewModel.class); - break; - } - } - setupStories(); - } - - private void setupStories() { - storiesViewModel = new ViewModelProvider(this).get(StoriesViewModel.class); - setupListeners(); - final Context context = getContext(); - if (context == null) return; - binding.storiesList.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)); - storiesAdapter = new StoriesAdapter((model, position) -> { - currentStory = model; - slidePos = position; - refreshStory(); - }); - binding.storiesList.setAdapter(storiesAdapter); - storiesViewModel.getList().observe(fragmentActivity, storiesAdapter::submitList); - resetView(); - } - - @SuppressLint("ClickableViewAccessibility") - private void setupListeners() { - final boolean hasFeedStories; - List models = null; - if (currentFeedStoryIndex >= 0) { - final Type type = options.getType(); - switch (type) { - case HIGHLIGHT: - final HighlightsViewModel highlightsViewModel = (HighlightsViewModel) viewModel; - models = highlightsViewModel.getList().getValue(); - break; - case FEED_STORY_POSITION: - final FeedStoriesViewModel feedStoriesViewModel = (FeedStoriesViewModel) viewModel; - models = feedStoriesViewModel.getList().getValue(); - break; - case STORY_ARCHIVE: - final ArchivesViewModel archivesViewModel = (ArchivesViewModel) viewModel; - models = archivesViewModel.getList().getValue(); - break; - } - } - hasFeedStories = models != null && !models.isEmpty(); - final List finalModels = models; - final Context context = getContext(); - if (context == null) return; - 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 ((isRightSwipe && index == 0) || (isLeftSwipe && index == finalModels.size() - 1)) { - Toast.makeText(context, R.string.no_more_stories, Toast.LENGTH_SHORT).show(); - return; - } - removeStickers(); - final Object feedStoryModel = isRightSwipe - ? finalModels.get(index - 1) - : finalModels.size() == index + 1 ? null : finalModels.get(index + 1); - paginateStories(feedStoryModel, finalModels.get(index), context, isRightSwipe, currentFeedStoryIndex == finalModels.size() - 2); - return; - } - removeStickers(); - if (isRightSwipe) { - if (--slidePos <= 0) { - slidePos = 0; - } - } else if (++slidePos >= storiesLen) { - slidePos = storiesLen - 1; - } - currentStory = storyModels.get(slidePos); - refreshStory(); - }; - gestureDetector = new GestureDetectorCompat(context, new SwipeGestureListener(swipeEvent)); - binding.playerView.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event)); - final GestureDetector.SimpleOnGestureListener simpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() { - @Override - public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX, final float velocityY) { - final float diffX = e2.getX() - e1.getX(); - try { - if (Math.abs(diffX) > Math.abs(e2.getY() - e1.getY()) && Math.abs(diffX) > SWIPE_THRESHOLD - && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { - swipeEvent.onSwipe(diffX > 0); - return true; - } - } catch (final Exception e) { - // if (logCollector != null) - // logCollector.appendException(e, LogCollector.LogFile.ACTIVITY_STORY_VIEWER, "setupListeners", - // new Pair<>("swipeEvent", swipeEvent), - // new Pair<>("diffX", diffX)); - if (BuildConfig.DEBUG) Log.e(TAG, "Error", e); - } - return false; - } - }; - - 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), - finalModels.get(currentFeedStoryIndex), - context, true, false)); - binding.btnForward.setOnClickListener(v -> paginateStories(finalModels.get(currentFeedStoryIndex + 1), - finalModels.get(currentFeedStoryIndex), - context, false, - currentFeedStoryIndex == finalModels.size() - 2)); - } - - binding.imageViewer.setTapListener(simpleOnGestureListener); - binding.spotify.setOnClickListener(v -> { - final Object tag = v.getTag(); - if (tag instanceof CharSequence) { - Utils.openURL(context, tag.toString()); - } - }); - binding.swipeUp.setOnClickListener(v -> { - final Object tag = v.getTag(); - if (tag instanceof CharSequence) { - new AlertDialog.Builder(context) - .setTitle(R.string.swipe_up_confirmation) - .setMessage(tag.toString()).setPositiveButton(R.string.yes, (d, w) -> Utils.openURL(context, tag.toString())) - .setNegativeButton(R.string.no, (d, w) -> d.dismiss()).show(); - } - }); - binding.viewStoryPost.setOnClickListener(v -> { - final Object tag = v.getTag(); - if (!(tag instanceof CharSequence)) return; - final String mediaId = tag.toString(); - final AlertDialog alertDialog = new AlertDialog.Builder(context) - .setCancelable(false) - .setView(R.layout.dialog_opening_post) - .create(); - alertDialog.show(); - mediaRepository.fetch( - Long.parseLong(mediaId), - CoroutineUtilsKt.getContinuation((media, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { - if (throwable != null) { - alertDialog.dismiss(); - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - return; - } - final NavController navController = NavHostFragment.findNavController(StoryViewerFragment.this); - final Bundle bundle = new Bundle(); - bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, media); - try { - navController.navigate(R.id.action_global_post_view, bundle); - alertDialog.dismiss(); - } catch (Exception e) { - Log.e(TAG, "openPostDialog: ", e); - } - }), Dispatchers.getIO()) - ); - }); - final View.OnClickListener storyActionListener = v -> { - final Object tag = v.getTag(); - if (tag instanceof PollSticker) { - poll = (PollSticker) tag; - final List tallies = poll.getTallies(); - final String[] choices = tallies.stream() - .map(t -> (poll.getViewerVote() != null && poll.getViewerVote() == tallies.indexOf(t) ? "√ " : "") - + t.getText() + " (" + t.getCount() + ")" ) - .toArray(String[]::new); - final ArrayAdapter adapter = new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, choices); - if (poll.getViewerVote() != null) { - new AlertDialog.Builder(context) - .setTitle(R.string.voted_story_poll) - .setAdapter(adapter, null) - .setPositiveButton(R.string.ok, null) - .show(); - } else { - new AlertDialog.Builder(context) - .setTitle(poll.getQuestion()) - .setAdapter(adapter, (d, w) -> { - sticking = true; - storiesRepository.respondToPoll( - csrfToken, - userId, - deviceId, - currentStory.getId().split("_")[0], - poll.getPollId(), - w, - CoroutineUtilsKt.getContinuation( - (storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { - if (throwable != null) { - sticking = false; - Log.e(TAG, "Error responding", throwable); - try { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - } catch (Exception ignored) {} - return; - } - sticking = false; - try { - poll.setViewerVote(w); - Toast.makeText(context, R.string.votef_story_poll, Toast.LENGTH_SHORT).show(); - } catch (Exception ignored) {} - }), - Dispatchers.getIO() - ) - ); - }) - .setPositiveButton(R.string.cancel, null) - .show(); - } - } else if (tag instanceof QuestionSticker) { - question = (QuestionSticker) tag; - final EditText input = new EditText(context); - input.setHint(R.string.answer_hint); - final AlertDialog ad = new AlertDialog.Builder(context) - .setTitle(question.getQuestion()) - .setView(input) - .setPositiveButton(R.string.confirm, (d, w) -> { - sticking = true; - storiesRepository.respondToQuestion( - csrfToken, - userId, - deviceId, - currentStory.getId().split("_")[0], - question.getQuestionId(), - input.getText().toString(), - CoroutineUtilsKt.getContinuation( - (storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { - if (throwable != null) { - sticking = false; - Log.e(TAG, "Error responding", throwable); - try { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - } catch (Exception ignored) {} - return; - } - sticking = false; - try { - Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); - } catch (Exception ignored) {} - }), - Dispatchers.getIO() - ) - ); - }) - .setNegativeButton(R.string.cancel, null) - .show(); - ad.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - input.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {} - - @Override - public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { - ad.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(!TextUtils.isEmpty(s)); - } - - @Override - public void afterTextChanged(final Editable s) {} - }); - } else if (tag instanceof String[]) { - final String[] rawMentions = (String[]) tag; - mentions = new ArrayList(Arrays.asList(rawMentions)); - new AlertDialog.Builder(context) - .setTitle(R.string.story_mentions) - .setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, rawMentions), (d, w) -> openProfile(mentions.get(w))) - .setPositiveButton(R.string.cancel, null) - .show(); - } else if (tag instanceof QuizSticker) { - quiz = (QuizSticker) tag; - final List tallies = quiz.getTallies(); - final String[] choices = tallies.stream().map( - t -> (quiz.getViewerAnswer() != null && quiz.getViewerAnswer() == tallies.indexOf(t) ? "√ " : "") + - (quiz.getCorrectAnswer() == tallies.indexOf(t) ? "*** " : "") + - t.getText() + " (" + t.getCount() + ")" - ).toArray(String[]::new); - new AlertDialog.Builder(context) - .setTitle(quiz.getViewerAnswer() != null ? getString(R.string.story_quizzed) : quiz.getQuestion()) - .setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, choices), (d, w) -> { - if (quiz.getViewerAnswer() == null) { - sticking = true; - storiesRepository.respondToQuiz( - csrfToken, - userId, - deviceId, - currentStory.getId().split("_")[0], - quiz.getQuizId(), - w, - CoroutineUtilsKt.getContinuation( - (storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { - if (throwable != null) { - sticking = false; - Log.e(TAG, "Error responding", throwable); - try { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - } catch (Exception ignored) {} - return; - } - sticking = false; - try { - quiz.setViewerAnswer(w); - Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); - } catch (Exception ignored) {} - }), - Dispatchers.getIO() - ) - ); - } - }) - .setPositiveButton(R.string.cancel, null) - .show(); - } else if (tag instanceof SliderSticker) { - slider = (SliderSticker) 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.getSliderVoteAverage() * 100; - input.setProgress((int) avg); - sliderView.addView(input); - sliderView.addView(tv); - if (slider.getViewerVote().isNaN() && slider.getViewerCanVote()) { - 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.getSliderVoteCount(), - slider.getSliderVoteCount(), - percentage.format(slider.getSliderVoteAverage()))) - .setView(sliderView) - .setPositiveButton(R.string.confirm, (d, w) -> { - sticking = true; - storiesRepository.respondToSlider( - csrfToken, - userId, - deviceId, - currentStory.getId().split("_")[0], - slider.getSliderId(), - sliderValue, - CoroutineUtilsKt.getContinuation( - (storyStickerResponse, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { - if (throwable != null) { - sticking = false; - Log.e(TAG, "Error responding", throwable); - try { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - } catch (Exception ignored) {} - return; - } - sticking = false; - try { - slider.setViewerVote(sliderValue); - Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); - } catch (Exception ignored) {} - }), Dispatchers.getIO() - ) - ); - }) - .setNegativeButton(R.string.cancel, null) - .show(); - } else { - input.setEnabled(false); - tv.setText(getString(R.string.slider_answer, percentage.format(slider.getViewerVote()))); - new AlertDialog.Builder(context) - .setTitle(TextUtils.isEmpty(slider.getQuestion()) ? slider.getEmoji() : slider.getQuestion()) - .setMessage(getResources().getQuantityString(R.plurals.slider_info, - slider.getSliderVoteCount(), - slider.getSliderVoteCount(), - percentage.format(slider.getSliderVoteAverage()))) - .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(); - if (context == null) return; - live = null; - slidePos = 0; - lastSlidePos = 0; - if (menuDownload != null) menuDownload.setVisible(false); - if (menuDm != null) menuDm.setVisible(false); - if (menuProfile != null) menuProfile.setVisible(false); - downloadVisible = false; - dmVisible = false; - profileVisible = false; - binding.imageViewer.setController(null); - releasePlayer(); - String currentStoryMediaId = null; - final Type type = options.getType(); - StoryViewerOptions fetchOptions = null; - switch (type) { - case HIGHLIGHT: { - final HighlightsViewModel highlightsViewModel = (HighlightsViewModel) viewModel; - final List models = highlightsViewModel.getList().getValue(); - if (models == null || models.isEmpty() || currentFeedStoryIndex >= models.size() || currentFeedStoryIndex < 0) { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - return; - } - final Story model = models.get(currentFeedStoryIndex); - currentStoryMediaId = model.getId(); - fetchOptions = StoryViewerOptions.forHighlight(model.getId()); - highlightTitle = model.getTitle(); - break; - } - case FEED_STORY_POSITION: { - final FeedStoriesViewModel feedStoriesViewModel = (FeedStoriesViewModel) viewModel; - final List models = feedStoriesViewModel.getList().getValue(); - if (models == null || currentFeedStoryIndex >= models.size() || currentFeedStoryIndex < 0) - return; - final Story model = models.get(currentFeedStoryIndex); - currentStoryMediaId = String.valueOf(model.getUser().getPk()); - currentStoryUsername = model.getUser().getUsername(); - fetchOptions = StoryViewerOptions.forUser(model.getUser().getPk(), currentStoryUsername); - live = model.getBroadcast(); - break; - } - case STORY_ARCHIVE: { - final ArchivesViewModel archivesViewModel = (ArchivesViewModel) viewModel; - final List models = archivesViewModel.getList().getValue(); - if (models == null || models.isEmpty() || currentFeedStoryIndex >= models.size() || currentFeedStoryIndex < 0) { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - return; - } - final Story model = models.get(currentFeedStoryIndex); - currentStoryMediaId = parseStoryMediaId(model.getId()); - currentStoryUsername = model.getTitle(); - fetchOptions = StoryViewerOptions.forStoryArchive(model.getId()); - break; - } - case USER: { - currentStoryMediaId = String.valueOf(options.getId()); - currentStoryUsername = options.getName(); - fetchOptions = StoryViewerOptions.forUser(options.getId(), currentStoryUsername); - break; - } - } - setTitle(type); - storiesViewModel.getList().setValue(Collections.emptyList()); - if (type == Type.STORY) { - storiesRepository.fetch( - options.getId(), - CoroutineUtilsKt.getContinuation((storyModel, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { - if (throwable != null) { - Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_SHORT).show(); - Log.e(TAG, "Error", throwable); - return; - } - 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(); - }), Dispatchers.getIO()) - ); - return; - } - if (currentStoryMediaId == null) return; - if (live != null) { - currentStory = null; - refreshLive(); - return; - } - final ServiceCallback> storyCallback = new ServiceCallback>() { - @Override - public void onSuccess(final List storyModels) { - fetching = false; - if (storyModels == null || storyModels.isEmpty()) { - storiesViewModel.getList().setValue(Collections.emptyList()); - currentStory = null; - binding.storiesList.setVisibility(View.GONE); - return; - } - binding.storiesList.setVisibility((storyModels.size() == 1 && currentFeedStoryIndex == -1) ? View.GONE : View.VISIBLE); - if (currentFeedStoryIndex == -1) { - binding.btnBackward.setVisibility(View.GONE); - binding.btnForward.setVisibility(View.GONE); - } - storiesViewModel.getList().setValue(storyModels); - currentStory = storyModels.get(0); - refreshStory(); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error", t); - } - }; - storiesRepository.getStories( - fetchOptions, - CoroutineUtilsKt.getContinuation((storyModels, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { - if (throwable != null) { - storyCallback.onFailure(throwable); - return; - } - //noinspection unchecked - storyCallback.onSuccess((List) storyModels); - }), Dispatchers.getIO()) - ); - } - - private void setTitle(final Type type) { - final boolean hasUsername = !TextUtils.isEmpty(currentStoryUsername); - if (type == Type.HIGHLIGHT) { - final ActionBar actionBar = fragmentActivity.getSupportActionBar(); - if (actionBar != null) { - actionBarTitle = highlightTitle; - actionBar.setTitle(highlightTitle); - } - } else if (hasUsername) { - currentStoryUsername = currentStoryUsername.replace("@", ""); - final ActionBar actionBar = fragmentActivity.getSupportActionBar(); - if (actionBar != null) { - actionBarTitle = currentStoryUsername; - actionBar.setTitle(currentStoryUsername); - } - } - } - - private synchronized void refreshLive() { - binding.storiesList.setVisibility(View.INVISIBLE); - binding.viewStoryPost.setVisibility(View.GONE); - binding.spotify.setVisibility(View.GONE); - binding.poll.setVisibility(View.GONE); - binding.answer.setVisibility(View.GONE); - binding.mention.setVisibility(View.GONE); - binding.quiz.setVisibility(View.GONE); - binding.slider.setVisibility(View.GONE); - lastSlidePos = slidePos; - releasePlayer(); - url = live.getDashPlaybackUrl(); - setupLive(); - final ActionBar actionBar = fragmentActivity.getSupportActionBar(); - actionBarSubtitle = TextUtils.epochSecondToString(live.getPublishedTime()); - if (actionBar != null) { - try { - actionBar.setSubtitle(actionBarSubtitle); - } catch (Exception e) { - Log.e(TAG, "refreshLive: ", e); - } - } - } - - private synchronized void refreshStory() { - if (binding.storiesList.getVisibility() == View.VISIBLE) { - final List storyModels = storiesViewModel.getList().getValue(); - if (storyModels != null && storyModels.size() > 0) { - StoryMedia item = storyModels.get(lastSlidePos); - if (item != null) { - item.setCurrentSlide(false); - storiesAdapter.notifyItemChanged(lastSlidePos, item); - } - item = storyModels.get(slidePos); - if (item != null) { - item.setCurrentSlide(true); - storiesAdapter.notifyItemChanged(slidePos, item); - } - } - } - lastSlidePos = slidePos; - - final MediaItemType itemType = currentStory.getType(); - - url = itemType == MediaItemType.MEDIA_TYPE_IMAGE - ? ResponseBodyUtils.getImageUrl(currentStory) - : ResponseBodyUtils.getVideoUrl(currentStory); - - if (currentStory.getStoryFeedMedia() != null) { - final String shortCode = currentStory.getStoryFeedMedia().get(0).getMediaId(); - binding.viewStoryPost.setVisibility(View.VISIBLE); - binding.viewStoryPost.setTag(shortCode); - } - - final StoryAppAttribution spotify = currentStory.getStoryAppAttribution(); - if (spotify != null) { - binding.spotify.setVisibility(View.VISIBLE); - binding.spotify.setText(spotify.getName()); - binding.spotify.setTag(spotify.getContentUrl().split("?")[0]); - } - - if (currentStory.getStoryPolls() != null) { - poll = currentStory.getStoryPolls().get(0).getPollSticker(); - binding.poll.setVisibility(View.VISIBLE); - binding.poll.setTag(poll); - } - - if (currentStory.getStoryQuestions() != null) { - question = currentStory.getStoryQuestions().get(0).getQuestionSticker(); - binding.answer.setVisibility(View.VISIBLE); - binding.answer.setTag(question); - } - - mentions.clear(); - if (currentStory.getReelMentions() != null) { - mentions.addAll(currentStory.getReelMentions().stream().map( - s -> s.getUser().getUsername() - ).distinct().collect(Collectors.toList())); - } - if (currentStory.getStoryHashtags() != null) { - mentions.addAll(currentStory.getStoryHashtags().stream().map( - s -> s.getHashtag().getName() - ).distinct().collect(Collectors.toList())); - } - if (currentStory.getStoryLocations() != null) { - mentions.addAll(currentStory.getStoryLocations().stream().map( - s -> s.getLocation().getShortName() + " (" + s.getLocation().getPk() + ")" - ).distinct().collect(Collectors.toList())); - } - if (mentions.size() > 0) { - binding.mention.setVisibility(View.VISIBLE); - binding.mention.setTag(mentions.stream().toArray(String[]::new)); - } - - if (currentStory.getStoryQuizs() != null) { - quiz = currentStory.getStoryQuizs().get(0).getQuizSticker(); - binding.quiz.setVisibility(View.VISIBLE); - binding.quiz.setTag(quiz); - } - - if (currentStory.getStorySliders() != null) { - slider = currentStory.getStorySliders().get(0).getSliderSticker(); - binding.slider.setVisibility(View.VISIBLE); - binding.slider.setTag(slider); - } - - if (currentStory.getStoryCta() != null) { - final StoryCta swipeUp = currentStory.getStoryCta().get(0).getLinks().get(0); - binding.swipeUp.setVisibility(View.VISIBLE); - binding.swipeUp.setText(currentStory.getLinkText()); - final String swipeUpUrl = swipeUp.getWebUri(); - final String actualLink = swipeUpUrl.startsWith("https://l.instagram.com/") - ? Uri.parse(swipeUpUrl).getQueryParameter("u") - : null; - binding.swipeUp.setTag(actualLink == null && actualLink.startsWith("http") - ? swipeUpUrl : actualLink); - } - - releasePlayer(); - final Type type = options.getType(); - if (type == Type.HASHTAG || type == Type.LOCATION) { - final ActionBar actionBar = fragmentActivity.getSupportActionBar(); - if (actionBar != null) { - actionBarTitle = currentStory.getUser().getUsername(); - actionBar.setTitle(currentStory.getUser().getUsername()); - } - } - if (itemType == MediaItemType.MEDIA_TYPE_VIDEO) setupVideo(); - else setupImage(); - - final ActionBar actionBar = fragmentActivity.getSupportActionBar(); - actionBarSubtitle = TextUtils.epochSecondToString(currentStory.getTakenAt()); - if (actionBar != null) { - try { - actionBar.setSubtitle(actionBarSubtitle); - } catch (Exception e) { - Log.e(TAG, "refreshStory: ", e); - } - } - - if (settingsHelper.getBoolean(MARK_AS_SEEN)) - storiesRepository.seen( - csrfToken, - userId, - deviceId, - currentStory.getId(), - currentStory.getTakenAt(), - System.currentTimeMillis() / 1000, - CoroutineUtilsKt.getContinuation((s, throwable) -> {}, Dispatchers.getIO()) - ); - } - - private void removeStickers() { - binding.swipeUp.setVisibility(View.GONE); - binding.quiz.setVisibility(View.GONE); - binding.spotify.setVisibility(View.GONE); - binding.mention.setVisibility(View.GONE); - binding.viewStoryPost.setVisibility(View.GONE); - binding.answer.setVisibility(View.GONE); - binding.slider.setVisibility(View.GONE); - } - - private void downloadStory() { - final Context context = getContext(); - if (context == null) return; - if (currentStory == null) { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - return; - } - DownloadUtils.download(context, currentStory); - } - - private void setupImage() { - binding.progressView.setVisibility(View.VISIBLE); - binding.playerView.setVisibility(View.GONE); - binding.imageViewer.setVisibility(View.VISIBLE); - final ImageRequest requestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(url)) - .setLocalThumbnailPreviewsEnabled(true) - .setProgressiveRenderingEnabled(true) - .build(); - final DraweeController controller = Fresco.newDraweeControllerBuilder() - .setImageRequest(requestBuilder) - .setOldController(binding.imageViewer.getController()) - .setControllerListener(new BaseControllerListener() { - - @Override - public void onFailure(final String id, final Throwable throwable) { - binding.progressView.setVisibility(View.GONE); - } - - @Override - public void onFinalImageSet(final String id, - final ImageInfo imageInfo, - final Animatable animatable) { - if (menuDownload != null) { - downloadVisible = true; - menuDownload.setVisible(true); - } - if (currentStory.getCanReply() && menuDm != null) { - dmVisible = true; - menuDm.setVisible(true); - } - if (!TextUtils.isEmpty(currentStory.getUser().getUsername())) { - profileVisible = true; - menuProfile.setVisible(true); - } - binding.progressView.setVisibility(View.GONE); - } - }) - .build(); - binding.imageViewer.setController(controller); - } - - private void setupVideo() { - binding.playerView.setVisibility(View.VISIBLE); - binding.progressView.setVisibility(View.GONE); - binding.imageViewer.setVisibility(View.GONE); - binding.imageViewer.setController(null); - - final Context context = getContext(); - if (context == null) return; - player = new SimpleExoPlayer.Builder(context).build(); - binding.playerView.setPlayer(player); - player.setPlayWhenReady(settingsHelper.getBoolean(PreferenceKeys.AUTOPLAY_VIDEOS_STORIES)); - - final Uri uri = Uri.parse(url); - final MediaItem mediaItem = MediaItem.fromUri(uri); - final ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory(new DefaultDataSourceFactory(context, "instagram")) - .createMediaSource(mediaItem); - mediaSource.addEventListener(new Handler(), new MediaSourceEventListener() { - @Override - public void onLoadCompleted(final int windowIndex, - @Nullable final MediaSource.MediaPeriodId mediaPeriodId, - @NonNull final LoadEventInfo loadEventInfo, - @NonNull final MediaLoadData mediaLoadData) { - if (menuDownload != null) { - downloadVisible = true; - menuDownload.setVisible(true); - } - if (currentStory.getCanReply() && menuDm != null) { - dmVisible = true; - menuDm.setVisible(true); - } - if (!TextUtils.isEmpty(currentStory.getUser().getUsername()) && menuProfile != null) { - profileVisible = true; - menuProfile.setVisible(true); - } - binding.progressView.setVisibility(View.GONE); - } - - @Override - public void onLoadStarted(final int windowIndex, - @Nullable final MediaSource.MediaPeriodId mediaPeriodId, - @NonNull final LoadEventInfo loadEventInfo, - @NonNull final MediaLoadData mediaLoadData) { - if (menuDownload != null) { - downloadVisible = true; - menuDownload.setVisible(true); - } - if (currentStory.getCanReply() && menuDm != null) { - dmVisible = true; - menuDm.setVisible(true); - } - if (!TextUtils.isEmpty(currentStory.getUser().getUsername()) && menuProfile != null) { - profileVisible = true; - menuProfile.setVisible(true); - } - binding.progressView.setVisibility(View.VISIBLE); - } - - @Override - public void onLoadCanceled(final int windowIndex, - @Nullable final MediaSource.MediaPeriodId mediaPeriodId, - @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, - @NonNull final LoadEventInfo loadEventInfo, - @NonNull final MediaLoadData mediaLoadData, - @NonNull final IOException error, - final boolean wasCanceled) { - if (menuDownload != null) { - downloadVisible = false; - menuDownload.setVisible(false); - } - if (menuDm != null) { - dmVisible = false; - menuDm.setVisible(false); - } - if (menuProfile != null) { - profileVisible = false; - menuProfile.setVisible(false); - } - binding.progressView.setVisibility(View.GONE); - } - }); - player.setMediaSource(mediaSource); - player.prepare(); - - binding.playerView.setOnClickListener(v -> { - if (player != null) { - if (player.getPlaybackState() == Player.STATE_ENDED) player.seekTo(0); - player.setPlayWhenReady(player.getPlaybackState() == Player.STATE_ENDED || !player.isPlaying()); - } - }); - } - - private void setupLive() { - binding.playerView.setVisibility(View.VISIBLE); - binding.progressView.setVisibility(View.GONE); - binding.imageViewer.setVisibility(View.GONE); - binding.imageViewer.setController(null); - - if (menuDownload != null) menuDownload.setVisible(false); - if (menuDm != null) menuDm.setVisible(false); - - final Context context = getContext(); - if (context == null) return; - player = new SimpleExoPlayer.Builder(context).build(); - binding.playerView.setPlayer(player); - player.setPlayWhenReady(settingsHelper.getBoolean(PreferenceKeys.AUTOPLAY_VIDEOS_STORIES)); - - final Uri uri = Uri.parse(url); - final MediaItem mediaItem = MediaItem.fromUri(uri); - final DashMediaSource mediaSource = new DashMediaSource.Factory(new DefaultDataSourceFactory(context, "instagram")) - .createMediaSource(mediaItem); - mediaSource.addEventListener(new Handler(), new MediaSourceEventListener() { - @Override - public void onLoadCompleted(final int windowIndex, - @Nullable final MediaSource.MediaPeriodId mediaPeriodId, - @NonNull final LoadEventInfo loadEventInfo, - @NonNull final MediaLoadData mediaLoadData) { - binding.progressView.setVisibility(View.GONE); - } - - @Override - public void onLoadStarted(final int windowIndex, - @Nullable final MediaSource.MediaPeriodId mediaPeriodId, - @NonNull final LoadEventInfo loadEventInfo, - @NonNull final MediaLoadData mediaLoadData) { - binding.progressView.setVisibility(View.VISIBLE); - } - - @Override - public void onLoadCanceled(final int windowIndex, - @Nullable final MediaSource.MediaPeriodId mediaPeriodId, - @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, - @NonNull final LoadEventInfo loadEventInfo, - @NonNull final MediaLoadData mediaLoadData, - @NonNull final IOException error, - final boolean wasCanceled) { - binding.progressView.setVisibility(View.GONE); - } - }); - player.setMediaSource(mediaSource); - player.prepare(); - - binding.playerView.setOnClickListener(v -> { - if (player != null) { - if (player.getPlaybackState() == Player.STATE_ENDED) player.seekTo(0); - player.setPlayWhenReady(player.getPlaybackState() == Player.STATE_ENDED || !player.isPlaying()); - } - }); - } - - private void openProfile(final String username) { - final ActionBar actionBar = fragmentActivity.getSupportActionBar(); - if (actionBar != null) { - actionBar.setSubtitle(null); - } - final char t = username.charAt(0); - if (t == '@') { - final NavDirections action = HashTagFragmentDirections.actionGlobalProfileFragment(username); - NavHostFragment.findNavController(this).navigate(action); - } else if (t == '#') { - final NavDirections action = HashTagFragmentDirections.actionGlobalHashTagFragment(username.substring(1)); - NavHostFragment.findNavController(this).navigate(action); - } else { - final NavDirections action = ProfileFragmentDirections - .actionGlobalLocationFragment(Long.parseLong(username.split(" \\(")[1].replace(")", ""))); - NavHostFragment.findNavController(this).navigate(action); - } - } - - private void releasePlayer() { - if (player == null) return; - try { player.stop(true); } catch (Exception ignored) { } - try { player.release(); } catch (Exception ignored) { } - player = 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 Story - && viewModel instanceof FeedStoriesViewModel) { - final FeedStoriesViewModel feedStoriesViewModel = (FeedStoriesViewModel) viewModel; - final Story oldFeedStoryModel = (Story) oldFeedStory; - if (oldFeedStoryModel.getSeen() == null || !oldFeedStoryModel.getSeen().equals(oldFeedStoryModel.getLatestReelMedia())) { - oldFeedStoryModel.setSeen(oldFeedStoryModel.getLatestReelMedia()); - final List models = feedStoriesViewModel.getList().getValue(); - final List modelsCopy = models == null ? new ArrayList<>() : new ArrayList<>(models); - modelsCopy.set(currentFeedStoryIndex, oldFeedStoryModel); - feedStoriesViewModel.getList().postValue(modelsCopy); - } - } - fetching = true; - binding.btnBackward.setVisibility(currentFeedStoryIndex == 1 && backward ? View.INVISIBLE : View.VISIBLE); - binding.btnForward.setVisibility(last ? View.INVISIBLE : View.VISIBLE); - currentFeedStoryIndex = backward ? (currentFeedStoryIndex - 1) : (currentFeedStoryIndex + 1); - resetView(); - } - } - - /** - * Parses the Story's media ID. For user stories this is a number, but for archive stories - * this is "archiveDay:" plus a number. - */ - private static String parseStoryMediaId(String rawId) { - final String regex = "(?:archiveDay:)?(.+)"; - final Pattern pattern = Pattern.compile(regex); - final Matcher matcher = pattern.matcher(rawId); - - if (matcher.matches() && matcher.groupCount() >= 1) { - return matcher.group(1); - } - - return rawId; - } -} diff --git a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.kt b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.kt new file mode 100644 index 00000000..121c8150 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.kt @@ -0,0 +1,902 @@ +package awais.instagrabber.fragments + +import android.annotation.SuppressLint +import android.content.DialogInterface.OnClickListener +import android.graphics.drawable.Animatable +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.util.Log +import android.view.* +import android.view.GestureDetector.SimpleOnGestureListener +import android.widget.* +import android.widget.SeekBar.OnSeekBarChangeListener +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ContextThemeWrapper +import androidx.appcompat.widget.PopupMenu +import androidx.core.view.GestureDetectorCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment +import androidx.recyclerview.widget.LinearLayoutManager +import awais.instagrabber.BuildConfig +import awais.instagrabber.R +import awais.instagrabber.adapters.StoriesAdapter +import awais.instagrabber.customviews.helpers.SwipeGestureListener +import awais.instagrabber.databinding.FragmentStoryViewerBinding +import awais.instagrabber.fragments.settings.PreferenceKeys +import awais.instagrabber.interfaces.SwipeEvent +import awais.instagrabber.models.Resource +import awais.instagrabber.models.enums.FavoriteType +import awais.instagrabber.models.enums.MediaItemType +import awais.instagrabber.models.enums.StoryPaginationType +import awais.instagrabber.repositories.requests.StoryViewerOptions +import awais.instagrabber.repositories.responses.directmessages.RankedRecipient +import awais.instagrabber.repositories.responses.stories.* +import awais.instagrabber.utils.* +import awais.instagrabber.utils.DownloadUtils.download +import awais.instagrabber.utils.TextUtils.epochSecondToString +import awais.instagrabber.utils.extensions.TAG +import awais.instagrabber.viewmodels.ArchivesViewModel +import awais.instagrabber.viewmodels.FeedStoriesViewModel +import awais.instagrabber.viewmodels.HighlightsViewModel +import awais.instagrabber.viewmodels.StoryFragmentViewModel +import awais.instagrabber.webservices.MediaRepository +import awais.instagrabber.webservices.StoriesRepository +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.drawee.controller.BaseControllerListener +import com.facebook.drawee.interfaces.DraweeController +import com.facebook.imagepipeline.image.ImageInfo +import com.facebook.imagepipeline.request.ImageRequestBuilder +import com.google.android.exoplayer2.MediaItem +import com.google.android.exoplayer2.Player +import com.google.android.exoplayer2.SimpleExoPlayer +import com.google.android.exoplayer2.source.* +import com.google.android.exoplayer2.source.dash.DashMediaSource +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory +import com.google.android.material.textfield.TextInputEditText +import java.io.IOException +import java.text.NumberFormat +import java.util.* + + +class StoryViewerFragment : Fragment() { + private val TAG = "StoryViewerFragment" + + private val cookie = Utils.settingsHelper.getString(Constants.COOKIE) + private var root: View? = null + private var currentStoryUsername: String? = null + private var highlightTitle: String? = null + private var storiesAdapter: StoriesAdapter? = null + private var swipeEvent: SwipeEvent? = null + private var gestureDetector: GestureDetectorCompat? = null + private val storiesRepository: StoriesRepository? = null + private val mediaRepository: MediaRepository? = null + private var live: Broadcast? = null + private var menuProfile: MenuItem? = null + private var profileVisible: Boolean = false + private var player: SimpleExoPlayer? = null + + private var actionBarTitle: String? = null + private var actionBarSubtitle: String? = null + private var fetching = false + private val sticking = false + private var shouldRefresh = true + private var dmVisible = false + private var currentFeedStoryIndex = 0 + private var sliderValue = 0.0 + private var options: StoryViewerOptions? = null + private var listViewModel: ViewModel? = null + private var backStackSavedStateResultLiveData: MutableLiveData? = null + private lateinit var fragmentActivity: AppCompatActivity + private lateinit var storiesViewModel: StoryFragmentViewModel + private lateinit var binding: FragmentStoryViewerBinding + + @Suppress("UNCHECKED_CAST") + private val backStackSavedStateObserver = Observer { result -> + if (result == null) return@Observer + if ((result is RankedRecipient)) { + if (context != null) { + Toast.makeText(context, R.string.sending, Toast.LENGTH_SHORT).show() + } + storiesViewModel.shareDm(result) + } else if ((result is Set<*>)) { + try { + if (context != null) { + Toast.makeText(context, R.string.sending, Toast.LENGTH_SHORT).show() + } + storiesViewModel.shareDm(result as Set) + } catch (e: Exception) { + Log.e(TAG, "share: ", e) + } + } + // clear result + backStackSavedStateResultLiveData?.postValue(null) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + fragmentActivity = requireActivity() as AppCompatActivity + storiesViewModel = ViewModelProvider(this).get(StoryFragmentViewModel::class.java) + setHasOptionsMenu(true) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + if (root != null) { + shouldRefresh = false + return root + } + binding = FragmentStoryViewerBinding.inflate(inflater, container, false) + root = binding.root + return root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + if (!shouldRefresh) return + init() + shouldRefresh = false + } + + override fun onCreateOptionsMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.story_menu, menu) + menuProfile = menu.findItem(R.id.action_profile) + menuProfile!!.isVisible = profileVisible + } + + override fun onPrepareOptionsMenu(menu: Menu) { + // hide menu items from activity + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val context = context ?: return false + val itemId = item.itemId + if (itemId == R.id.action_profile) { + val username = storiesViewModel.getCurrentStory().value?.user?.username + openProfile(Pair(username, FavoriteType.USER)) + return true + } + return false + } + + override fun onPause() { + super.onPause() + player?.pause() ?: return + } + + override fun onResume() { + super.onResume() + setHasOptionsMenu(true) + try { + val backStackEntry = NavHostFragment.findNavController(this).currentBackStackEntry + if (backStackEntry != null) { + backStackSavedStateResultLiveData = backStackEntry.savedStateHandle.getLiveData("result") + backStackSavedStateResultLiveData?.observe(viewLifecycleOwner, backStackSavedStateObserver) + } + } catch (e: Exception) { + Log.e(TAG, "onResume: ", e) + } + val actionBar = fragmentActivity.supportActionBar ?: return + actionBar.title = storiesViewModel.getTitle().value + actionBar.subtitle = storiesViewModel.getDate().value + } + + override fun onDestroy() { + releasePlayer() + val actionBar = fragmentActivity.supportActionBar + actionBar?.subtitle = null + super.onDestroy() + } + + private fun init() { + val args = arguments + if (args == null) return + val fragmentArgs = StoryViewerFragmentArgs.fromBundle(args) + options = fragmentArgs.options + currentFeedStoryIndex = options!!.currentFeedStoryIndex + val type = options!!.type + if (currentFeedStoryIndex >= 0) { + listViewModel = when (type) { + StoryViewerOptions.Type.HIGHLIGHT -> ViewModelProvider(fragmentActivity).get( + HighlightsViewModel::class.java + ) + StoryViewerOptions.Type.STORY_ARCHIVE -> ViewModelProvider(fragmentActivity).get( + ArchivesViewModel::class.java + ) + StoryViewerOptions.Type.FEED_STORY_POSITION -> ViewModelProvider(fragmentActivity).get( + FeedStoriesViewModel::class.java + ) + else -> ViewModelProvider(fragmentActivity).get( + FeedStoriesViewModel::class.java + ) + } + } + setupButtons() + setupStories() + } + + private fun setupStories() { + setupListeners() + val context = context ?: return + binding.storiesList.layoutManager = + LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + storiesAdapter = StoriesAdapter { model: StoryMedia?, position: Int -> + storiesViewModel.setMedia(position) + } + binding.storiesList.adapter = storiesAdapter + storiesViewModel.getCurrentStory().observe(fragmentActivity, { + if (it?.items != null) { + val storyMedias = it.items.toMutableList() + val newItem = storyMedias.get(0) + newItem.isCurrentSlide = true + storyMedias.set(0, newItem) + storiesAdapter!!.submitList(storyMedias) + storiesViewModel.setMedia(0) + } + }) + storiesViewModel.getDate().observe(fragmentActivity, { + val actionBar = fragmentActivity.supportActionBar + if (actionBar != null && it != null) actionBar.subtitle = it + }) + storiesViewModel.getTitle().observe(fragmentActivity, { + val actionBar = fragmentActivity.supportActionBar + if (actionBar != null && it != null) actionBar.title = it + }) + storiesViewModel.getCurrentMedia().observe(fragmentActivity, { refreshStory(it) }) + storiesViewModel.getCurrentIndex().observe(fragmentActivity, { + storiesAdapter!!.paginate(it) + }) + storiesViewModel.getOptions().observe(fragmentActivity, { + binding.stickers.isEnabled = it.first.size > 0 + }) + + resetView() + } + + private fun setupButtons() { + binding.btnDownload.setOnClickListener({ _ -> downloadStory() }) + binding.btnForward.setOnClickListener({ _ -> storiesViewModel.skip(false) }) + binding.btnBackward.setOnClickListener({ _ -> storiesViewModel.skip(true) }) + binding.btnShare.setOnClickListener({ _ -> shareStoryViaDm() }) + binding.btnReply.setOnClickListener({ _ -> createReplyDialog(null) }) + binding.stickers.setOnClickListener({ _ -> showStickerMenu() }) + } + + @SuppressLint("ClickableViewAccessibility") + private fun setupListeners() { + val hasFeedStories: Boolean + var models: List? = null + if (currentFeedStoryIndex >= 0) { + val type = options!!.type + when (type) { + StoryViewerOptions.Type.HIGHLIGHT -> { + val highlightsViewModel = listViewModel as HighlightsViewModel? + models = highlightsViewModel!!.list.value + } + StoryViewerOptions.Type.FEED_STORY_POSITION -> { + val feedStoriesViewModel = listViewModel as FeedStoriesViewModel? + models = feedStoriesViewModel!!.list.value + } + StoryViewerOptions.Type.STORY_ARCHIVE -> { + val archivesViewModel = listViewModel as ArchivesViewModel? + models = archivesViewModel!!.list.value + } + } + } + hasFeedStories = models != null && !models.isEmpty() + + storiesViewModel.getPagination().observe(fragmentActivity, { + if (models != null) { + when (it) { + StoryPaginationType.FORWARD -> { + paginateStories(false, currentFeedStoryIndex == models.size - 2) + } + StoryPaginationType.BACKWARD -> { + paginateStories(true, false) + } + StoryPaginationType.ERROR -> { + Toast.makeText( + context, + R.string.downloader_unknown_error, + Toast.LENGTH_SHORT + ).show() + } + StoryPaginationType.DO_NOTHING -> { + } // do nothing + } + } + }) + + val context = context ?: return + swipeEvent = label@ SwipeEvent { isRightSwipe: Boolean -> + storiesViewModel.paginate(isRightSwipe) + } + gestureDetector = GestureDetectorCompat(context, SwipeGestureListener(swipeEvent)) + binding.playerView.setOnTouchListener { _, event -> gestureDetector!!.onTouchEvent(event) } + val simpleOnGestureListener: SimpleOnGestureListener = object : SimpleOnGestureListener() { + override fun onFling( + e1: MotionEvent, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + val diffX = e2.x - e1.x + try { + if (Math.abs(diffX) > Math.abs(e2.y - e1.y) && Math.abs(diffX) > SwipeGestureListener.SWIPE_THRESHOLD && Math.abs( + velocityX + ) > SwipeGestureListener.SWIPE_VELOCITY_THRESHOLD + ) { + storiesViewModel.paginate(diffX > 0) + return true + } + } catch (e: Exception) { + if (BuildConfig.DEBUG) Log.e(TAG, "Error", e) + } + return false + } + } + if (hasFeedStories) { + binding.btnBackward.isEnabled = currentFeedStoryIndex != 0 + binding.btnForward.isEnabled = currentFeedStoryIndex != models!!.size - 1 + } + binding.imageViewer.setTapListener(simpleOnGestureListener) + + // process stickers + } + + private fun resetView() { + val context = context ?: return + live = null + if (menuProfile != null) menuProfile!!.isVisible = false + profileVisible = false + binding.imageViewer.controller = null + releasePlayer() + val type = options!!.type + var fetchOptions: StoryViewerOptions? = null + when (type) { + StoryViewerOptions.Type.HIGHLIGHT -> { + val highlightsViewModel = listViewModel as HighlightsViewModel? + val models = highlightsViewModel!!.list.value + if (models == null || models.isEmpty() || currentFeedStoryIndex >= models.size || currentFeedStoryIndex < 0) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT) + .show() + return + } + val (id, _, _, _, _, _, _, _, _, title) = models[currentFeedStoryIndex] + fetchOptions = StoryViewerOptions.forHighlight(id) + } + StoryViewerOptions.Type.FEED_STORY_POSITION -> { + val feedStoriesViewModel = listViewModel as FeedStoriesViewModel? + val models = feedStoriesViewModel!!.list.value + if (models == null || currentFeedStoryIndex >= models.size || currentFeedStoryIndex < 0) return + val (_, _, _, _, user, _, _, _, _, _, _, broadcast) = models[currentFeedStoryIndex] + currentStoryUsername = user!!.username + fetchOptions = StoryViewerOptions.forUser(user.pk, currentStoryUsername) + live = broadcast + } + StoryViewerOptions.Type.STORY_ARCHIVE -> { + val archivesViewModel = listViewModel as ArchivesViewModel? + val models = archivesViewModel!!.list.value + if (models == null || models.isEmpty() || currentFeedStoryIndex >= models.size || currentFeedStoryIndex < 0) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT) + .show() + return + } + val (id, _, _, _, _, _, _, _, _, title) = models[currentFeedStoryIndex] + currentStoryUsername = title + fetchOptions = StoryViewerOptions.forStoryArchive(id) + } + StoryViewerOptions.Type.USER -> { + currentStoryUsername = options!!.name + fetchOptions = StoryViewerOptions.forUser(options!!.id, currentStoryUsername) + } + } + if (type == StoryViewerOptions.Type.STORY) { + storiesViewModel.fetchSingleMedia(options!!.id) + return + } + if (live != null) { + refreshLive() + return + } + storiesViewModel.fetchStory(fetchOptions).observe(fragmentActivity, { + // toast error if necessary? + }) + } + + @Synchronized + private fun refreshLive() { + releasePlayer() + setupLive(live!!.dashPlaybackUrl ?: live!!.dashAbrPlaybackUrl ?: return) + val actionBar = fragmentActivity.supportActionBar + actionBarSubtitle = epochSecondToString(live!!.publishedTime!!) + if (actionBar != null) { + try { + actionBar.setSubtitle(actionBarSubtitle) + } catch (e: Exception) { + Log.e(TAG, "refreshLive: ", e) + } + } + } + + @Synchronized + private fun refreshStory(currentStory: StoryMedia) { + val itemType = currentStory.type + val url = if (itemType === MediaItemType.MEDIA_TYPE_IMAGE) ResponseBodyUtils.getImageUrl(currentStory) + else ResponseBodyUtils.getVideoUrl(currentStory) + + releasePlayer() + + binding.btnDownload.isEnabled = false + binding.btnShare.isEnabled = currentStory.canReshare + binding.btnReply.isEnabled = currentStory.canReply + if (itemType === MediaItemType.MEDIA_TYPE_VIDEO) setupVideo(url) else setupImage(url) + +// if (Utils.settingsHelper.getBoolean(MARK_AS_SEEN)) storiesRepository!!.seen( +// csrfToken, +// userId, +// deviceId, +// currentStory!!.id!!, +// currentStory!!.takenAt, +// System.currentTimeMillis() / 1000 +// ) + } + + private fun downloadStory() { + val context = context ?: return + val currentStory = storiesViewModel.getMedia().value + if (currentStory == null) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show() + return + } + download(context, currentStory) + } + + private fun setupImage(url: String) { + binding.progressView.visibility = View.VISIBLE + binding.playerView.visibility = View.GONE + binding.imageViewer.visibility = View.VISIBLE + val requestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(url)) + .setLocalThumbnailPreviewsEnabled(true) + .setProgressiveRenderingEnabled(true) + .build() + val controller: DraweeController = Fresco.newDraweeControllerBuilder() + .setImageRequest(requestBuilder) + .setOldController(binding.imageViewer.controller) + .setControllerListener(object : BaseControllerListener() { + override fun onFailure(id: String, throwable: Throwable) { + binding.btnDownload.isEnabled = false + binding.progressView.visibility = View.GONE + } + + override fun onFinalImageSet( + id: String, + imageInfo: ImageInfo?, + animatable: Animatable? + ) { + binding.btnDownload.isEnabled = true + binding.progressView.visibility = View.GONE + } + }) + .build() + binding.imageViewer.controller = controller + } + + private fun setupVideo(url: String) { + binding.playerView.visibility = View.VISIBLE + binding.progressView.visibility = View.GONE + binding.imageViewer.visibility = View.GONE + binding.imageViewer.controller = null + val context = context ?: return + player = SimpleExoPlayer.Builder(context).build() + binding.playerView.player = player + player!!.playWhenReady = + Utils.settingsHelper.getBoolean(PreferenceKeys.AUTOPLAY_VIDEOS_STORIES) + val uri = Uri.parse(url) + val mediaItem = MediaItem.fromUri(uri) + val mediaSource = + ProgressiveMediaSource.Factory(DefaultDataSourceFactory(context, "instagram")) + .createMediaSource(mediaItem) + mediaSource.addEventListener(Handler(), object : MediaSourceEventListener { + override fun onLoadCompleted( + windowIndex: Int, + mediaPeriodId: MediaSource.MediaPeriodId?, + loadEventInfo: LoadEventInfo, + mediaLoadData: MediaLoadData + ) { + binding.btnDownload.isEnabled = true + binding.progressView.visibility = View.GONE + } + + override fun onLoadStarted( + windowIndex: Int, + mediaPeriodId: MediaSource.MediaPeriodId?, + loadEventInfo: LoadEventInfo, + mediaLoadData: MediaLoadData + ) { + binding.btnDownload.isEnabled = true + binding.progressView.visibility = View.VISIBLE + } + + override fun onLoadCanceled( + windowIndex: Int, + mediaPeriodId: MediaSource.MediaPeriodId?, + loadEventInfo: LoadEventInfo, + mediaLoadData: MediaLoadData + ) { + binding.progressView.visibility = View.GONE + } + + override fun onLoadError( + windowIndex: Int, + mediaPeriodId: MediaSource.MediaPeriodId?, + loadEventInfo: LoadEventInfo, + mediaLoadData: MediaLoadData, + error: IOException, + wasCanceled: Boolean + ) { + binding.btnDownload.isEnabled = false + if (menuProfile != null) { + profileVisible = false + menuProfile!!.isVisible = false + } + binding.progressView.visibility = View.GONE + } + }) + player!!.setMediaSource(mediaSource) + player!!.prepare() + binding.playerView.setOnClickListener { v: View? -> + if (player != null) { + if (player!!.playbackState == Player.STATE_ENDED) player!!.seekTo(0) + player!!.playWhenReady = + player!!.playbackState == Player.STATE_ENDED || !player!!.isPlaying + } + } + } + + private fun setupLive(url: String) { + binding.playerView.visibility = View.VISIBLE + binding.progressView.visibility = View.GONE + binding.imageViewer.visibility = View.GONE + binding.imageViewer.controller = null + val context = context ?: return + player = SimpleExoPlayer.Builder(context).build() + binding.playerView.player = player + player!!.playWhenReady = + Utils.settingsHelper.getBoolean(PreferenceKeys.AUTOPLAY_VIDEOS_STORIES) + val uri = Uri.parse(url) + val mediaItem = MediaItem.fromUri(uri) + val mediaSource = DashMediaSource.Factory(DefaultDataSourceFactory(context, "instagram")) + .createMediaSource(mediaItem) + mediaSource.addEventListener(Handler(), object : MediaSourceEventListener { + override fun onLoadCompleted( + windowIndex: Int, + mediaPeriodId: MediaSource.MediaPeriodId?, + loadEventInfo: LoadEventInfo, + mediaLoadData: MediaLoadData + ) { + binding.progressView.visibility = View.GONE + } + + override fun onLoadStarted( + windowIndex: Int, + mediaPeriodId: MediaSource.MediaPeriodId?, + loadEventInfo: LoadEventInfo, + mediaLoadData: MediaLoadData + ) { + binding.progressView.visibility = View.VISIBLE + } + + override fun onLoadCanceled( + windowIndex: Int, + mediaPeriodId: MediaSource.MediaPeriodId?, + loadEventInfo: LoadEventInfo, + mediaLoadData: MediaLoadData + ) { + binding.progressView.visibility = View.GONE + } + + override fun onLoadError( + windowIndex: Int, + mediaPeriodId: MediaSource.MediaPeriodId?, + loadEventInfo: LoadEventInfo, + mediaLoadData: MediaLoadData, + error: IOException, + wasCanceled: Boolean + ) { + binding.progressView.visibility = View.GONE + } + }) + player!!.setMediaSource(mediaSource) + player!!.prepare() + binding.playerView.setOnClickListener { _ -> + if (player != null) { + if (player!!.playbackState == Player.STATE_ENDED) player!!.seekTo(0) + player!!.playWhenReady = + player!!.playbackState == Player.STATE_ENDED || !player!!.isPlaying + } + } + } + + private fun openProfile(data: Pair) { + val navController: NavController = NavHostFragment.findNavController(this) + val bundle = Bundle() + if (data.first == null) { + // toast + return + } + val actionBar = fragmentActivity.supportActionBar + if (actionBar != null) { + actionBar.title = null + actionBar.subtitle = null + } + when (data.second) { + FavoriteType.USER -> { + bundle.putString("username", data.first) + navController.navigate(R.id.action_global_profileFragment, bundle) + } + FavoriteType.HASHTAG -> { + bundle.putString("hashtag", data.first) + navController.navigate(R.id.action_global_hashTagFragment, bundle) + } + FavoriteType.LOCATION -> { + bundle.putLong("locationId", data.first!!.toLong()) + navController.navigate(R.id.action_global_locationFragment, bundle) + } + } + } + + private fun releasePlayer() { + if (player == null) return + try { + player!!.stop(true) + } catch (ignored: Exception) { + } + try { + player!!.release() + } catch (ignored: Exception) { + } + player = null + } + + private fun paginateStories( + backward: Boolean, + last: Boolean + ) { + binding.btnBackward.isEnabled = currentFeedStoryIndex != 1 || !backward + binding.btnForward.isEnabled = !last + currentFeedStoryIndex = if (backward) currentFeedStoryIndex - 1 else currentFeedStoryIndex + 1 + resetView() + } + + private fun createChoiceDialog( + title: String?, + tallies: List, + onClickListener: OnClickListener, + viewerVote: Int?, + correctAnswer: Int? + ) { + val context = context ?: return + val choices = tallies.map { + (if (viewerVote == tallies.indexOf(it)) "√ " else "") + + (if (correctAnswer == tallies.indexOf(it)) "*** " else "") + + it.text + " (" + it.count + ")" } + val builder = AlertDialog.Builder(context) + if (title != null) builder.setTitle(title) + if (viewerVote != null) builder.setMessage(R.string.story_quizzed) + builder.setPositiveButton(if (viewerVote == null) R.string.cancel else R.string.ok, null) + val adapter = ArrayAdapter(context, android.R.layout.simple_list_item_1, choices.toTypedArray()) + builder.setAdapter(adapter, onClickListener) + builder.show() + } + + private fun createMentionDialog() { + val context = context ?: return + val adapter = ArrayAdapter(context, android.R.layout.simple_list_item_1, storiesViewModel.getMentionTexts()) + val builder = AlertDialog.Builder(context) + .setPositiveButton(R.string.ok, null) + .setAdapter(adapter, { _, w -> + val data = storiesViewModel.getMention(w) + if (data != null) openProfile(Pair(data.second, data.third)) + }) + builder.show() + } + + private fun createSliderDialog() { + val slider = storiesViewModel.getSlider().value ?: return + val context = context ?: return + val percentage: NumberFormat = NumberFormat.getPercentInstance() + percentage.maximumFractionDigits = 2 + val sliderView = LinearLayout(context) + sliderView.layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + sliderView.orientation = LinearLayout.VERTICAL + val tv = TextView(context) + tv.gravity = Gravity.CENTER_HORIZONTAL + val input = SeekBar(context) + val avg: Double = slider.sliderVoteAverage ?: 0.5 + input.progress = (avg * 100).toInt() + var onClickListener: OnClickListener? = null + + if (slider.viewerVote == null && slider.viewerCanVote == true) { + input.isEnabled = true + input.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + sliderValue = progress / 100.0 + tv.text = percentage.format(sliderValue) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) {} + override fun onStopTrackingTouch(seekBar: SeekBar) {} + }) + onClickListener = OnClickListener { _, _ -> storiesViewModel.answerSlider(sliderValue) } + } + else { + input.isEnabled = false + tv.text = getString(R.string.slider_answer, percentage.format(slider.viewerVote)) + } + sliderView.addView(input) + sliderView.addView(tv) + val builder = AlertDialog.Builder(context) + .setTitle(if (slider.question.isNullOrEmpty()) slider.emoji else slider.question) + .setMessage( + resources.getQuantityString(R.plurals.slider_info, + slider.sliderVoteCount ?: 0, + slider.sliderVoteCount ?: 0, + percentage.format(avg))) + .setView(sliderView) + .setPositiveButton(R.string.ok, onClickListener) + + builder.show() + } + + private fun createReplyDialog(question: String?) { + val context = context ?: return + val input = TextInputEditText(context) + input.setHint(R.string.reply_hint) + val builder = AlertDialog.Builder(context) + .setTitle(question ?: context.getString(R.string.reply_story)) + .setView(input) + val onClickListener = OnClickListener{ _, _ -> + val result = + if (question != null) storiesViewModel.answerQuestion(input.text.toString()) + else storiesViewModel.reply(input.text.toString()) + if (result == null) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT) + .show() + } + else result.observe(viewLifecycleOwner, { + when (it.status) { + Resource.Status.SUCCESS -> { + Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT) + .show() + } + Resource.Status.ERROR -> { + Toast.makeText(context, "Error: " + it.message, Toast.LENGTH_SHORT) + .show() + } + Resource.Status.LOADING -> { + Toast.makeText(context, R.string.sending, Toast.LENGTH_SHORT).show() + } + } + }) + } + builder.setPositiveButton(R.string.confirm, onClickListener) + builder.show() + } + + private fun shareStoryViaDm() { + val actionGlobalUserSearch = UserSearchFragmentDirections.actionGlobalUserSearch().apply { + title = getString(R.string.share) + setActionLabel(getString(R.string.send)) + showGroups = true + multiple = true + setSearchMode(UserSearchFragment.SearchMode.RAVEN) + } + try { + val navController = NavHostFragment.findNavController(this@StoryViewerFragment) + navController.navigate(actionGlobalUserSearch) + } catch (e: Exception) { + Log.e(TAG, "shareStoryViaDm: ", e) + } + } + + private fun showStickerMenu() { + val data = storiesViewModel.getOptions().value + if (data == null) return + val themeWrapper = ContextThemeWrapper(context, R.style.popupMenuStyle) + val popupMenu = PopupMenu(themeWrapper, binding.stickers) + val menu = popupMenu.menu + data.first.map { + if (it.second != 0) menu.add(0, it.first, 0, it.second) + if (it.first == R.id.swipeUp) menu.add(0, R.id.swipeUp, 0, data.second) + if (it.first == R.id.spotify) menu.add(0, R.id.spotify, 0, data.third) + } + popupMenu.setOnMenuItemClickListener { item: MenuItem -> + val itemId = item.itemId + if (itemId == R.id.spotify) openExternalLink(storiesViewModel.getAppAttribution()) + else if (itemId == R.id.swipeUp) openExternalLink(storiesViewModel.getSwipeUp()) + else if (itemId == R.id.mentions) createMentionDialog() + else if (itemId == R.id.slider) createSliderDialog() + else if (itemId == R.id.question) { + val question = storiesViewModel.getQuestion().value + if (question != null) createReplyDialog(question.question) + } + else if (itemId == R.id.quiz) { + val quiz = storiesViewModel.getQuiz().value + if (quiz != null) createChoiceDialog( + quiz.question, + quiz.tallies, + { _, w -> storiesViewModel.answerQuiz(w) }, + quiz.viewerAnswer, + quiz.correctAnswer + ) + } + else if (itemId == R.id.poll) { + val poll = storiesViewModel.getPoll().value + if (poll != null) createChoiceDialog( + poll.question, + poll.tallies, + { _, w -> storiesViewModel.answerPoll(w) }, + poll.viewerVote, + null + ) + } + else if (itemId == R.id.viewStoryPost) { + storiesViewModel.getLinkedPost().observe(viewLifecycleOwner, { + if (it == null) Toast.makeText(context, "Error: LiveData is null", Toast.LENGTH_SHORT).show() + else when (it.status) { + Resource.Status.SUCCESS -> { + if (it.data != null) { + val actionBar = fragmentActivity.supportActionBar + if (actionBar != null) { + actionBar.title = null + actionBar.subtitle = null + } + val navController = + NavHostFragment.findNavController(this@StoryViewerFragment) + val bundle = Bundle() + bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, it.data) + try { + navController.navigate(R.id.action_global_post_view, bundle) + } catch (e: Exception) { + Log.e(TAG, "openPostDialog: ", e) + } + } + } + Resource.Status.ERROR -> { + Toast.makeText(context, "Error: " + it.message, Toast.LENGTH_SHORT) + .show() + } + Resource.Status.LOADING -> { + Toast.makeText(context, R.string.opening_post, Toast.LENGTH_SHORT) + .show() + } + } + }) + } + false + } + popupMenu.show() + } + + private fun openExternalLink(url: String?) { + val context = context ?: return + if (url == null) return + AlertDialog.Builder(context) + .setTitle(R.string.swipe_up_confirmation) + .setMessage(url).setPositiveButton(R.string.yes, { _, _ -> Utils.openURL(context, url) }) + .setNegativeButton(R.string.no, null) + .show() + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.kt b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.kt index 9c2ef9ae..b043e0f0 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.kt +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.kt @@ -199,7 +199,7 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall } } private val onProfilePicClickListener = View.OnClickListener { - val hasStories = viewModel.userStories.value?.data?.isNotEmpty() ?: false + val hasStories = viewModel.userStories.value?.data != null if (!hasStories) { showProfilePicDialog() return@OnClickListener @@ -514,7 +514,7 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall highlightsAdapter?.submitList(it.data) } viewModel.userStories.observe(viewLifecycleOwner) { - binding.header.mainProfileImage.setStoriesBorder(if (it.data.isNullOrEmpty()) 0 else 1) + binding.header.mainProfileImage.setStoriesBorder(if (it.data == null) 0 else 1) } viewModel.eventLiveData.observe(viewLifecycleOwner) { val event = it?.getContentIfNotHandled() ?: return@observe diff --git a/app/src/main/java/awais/instagrabber/models/enums/StoryPaginationType.kt b/app/src/main/java/awais/instagrabber/models/enums/StoryPaginationType.kt new file mode 100644 index 00000000..5bdb4f8f --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/enums/StoryPaginationType.kt @@ -0,0 +1,7 @@ +package awais.instagrabber.models.enums + +import java.io.Serializable + +enum class StoryPaginationType : Serializable { + FORWARD, BACKWARD, DO_NOTHING, ERROR +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/repositories/StoriesService.kt b/app/src/main/java/awais/instagrabber/repositories/StoriesService.kt index e964f54b..f7793767 100644 --- a/app/src/main/java/awais/instagrabber/repositories/StoriesService.kt +++ b/app/src/main/java/awais/instagrabber/repositories/StoriesService.kt @@ -34,7 +34,7 @@ interface StoriesService { @FormUrlEncoded @POST("/api/v1/media/{storyId}/{stickerId}/{action}/") suspend fun respondToSticker( - @Path("storyId") storyId: String, + @Path("storyId") storyId: Long, @Path("stickerId") stickerId: Long, @Path("action") action: String, // story_poll_vote, story_question_response, story_slider_vote, story_quiz_answer @FieldMap form: Map, diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/stories/StoryMedia.kt b/app/src/main/java/awais/instagrabber/repositories/responses/stories/StoryMedia.kt index f9559fd0..399e11ca 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/stories/StoryMedia.kt +++ b/app/src/main/java/awais/instagrabber/repositories/responses/stories/StoryMedia.kt @@ -10,8 +10,8 @@ import java.io.Serializable data class StoryMedia( // inherited from Media - val pk: String? = null, - val id: String? = null, + val pk: Long = -1, + val id: String = "", val takenAt: Long = -1, val user: User? = null, val canReshare: Boolean = false, diff --git a/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt b/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt index de704c59..5f4a0678 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt +++ b/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt @@ -16,7 +16,6 @@ import awais.instagrabber.repositories.responses.User import awais.instagrabber.repositories.responses.UserProfileContextLink import awais.instagrabber.repositories.responses.directmessages.RankedRecipient import awais.instagrabber.repositories.responses.stories.Story -import awais.instagrabber.repositories.responses.stories.StoryMedia import awais.instagrabber.utils.ControlledRunner import awais.instagrabber.utils.Event import awais.instagrabber.utils.SingleRunner @@ -153,9 +152,9 @@ class ProfileFragmentViewModel( } } - private val storyFetchControlledRunner = ControlledRunner?>() - val userStories: LiveData?>> = currentUserProfileActionLiveData.switchMap { currentUserAndProfilePair -> - liveData?>>(context = viewModelScope.coroutineContext + ioDispatcher) { + private val storyFetchControlledRunner = ControlledRunner() + val userStories: LiveData> = currentUserProfileActionLiveData.switchMap { currentUserAndProfilePair -> + liveData>(context = viewModelScope.coroutineContext + ioDispatcher) { val (currentUserResource, profileResource, action) = currentUserAndProfilePair if (action != INIT && action != REFRESH) { return@liveData @@ -231,7 +230,7 @@ class ProfileFragmentViewModel( return graphQLRepository.fetchUser(stateUsername) } - private suspend fun fetchUserStory(fetchedUser: User): List = storiesRepository.getStories( + private suspend fun fetchUserStory(fetchedUser: User): Story? = storiesRepository.getStories( StoryViewerOptions.forUser(fetchedUser.pk, fetchedUser.fullName) ) diff --git a/app/src/main/java/awais/instagrabber/viewmodels/StoryFragmentViewModel.kt b/app/src/main/java/awais/instagrabber/viewmodels/StoryFragmentViewModel.kt new file mode 100644 index 00000000..8fe4d0ab --- /dev/null +++ b/app/src/main/java/awais/instagrabber/viewmodels/StoryFragmentViewModel.kt @@ -0,0 +1,458 @@ +package awais.instagrabber.viewmodels + +import android.net.Uri +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import awais.instagrabber.R +import awais.instagrabber.managers.DirectMessagesManager +import awais.instagrabber.models.enums.FavoriteType +import awais.instagrabber.models.enums.MediaItemType +import awais.instagrabber.models.enums.StoryPaginationType +import awais.instagrabber.models.Resource +import awais.instagrabber.models.Resource.Companion.error +import awais.instagrabber.models.Resource.Companion.loading +import awais.instagrabber.models.Resource.Companion.success +import awais.instagrabber.models.enums.BroadcastItemType +import awais.instagrabber.repositories.requests.StoryViewerOptions +import awais.instagrabber.repositories.responses.directmessages.RankedRecipient +import awais.instagrabber.repositories.responses.stories.* +import awais.instagrabber.repositories.responses.Media +import awais.instagrabber.utils.* +import awais.instagrabber.webservices.MediaRepository +import awais.instagrabber.webservices.StoriesRepository +import com.google.common.collect.ImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class StoryFragmentViewModel : ViewModel() { + // large data + private val currentStory = MutableLiveData() + private val currentMedia = MutableLiveData() + + // small data + private val storyTitle = MutableLiveData() + private val date = MutableLiveData() + private val type = MutableLiveData() + private val poll = MutableLiveData() + private val quiz = MutableLiveData() + private val question = MutableLiveData() + private val slider = MutableLiveData() + private val swipeUp = MutableLiveData() + private val linkedPost = MutableLiveData() + private val appAttribution = MutableLiveData() + private val reelMentions = MutableLiveData>>() + + // process + private val currentIndex = MutableLiveData() + private val pagination = MutableLiveData(StoryPaginationType.DO_NOTHING) + private val options = MutableLiveData>, String?, String?>>() + private val seen = MutableLiveData>() + + // utils + private var messageManager: DirectMessagesManager? = null + private val cookie = Utils.settingsHelper.getString(Constants.COOKIE) + private val deviceId = Utils.settingsHelper.getString(Constants.DEVICE_UUID) + private val csrfToken = getCsrfTokenFromCookie(cookie) + private val userId = getUserIdFromCookie(cookie) + private val storiesRepository: StoriesRepository by lazy { StoriesRepository.getInstance() } + private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() } + + /* set functions */ + + fun setStory(story: Story) { + if (story.items == null || story.items.size == 0) { + pagination.postValue(StoryPaginationType.ERROR) + return + } + currentStory.postValue(story) + storyTitle.postValue(story.title ?: story.user?.username) + if (story.broadcast != null) { + date.postValue(story.dateTime) + type.postValue(MediaItemType.MEDIA_TYPE_LIVE) + pagination.postValue(StoryPaginationType.DO_NOTHING) + } + } + + fun setMedia(index: Int) { + if (currentStory.value?.items == null) return + if (index < 0 || index >= currentStory.value!!.items!!.size) { + pagination.postValue(if (index < 0) StoryPaginationType.BACKWARD else StoryPaginationType.FORWARD) + return + } + currentIndex.postValue(index) + val story: Story? = currentStory.value + val media = story!!.items!!.get(index) + currentMedia.postValue(media) + date.postValue(media.date) + type.postValue(media.type) + initStickers(media) + } + + fun setSingleMedia(media: StoryMedia) { + currentStory.postValue(null) + currentIndex.postValue(0) + currentMedia.postValue(media) + date.postValue(media.date) + type.postValue(media.type) + } + + private fun initStickers(media: StoryMedia) { + val builder = ImmutableList.builder>() + var linkedText: String? = null + var appText: String? = null + if (setMentions(media)) builder.add(Pair(R.id.mentions, R.string.story_mentions)) + if (setQuiz(media)) builder.add(Pair(R.id.quiz, R.string.story_quiz)) + if (setQuestion(media)) builder.add(Pair(R.id.question, R.string.story_question)) + if (setPoll(media)) builder.add(Pair(R.id.poll, R.string.story_poll)) + if (setSlider(media)) builder.add(Pair(R.id.slider, R.string.story_slider)) + if (setLinkedPost(media)) builder.add(Pair(R.id.viewStoryPost, R.string.view_post)) + if (setStoryCta(media)) { + linkedText = media.linkText + builder.add(Pair(R.id.swipeUp, 0)) + } + if (setStoryAppAttribution(media)) { + appText = media.storyAppAttribution!!.appActionText + builder.add(Pair(R.id.spotify, 0)) + } + options.postValue(Triple(builder.build(), linkedText, appText)) + } + + private fun setMentions(media: StoryMedia): Boolean { + val mentions: MutableList> = mutableListOf() + if (media.reelMentions != null) + mentions.addAll(media.reelMentions.map{ + Triple("@" + it.user?.username, it.user?.username, FavoriteType.USER) + }) + if (media.storyHashtags != null) + mentions.addAll(media.storyHashtags.map{ + Triple("#" + it.hashtag?.name, it.hashtag?.name, FavoriteType.HASHTAG) + }) + if (media.storyLocations != null) + mentions.addAll(media.storyLocations.map{ + Triple(it.location?.name ?: "", it.location?.pk?.toString(10), FavoriteType.LOCATION) + }) + reelMentions.postValue(mentions.filterNot { it.second.isNullOrEmpty() } .distinct()) + return !mentions.isEmpty() + } + + private fun setPoll(media: StoryMedia): Boolean { + poll.postValue(media.storyPolls?.get(0)?.pollSticker ?: return false) + return true + } + + private fun setQuiz(media: StoryMedia): Boolean { + quiz.postValue(media.storyQuizs?.get(0)?.quizSticker ?: return false) + return true + } + + private fun setQuestion(media: StoryMedia): Boolean { + val questionSticker = media.storyQuestions?.get(0)?.questionSticker ?: return false + if (questionSticker.questionType.equals("music")) return false + question.postValue(questionSticker) + return true + } + + private fun setSlider(media: StoryMedia): Boolean { + slider.postValue(media.storySliders?.get(0)?.sliderSticker ?: return false) + return true + } + + private fun setLinkedPost(media: StoryMedia): Boolean { + linkedPost.postValue(media.storyFeedMedia?.get(0)?.mediaId ?: return false) + return true + } + + private fun setStoryCta(media: StoryMedia): Boolean { + val webUri = media.storyCta?.get(0)?.links?.get(0)?.webUri ?: return false + val parsedUri = Uri.parse(webUri) + val cleanUri = if (parsedUri.host.equals("l.instagram.com")) parsedUri.getQueryParameter("u") + else null + swipeUp.postValue(if (cleanUri != null && Uri.parse(cleanUri).scheme?.startsWith("http") == true) cleanUri + else webUri) + return true + } + + private fun setStoryAppAttribution(media: StoryMedia): Boolean { + appAttribution.postValue(media.storyAppAttribution ?: return false) + return true + } + + /* get functions */ + + fun getCurrentStory(): LiveData { + return currentStory + } + + fun getCurrentIndex(): LiveData { + return currentIndex + } + + fun getCurrentMedia(): LiveData { + return currentMedia + } + + fun getPagination(): LiveData { + return pagination + } + + fun getDate(): LiveData { + return date + } + + fun getTitle(): LiveData { + return storyTitle + } + + fun getType(): LiveData { + return type + } + + fun getMedia(): LiveData { + return currentMedia + } + + fun getMention(index: Int): Triple? { + return reelMentions.value?.get(index) + } + + fun getMentionTexts(): Array { + return reelMentions.value!!.map { it.first } .toTypedArray() + } + + fun getPoll(): LiveData { + return poll + } + + fun getQuestion(): LiveData { + return question + } + + fun getQuiz(): LiveData { + return quiz + } + + fun getSlider(): LiveData { + return slider + } + + fun getLinkedPost(): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + val postId = linkedPost.value + if (postId == null) data.postValue(error("No post ID supplied", null)) + else viewModelScope.launch(Dispatchers.IO) { + try { + val media = mediaRepository.fetch(postId.toLong()) + data.postValue(success(media)) + } + catch (e: Exception) { + data.postValue(error(e.message, null)) + } + } + return data + } + + fun getSwipeUp(): String? { + return swipeUp.value + } + + fun getAppAttribution(): String? { + return appAttribution.value?.url + } + + fun getOptions(): LiveData>, String?, String?>> { + return options + } + + /* action functions */ + + fun answerPoll(w: Int): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + viewModelScope.launch(Dispatchers.IO) { + try { + val oldPoll: PollSticker = poll.value!! + val response = storiesRepository.respondToPoll( + csrfToken!!, + userId, + deviceId, + currentMedia.value!!.pk, + oldPoll.pollId, + w + ) + if (!"ok".equals(response.status)) + throw Exception("Instagram returned status \"" + response.status + "\"") + val tally = oldPoll.tallies.get(w) + val newTally = tally.copy(count = tally.count + 1) + val newTallies = oldPoll.tallies.toMutableList() + newTallies.set(w, newTally) + poll.postValue(oldPoll.copy(viewerVote = w, tallies = newTallies.toList())) + data.postValue(success(null)) + } + catch (e: Exception) { + data.postValue(error(e.message, null)) + } + } + return data + } + + fun answerQuiz(w: Int): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + viewModelScope.launch(Dispatchers.IO) { + try { + val oldQuiz = quiz.value!! + val response = storiesRepository.respondToQuiz( + csrfToken!!, + userId, + deviceId, + currentMedia.value!!.pk, + oldQuiz.quizId, + w + ) + if (!"ok".equals(response.status)) + throw Exception("Instagram returned status \"" + response.status + "\"") + val tally = oldQuiz.tallies.get(w) + val newTally = tally.copy(count = tally.count + 1) + val newTallies = oldQuiz.tallies.toMutableList() + newTallies.set(w, newTally) + quiz.postValue(oldQuiz.copy(viewerAnswer = w, tallies = newTallies.toList())) + data.postValue(success(null)) + } + catch (e: Exception) { + data.postValue(error(e.message, null)) + } + } + return data + } + + fun answerQuestion(a: String): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + viewModelScope.launch(Dispatchers.IO) { + try { + val response = storiesRepository.respondToQuestion( + csrfToken!!, + userId, + deviceId, + currentMedia.value!!.pk, + question.value!!.questionId, + a + ) + if (!"ok".equals(response.status)) + throw Exception("Instagram returned status \"" + response.status + "\"") + data.postValue(success(null)) + } + catch (e: Exception) { + data.postValue(error(e.message, null)) + } + } + return data + } + + fun answerSlider(a: Double): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + viewModelScope.launch(Dispatchers.IO) { + try { + val oldSlider = slider.value!! + val response = storiesRepository.respondToSlider( + csrfToken!!, + userId, + deviceId, + currentMedia.value!!.pk, + oldSlider.sliderId, + a + ) + if (!"ok".equals(response.status)) + throw Exception("Instagram returned status \"" + response.status + "\"") + val newVoteCount = (oldSlider.sliderVoteCount ?: 0) + 1 + val newAverage = if (oldSlider.sliderVoteAverage == null) a + else (oldSlider.sliderVoteAverage * oldSlider.sliderVoteCount!! + a) / newVoteCount + slider.postValue(oldSlider.copy(viewerCanVote = false, + sliderVoteCount = newVoteCount, + viewerVote = a, + sliderVoteAverage = newAverage)) + data.postValue(success(null)) + } + catch (e: Exception) { + data.postValue(error(e.message, null)) + } + } + return data + } + + fun reply(a: String): LiveData>? { + if (messageManager == null) { + messageManager = DirectMessagesManager + } + return messageManager?.replyToStory( + currentStory.value?.user?.pk, + currentStory.value?.id, + currentMedia.value?.id, + a, + viewModelScope + ) + } + + fun shareDm(result: RankedRecipient) { + if (messageManager == null) { + messageManager = DirectMessagesManager + } + val mediaId = currentMedia.value?.id ?: return + val reelId = currentStory.value?.id ?: return + messageManager?.sendMedia(result, mediaId, reelId, BroadcastItemType.STORY, viewModelScope) + } + + fun shareDm(recipients: Set) { + if (messageManager == null) { + messageManager = DirectMessagesManager + } + val mediaId = currentMedia.value?.id ?: return + val reelId = currentStory.value?.id ?: return + messageManager?.sendMedia(recipients, mediaId, reelId, BroadcastItemType.STORY, viewModelScope) + } + + fun paginate(backward: Boolean) { + var index = currentIndex.value!! + index = if (backward) index - 1 else index + 1 + if (index < 0 || index >= currentStory.value!!.items!!.size) skip(backward) + setMedia(index) + } + + fun skip(backward: Boolean) { + pagination.postValue(if (backward) StoryPaginationType.BACKWARD else StoryPaginationType.FORWARD) + } + + fun fetchStory(fetchOptions: StoryViewerOptions?): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + viewModelScope.launch(Dispatchers.IO) { + try { + val story = storiesRepository.getStories(fetchOptions!!) + setStory(story!!) + data.postValue(success(null)) + } catch (e: Exception) { + data.postValue(error(e.message, null)) + } + } + return data + } + + fun fetchSingleMedia(mediaId: Long): LiveData> { + val data = MutableLiveData>() + data.postValue(loading(null)) + viewModelScope.launch(Dispatchers.IO) { + try { + val storyMedia = storiesRepository.fetch(mediaId) + setSingleMedia(storyMedia!!) + data.postValue(success(null)) + } catch (e: Exception) { + data.postValue(error(e.message, null)) + } + } + return data + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/webservices/StoriesRepository.kt b/app/src/main/java/awais/instagrabber/webservices/StoriesRepository.kt index 62c27847..e3713df1 100644 --- a/app/src/main/java/awais/instagrabber/webservices/StoriesRepository.kt +++ b/app/src/main/java/awais/instagrabber/webservices/StoriesRepository.kt @@ -7,7 +7,6 @@ import awais.instagrabber.repositories.responses.stories.ArchiveResponse import awais.instagrabber.repositories.responses.stories.Story import awais.instagrabber.repositories.responses.stories.StoryMedia import awais.instagrabber.repositories.responses.stories.StoryStickerResponse -import awais.instagrabber.utils.TextUtils.isEmpty import awais.instagrabber.utils.Utils import awais.instagrabber.webservices.RetrofitFactory.retrofit import java.util.UUID @@ -60,35 +59,34 @@ open class StoriesRepository(private val service: StoriesService) { "is_in_archive_home" to "true", "include_cover" to "1", ) - if (!isEmpty(maxId)) { + if (!maxId.isNullOrEmpty()) { form["max_id"] = maxId // NOT TESTED } return service.fetchArchive(form) } - open suspend fun getStories(options: StoryViewerOptions): List { + open suspend fun getStories(options: StoryViewerOptions): Story? { return when (options.type) { StoryViewerOptions.Type.HIGHLIGHT, StoryViewerOptions.Type.STORY_ARCHIVE -> { val response = service.getReelsMedia(options.name) - val story: Story? = response.reels?.get(options.name) - story?.items ?: emptyList() + response.reels?.get(options.name) } StoryViewerOptions.Type.USER -> { val response = service.getUserStories(options.id.toString()) - response.reel?.items ?: emptyList() + response.reel } // should not reach beyond this point StoryViewerOptions.Type.LOCATION -> { val response = service.getStories("locations", options.id.toString()) - response.story?.items ?: emptyList() + response.story } StoryViewerOptions.Type.HASHTAG -> { val response = service.getStories("tags", options.name) - response.story?.items ?: emptyList() + response.story } - else -> emptyList() + else -> null } } @@ -96,7 +94,7 @@ open class StoriesRepository(private val service: StoriesService) { csrfToken: String, userId: Long, deviceUuid: String, - storyId: String, + storyId: Long, stickerId: Long, action: String, arg1: String, @@ -119,7 +117,7 @@ open class StoriesRepository(private val service: StoriesService) { csrfToken: String, userId: Long, deviceUuid: String, - storyId: String, + storyId: Long, stickerId: Long, answer: String, ): StoryStickerResponse = respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_question_response", "response", answer) @@ -128,7 +126,7 @@ open class StoriesRepository(private val service: StoriesService) { csrfToken: String, userId: Long, deviceUuid: String, - storyId: String, + storyId: Long, stickerId: Long, answer: Int, ): StoryStickerResponse { @@ -139,7 +137,7 @@ open class StoriesRepository(private val service: StoriesService) { csrfToken: String, userId: Long, deviceUuid: String, - storyId: String, + storyId: Long, stickerId: Long, answer: Int, ): StoryStickerResponse = respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_poll_vote", "vote", answer.toString()) @@ -148,7 +146,7 @@ open class StoriesRepository(private val service: StoriesService) { csrfToken: String, userId: Long, deviceUuid: String, - storyId: String, + storyId: Long, stickerId: Long, answer: Double, ): StoryStickerResponse = respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_slider_vote", "vote", answer.toString()) diff --git a/app/src/main/res/drawable/ic_story_sticker.xml b/app/src/main/res/drawable/ic_story_sticker.xml new file mode 100644 index 00000000..a9079fe0 --- /dev/null +++ b/app/src/main/res/drawable/ic_story_sticker.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_story_viewer_list.xml b/app/src/main/res/drawable/ic_story_viewer_list.xml new file mode 100644 index 00000000..c0450237 --- /dev/null +++ b/app/src/main/res/drawable/ic_story_viewer_list.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_story_viewer.xml b/app/src/main/res/layout/fragment_story_viewer.xml index cb6b66ed..bbce26f2 100644 --- a/app/src/main/res/layout/fragment_story_viewer.xml +++ b/app/src/main/res/layout/fragment_story_viewer.xml @@ -9,7 +9,7 @@ android:id="@+id/story_container" android:layout_width="match_parent" android:layout_height="0dp" - app:layout_constraintBottom_toTopOf="@id/postActions" + app:layout_constraintBottom_toTopOf="@id/buttons_barrier" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -38,132 +38,150 @@ - + app:layout_constraintStart_toStartOf="parent" /> - - - - - - - - - - - - - - - + + app:layout_constraintTop_toBottomOf="@id/buttons_barrier" + app:rippleColor="@color/grey_300" /> - + + + + + + + + + app:layout_constraintStart_toEndOf="@id/btnDownload" + app:layout_constraintTop_toBottomOf="@id/buttons_barrier" + app:rippleColor="@color/grey_300" /> + app:layout_constraintStart_toEndOf="@id/btnReply" + app:layout_constraintTop_toBottomOf="@id/buttons_barrier" + app:rippleColor="@color/grey_300" /> \ No newline at end of file diff --git a/app/src/main/res/menu/story_menu.xml b/app/src/main/res/menu/story_menu.xml index 8169fd62..e7872ad5 100644 --- a/app/src/main/res/menu/story_menu.xml +++ b/app/src/main/res/menu/story_menu.xml @@ -2,21 +2,9 @@ - - \ No newline at end of file diff --git a/app/src/main/res/navigation/direct_messages_nav_graph.xml b/app/src/main/res/navigation/direct_messages_nav_graph.xml index dce30b6a..547ea1ad 100644 --- a/app/src/main/res/navigation/direct_messages_nav_graph.xml +++ b/app/src/main/res/navigation/direct_messages_nav_graph.xml @@ -188,7 +188,6 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c74b7e82..5b17f849 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -69,8 +69,7 @@ Be patient! View Post View Post - Spotify - Vote + Poll Vote successful! You have already voted! Respond @@ -87,6 +86,7 @@ Slider You have already answered! Mentions + Question This Account is Private You won\'t be able to access posts after unfollowing! Are you sure? Are you sure? @@ -104,7 +104,7 @@ Delete collection Are you sure you want to delete this collection? All contained media will remain in other collections. - Add to collection... + Add to collection… Remove from collection Liked Saved @@ -183,9 +183,9 @@ Screenshotted Cannot deliver Unseen count response is null! - Message... + Message… Press and hold to record audio - Updating... + Updating… Leave chat Leave this chat? Kick @@ -333,7 +333,7 @@ Comment Layout Feed stories - Opening post... + Opening post… Share Layout style Column count @@ -511,7 +511,7 @@ Click to show full like count No profile pic found! Are you sure you want to open this link? - Sending... + Sending… Share via DM Share link… Slide to Cancel