kopia lustrzana https://github.com/ryukoposting/Signal-Android
Update MediaPreviewV2 to use thumbnail rail & menu items.
rodzic
2edb9eeb52
commit
a9a64a3f60
|
@ -1,13 +1,22 @@
|
||||||
package org.thoughtcrime.securesms.mediapreview
|
package org.thoughtcrime.securesms.mediapreview
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
|
|
||||||
class MediaPreviewV2Activity : AppCompatActivity(R.layout.activity_mediapreview_v2) {
|
class MediaPreviewV2Activity : AppCompatActivity(R.layout.activity_mediapreview_v2) {
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context) {
|
||||||
|
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
|
||||||
|
super.attachBaseContext(newBase)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
setTheme(R.style.TextSecure_MediaPreview)
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
val args = MediaIntentFactory.requireArguments(intent.extras!!)
|
val args = MediaIntentFactory.requireArguments(intent.extras!!)
|
||||||
|
|
|
@ -1,23 +1,52 @@
|
||||||
package org.thoughtcrime.securesms.mediapreview
|
package org.thoughtcrime.securesms.mediapreview
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.ShareCompat
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||||
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.animation.DepthPageTransformer
|
import org.thoughtcrime.securesms.animation.DepthPageTransformer
|
||||||
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||||
|
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
|
||||||
|
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
|
||||||
import org.thoughtcrime.securesms.database.MediaDatabase
|
import org.thoughtcrime.securesms.database.MediaDatabase
|
||||||
import org.thoughtcrime.securesms.databinding.FragmentMediaPreviewV2Binding
|
import org.thoughtcrime.securesms.databinding.FragmentMediaPreviewV2Binding
|
||||||
|
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
|
||||||
|
import org.thoughtcrime.securesms.mediasend.Media
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
|
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||||
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import org.thoughtcrime.securesms.util.FullscreenHelper
|
import org.thoughtcrime.securesms.util.FullscreenHelper
|
||||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||||
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
||||||
|
import org.thoughtcrime.securesms.util.StorageUtil
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), MediaPreviewFragment.Events {
|
class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), MediaPreviewFragment.Events {
|
||||||
private val TAG = Log.tag(MediaPreviewV2Fragment::class.java)
|
private val TAG = Log.tag(MediaPreviewV2Fragment::class.java)
|
||||||
|
@ -35,7 +64,11 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
initializeViewModel()
|
|
||||||
|
val args = MediaIntentFactory.requireArguments(requireArguments())
|
||||||
|
|
||||||
|
initializeViewModel(args)
|
||||||
|
initializeToolbar(binding.toolbar, args)
|
||||||
binding.mediaPager.offscreenPageLimit = 1
|
binding.mediaPager.offscreenPageLimit = 1
|
||||||
binding.mediaPager.setPageTransformer(DepthPageTransformer())
|
binding.mediaPager.setPageTransformer(DepthPageTransformer())
|
||||||
val adapter = MediaPreviewV2Adapter(this)
|
val adapter = MediaPreviewV2Adapter(this)
|
||||||
|
@ -47,18 +80,59 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
initializeFullScreenUi()
|
initializeFullScreenUi()
|
||||||
|
initializeAlbumRail()
|
||||||
|
anchorMarginsToBottomInsets(binding.mediaPreviewDetailsContainer)
|
||||||
lifecycleDisposable += viewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe {
|
lifecycleDisposable += viewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe {
|
||||||
bindCurrentState(it)
|
bindCurrentState(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeFullScreenUi() {
|
private fun initializeToolbar(toolbar: MaterialToolbar, args: MediaIntentFactory.MediaPreviewArgs) {
|
||||||
fullscreenHelper.configureToolbarLayout(binding.toolbarCutoutSpacer, binding.toolbar)
|
toolbar.setNavigationOnClickListener {
|
||||||
fullscreenHelper.hideSystemUI()
|
requireActivity().onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.toolbar.inflateMenu(R.menu.media_preview)
|
||||||
|
|
||||||
|
// Restricted to API26 because of MemoryFileUtil not supporting lower API levels well
|
||||||
|
binding.toolbar.menu.findItem(R.id.media_preview__share).isVisible = Build.VERSION.SDK_INT >= 26
|
||||||
|
|
||||||
|
if (args.hideAllMedia) {
|
||||||
|
binding.toolbar.menu.findItem(R.id.media_preview__overview).isVisible = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeViewModel() {
|
private fun initializeAlbumRail() {
|
||||||
val args = MediaIntentFactory.requireArguments(requireArguments())
|
binding.mediaPreviewAlbumRail.itemAnimator = null // Or can crash when set to INVISIBLE while animating by FullscreenHelper https://issuetracker.google.com/issues/148720682
|
||||||
|
binding.mediaPreviewAlbumRail.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
binding.mediaPreviewAlbumRail.adapter = MediaRailAdapter(
|
||||||
|
GlideApp.with(this),
|
||||||
|
object : MediaRailAdapter.RailItemListener {
|
||||||
|
override fun onRailItemClicked(distanceFromActive: Int) {
|
||||||
|
binding.mediaPager.currentItem += distanceFromActive
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRailItemDeleteClicked(distanceFromActive: Int) {
|
||||||
|
throw UnsupportedOperationException("Callback unsupported.")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeFullScreenUi() {
|
||||||
|
fullscreenHelper.configureToolbarLayout(binding.toolbarCutoutSpacer, binding.toolbar)
|
||||||
|
fullscreenHelper.showAndHideWithSystemUI(requireActivity().window, binding.toolbarLayout, binding.mediaPreviewDetailsContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeViewModel(args: MediaIntentFactory.MediaPreviewArgs) {
|
||||||
|
if (!MediaUtil.isImageType(args.initialMediaType) && !MediaUtil.isVideoType(args.initialMediaType)) {
|
||||||
|
Log.w(TAG, "Unsupported media type sent to MediaPreviewV2Fragment, finishing.")
|
||||||
|
Snackbar.make(binding.root, R.string.MediaPreviewActivity_unssuported_media_type, Snackbar.LENGTH_LONG)
|
||||||
|
.setAction(R.string.MediaPreviewActivity_dismiss_due_to_error) {
|
||||||
|
requireActivity().finish()
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
viewModel.setShowThread(args.showThread)
|
viewModel.setShowThread(args.showThread)
|
||||||
val sorting = MediaDatabase.Sorting.values()[args.sorting]
|
val sorting = MediaDatabase.Sorting.values()[args.sorting]
|
||||||
viewModel.fetchAttachments(args.initialMediaUri, args.threadId, sorting)
|
viewModel.fetchAttachments(args.initialMediaUri, args.threadId, sorting)
|
||||||
|
@ -67,7 +141,11 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
|
||||||
private fun bindCurrentState(currentState: MediaPreviewV2State) {
|
private fun bindCurrentState(currentState: MediaPreviewV2State) {
|
||||||
when (currentState.loadState) {
|
when (currentState.loadState) {
|
||||||
MediaPreviewV2State.LoadState.READY -> bindReadyState(currentState)
|
MediaPreviewV2State.LoadState.READY -> bindReadyState(currentState)
|
||||||
// INIT, else -> no-op
|
MediaPreviewV2State.LoadState.LOADED -> {
|
||||||
|
bindReadyState(currentState)
|
||||||
|
bindLoadedState(currentState)
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +154,58 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
|
||||||
val currentItem: MediaDatabase.MediaRecord = currentState.mediaRecords[currentState.position]
|
val currentItem: MediaDatabase.MediaRecord = currentState.mediaRecords[currentState.position]
|
||||||
binding.toolbar.title = getTitleText(currentItem, currentState.showThread)
|
binding.toolbar.title = getTitleText(currentItem, currentState.showThread)
|
||||||
binding.toolbar.subtitle = getSubTitleText(currentItem)
|
binding.toolbar.subtitle = getSubTitleText(currentItem)
|
||||||
|
|
||||||
|
val menu: Menu = binding.toolbar.menu
|
||||||
|
if (currentItem.threadId == MediaIntentFactory.NOT_IN_A_THREAD.toLong()) {
|
||||||
|
menu.findItem(R.id.media_preview__overview).isVisible = false
|
||||||
|
menu.findItem(R.id.delete).isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.toolbar.setOnMenuItemClickListener {
|
||||||
|
when (it.itemId) {
|
||||||
|
R.id.media_preview__overview -> showOverview(currentItem.threadId)
|
||||||
|
R.id.media_preview__forward -> forward(currentItem)
|
||||||
|
R.id.media_preview__share -> share(currentItem)
|
||||||
|
R.id.save -> saveToDisk(currentItem)
|
||||||
|
R.id.delete -> deleteMedia(currentItem)
|
||||||
|
android.R.id.home -> requireActivity().finish()
|
||||||
|
else -> return@setOnMenuItemClickListener false
|
||||||
|
}
|
||||||
|
return@setOnMenuItemClickListener true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are binding steps that need a reference to the actual fragment within the pager.
|
||||||
|
* This is not available until after a page has been chosen by the ViewPager, and we receive the
|
||||||
|
* {@link OnPageChangeCallback}.
|
||||||
|
*/
|
||||||
|
private fun bindLoadedState(currentState: MediaPreviewV2State) {
|
||||||
|
val currentItem: MediaDatabase.MediaRecord = currentState.mediaRecords[currentState.position]
|
||||||
|
val currentFragment: Fragment? = childFragmentManager.findFragmentByTag("f${currentState.position}")
|
||||||
|
val playbackControls = (currentFragment as? MediaPreviewFragment)?.playbackControls
|
||||||
|
val albumThumbnailMedia = currentState.mediaRecords.map { it.toMedia() }
|
||||||
|
val caption = currentItem.attachment?.caption
|
||||||
|
if (albumThumbnailMedia.isEmpty() && caption == null && playbackControls == null) {
|
||||||
|
binding.mediaPreviewDetailsContainer.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
binding.mediaPreviewDetailsContainer.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
binding.mediaPreviewAlbumRail.visibility = if (albumThumbnailMedia.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
(binding.mediaPreviewAlbumRail.adapter as MediaRailAdapter).setMedia(albumThumbnailMedia, currentState.position)
|
||||||
|
binding.mediaPreviewAlbumRail.smoothScrollToPosition(currentState.position)
|
||||||
|
|
||||||
|
binding.mediaPreviewCaptionContainer.visibility = if (caption == null) View.GONE else View.VISIBLE
|
||||||
|
binding.mediaPreviewCaption.text = caption
|
||||||
|
|
||||||
|
if (playbackControls != null) {
|
||||||
|
val params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
|
playbackControls.layoutParams = params
|
||||||
|
binding.mediaPreviewPlaybackControlsContainer.removeAllViews()
|
||||||
|
binding.mediaPreviewPlaybackControlsContainer.addView(playbackControls)
|
||||||
|
} else {
|
||||||
|
binding.mediaPreviewPlaybackControlsContainer.removeAllViews()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTitleText(mediaRecord: MediaDatabase.MediaRecord, showThread: Boolean): String {
|
private fun getTitleText(mediaRecord: MediaDatabase.MediaRecord, showThread: Boolean): String {
|
||||||
|
@ -112,20 +242,148 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
|
||||||
getString(R.string.MediaPreviewActivity_draft)
|
getString(R.string.MediaPreviewActivity_draft)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun anchorMarginsToBottomInsets(viewToAnchor: View) {
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(viewToAnchor) { view: View, windowInsetsCompat: WindowInsetsCompat ->
|
||||||
|
val layoutParams = view.layoutParams as MarginLayoutParams
|
||||||
|
val systemBarInsets = windowInsetsCompat.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
layoutParams.setMargins(
|
||||||
|
systemBarInsets.left,
|
||||||
|
layoutParams.topMargin,
|
||||||
|
systemBarInsets.right,
|
||||||
|
systemBarInsets.bottom
|
||||||
|
)
|
||||||
|
view.layoutParams = layoutParams
|
||||||
|
windowInsetsCompat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MediaDatabase.MediaRecord.toMedia(): Media? {
|
||||||
|
val attachment = this.attachment
|
||||||
|
val uri = attachment?.uri
|
||||||
|
if (attachment == null || uri == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return Media(
|
||||||
|
uri,
|
||||||
|
this.contentType,
|
||||||
|
this.date,
|
||||||
|
attachment.width,
|
||||||
|
attachment.height,
|
||||||
|
attachment.size,
|
||||||
|
0,
|
||||||
|
attachment.isBorderless,
|
||||||
|
attachment.isVideoGif,
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.ofNullable(attachment.caption),
|
||||||
|
Optional.empty()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun singleTapOnMedia(): Boolean {
|
override fun singleTapOnMedia(): Boolean {
|
||||||
Log.d(TAG, "singleTapOnMedia()")
|
fullscreenHelper.toggleUiVisibility()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mediaNotAvailable() {
|
override fun mediaNotAvailable() {
|
||||||
Log.d(TAG, "mediaNotAvailable()")
|
Snackbar.make(binding.root, R.string.MediaPreviewActivity_media_no_longer_available, Snackbar.LENGTH_LONG)
|
||||||
|
.setAction(R.string.MediaPreviewActivity_dismiss_due_to_error) {
|
||||||
|
requireActivity().finish()
|
||||||
|
}.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMediaReady() {
|
override fun onMediaReady() {
|
||||||
Log.d(TAG, "onMediaReady()")
|
Log.d(TAG, "onMediaReady()")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showOverview(threadId: Long) {
|
||||||
|
val context = requireContext()
|
||||||
|
context.startActivity(MediaOverviewActivity.forThread(context, threadId))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun forward(mediaItem: MediaDatabase.MediaRecord) {
|
||||||
|
val attachment = mediaItem.attachment
|
||||||
|
val uri = attachment?.uri
|
||||||
|
if (attachment != null && uri != null) {
|
||||||
|
MultiselectForwardFragmentArgs.create(
|
||||||
|
requireContext(),
|
||||||
|
mediaItem.threadId,
|
||||||
|
uri,
|
||||||
|
attachment.contentType
|
||||||
|
) { args: MultiselectForwardFragmentArgs ->
|
||||||
|
MultiselectForwardFragment.showBottomSheet(childFragmentManager, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun share(mediaItem: MediaDatabase.MediaRecord) {
|
||||||
|
val attachment = mediaItem.attachment
|
||||||
|
val uri = attachment?.uri
|
||||||
|
if (attachment != null && uri != null) {
|
||||||
|
val publicUri = PartAuthority.getAttachmentPublicUri(uri)
|
||||||
|
val mimeType = Intent.normalizeMimeType(attachment.contentType)
|
||||||
|
val shareIntent = ShareCompat.IntentBuilder(requireActivity())
|
||||||
|
.setStream(publicUri)
|
||||||
|
.setType(mimeType)
|
||||||
|
.createChooserIntent()
|
||||||
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivity(shareIntent)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Log.w(TAG, "No activity existed to share the media.", e)
|
||||||
|
Toast.makeText(requireContext(), R.string.MediaPreviewActivity_cant_find_an_app_able_to_share_this_media, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveToDisk(mediaItem: MediaDatabase.MediaRecord) {
|
||||||
|
SaveAttachmentTask.showWarningDialog(requireContext()) { _: DialogInterface?, _: Int ->
|
||||||
|
if (StorageUtil.canWriteToMediaStore()) {
|
||||||
|
performSaveToDisk(mediaItem)
|
||||||
|
return@showWarningDialog
|
||||||
|
}
|
||||||
|
Permissions.with(this)
|
||||||
|
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
.ifNecessary()
|
||||||
|
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||||
|
.onAnyDenied { Toast.makeText(requireContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show() }
|
||||||
|
.onAllGranted { performSaveToDisk(mediaItem) }
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun performSaveToDisk(mediaItem: MediaDatabase.MediaRecord) {
|
||||||
|
val saveTask = SaveAttachmentTask(requireContext())
|
||||||
|
val saveDate = if (mediaItem.date > 0) mediaItem.date else System.currentTimeMillis()
|
||||||
|
val attachment = mediaItem.attachment
|
||||||
|
val uri = attachment?.uri
|
||||||
|
if (attachment != null && uri != null) {
|
||||||
|
saveTask.executeOnExecutor(SignalExecutors.BOUNDED_IO, SaveAttachmentTask.Attachment(uri, attachment.contentType, saveDate, null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteMedia(mediaItem: MediaDatabase.MediaRecord) {
|
||||||
|
val attachment: DatabaseAttachment = mediaItem.attachment ?: return
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setIcon(R.drawable.ic_warning)
|
||||||
|
.setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title)
|
||||||
|
.setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setPositiveButton(R.string.delete) { _, _ ->
|
||||||
|
viewModel.deleteItem(requireContext(), attachment, onSuccess = {
|
||||||
|
requireActivity().finish()
|
||||||
|
}, onError = {
|
||||||
|
Log.e(TAG, "Delete failed!", it)
|
||||||
|
requireActivity().finish()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val ARGS_KEY: String = "args"
|
const val ARGS_KEY: String = "args"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,5 +8,5 @@ data class MediaPreviewV2State(
|
||||||
val position: Int = 0,
|
val position: Int = 0,
|
||||||
val showThread: Boolean = false
|
val showThread: Boolean = false
|
||||||
) {
|
) {
|
||||||
enum class LoadState { INIT, READY, }
|
enum class LoadState { INIT, READY, LOADED }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
package org.thoughtcrime.securesms.mediapreview
|
package org.thoughtcrime.securesms.mediapreview
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.rxjava3.functions.Consumer
|
||||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||||
import org.thoughtcrime.securesms.database.MediaDatabase
|
import org.thoughtcrime.securesms.database.MediaDatabase
|
||||||
|
import org.thoughtcrime.securesms.util.AttachmentUtil
|
||||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||||
|
|
||||||
class MediaPreviewV2ViewModel : ViewModel() {
|
class MediaPreviewV2ViewModel : ViewModel() {
|
||||||
|
@ -19,7 +25,8 @@ class MediaPreviewV2ViewModel : ViewModel() {
|
||||||
val state: Flowable<MediaPreviewV2State> = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
|
val state: Flowable<MediaPreviewV2State> = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
|
||||||
fun fetchAttachments(startingUri: Uri, threadId: Long, sorting: MediaDatabase.Sorting) {
|
fun fetchAttachments(startingUri: Uri, threadId: Long, sorting: MediaDatabase.Sorting) {
|
||||||
disposables += store.update(repository.getAttachments(startingUri, threadId, sorting)) { mediaRecords: List<MediaDatabase.MediaRecord>, oldState: MediaPreviewV2State ->
|
disposables += store.update(repository.getAttachments(startingUri, threadId, sorting)) {
|
||||||
|
mediaRecords: List<MediaDatabase.MediaRecord>, oldState: MediaPreviewV2State ->
|
||||||
oldState.copy(
|
oldState.copy(
|
||||||
mediaRecords = mediaRecords,
|
mediaRecords = mediaRecords,
|
||||||
loadState = MediaPreviewV2State.LoadState.READY
|
loadState = MediaPreviewV2State.LoadState.READY
|
||||||
|
@ -35,10 +42,17 @@ class MediaPreviewV2ViewModel : ViewModel() {
|
||||||
|
|
||||||
fun setCurrentPage(position: Int) {
|
fun setCurrentPage(position: Int) {
|
||||||
store.update { oldState ->
|
store.update { oldState ->
|
||||||
oldState.copy(position = position)
|
oldState.copy(position = position, loadState = MediaPreviewV2State.LoadState.LOADED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteItem(context: Context, attachment: DatabaseAttachment, onSuccess: Consumer<in Unit>, onError: Consumer<in Throwable>) {
|
||||||
|
disposables += Single.fromCallable { AttachmentUtil.deleteAttachment(context.applicationContext, attachment) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(onSuccess, onError)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
disposables.dispose()
|
disposables.dispose()
|
||||||
store.dispose()
|
store.dispose()
|
||||||
|
|
|
@ -3,4 +3,5 @@
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/fragment_container_view"
|
android:id="@+id/fragment_container_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/signal_dark_colorNeutral"/>
|
|
@ -1,11 +1,67 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@color/core_grey_95"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:theme="@style/TextSecure.MediaPreview">
|
android:background="@color/signal_dark_colorNeutral">
|
||||||
|
<androidx.viewpager2.widget.ViewPager2
|
||||||
|
android:id="@+id/media_pager"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/media_preview_details_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:animateLayoutChanges="true"
|
||||||
|
android:background="@drawable/image_preview_shade"
|
||||||
|
android:gravity="bottom"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.MaxHeightScrollView
|
||||||
|
android:id="@+id/media_preview_caption_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="32dp"
|
||||||
|
android:animateLayoutChanges="true"
|
||||||
|
app:scrollView_maxHeight="120dp">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||||
|
android:id="@+id/media_preview_caption"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
style="@style/Signal.Text.Body"
|
||||||
|
android:textColor="@color/core_white"
|
||||||
|
android:gravity="bottom"
|
||||||
|
tools:text="With great power comes great responsibility." />
|
||||||
|
|
||||||
|
</org.thoughtcrime.securesms.components.MaxHeightScrollView>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/media_preview_album_rail"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
tools:layout_height="64dp"/>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/media_preview_playback_controls_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:animateLayoutChanges="true"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/toolbar_layout"
|
android:id="@+id/toolbar_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -19,18 +75,14 @@
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
android:theme="?actionBarStyle"
|
android:theme="?actionBarStyle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize"
|
||||||
android:background="@android:color/transparent" />
|
android:background="@android:color/transparent"
|
||||||
|
app:navigationIcon="@drawable/ic_arrow_left_white_24" />
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
<androidx.viewpager2.widget.ViewPager2
|
|
||||||
android:id="@+id/media_pager"
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true" />
|
|
||||||
</FrameLayout>
|
|
|
@ -1965,6 +1965,7 @@
|
||||||
<string name="MediaPreviewActivity_s_to_you">%1$s to you</string>
|
<string name="MediaPreviewActivity_s_to_you">%1$s to you</string>
|
||||||
<string name="MediaPreviewActivity_media_no_longer_available">Media no longer available.</string>
|
<string name="MediaPreviewActivity_media_no_longer_available">Media no longer available.</string>
|
||||||
<string name="MediaPreviewActivity_cant_find_an_app_able_to_share_this_media">Can\'t find an app able to share this media.</string>
|
<string name="MediaPreviewActivity_cant_find_an_app_able_to_share_this_media">Can\'t find an app able to share this media.</string>
|
||||||
|
<string name="MediaPreviewActivity_dismiss_due_to_error">Close</string>
|
||||||
|
|
||||||
<!-- MessageNotifier -->
|
<!-- MessageNotifier -->
|
||||||
<string name="MessageNotifier_d_new_messages_in_d_conversations">%1$d new messages in %2$d conversations</string>
|
<string name="MessageNotifier_d_new_messages_in_d_conversations">%1$d new messages in %2$d conversations</string>
|
||||||
|
|
Ładowanie…
Reference in New Issue