Browse Source
story viewmodel (wip)
story viewmodel (wip)
hiding the storylist doesn't work yet but everything else should be goodrenovate/org.jetbrains.kotlinx-kotlinx-coroutines-test-1.x
Austin Huang
3 years ago
No known key found for this signature in database
GPG Key ID: 84C23AA04587A91F
23 changed files with 1554 additions and 1457 deletions
-
12app/src/main/java/awais/instagrabber/adapters/StoriesAdapter.java
-
1307app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java
-
902app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.kt
-
4app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.kt
-
7app/src/main/java/awais/instagrabber/models/enums/StoryPaginationType.kt
-
2app/src/main/java/awais/instagrabber/repositories/StoriesService.kt
-
4app/src/main/java/awais/instagrabber/repositories/responses/stories/StoryMedia.kt
-
9app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt
-
458app/src/main/java/awais/instagrabber/viewmodels/StoryFragmentViewModel.kt
-
26app/src/main/java/awais/instagrabber/webservices/StoriesRepository.kt
-
10app/src/main/res/drawable/ic_story_sticker.xml
-
10app/src/main/res/drawable/ic_story_viewer_list.xml
-
216app/src/main/res/layout/fragment_story_viewer.xml
-
12app/src/main/res/menu/story_menu.xml
-
1app/src/main/res/navigation/direct_messages_nav_graph.xml
-
1app/src/main/res/navigation/feed_nav_graph.xml
-
1app/src/main/res/navigation/hashtag_nav_graph.xml
-
1app/src/main/res/navigation/location_nav_graph.xml
-
1app/src/main/res/navigation/notification_viewer_nav_graph.xml
-
1app/src/main/res/navigation/profile_nav_graph.xml
-
1app/src/main/res/navigation/story_list_nav_graph.xml
-
9app/src/main/res/values/ids.xml
-
14app/src/main/res/values/strings.xml
1307
app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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<Any?>? = null |
|||
private lateinit var fragmentActivity: AppCompatActivity |
|||
private lateinit var storiesViewModel: StoryFragmentViewModel |
|||
private lateinit var binding: FragmentStoryViewerBinding |
|||
|
|||
@Suppress("UNCHECKED_CAST") |
|||
private val backStackSavedStateObserver = Observer<Any?> { 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<RankedRecipient>) |
|||
} 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<Story>? = 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<ImageInfo?>() { |
|||
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<String?, FavoriteType>) { |
|||
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<Tally>, |
|||
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() |
|||
} |
|||
} |
@ -0,0 +1,7 @@ |
|||
package awais.instagrabber.models.enums |
|||
|
|||
import java.io.Serializable |
|||
|
|||
enum class StoryPaginationType : Serializable { |
|||
FORWARD, BACKWARD, DO_NOTHING, ERROR |
|||
} |
@ -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<Story>() |
|||
private val currentMedia = MutableLiveData<StoryMedia>() |
|||
|
|||
// small data |
|||
private val storyTitle = MutableLiveData<String>() |
|||
private val date = MutableLiveData<String>() |
|||
private val type = MutableLiveData<MediaItemType>() |
|||
private val poll = MutableLiveData<PollSticker>() |
|||
private val quiz = MutableLiveData<QuizSticker>() |
|||
private val question = MutableLiveData<QuestionSticker>() |
|||
private val slider = MutableLiveData<SliderSticker>() |
|||
private val swipeUp = MutableLiveData<String>() |
|||
private val linkedPost = MutableLiveData<String>() |
|||
private val appAttribution = MutableLiveData<StoryAppAttribution>() |
|||
private val reelMentions = MutableLiveData<List<Triple<String, String?, FavoriteType>>>() |
|||
|
|||
// process |
|||
private val currentIndex = MutableLiveData<Int>() |
|||
private val pagination = MutableLiveData(StoryPaginationType.DO_NOTHING) |
|||
private val options = MutableLiveData<Triple<List<Pair<Int, Int>>, String?, String?>>() |
|||
private val seen = MutableLiveData<Triple<String, Long, Long>>() |
|||
|
|||
// 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<Pair<Int, Int>>() |
|||
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<Triple<String, String?, FavoriteType>> = 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<Story> { |
|||
return currentStory |
|||
} |
|||
|
|||
fun getCurrentIndex(): LiveData<Int> { |
|||
return currentIndex |
|||
} |
|||
|
|||
fun getCurrentMedia(): LiveData<StoryMedia> { |
|||
return currentMedia |
|||
} |
|||
|
|||
fun getPagination(): LiveData<StoryPaginationType> { |
|||
return pagination |
|||
} |
|||
|
|||
fun getDate(): LiveData<String> { |
|||
return date |
|||
} |
|||
|
|||
fun getTitle(): LiveData<String> { |
|||
return storyTitle |
|||
} |
|||
|
|||
fun getType(): LiveData<MediaItemType> { |
|||
return type |
|||
} |
|||
|
|||
fun getMedia(): LiveData<StoryMedia> { |
|||
return currentMedia |
|||
} |
|||
|
|||
fun getMention(index: Int): Triple<String, String?, FavoriteType>? { |
|||
return reelMentions.value?.get(index) |
|||
} |
|||
|
|||
fun getMentionTexts(): Array<String> { |
|||
return reelMentions.value!!.map { it.first } .toTypedArray() |
|||
} |
|||
|
|||
fun getPoll(): LiveData<PollSticker> { |
|||
return poll |
|||
} |
|||
|
|||
fun getQuestion(): LiveData<QuestionSticker> { |
|||
return question |
|||
} |
|||
|
|||
fun getQuiz(): LiveData<QuizSticker> { |
|||
return quiz |
|||
} |
|||
|
|||
fun getSlider(): LiveData<SliderSticker> { |
|||
return slider |
|||
} |
|||
|
|||
fun getLinkedPost(): LiveData<Resource<Media?>> { |
|||
val data = MutableLiveData<Resource<Media?>>() |
|||
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<Triple<List<Pair<Int, Int>>, String?, String?>> { |
|||
return options |
|||
} |
|||
|
|||
/* action functions */ |
|||
|
|||
fun answerPoll(w: Int): LiveData<Resource<Any?>> { |
|||
val data = MutableLiveData<Resource<Any?>>() |
|||
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<Resource<Any?>> { |
|||
val data = MutableLiveData<Resource<Any?>>() |
|||
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<Resource<Any?>> { |
|||
val data = MutableLiveData<Resource<Any?>>() |
|||
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<Resource<Any?>> { |
|||
val data = MutableLiveData<Resource<Any?>>() |
|||
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<Resource<Any?>>? { |
|||
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<RankedRecipient>) { |
|||
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<Resource<Any?>> { |
|||
val data = MutableLiveData<Resource<Any?>>() |
|||
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<Resource<Any?>> { |
|||
val data = MutableLiveData<Resource<Any?>>() |
|||
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 |
|||
} |
|||
} |
@ -0,0 +1,10 @@ |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:width="24dp" |
|||
android:height="24dp" |
|||
android:viewportWidth="24" |
|||
android:viewportHeight="24" |
|||
android:tint="?attr/colorControlNormal"> |
|||
<path |
|||
android:fillColor="@android:color/white" |
|||
android:pathData="M22,10l-6,-6L4,4c-1.1,0 -2,0.9 -2,2v12.01c0,1.1 0.9,1.99 2,1.99l16,-0.01c1.1,0 2,-0.89 2,-1.99v-8zM15,5.5l5.5,5.5L15,11L15,5.5z"/> |
|||
</vector> |
@ -0,0 +1,10 @@ |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:width="24dp" |
|||
android:height="24dp" |
|||
android:viewportWidth="24" |
|||
android:viewportHeight="24" |
|||
android:tint="?attr/colorControlNormal"> |
|||
<path |
|||
android:fillColor="@android:color/white" |
|||
android:pathData="M7,19h10L17,4L7,4v15zM2,17h4L6,6L2,6v11zM18,6v11h4L22,6h-4z"/> |
|||
</vector> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue