mirror of
https://github.com/KokaKiwi/BarInsta
synced 2024-11-12 17:57:29 +00:00
story viewmodel (wip)
hiding the storylist doesn't work yet but everything else should be good
This commit is contained in:
parent
31ea42d105
commit
bb5244665b
@ -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<StoryMedia, StoriesAdapter
|
||||
}
|
||||
}
|
||||
|
||||
public void paginate(final int newIndex) {
|
||||
final List<StoryMedia> 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);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -0,0 +1,7 @@
|
||||
package awais.instagrabber.models.enums
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
enum class StoryPaginationType : Serializable {
|
||||
FORWARD, BACKWARD, DO_NOTHING, ERROR
|
||||
}
|
@ -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<String, String>,
|
||||
|
@ -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,
|
||||
|
@ -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<List<StoryMedia>?>()
|
||||
val userStories: LiveData<Resource<List<StoryMedia>?>> = currentUserProfileActionLiveData.switchMap { currentUserAndProfilePair ->
|
||||
liveData<Resource<List<StoryMedia>?>>(context = viewModelScope.coroutineContext + ioDispatcher) {
|
||||
private val storyFetchControlledRunner = ControlledRunner<Story?>()
|
||||
val userStories: LiveData<Resource<Story?>> = currentUserProfileActionLiveData.switchMap { currentUserAndProfilePair ->
|
||||
liveData<Resource<Story?>>(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<StoryMedia> = storiesRepository.getStories(
|
||||
private suspend fun fetchUserStory(fetchedUser: User): Story? = storiesRepository.getStories(
|
||||
StoryViewerOptions.forUser(fetchedUser.pk, fetchedUser.fullName)
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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<StoryMedia> {
|
||||
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())
|
||||
|
10
app/src/main/res/drawable/ic_story_sticker.xml
Normal file
10
app/src/main/res/drawable/ic_story_sticker.xml
Normal file
@ -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>
|
10
app/src/main/res/drawable/ic_story_viewer_list.xml
Normal file
10
app/src/main/res/drawable/ic_story_viewer_list.xml
Normal file
@ -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>
|
@ -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 @@
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/postActions"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/storiesList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
app:layout_constraintTop_toBottomOf="@id/story_container"
|
||||
app:layout_constraintBottom_toTopOf="@id/storiesList"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintBottom_toTopOf="@id/buttons_barrier"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:background="#0000">
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/viewStoryPost"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/view_story_post"
|
||||
android:textColor="@color/btn_green_text_color"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/btn_green_background" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/poll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/vote_story_poll"
|
||||
android:textColor="@color/btn_blue_text_color"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/btn_blue_background" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/answer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/respond_story"
|
||||
android:textColor="@color/btn_blue_text_color"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/btn_blue_background" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/quiz"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/story_quiz"
|
||||
android:textColor="@color/btn_blue_text_color"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/btn_blue_background" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/slider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/story_slider"
|
||||
android:textColor="@color/btn_blue_text_color"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/btn_blue_background" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/swipeUp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="SAMPLE TEXT"
|
||||
android:textColor="@color/btn_blue_text_color"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/btn_blue_background" />
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/mention"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/story_mentions"
|
||||
android:textColor="@color/btn_orange_text_color"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/btn_orange_background" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/spotify"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/spotify"
|
||||
android:textColor="@color/btn_green_text_color"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/btn_green_background" />
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/buttons_barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierAllowsGoneWidgets="true"
|
||||
app:barrierDirection="bottom"
|
||||
app:layout_constraintTop_toBottomOf="@id/story_container"
|
||||
app:layout_constraintBottom_toTopOf="@id/btnBackward"
|
||||
app:constraint_referenced_ids="story_container" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnBackward"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="@dimen/story_item_height"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:enabled="false"
|
||||
android:visibility="visible"
|
||||
app:icon="@drawable/exo_ic_skip_previous"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@color/ic_read_button_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/storiesList"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnShare"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/storiesList" />
|
||||
app:layout_constraintTop_toBottomOf="@id/buttons_barrier"
|
||||
app:rippleColor="@color/grey_300" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/storiesList"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnShare"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintTop_toBottomOf="@id/postActions"
|
||||
android:layout_height="48dp"
|
||||
android:enabled="false"
|
||||
android:visibility="visible"
|
||||
app:icon="?attr/actionModeShareDrawable"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@color/ic_read_button_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/stickers"
|
||||
app:layout_constraintStart_toEndOf="@id/btnBackward"
|
||||
app:layout_constraintTop_toBottomOf="@id/buttons_barrier"
|
||||
app:rippleColor="@color/grey_300" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/stickers"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:enabled="false"
|
||||
android:visibility="visible"
|
||||
app:icon="@drawable/ic_story_sticker"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@color/ic_read_button_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/list_toggle"
|
||||
app:layout_constraintStart_toEndOf="@id/btnShare"
|
||||
app:layout_constraintTop_toBottomOf="@id/buttons_barrier"
|
||||
app:rippleColor="@color/grey_300" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/list_toggle"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:enabled="false"
|
||||
android:visibility="visible"
|
||||
app:icon="@drawable/ic_story_viewer_list"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@color/ic_read_button_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnDownload"
|
||||
app:layout_constraintStart_toEndOf="@id/stickers"
|
||||
app:layout_constraintTop_toBottomOf="@id/buttons_barrier"
|
||||
app:rippleColor="@color/grey_300" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnDownload"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:enabled="false"
|
||||
android:visibility="visible"
|
||||
app:icon="@drawable/ic_download"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@color/ic_read_button_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnReply"
|
||||
app:layout_constraintStart_toEndOf="@id/list_toggle"
|
||||
app:layout_constraintTop_toBottomOf="@id/buttons_barrier"
|
||||
app:rippleColor="@color/grey_300" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnReply"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:enabled="false"
|
||||
android:visibility="visible"
|
||||
app:icon="@drawable/ic_round_send_24"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@color/ic_read_button_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnForward"
|
||||
app:layout_constraintStart_toEndOf="@id/btnBackward" />
|
||||
app:layout_constraintStart_toEndOf="@id/btnDownload"
|
||||
app:layout_constraintTop_toBottomOf="@id/buttons_barrier"
|
||||
app:rippleColor="@color/grey_300" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnForward"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="@dimen/story_item_height"
|
||||
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:enabled="false"
|
||||
android:visibility="visible"
|
||||
app:icon="@drawable/exo_ic_skip_next"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@color/ic_read_button_tint"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/storiesList"
|
||||
app:layout_constraintTop_toTopOf="@id/storiesList" />
|
||||
app:layout_constraintStart_toEndOf="@id/btnReply"
|
||||
app:layout_constraintTop_toBottomOf="@id/buttons_barrier"
|
||||
app:rippleColor="@color/grey_300" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -2,21 +2,9 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_dms"
|
||||
android:icon="@drawable/ic_round_send_24"
|
||||
android:title="@string/reply_story"
|
||||
android:titleCondensed="@string/reply_story"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/action_profile"
|
||||
android:title="@string/open_profile"
|
||||
android:titleCondensed="@string/open_profile"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/action_download"
|
||||
android:icon="@drawable/ic_download"
|
||||
android:title="@string/action_download"
|
||||
android:titleCondensed="@string/action_download"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
@ -188,7 +188,6 @@
|
||||
<fragment
|
||||
android:id="@+id/storyViewerFragment"
|
||||
android:name="awais.instagrabber.fragments.StoryViewerFragment"
|
||||
android:label="StoryViewerFragment"
|
||||
tools:layout="@layout/fragment_story_viewer">
|
||||
<argument
|
||||
android:name="options"
|
||||
|
@ -140,7 +140,6 @@
|
||||
<fragment
|
||||
android:id="@+id/storyViewerFragment"
|
||||
android:name="awais.instagrabber.fragments.StoryViewerFragment"
|
||||
android:label="StoryViewerFragment"
|
||||
tools:layout="@layout/fragment_story_viewer">
|
||||
<argument
|
||||
android:name="options"
|
||||
|
@ -103,7 +103,6 @@
|
||||
<fragment
|
||||
android:id="@+id/storyViewerFragment"
|
||||
android:name="awais.instagrabber.fragments.StoryViewerFragment"
|
||||
android:label="StoryViewerFragment"
|
||||
tools:layout="@layout/fragment_story_viewer">
|
||||
<argument
|
||||
android:name="options"
|
||||
|
@ -110,7 +110,6 @@
|
||||
<fragment
|
||||
android:id="@+id/storyViewerFragment"
|
||||
android:name="awais.instagrabber.fragments.StoryViewerFragment"
|
||||
android:label="StoryViewerFragment"
|
||||
tools:layout="@layout/fragment_story_viewer">
|
||||
<argument
|
||||
android:name="options"
|
||||
|
@ -93,7 +93,6 @@
|
||||
<fragment
|
||||
android:id="@+id/storyViewerFragment"
|
||||
android:name="awais.instagrabber.fragments.StoryViewerFragment"
|
||||
android:label="StoryViewerFragment"
|
||||
tools:layout="@layout/fragment_story_viewer">
|
||||
<argument
|
||||
android:name="options"
|
||||
|
@ -203,7 +203,6 @@
|
||||
<fragment
|
||||
android:id="@+id/storyViewerFragment"
|
||||
android:name="awais.instagrabber.fragments.StoryViewerFragment"
|
||||
android:label="StoryViewerFragment"
|
||||
tools:layout="@layout/fragment_story_viewer">
|
||||
|
||||
<argument
|
||||
|
@ -40,7 +40,6 @@
|
||||
<fragment
|
||||
android:id="@+id/storyViewerFragment"
|
||||
android:name="awais.instagrabber.fragments.StoryViewerFragment"
|
||||
android:label="StoryViewerFragment"
|
||||
tools:layout="@layout/fragment_story_viewer">
|
||||
<argument
|
||||
android:name="options"
|
||||
|
@ -8,4 +8,13 @@
|
||||
<item name="share_dm" type="id" />
|
||||
<item name="download_current" type="id" />
|
||||
<item name="download_all" type="id" />
|
||||
<!-- story stickers -->
|
||||
<item name="mentions" type="id" />
|
||||
<item name="spotify" type="id" />
|
||||
<item name="poll" type="id" />
|
||||
<item name="question" type="id" />
|
||||
<item name="quiz" type="id" />
|
||||
<item name="slider" type="id" />
|
||||
<item name="viewStoryPost" type="id" />
|
||||
<item name="swipeUp" type="id" />
|
||||
</resources>
|
@ -69,8 +69,7 @@
|
||||
<string name="be_patient">Be patient!</string>
|
||||
<string name="view_story_post">View Post</string>
|
||||
<string name="view_post">View Post</string>
|
||||
<string name="spotify" translatable="false">Spotify</string>
|
||||
<string name="vote_story_poll">Vote</string>
|
||||
<string name="story_poll">Poll</string>
|
||||
<string name="votef_story_poll">Vote successful!</string>
|
||||
<string name="voted_story_poll">You have already voted!</string>
|
||||
<string name="respond_story">Respond</string>
|
||||
@ -87,6 +86,7 @@
|
||||
<string name="story_slider">Slider</string>
|
||||
<string name="story_quizzed">You have already answered!</string>
|
||||
<string name="story_mentions">Mentions</string>
|
||||
<string name="story_question">Question</string>
|
||||
<string name="priv_acc">This Account is Private</string>
|
||||
<string name="priv_acc_confirm">You won\'t be able to access posts after unfollowing! Are you sure?</string>
|
||||
<string name="are_you_sure">Are you sure?</string>
|
||||
@ -104,7 +104,7 @@
|
||||
<string name="delete_collection">Delete collection</string>
|
||||
<string name="delete_collection_confirm">Are you sure you want to delete this collection?</string>
|
||||
<string name="delete_collection_note">All contained media will remain in other collections.</string>
|
||||
<string name="add_to_collection">Add to collection...</string>
|
||||
<string name="add_to_collection">Add to collection…</string>
|
||||
<string name="remove_from_collection">Remove from collection</string>
|
||||
<string name="liked">Liked</string>
|
||||
<string name="saved">Saved</string>
|
||||
@ -183,9 +183,9 @@
|
||||
<string name="dms_inbox_raven_media_screenshot">Screenshotted</string>
|
||||
<string name="dms_inbox_raven_media_cant_deliver">Cannot deliver</string>
|
||||
<string name="dms_inbox_error_null_count">Unseen count response is null!</string>
|
||||
<string name="dms_thread_message_hint">Message...</string>
|
||||
<string name="dms_thread_message_hint">Message…</string>
|
||||
<string name="dms_thread_audio_hint">Press and hold to record audio</string>
|
||||
<string name="dms_thread_updating">Updating...</string>
|
||||
<string name="dms_thread_updating">Updating…</string>
|
||||
<string name="dms_action_leave">Leave chat</string>
|
||||
<string name="dms_action_leave_question">Leave this chat?</string>
|
||||
<string name="dms_action_kick">Kick</string>
|
||||
@ -333,7 +333,7 @@
|
||||
<string name="comment">Comment</string>
|
||||
<string name="layout">Layout</string>
|
||||
<string name="feed_stories">Feed stories</string>
|
||||
<string name="opening_post">Opening post...</string>
|
||||
<string name="opening_post">Opening post…</string>
|
||||
<string name="share">Share</string>
|
||||
<string name="layout_style">Layout style</string>
|
||||
<string name="column_count">Column count</string>
|
||||
@ -511,7 +511,7 @@
|
||||
<string name="click_to_show_full">Click to show full like count</string>
|
||||
<string name="no_profile_pic_found">No profile pic found!</string>
|
||||
<string name="swipe_up_confirmation">Are you sure you want to open this link?</string>
|
||||
<string name="sending">Sending...</string>
|
||||
<string name="sending">Sending…</string>
|
||||
<string name="share_via_dm">Share via DM</string>
|
||||
<string name="share_link">Share link…</string>
|
||||
<string name="slide_to_cancel">Slide to Cancel</string>
|
||||
|
Loading…
Reference in New Issue
Block a user