mirror of
https://gitlab.shinice.net/pixeldroid/PixelDroid
synced 2025-02-07 02:43:30 +01:00
huge refactor of PostCreation to use ViewModel
This commit is contained in:
parent
8cecfa3de6
commit
5c221e004d
@ -44,9 +44,7 @@
|
||||
<activity
|
||||
android:name=".postCreation.PostCreationActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
android:theme="@style/AppTheme.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
|
@ -7,7 +7,6 @@ import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.*
|
||||
import android.provider.MediaStore
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.View.INVISIBLE
|
||||
@ -20,14 +19,19 @@ import androidx.activity.viewModels
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.os.HandlerCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.arthenica.ffmpegkit.*
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.MultipartBody
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
@ -48,16 +52,11 @@ import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
import kotlin.math.ceil
|
||||
import com.arthenica.ffmpegkit.FFprobeKit
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
private const val TAG = "Post Creation Activity"
|
||||
const val TAG = "Post Creation Activity"
|
||||
|
||||
data class PhotoData(
|
||||
var imageUri: Uri,
|
||||
@ -74,16 +73,11 @@ class PostCreationActivity : BaseActivity() {
|
||||
private var user: UserDatabaseEntity? = null
|
||||
private lateinit var instance: InstanceDatabaseEntity
|
||||
|
||||
private val photoData: ArrayList<PhotoData> = ArrayList()
|
||||
|
||||
private lateinit var binding: ActivityPostCreationBinding
|
||||
|
||||
private val resultHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper())
|
||||
|
||||
// Map photoData indexes to FFmpeg Session IDs
|
||||
private val sessionMap: MutableMap<Int, Long> = mutableMapOf()
|
||||
// Keep track of temporary files to delete them (avoids filling cache super fast with videos)
|
||||
private val tempFiles: ArrayList<File> = ArrayList()
|
||||
private lateinit var model: PostCreationViewModel
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -91,9 +85,6 @@ class PostCreationActivity : BaseActivity() {
|
||||
binding = ActivityPostCreationBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
|
||||
|
||||
user = db.userDao().getActiveUser()
|
||||
|
||||
instance = user?.run {
|
||||
@ -102,47 +93,87 @@ class PostCreationActivity : BaseActivity() {
|
||||
}
|
||||
} ?: InstanceDatabaseEntity("", "")
|
||||
|
||||
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
|
||||
val _model: PostCreationViewModel by viewModels { PostCreationViewModelFactory(application, intent.clipData!!, instance) }
|
||||
model = _model
|
||||
|
||||
// get image URIs
|
||||
intent.clipData?.let { addPossibleImages(it) }
|
||||
model.getPhotoData().observe(this) { newPhotoData ->
|
||||
// update UI
|
||||
binding.carousel.addData(
|
||||
newPhotoData.map {
|
||||
CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val carousel: ImageCarousel = binding.carousel
|
||||
carousel.addData(photoData.map { CarouselItem(it.imageUri, video = it.video, encodeProgress = null) })
|
||||
carousel.layoutCarouselCallback = {
|
||||
if(it){
|
||||
// Became a carousel
|
||||
binding.toolbarPostCreation.visibility = VISIBLE
|
||||
} else {
|
||||
// Became a grid
|
||||
binding.toolbarPostCreation.visibility = INVISIBLE
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.uiState.collect { uiState ->
|
||||
uiState.userMessage?.let {
|
||||
AlertDialog.Builder(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
|
||||
enableButton(uiState.postCreationSendButtonEnabled)
|
||||
binding.uploadProgressBar.visibility = if(uiState.uploadProgressBarVisible) VISIBLE else INVISIBLE
|
||||
binding.uploadProgressBar.progress = uiState.uploadProgress
|
||||
binding.uploadCompletedTextview.visibility = if(uiState.uploadCompletedTextviewVisible) VISIBLE else INVISIBLE
|
||||
binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled
|
||||
binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled
|
||||
binding.uploadError.visibility = if(uiState.uploadErrorVisible) VISIBLE else INVISIBLE
|
||||
binding.uploadErrorTextExplanation.visibility = if(uiState.uploadErrorExplanationVisible) VISIBLE else INVISIBLE
|
||||
|
||||
binding.toolbarPostCreation.visibility = if(uiState.isCarousel) VISIBLE else INVISIBLE
|
||||
binding.carousel.layoutCarousel = uiState.isCarousel
|
||||
|
||||
|
||||
binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
|
||||
|
||||
uiState.newEncodingJobPosition?.let { position ->
|
||||
uiState.newEncodingJobMuted?.let { muted ->
|
||||
uiState.newEncodingJobVideoStart?.let { videoStart ->
|
||||
uiState.newEncodingJobVideoEnd?.let { videoEnd ->
|
||||
startEncoding(position, muted, videoStart, videoEnd)
|
||||
model.encodingStarted()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
carousel.maxEntries = instance.albumLimit
|
||||
carousel.addPhotoButtonCallback = {
|
||||
addPhoto()
|
||||
}
|
||||
carousel.updateDescriptionCallback = { position: Int, description: String ->
|
||||
photoData.getOrNull(position)?.imageDescription = description
|
||||
binding.newPostDescriptionInputField.doAfterTextChanged {
|
||||
model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text)
|
||||
}
|
||||
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
|
||||
|
||||
binding.carousel.apply {
|
||||
layoutCarouselCallback = { model.becameCarousel(it)}
|
||||
maxEntries = instance.albumLimit
|
||||
addPhotoButtonCallback = {
|
||||
addPhoto()
|
||||
}
|
||||
updateDescriptionCallback = { position: Int, description: String ->
|
||||
model.updateDescription(position, description)
|
||||
}
|
||||
}
|
||||
// get the description and send the post
|
||||
binding.postCreationSendButton.setOnClickListener {
|
||||
if (validatePost() && photoData.isNotEmpty()) upload()
|
||||
if (validatePost() && model.isNotEmpty()) model.upload()
|
||||
}
|
||||
|
||||
// Button to retry image upload when it fails
|
||||
binding.retryUploadButton.setOnClickListener {
|
||||
binding.uploadError.visibility = View.GONE
|
||||
photoData.forEach {
|
||||
it.uploadId = null
|
||||
it.progress = null
|
||||
}
|
||||
upload()
|
||||
model.resetUploadStatus()
|
||||
model.upload()
|
||||
}
|
||||
|
||||
binding.editPhotoButton.setOnClickListener {
|
||||
carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
||||
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
||||
edit(currentPosition)
|
||||
}
|
||||
}
|
||||
@ -152,104 +183,25 @@ class PostCreationActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
binding.savePhotoButton.setOnClickListener {
|
||||
carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
||||
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
||||
savePicture(it, currentPosition)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
binding.removePhotoButton.setOnClickListener {
|
||||
carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
||||
photoData.removeAt(currentPosition)
|
||||
sessionMap[currentPosition]?.let { FFmpegKit.cancel(it) }
|
||||
carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) })
|
||||
binding.addPhotoButton.isEnabled = true
|
||||
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
||||
model.removeAt(currentPosition)
|
||||
model.cancelEncode(currentPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
FFmpegKit.cancel()
|
||||
tempFiles.forEach {
|
||||
it.delete()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will add as many images as possible to [photoData], from the [clipData], and if
|
||||
* ([photoData].size + [clipData].itemCount) > [InstanceDatabaseEntity.albumLimit] then it will only add as many images
|
||||
* as are legal (if any) and a dialog will be shown to the user alerting them of this fact.
|
||||
*/
|
||||
private fun addPossibleImages(clipData: ClipData) {
|
||||
var count = clipData.itemCount
|
||||
if(count + photoData.size > instance.albumLimit){
|
||||
AlertDialog.Builder(this).apply {
|
||||
setMessage(getString(R.string.total_exceeds_album_limit).format(instance.albumLimit))
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
count = count.coerceAtMost(instance.albumLimit - photoData.size)
|
||||
}
|
||||
if (count + photoData.size >= instance.albumLimit) {
|
||||
// Disable buttons to add more images
|
||||
binding.addPhotoButton.isEnabled = false
|
||||
}
|
||||
for (i in 0 until count) {
|
||||
clipData.getItemAt(i).uri.let {
|
||||
val sizeAndVideoPair: Pair<Long, Boolean> = it.getSizeAndVideoValidate(photoData.size + 1)
|
||||
photoData.add(PhotoData(imageUri = it, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the file of the Uri, and whether it is a video,
|
||||
* and opens a dialog in case it is too big or in case the file is unsupported.
|
||||
*/
|
||||
private fun Uri.getSizeAndVideoValidate(editPosition: Int): Pair<Long, Boolean> {
|
||||
val size: Long =
|
||||
if (toString().startsWith("content")) {
|
||||
contentResolver.query(this, null, null, null, null)
|
||||
?.use { cursor ->
|
||||
/* Get the column indexes of the data in the Cursor,
|
||||
* move to the first row in the Cursor, get the data,
|
||||
* and display it.
|
||||
*/
|
||||
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||
cursor.moveToFirst()
|
||||
cursor.getLong(sizeIndex)
|
||||
} ?: 0
|
||||
} else {
|
||||
toFile().length()
|
||||
}
|
||||
|
||||
val sizeInkBytes = ceil(size.toDouble() / 1000).toLong()
|
||||
val type = contentResolver.getType(this)
|
||||
val isVideo = type?.startsWith("video/") == true
|
||||
|
||||
if(isVideo && !instance.videoEnabled){
|
||||
AlertDialog.Builder(this@PostCreationActivity).apply {
|
||||
setMessage(R.string.video_not_supported)
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
|
||||
if (sizeInkBytes > instance.maxPhotoSize || sizeInkBytes > instance.maxVideoSize) {
|
||||
val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize
|
||||
AlertDialog.Builder(this@PostCreationActivity).apply {
|
||||
setMessage(getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize))
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
return Pair(size, isVideo)
|
||||
}
|
||||
|
||||
private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK && result.data?.clipData != null) {
|
||||
result.data?.clipData?.let {
|
||||
addPossibleImages(it)
|
||||
model.setImages(model.addPossibleImages(it))
|
||||
}
|
||||
binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) })
|
||||
} else if (result.resultCode != Activity.RESULT_CANCELED) {
|
||||
Toast.makeText(applicationContext, "Error while adding images", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@ -268,7 +220,7 @@ class PostCreationActivity : BaseActivity() {
|
||||
val outputStream: OutputStream = pair.first
|
||||
val path: String = pair.second
|
||||
|
||||
contentResolver.openInputStream(photoData[currentPosition].imageUri)!!.use { input ->
|
||||
contentResolver.openInputStream(model.getPhotoData().value!![currentPosition].imageUri)!!.use { input ->
|
||||
outputStream.use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
@ -331,7 +283,7 @@ class PostCreationActivity : BaseActivity() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if(!photoData.all { it.videoEncodeProgress == null }){
|
||||
if(model.getPhotoData().value?.all { it.videoEncodeProgress == null } == false){
|
||||
AlertDialog.Builder(this).apply {
|
||||
setMessage(R.string.still_encoding)
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
@ -341,118 +293,6 @@ class PostCreationActivity : BaseActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the images that are in the [photoData] array.
|
||||
* Keeps track of them in the [PhotoData.progress] (for the upload progress), and the
|
||||
* [PhotoData.uploadId] (for the list of ids of the uploads).
|
||||
*/
|
||||
private fun upload() {
|
||||
enableButton(false)
|
||||
binding.uploadProgressBar.visibility = VISIBLE
|
||||
binding.uploadCompletedTextview.visibility = INVISIBLE
|
||||
binding.removePhotoButton.isEnabled = false
|
||||
binding.editPhotoButton.isEnabled = false
|
||||
binding.addPhotoButton.isEnabled = false
|
||||
|
||||
for (data: PhotoData in photoData) {
|
||||
val imageUri = data.imageUri
|
||||
val imageInputStream = try {
|
||||
contentResolver.openInputStream(imageUri)!!
|
||||
} catch (e: FileNotFoundException){
|
||||
AlertDialog.Builder(this).apply {
|
||||
setMessage(getString(R.string.file_not_found).format(imageUri))
|
||||
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
return
|
||||
}
|
||||
|
||||
val imagePart = ProgressRequestBody(imageInputStream, data.size)
|
||||
val requestBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("file", System.currentTimeMillis().toString(), imagePart)
|
||||
.build()
|
||||
|
||||
val sub = imagePart.progressSubject
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe { percentage ->
|
||||
data.progress = percentage.toInt()
|
||||
binding.uploadProgressBar.progress =
|
||||
photoData.sumOf { it.progress ?: 0 } / photoData.size
|
||||
}
|
||||
|
||||
var postSub: Disposable? = null
|
||||
|
||||
val description = data.imageDescription?.let { MultipartBody.Part.createFormData("description", it) }
|
||||
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
val inter = api.mediaUpload(description, requestBody.parts[0])
|
||||
|
||||
postSub = inter
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ attachment: Attachment ->
|
||||
data.progress = 0
|
||||
data.uploadId = attachment.id!!
|
||||
},
|
||||
{ e: Throwable ->
|
||||
binding.uploadError.visibility = View.VISIBLE
|
||||
if(e is HttpException){
|
||||
binding.uploadErrorTextExplanation.text =
|
||||
getString(R.string.upload_error, e.code())
|
||||
binding.uploadErrorTextExplanation.visibility= VISIBLE
|
||||
} else {
|
||||
binding.uploadErrorTextExplanation.visibility= View.GONE
|
||||
}
|
||||
e.printStackTrace()
|
||||
postSub?.dispose()
|
||||
sub.dispose()
|
||||
},
|
||||
{
|
||||
data.progress = 100
|
||||
if (photoData.all { it.progress == 100 && it.uploadId != null }) {
|
||||
binding.uploadProgressBar.visibility = View.GONE
|
||||
binding.uploadCompletedTextview.visibility = View.VISIBLE
|
||||
post()
|
||||
}
|
||||
postSub?.dispose()
|
||||
sub.dispose()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun post() {
|
||||
val description = binding.newPostDescriptionInputField.text.toString()
|
||||
enableButton(false)
|
||||
lifecycleScope.launchWhenCreated {
|
||||
try {
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
|
||||
api.postStatus(
|
||||
statusText = description,
|
||||
media_ids = photoData.mapNotNull { it.uploadId }.toList()
|
||||
)
|
||||
Toast.makeText(applicationContext, getString(R.string.upload_post_success),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
val intent = Intent(this@PostCreationActivity, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
startActivity(intent)
|
||||
} catch (exception: IOException) {
|
||||
Toast.makeText(applicationContext, getString(R.string.upload_post_error),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Log.e(TAG, exception.toString())
|
||||
enableButton(true)
|
||||
} catch (exception: HttpException) {
|
||||
Toast.makeText(applicationContext, getString(R.string.upload_post_failed),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Log.e(TAG, exception.response().toString() + exception.message().toString())
|
||||
enableButton(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableButton(enable: Boolean = true){
|
||||
binding.postCreationSendButton.isEnabled = enable
|
||||
if(enable){
|
||||
@ -469,32 +309,8 @@ class PostCreationActivity : BaseActivity() {
|
||||
result: ActivityResult? ->
|
||||
if (result?.resultCode == Activity.RESULT_OK && result.data != null) {
|
||||
val position: Int = result.data!!.getIntExtra(PhotoEditActivity.PICTURE_POSITION, 0)
|
||||
photoData.getOrNull(position)?.apply {
|
||||
if (video) {
|
||||
val muted: Boolean = result.data!!.getBooleanExtra(VideoEditActivity.MUTED, false)
|
||||
val videoStart: Float? = result.data!!.getFloatExtra(VideoEditActivity.VIDEO_START, -1f).let {
|
||||
if(it == -1f) null else it
|
||||
}
|
||||
val modified: Boolean = result.data!!.getBooleanExtra(VideoEditActivity.MODIFIED, false)
|
||||
val videoEnd: Float? = result.data!!.getFloatExtra(VideoEditActivity.VIDEO_END, -1f).let {
|
||||
if(it == -1f) null else it
|
||||
}
|
||||
if(modified){
|
||||
videoEncodeProgress = 0
|
||||
sessionMap[position]?.let { FFmpegKit.cancel(it) }
|
||||
startEncoding(position, muted, videoStart, videoEnd)
|
||||
}
|
||||
} else {
|
||||
imageUri = result.data!!.getStringExtra(PhotoEditActivity.PICTURE_URI)!!.toUri()
|
||||
val (imageSize, imageVideo) = imageUri.getSizeAndVideoValidate(position)
|
||||
size = imageSize
|
||||
video = imageVideo
|
||||
}
|
||||
progress = null
|
||||
uploadId = null
|
||||
} ?: Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show()
|
||||
|
||||
binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) })
|
||||
model.modifyAt(position, result.data!!)
|
||||
?: Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show()
|
||||
} else if(result?.resultCode != Activity.RESULT_CANCELED){
|
||||
Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@ -508,21 +324,21 @@ class PostCreationActivity : BaseActivity() {
|
||||
* don't want to remove the end
|
||||
*/
|
||||
private fun startEncoding(position: Int, muted: Boolean, videoStart: Float?, videoEnd: Float?) {
|
||||
val originalUri = photoData[position].imageUri
|
||||
val originalUri = model.getPhotoData().value!![position].imageUri
|
||||
|
||||
// Having a meaningful suffix is necessary so that ffmpeg knows what to put in output
|
||||
val suffix = if(originalUri.scheme == "content") {
|
||||
contentResolver.getType(photoData[position].imageUri)?.takeLastWhile { it != '/' }
|
||||
contentResolver.getType(model.getPhotoData().value!![position].imageUri)?.takeLastWhile { it != '/' }
|
||||
} else {
|
||||
originalUri.toString().takeLastWhile { it != '/' }
|
||||
}
|
||||
val file = File.createTempFile("temp_video", ".$suffix")
|
||||
//val file = File.createTempFile("temp_video", ".webm")
|
||||
tempFiles.add(file)
|
||||
model.trackTempFile(file)
|
||||
val fileUri = file.toUri()
|
||||
val outputVideoPath = ffmpegSafeUri(fileUri)
|
||||
|
||||
val inputUri = photoData[position].imageUri
|
||||
val inputUri = model.getPhotoData().value!![position].imageUri
|
||||
|
||||
val inputSafePath = ffmpegSafeUri(inputUri)
|
||||
|
||||
@ -542,18 +358,12 @@ class PostCreationActivity : BaseActivity() {
|
||||
fun successResult() {
|
||||
// Hide progress indicator in carousel
|
||||
binding.carousel.updateProgress(null, position, false)
|
||||
val (imageSize, imageVideo) = outputVideoPath.toUri().let {
|
||||
photoData[position].imageUri = it
|
||||
it.getSizeAndVideoValidate(position)
|
||||
val (imageSize, _) = outputVideoPath.toUri().let {
|
||||
model.setUriAtPosition(it, position)
|
||||
model.getSizeAndVideoValidate(it, position)
|
||||
}
|
||||
photoData[position].videoEncodeProgress = null
|
||||
photoData[position].size = imageSize
|
||||
binding.carousel.addData(photoData.map {
|
||||
CarouselItem(it.imageUri,
|
||||
it.imageDescription,
|
||||
it.video,
|
||||
it.videoEncodeProgress)
|
||||
})
|
||||
model.setVideoEncodeAtPosition(position, null)
|
||||
model.setSizeAtPosition(imageSize, position)
|
||||
}
|
||||
|
||||
val post = resultHandler.post {
|
||||
@ -567,7 +377,7 @@ class PostCreationActivity : BaseActivity() {
|
||||
} else {
|
||||
resultHandler.post {
|
||||
binding.carousel.updateProgress(null, position, error = true)
|
||||
photoData[position].videoEncodeProgress = null
|
||||
model.setVideoEncodeAtPosition(position, null)
|
||||
}
|
||||
Log.e(TAG, "Encode failed with state ${session.state} and rc $returnCode.${session.failStackTrace}")
|
||||
}
|
||||
@ -584,8 +394,8 @@ class PostCreationActivity : BaseActivity() {
|
||||
}
|
||||
resultHandler.post {
|
||||
completePercentage?.let {
|
||||
val rounded = it.roundToInt()
|
||||
photoData[position].videoEncodeProgress = rounded
|
||||
val rounded: Int = it.roundToInt()
|
||||
model.setVideoEncodeAtPosition(position, rounded)
|
||||
binding.carousel.updateProgress(rounded, position, false)
|
||||
}
|
||||
}
|
||||
@ -593,15 +403,15 @@ class PostCreationActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
sessionMap[position] = session.sessionId
|
||||
model.registerNewFFmpegSession(position, session.sessionId)
|
||||
}
|
||||
|
||||
private fun edit(position: Int) {
|
||||
val intent = Intent(
|
||||
this,
|
||||
if(photoData[position].video) VideoEditActivity::class.java else PhotoEditActivity::class.java
|
||||
if(model.getPhotoData().value!![position].video) VideoEditActivity::class.java else PhotoEditActivity::class.java
|
||||
)
|
||||
.putExtra(PhotoEditActivity.PICTURE_URI, photoData[position].imageUri)
|
||||
.putExtra(PhotoEditActivity.PICTURE_URI, model.getPhotoData().value!![position].imageUri)
|
||||
.putExtra(PhotoEditActivity.PICTURE_POSITION, position)
|
||||
|
||||
editResultContract.launch(intent)
|
||||
|
@ -1,30 +1,436 @@
|
||||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ClipData
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.text.Editable
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.*
|
||||
import com.arthenica.ffmpegkit.FFmpegKit
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.MultipartBody
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.postCreation.photoEdit.PhotoEditActivity
|
||||
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import retrofit2.HttpException
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.ceil
|
||||
|
||||
class PostCreationViewModel : ViewModel() {
|
||||
private val photoData: MutableLiveData<List<PhotoData>> by lazy {
|
||||
MutableLiveData<List<PhotoData>>().also {
|
||||
loadUsers()
|
||||
// Models the UI state for the PostCreationActivity
|
||||
data class PostCreationActivityUiState(
|
||||
val userMessage: String? = null,
|
||||
|
||||
val addPhotoButtonEnabled: Boolean = true,
|
||||
val editPhotoButtonEnabled: Boolean = true,
|
||||
val removePhotoButtonEnabled: Boolean = true,
|
||||
val postCreationSendButtonEnabled: Boolean = true,
|
||||
|
||||
val isCarousel: Boolean = true,
|
||||
|
||||
val newPostDescriptionText: String = "",
|
||||
|
||||
val uploadProgressBarVisible: Boolean = false,
|
||||
val uploadProgress: Int = 0,
|
||||
val uploadCompletedTextviewVisible: Boolean = false,
|
||||
val uploadErrorVisible: Boolean = false,
|
||||
val uploadErrorExplanationText: String = "",
|
||||
val uploadErrorExplanationVisible: Boolean = false,
|
||||
|
||||
val newEncodingJobPosition: Int? = null,
|
||||
val newEncodingJobMuted: Boolean? = null,
|
||||
val newEncodingJobVideoStart: Float? = null,
|
||||
val newEncodingJobVideoEnd: Float? = null,
|
||||
)
|
||||
|
||||
class PostCreationViewModel(application: Application, clipdata: ClipData? = null, val instance: InstanceDatabaseEntity? = null) : AndroidViewModel(application) {
|
||||
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
|
||||
MutableLiveData<MutableList<PhotoData>>().also {
|
||||
it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) }
|
||||
}
|
||||
}
|
||||
|
||||
fun getUsers(): LiveData<List<PhotoData>> {
|
||||
return photoData
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
init {
|
||||
(application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
}
|
||||
|
||||
private fun loadUsers() {
|
||||
// Do an asynchronous operation to fetch users.
|
||||
// Map photoData indexes to FFmpeg Session IDs
|
||||
private val sessionMap: MutableMap<Int, Long> = mutableMapOf()
|
||||
// Keep track of temporary files to delete them (avoids filling cache super fast with videos)
|
||||
private val tempFiles: java.util.ArrayList<File> = java.util.ArrayList()
|
||||
|
||||
|
||||
private val _uiState = MutableStateFlow(PostCreationActivityUiState())
|
||||
val uiState: StateFlow<PostCreationActivityUiState> = _uiState
|
||||
|
||||
fun userMessageShown() {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(userMessage = null)
|
||||
}
|
||||
}
|
||||
|
||||
fun encodingStarted() {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
newEncodingJobPosition = null,
|
||||
newEncodingJobMuted = null,
|
||||
newEncodingJobVideoStart = null,
|
||||
newEncodingJobVideoEnd = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getPhotoData(): LiveData<MutableList<PhotoData>> = photoData
|
||||
|
||||
/**
|
||||
* Will add as many images as possible to [photoData], from the [clipData], and if
|
||||
* ([photoData].size + [clipData].itemCount) > [InstanceDatabaseEntity.albumLimit] then it will only add as many images
|
||||
* as are legal (if any) and a dialog will be shown to the user alerting them of this fact.
|
||||
*/
|
||||
fun addPossibleImages(clipData: ClipData, previousList: MutableList<PhotoData>? = photoData.value): MutableList<PhotoData> {
|
||||
val dataToAdd: ArrayList<PhotoData> = arrayListOf()
|
||||
var count = clipData.itemCount
|
||||
if(count + (previousList?.size ?: 0) > instance!!.albumLimit){
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.total_exceeds_album_limit).format(instance.albumLimit))
|
||||
}
|
||||
count = count.coerceAtMost(instance.albumLimit - (previousList?.size ?: 0))
|
||||
}
|
||||
if (count + (previousList?.size ?: 0) >= instance.albumLimit) {
|
||||
// Disable buttons to add more images
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(addPhotoButtonEnabled = false)
|
||||
}
|
||||
}
|
||||
for (i in 0 until count) {
|
||||
clipData.getItemAt(i).uri.let {
|
||||
val sizeAndVideoPair: Pair<Long, Boolean> =
|
||||
getSizeAndVideoValidate(it, (previousList?.size ?: 0) + dataToAdd.size + 1)
|
||||
dataToAdd.add(PhotoData(imageUri = it, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second))
|
||||
}
|
||||
}
|
||||
return previousList?.plus(dataToAdd)?.toMutableList() ?: mutableListOf()
|
||||
}
|
||||
|
||||
fun setImages(addPossibleImages: MutableList<PhotoData>) {
|
||||
photoData.value = addPossibleImages
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the file of the Uri, and whether it is a video,
|
||||
* and opens a dialog in case it is too big or in case the file is unsupported.
|
||||
*/
|
||||
fun getSizeAndVideoValidate(uri: Uri, editPosition: Int): Pair<Long, Boolean> {
|
||||
val size: Long =
|
||||
if (uri.scheme =="content") {
|
||||
getApplication<PixelDroidApplication>().contentResolver.query(uri, null, null, null, null)
|
||||
?.use { cursor ->
|
||||
/* Get the column indexes of the data in the Cursor,
|
||||
* move to the first row in the Cursor, get the data,
|
||||
* and display it.
|
||||
*/
|
||||
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||
cursor.moveToFirst()
|
||||
cursor.getLong(sizeIndex)
|
||||
} ?: 0
|
||||
} else {
|
||||
uri.toFile().length()
|
||||
}
|
||||
|
||||
val sizeInkBytes = ceil(size.toDouble() / 1000).toLong()
|
||||
val type = getApplication<PixelDroidApplication>().contentResolver.getType(uri)
|
||||
val isVideo = type?.startsWith("video/") == true
|
||||
|
||||
if(isVideo && !instance!!.videoEnabled){
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.video_not_supported))
|
||||
}
|
||||
}
|
||||
|
||||
if (sizeInkBytes > instance!!.maxPhotoSize || sizeInkBytes > instance.maxVideoSize) {
|
||||
val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
userMessage = getApplication<PixelDroidApplication>().getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
return Pair(size, isVideo)
|
||||
}
|
||||
|
||||
fun isNotEmpty(): Boolean = photoData.value?.isNotEmpty() ?: false
|
||||
|
||||
fun updateDescription(position: Int, description: String) {
|
||||
photoData.value?.getOrNull(position)?.imageDescription = description
|
||||
photoData.value = photoData.value
|
||||
}
|
||||
|
||||
fun resetUploadStatus() {
|
||||
photoData.value = photoData.value?.map { it.copy(uploadId = null, progress = null) }?.toMutableList()
|
||||
}
|
||||
|
||||
fun setVideoEncodeAtPosition(position: Int, progress: Int?) {
|
||||
photoData.value?.set(position, photoData.value!![position].copy(videoEncodeProgress = progress))
|
||||
photoData.value = photoData.value
|
||||
}
|
||||
|
||||
fun setUriAtPosition(uri: Uri, position: Int) {
|
||||
photoData.value?.set(position, photoData.value!![position].copy(imageUri = uri))
|
||||
photoData.value = photoData.value
|
||||
}
|
||||
|
||||
fun setSizeAtPosition(imageSize: Long, position: Int) {
|
||||
photoData.value?.set(position, photoData.value!![position].copy(size = imageSize))
|
||||
photoData.value = photoData.value
|
||||
}
|
||||
|
||||
fun removeAt(currentPosition: Int) {
|
||||
photoData.value?.removeAt(currentPosition)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
addPhotoButtonEnabled = true
|
||||
)
|
||||
}
|
||||
photoData.value = photoData.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the images that are in the [photoData] array.
|
||||
* Keeps track of them in the [PhotoData.progress] (for the upload progress), and the
|
||||
* [PhotoData.uploadId] (for the list of ids of the uploads).
|
||||
*/
|
||||
fun upload() {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
postCreationSendButtonEnabled = false,
|
||||
addPhotoButtonEnabled = false,
|
||||
editPhotoButtonEnabled = false,
|
||||
removePhotoButtonEnabled = false,
|
||||
uploadCompletedTextviewVisible = false,
|
||||
uploadErrorVisible = false,
|
||||
uploadProgressBarVisible = true
|
||||
)
|
||||
}
|
||||
|
||||
for (data: PhotoData in getPhotoData().value ?: emptyList()) {
|
||||
val imageUri = data.imageUri
|
||||
val imageInputStream = try {
|
||||
getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!
|
||||
} catch (e: FileNotFoundException){
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
|
||||
imageUri)
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val imagePart = ProgressRequestBody(imageInputStream, data.size)
|
||||
val requestBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("file", System.currentTimeMillis().toString(), imagePart)
|
||||
.build()
|
||||
|
||||
val sub = imagePart.progressSubject
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe { percentage ->
|
||||
data.progress = percentage.toInt()
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
uploadProgress = getPhotoData().value!!.sumOf { it.progress ?: 0 } / getPhotoData().value!!.size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var postSub: Disposable? = null
|
||||
|
||||
val description = data.imageDescription?.let { MultipartBody.Part.createFormData("description", it) }
|
||||
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
val inter = api.mediaUpload(description, requestBody.parts[0])
|
||||
|
||||
postSub = inter
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ attachment: Attachment ->
|
||||
data.progress = 0
|
||||
data.uploadId = attachment.id!!
|
||||
},
|
||||
{ e: Throwable ->
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
uploadErrorVisible = true,
|
||||
uploadErrorExplanationText = if(e is HttpException){
|
||||
getApplication<PixelDroidApplication>().getString(R.string.upload_error, e.code())
|
||||
} else "",
|
||||
uploadErrorExplanationVisible = e is HttpException,
|
||||
)
|
||||
}
|
||||
e.printStackTrace()
|
||||
postSub?.dispose()
|
||||
sub.dispose()
|
||||
},
|
||||
{
|
||||
data.progress = 100
|
||||
if (getPhotoData().value!!.all { it.progress == 100 && it.uploadId != null }) {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
uploadProgressBarVisible = false,
|
||||
uploadCompletedTextviewVisible = true
|
||||
)
|
||||
}
|
||||
post()
|
||||
}
|
||||
postSub?.dispose()
|
||||
sub.dispose()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun post() {
|
||||
val description = uiState.value.newPostDescriptionText
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
postCreationSendButtonEnabled = false
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
|
||||
api.postStatus(
|
||||
statusText = description,
|
||||
media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList()
|
||||
)
|
||||
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_success),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
val intent = Intent(getApplication(), MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
//TODO make the activity launch this instead (and surrounding toasts too)L
|
||||
getApplication<PixelDroidApplication>().startActivity(intent)
|
||||
} catch (exception: IOException) {
|
||||
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_error),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Log.e(TAG, exception.toString())
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
postCreationSendButtonEnabled = true
|
||||
)
|
||||
}
|
||||
} catch (exception: HttpException) {
|
||||
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_failed),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Log.e(TAG, exception.response().toString() + exception.message().toString())
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
postCreationSendButtonEnabled = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun modifyAt(position: Int, data: Intent): Unit? {
|
||||
val result: PhotoData = photoData.value?.get(position)?.run {
|
||||
if (video) {
|
||||
val muted: Boolean = data.getBooleanExtra(VideoEditActivity.MUTED, false)
|
||||
val videoStart: Float? = data.getFloatExtra(VideoEditActivity.VIDEO_START, -1f).let {
|
||||
if(it == -1f) null else it
|
||||
}
|
||||
val modified: Boolean = data.getBooleanExtra(VideoEditActivity.MODIFIED, false)
|
||||
val videoEnd: Float? = data.getFloatExtra(VideoEditActivity.VIDEO_END, -1f).let {
|
||||
if(it == -1f) null else it
|
||||
}
|
||||
if(modified){
|
||||
videoEncodeProgress = 0
|
||||
sessionMap[position]?.let { FFmpegKit.cancel(it) }
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
newEncodingJobPosition = position,
|
||||
newEncodingJobMuted = muted,
|
||||
newEncodingJobVideoStart = videoStart,
|
||||
newEncodingJobVideoEnd = videoEnd
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
imageUri = data.getStringExtra(PhotoEditActivity.PICTURE_URI)!!.toUri()
|
||||
val (imageSize, imageVideo) = getSizeAndVideoValidate(imageUri, position)
|
||||
size = imageSize
|
||||
video = imageVideo
|
||||
}
|
||||
progress = null
|
||||
uploadId = null
|
||||
this
|
||||
} ?: return null
|
||||
result.let {
|
||||
photoData.value?.set(position, it)
|
||||
photoData.value = photoData.value
|
||||
}
|
||||
return Unit
|
||||
}
|
||||
|
||||
fun newPostDescriptionChanged(text: Editable?) {
|
||||
_uiState.update { it.copy(newPostDescriptionText = text.toString()) }
|
||||
}
|
||||
|
||||
fun trackTempFile(file: File) {
|
||||
tempFiles.add(file)
|
||||
}
|
||||
|
||||
fun cancelEncode(currentPosition: Int) {
|
||||
sessionMap[currentPosition]?.let { FFmpegKit.cancel(it) }
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
FFmpegKit.cancel()
|
||||
tempFiles.forEach {
|
||||
it.delete()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun registerNewFFmpegSession(position: Int, sessionId: Long) {
|
||||
sessionMap[position] = sessionId
|
||||
}
|
||||
|
||||
fun becameCarousel(became: Boolean) {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
isCarousel = became
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
class PostCreationViewModelFactory(val bundle: ClipData? = null) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.getConstructor(ClipData::class.java).newInstance(bundle)
|
||||
}
|
||||
|
||||
class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java).newInstance(application, clipdata, instance)
|
||||
}
|
||||
}
|
@ -59,15 +59,7 @@ class ImageCarousel(
|
||||
|
||||
initIndicator()
|
||||
}
|
||||
|
||||
|
||||
private var btnPrevious: View? = null
|
||||
private var btnNext: View? = null
|
||||
|
||||
private var btnGrid: View? = null
|
||||
private var btnCarousel: View? = null
|
||||
|
||||
|
||||
|
||||
private var isBuiltInIndicator = false
|
||||
private var data: MutableList<CarouselItem>? = null
|
||||
|
||||
@ -231,27 +223,24 @@ class ImageCarousel(
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
btnGrid = binding.switchToGridButton
|
||||
btnCarousel = binding.switchToCarouselButton
|
||||
|
||||
btnGrid?.setOnClickListener {
|
||||
binding.switchToGridButton.setOnClickListener {
|
||||
layoutCarousel = false
|
||||
}
|
||||
btnCarousel?.setOnClickListener {
|
||||
binding.switchToCarouselButton.setOnClickListener {
|
||||
layoutCarousel = true
|
||||
}
|
||||
|
||||
if(value){
|
||||
if(layoutCarousel){
|
||||
btnGrid?.visibility = VISIBLE
|
||||
btnCarousel?.visibility = GONE
|
||||
binding.switchToGridButton.visibility = VISIBLE
|
||||
binding.switchToCarouselButton.visibility = GONE
|
||||
} else {
|
||||
btnGrid?.visibility = GONE
|
||||
btnCarousel?.visibility = VISIBLE
|
||||
binding.switchToGridButton.visibility = GONE
|
||||
binding.switchToCarouselButton.visibility = VISIBLE
|
||||
}
|
||||
} else {
|
||||
btnGrid?.visibility = GONE
|
||||
btnCarousel?.visibility = GONE
|
||||
binding.switchToGridButton.visibility = GONE
|
||||
binding.switchToCarouselButton.visibility = GONE
|
||||
}
|
||||
}
|
||||
|
||||
@ -267,15 +256,15 @@ class ImageCarousel(
|
||||
if(value){
|
||||
recyclerView.layoutManager = CarouselLinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||
|
||||
btnNext?.visibility = VISIBLE
|
||||
btnPrevious?.visibility = VISIBLE
|
||||
binding.btnNext.visibility = VISIBLE
|
||||
binding.btnPrevious.visibility = VISIBLE
|
||||
|
||||
binding.editMediaDescriptionLayout.visibility = if(editingMediaDescription) VISIBLE else INVISIBLE
|
||||
tvCaption.visibility = if(editingMediaDescription) INVISIBLE else VISIBLE
|
||||
} else {
|
||||
recyclerView.layoutManager = GridLayoutManager(context, 3)
|
||||
btnNext?.visibility = GONE
|
||||
btnPrevious?.visibility = GONE
|
||||
binding.btnNext.visibility = GONE
|
||||
binding.btnPrevious.visibility = GONE
|
||||
|
||||
binding.editMediaDescriptionLayout.visibility = INVISIBLE
|
||||
tvCaption.visibility = INVISIBLE
|
||||
|
@ -15,8 +15,6 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.SeekBar
|
||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.os.HandlerCompat
|
||||
import androidx.media.AudioAttributesCompat
|
||||
@ -33,10 +31,6 @@ import org.pixeldroid.app.postCreation.carousel.dpToPx
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.ffmpegSafeUri
|
||||
import java.io.File
|
||||
import java.text.NumberFormat
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
class VideoEditActivity : BaseActivity() {
|
||||
|
@ -7,6 +7,7 @@ import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import dagger.Component
|
||||
import org.pixeldroid.app.postCreation.PostCreationViewModel
|
||||
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker
|
||||
import javax.inject.Singleton
|
||||
|
||||
@ -18,6 +19,7 @@ interface ApplicationComponent {
|
||||
fun inject(activity: BaseActivity?)
|
||||
fun inject(feedFragment: BaseFragment)
|
||||
fun inject(notificationsWorker: NotificationsWorker)
|
||||
fun inject(postCreationViewModel: PostCreationViewModel)
|
||||
|
||||
val context: Context?
|
||||
val application: Application?
|
||||
|
Loading…
x
Reference in New Issue
Block a user