PixelDroid-App-Android/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt

331 lines
14 KiB
Kotlin
Raw Normal View History

package org.pixeldroid.app.postCreation
import android.app.Activity
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Intent
import android.media.MediaScannerConnection
import android.net.Uri
2022-11-28 12:30:30 +01:00
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
2022-11-28 12:30:30 +01:00
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.FragmentPostCreationBinding
import org.pixeldroid.app.postCreation.camera.CameraActivity
import org.pixeldroid.app.postCreation.carousel.CarouselItem
import org.pixeldroid.app.utils.BaseFragment
2023-12-21 13:35:45 +01:00
import org.pixeldroid.app.utils.bindingLifecycleAware
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.fileExtension
import org.pixeldroid.app.utils.getMimeType
2024-03-29 09:03:48 +01:00
import org.pixeldroid.media_editor.common.PICTURE_POSITION
import org.pixeldroid.media_editor.common.PICTURE_URI
import org.pixeldroid.media_editor.photoEdit.PhotoEditActivity
2024-01-31 12:41:20 +01:00
import org.pixeldroid.media_editor.videoEdit.VideoEditActivity
import java.io.File
import java.io.OutputStream
import java.text.SimpleDateFormat
2022-11-28 12:30:30 +01:00
import java.util.Locale
class PostCreationFragment : BaseFragment() {
2023-12-21 13:35:45 +01:00
private var binding: FragmentPostCreationBinding by bindingLifecycleAware()
2024-02-10 18:43:07 +01:00
private val model: PostCreationViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreateView(inflater, container, savedInstanceState)
// Inflate the layout for this fragment
binding = FragmentPostCreationBinding.inflate(layoutInflater)
2023-12-21 14:06:20 +01:00
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
2024-02-10 18:43:07 +01:00
val user = db.userDao().getActiveUser()
2024-02-10 18:43:07 +01:00
val instance = user?.run {
db.instanceDao().getInstance(instance_uri)
} ?: InstanceDatabaseEntity("", "")
2024-02-10 18:43:07 +01:00
model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData: MutableList<PhotoData>? ->
// update UI
binding.carousel.addData(
2024-02-10 18:43:07 +01:00
newPhotoData.orEmpty().map {
CarouselItem(
it.imageUri, it.imageDescription, it.video,
it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass,
it.videoEncodeComplete, it.videoEncodeError,
)
}
)
2024-02-10 18:43:07 +01:00
binding.postCreationNextButton.isEnabled = newPhotoData?.isNotEmpty() ?: false
}
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState ->
uiState.userMessage?.let {
2022-11-28 12:30:30 +01:00
MaterialAlertDialogBuilder(binding.root.context).apply {
setMessage(it)
setNegativeButton(android.R.string.ok) { _, _ -> }
}.show()
// Notify the ViewModel the message is displayed
model.userMessageShown()
}
binding.addPhotoButton.isEnabled = uiState.addPhotoButtonEnabled
binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled
binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled
binding.toolbarPostCreation.visibility =
if (uiState.isCarousel) View.VISIBLE else View.INVISIBLE
binding.carousel.layoutCarousel = uiState.isCarousel
2023-12-21 14:06:20 +01:00
if(uiState.storyCreation){
binding.toggleStoryPost.check(binding.buttonStory.id)
binding.buttonStory.isPressed = true
binding.carousel.showLayoutSwitchButton = false
binding.carousel.showIndicator = false
} else {
binding.toggleStoryPost.check(binding.buttonPost.id)
binding.carousel.showLayoutSwitchButton = true
binding.carousel.showIndicator = true
}
binding.carousel.maxEntries = uiState.maxEntries
}
}
}
binding.carousel.apply {
layoutCarouselCallback = { model.becameCarousel(it)}
2023-12-21 14:06:20 +01:00
maxEntries = if(model.uiState.value.storyCreation) 1 else instance.albumLimit
addPhotoButtonCallback = {
addPhoto()
}
updateDescriptionCallback = { position: Int, description: String ->
model.updateDescription(position, description)
}
}
2023-12-21 14:06:20 +01:00
// Validate the post and go to the next step of the post creation process
binding.postCreationNextButton.setOnClickListener {
if (validatePost()) {
findNavController().navigate(R.id.action_postCreationFragment_to_postSubmissionFragment)
}
}
binding.editPhotoButton.setOnClickListener {
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
edit(currentPosition)
}
}
binding.addPhotoButton.setOnClickListener {
addPhoto()
}
binding.savePhotoButton.setOnClickListener {
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
savePicture(it, currentPosition)
}
}
binding.removePhotoButton.setOnClickListener {
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
model.removeAt(currentPosition)
model.cancelEncode(currentPosition)
}
}
2023-12-21 14:06:20 +01:00
binding.toggleStoryPost.addOnButtonCheckedListener { _, checkedId, isChecked ->
// Only handle checked events
if (!isChecked) return@addOnButtonCheckedListener
when (checkedId) {
R.id.buttonStory -> {
model.storyMode(true)
}
R.id.buttonPost -> {
model.storyMode(false)
}
}
}
binding.backbutton.setOnClickListener{requireActivity().onBackPressedDispatcher.onBackPressed()}
// Clean up temporary files, if any
val tempFiles = requireActivity().intent.getStringArrayExtra(PostCreationActivity.TEMP_FILES)
tempFiles?.asList()?.forEach {
val file = File(binding.root.context.cacheDir, it)
model.trackTempFile(file)
}
// Handle back pressed button
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val redraft = requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_REDRAFT, false)
if (redraft) {
2022-11-28 12:30:30 +01:00
MaterialAlertDialogBuilder(binding.root.context).apply {
setMessage(R.string.redraft_dialog_cancel)
setPositiveButton(android.R.string.ok) { _, _ ->
requireActivity().finish()
}
setNegativeButton(android.R.string.cancel) { _, _ -> }
show()
}
} else {
requireActivity().finish()
}
}
})
}
private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
2024-02-10 18:43:07 +01:00
val uris = result.data?.extras?.getParcelableArrayList<Uri>(Intent.EXTRA_STREAM)
if (result.resultCode == Activity.RESULT_OK && uris != null) {
model.setImages(model.addPossibleImages(uris, emptyList()))
} else if (result.resultCode != Activity.RESULT_CANCELED) {
Toast.makeText(requireActivity(), R.string.add_images_error, Toast.LENGTH_SHORT).show()
}
}
private fun addPhoto(){
addPhotoResultContract.launch(
Intent(requireActivity(), CameraActivity::class.java)
)
}
private fun savePicture(button: View, currentPosition: Int) {
val originalUri = model.getPhotoData().value!![currentPosition].imageUri
val pair = getOutputFile(originalUri)
val outputStream: OutputStream = pair.first
val path: String = pair.second
requireActivity().contentResolver.openInputStream(originalUri)!!.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
if(path.startsWith("file")) {
MediaScannerConnection.scanFile(
requireActivity(),
arrayOf(path.toUri().toFile().absolutePath),
null
2022-11-19 23:23:11 +01:00
) { tried_path, uri ->
if (uri == null) {
Log.e(
"NEW IMAGE SCAN FAILED",
2022-11-19 23:23:11 +01:00
"Tried to scan $tried_path, but it failed"
)
}
}
}
Snackbar.make(
button, getString(R.string.save_image_success),
Snackbar.LENGTH_LONG
).show()
}
private fun getOutputFile(uri: Uri): Pair<OutputStream, String> {
val extension = uri.fileExtension(requireActivity().contentResolver)
val name = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
.format(System.currentTimeMillis()) + ".$extension"
val outputStream: OutputStream
val path: String
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver: ContentResolver = requireActivity().contentResolver
val type = uri.getMimeType(requireActivity().contentResolver)
val contentValues = ContentValues()
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, type)
contentValues.put(
MediaStore.MediaColumns.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES
)
val store =
if (type.startsWith("video")) MediaStore.Video.Media.EXTERNAL_CONTENT_URI
else MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val imageUri: Uri = resolver.insert(store, contentValues)!!
path = imageUri.toString()
outputStream = resolver.openOutputStream(imageUri)!!
} else {
2022-11-19 23:23:11 +01:00
val imagesDir = Environment.getExternalStoragePublicDirectory(getString(R.string.app_name))
imagesDir.mkdir()
val file = File(imagesDir, name)
path = Uri.fromFile(file).toString()
outputStream = file.outputStream()
}
return Pair(outputStream, path)
}
private fun validatePost(): Boolean {
if (model.getPhotoData().value?.none { it.video && it.videoEncodeComplete == false } == true) {
2023-12-21 14:06:20 +01:00
// Encoding is done, i.e. none of the items are both a video and not done encoding.
// We return true if the post is not empty, false otherwise.
return model.getPhotoData().value?.isNotEmpty() == true
}
// Encoding is not done, show a dialog and return false to indicate validation failed
MaterialAlertDialogBuilder(requireActivity()).apply {
setMessage(R.string.still_encoding)
setNegativeButton(android.R.string.ok) { _, _ -> }
}.show()
return false
}
private val editResultContract: ActivityResultLauncher<Intent> = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()){
result: ActivityResult? ->
if (result?.resultCode == Activity.RESULT_OK && result.data != null) {
2024-03-29 09:03:48 +01:00
val position: Int = result.data!!.getIntExtra(PICTURE_POSITION, 0)
model.modifyAt(position, result.data!!)
?: Toast.makeText(requireActivity(), R.string.error_editing, Toast.LENGTH_SHORT).show()
} else if(result?.resultCode != Activity.RESULT_CANCELED){
Toast.makeText(requireActivity(), R.string.error_editing, Toast.LENGTH_SHORT).show()
}
}
private fun edit(position: Int) {
val intent = Intent(
requireActivity(),
if (model.getPhotoData().value!![position].video) VideoEditActivity::class.java else PhotoEditActivity::class.java
)
2024-03-29 09:03:48 +01:00
.putExtra(PICTURE_URI, model.getPhotoData().value!![position].imageUri)
.putExtra(PICTURE_POSITION, position)
editResultContract.launch(intent)
}
}