Refactor working

This commit is contained in:
Matthieu 2022-10-30 23:34:26 +01:00
parent 851d95bf0f
commit 5644a22d38
9 changed files with 169 additions and 498 deletions

View File

@ -13,16 +13,13 @@ import android.util.Log
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.View.GONE
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
@ -33,7 +30,6 @@ import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
import org.pixeldroid.app.postCreation.camera.CameraActivity
import org.pixeldroid.app.postCreation.carousel.CarouselItem
import org.pixeldroid.app.searchDiscover.TrendingActivity
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
@ -110,10 +106,6 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
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.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled
binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled
binding.toolbarPostCreation.visibility =
@ -136,7 +128,7 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
// get the description and send the post
binding.postCreationSendButton.setOnClickListener {
if (validatePost() && model.isNotEmpty()) {
model.upload(it.context, binding.root.context)
model.nextStep(binding.root.context)
}
}
@ -274,7 +266,7 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
private fun validatePost(): Boolean {
if(model.getPhotoData().value?.all { it.videoEncodeProgress == null } == false){
if(model.getPhotoData().value?.all { !it.video || it.videoEncodeComplete } == false){
AlertDialog.Builder(this).apply {
setMessage(R.string.still_encoding)
setNegativeButton(android.R.string.ok) { _, _ -> }
@ -284,18 +276,6 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
return true
}
private fun enableButton(enable: Boolean = true){
binding.postCreationSendButton.isEnabled = enable
if(enable){
binding.submittingProgressBar.visibility = GONE
binding.postCreationSendButton.visibility = VISIBLE
} else {
binding.submittingProgressBar.visibility = VISIBLE
binding.postCreationSendButton.visibility = GONE
}
}
private val editResultContract: ActivityResultLauncher<Intent> = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){
result: ActivityResult? ->
if (result?.resultCode == Activity.RESULT_OK && result.data != null) {

View File

@ -7,43 +7,43 @@ import android.content.Intent
import android.net.Uri
import android.os.Parcelable
import android.provider.OpenableColumns
import android.text.Editable
import android.util.Log
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.exifinterface.media.ExifInterface
import androidx.lifecycle.*
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager
import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException
import com.jarsilio.android.scrambler.stripMetadata
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 kotlinx.parcelize.Parcelize
import okhttp3.MultipartBody
import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity.RelativeCropPosition
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 org.pixeldroid.app.utils.fileExtension
import org.pixeldroid.app.utils.getMimeType
import retrofit2.HttpException
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.Serializable
import java.net.URI
import javax.inject.Inject
import kotlin.collections.ArrayList
import kotlin.collections.MutableList
import kotlin.collections.MutableMap
import kotlin.collections.arrayListOf
import kotlin.collections.forEach
import kotlin.collections.get
import kotlin.collections.getOrNull
import kotlin.collections.indexOfFirst
import kotlin.collections.isNotEmpty
import kotlin.collections.mutableListOf
import kotlin.collections.mutableMapOf
import kotlin.collections.plus
import kotlin.collections.set
import kotlin.collections.toMutableList
import kotlin.math.ceil
@ -54,18 +54,10 @@ data class PostCreationActivityUiState(
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,
)
@Parcelize
@ -201,33 +193,6 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
photoData.value = photoData.value
}
fun resetUploadStatus() {
photoData.value = photoData.value?.map { it.copy(uploadId = null, progress = null) }?.toMutableList()
}
fun setVideoEncodeAtPosition(uri: Uri, progress: Int?, stabilizationFirstPass: Boolean = false, error: Boolean = false) {
photoData.value?.indexOfFirst { it.imageUri == uri }?.let { position ->
photoData.value?.set(position,
photoData.value!![position].copy(
videoEncodeProgress = progress,
videoEncodeStabilizationFirstPass = stabilizationFirstPass,
videoEncodeError = error,
)
)
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 {
@ -239,183 +204,12 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
}
/**
* 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).
* Next step
*/
@OptIn(ExperimentalUnsignedTypes::class)
fun upload(context: Context, bindingContext: Context) {
_uiState.update { currentUiState ->
currentUiState.copy(
postCreationSendButtonEnabled = false,
addPhotoButtonEnabled = false,
editPhotoButtonEnabled = false,
removePhotoButtonEnabled = false,
uploadCompletedTextviewVisible = false,
uploadErrorVisible = false,
uploadProgressBarVisible = true
)
}
fun nextStep(context: Context) {
val intent = Intent(context, PostSubmissionActivity::class.java)
intent.putExtra(PostSubmissionActivity.PHOTO_DATA, getPhotoData().value?.let { ArrayList(it) })
ContextCompat.startActivity(bindingContext, intent, null)
for (data: PhotoData in getPhotoData().value ?: emptyList()) {
val extension = data.imageUri.fileExtension(getApplication<PixelDroidApplication>().contentResolver)
val strippedImage = File.createTempFile("temp_img", ".$extension", getApplication<PixelDroidApplication>().cacheDir)
val imageUri = data.imageUri
val (strippedOrNot, size) = try {
val orientation = ExifInterface(getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
stripMetadata(imageUri, strippedImage, getApplication<PixelDroidApplication>().contentResolver)
// Restore EXIF orientation
val exifInterface = ExifInterface(strippedImage)
exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, orientation.toString())
exifInterface.saveAttributes()
Pair(strippedImage.inputStream(), strippedImage.length())
} catch (e: UnsupportedFileFormatException){
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
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,
data.imageUri)
)
}
return
}
Pair(imageInputStream, data.size)
} catch (e: IOException){
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
_uiState.update { currentUiState ->
currentUiState.copy(
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
data.imageUri)
)
}
return
}
val type = data.imageUri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
val imagePart = ProgressRequestBody(strippedOrNot, size, type)
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,
)
}
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
e.printStackTrace()
postSub?.dispose()
sub.dispose()
},
{
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
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)
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
)
}
}
}
ContextCompat.startActivity(context, intent, null)
}
fun modifyAt(position: Int, data: Intent): Unit? {
@ -512,7 +306,6 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
tempFiles.forEach {
it.delete()
}
}
fun registerNewFFmpegSession(position: Uri, sessionId: Long) {

View File

@ -1,60 +1,39 @@
package org.pixeldroid.app.postCreation
import android.app.Activity
import android.app.AlertDialog
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Intent
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.*
import android.provider.MediaStore
import android.util.Log
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.View.GONE
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
import org.pixeldroid.app.databinding.ActivityPostSubmissionBinding
import org.pixeldroid.app.postCreation.camera.CameraActivity
import org.pixeldroid.app.postCreation.carousel.CarouselItem
import org.pixeldroid.app.postCreation.PostCreationActivity.Companion.TEMP_FILES
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.fileExtension
import org.pixeldroid.app.utils.getMimeType
import org.pixeldroid.app.utils.setSquareImageFromURL
import java.io.File
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
class PostSubmissionActivity : BaseThemedWithoutBarActivity() {
companion object {
internal const val PICTURE_DESCRIPTION = "picture_description"
internal const val TEMP_FILES = "temp_files"
internal const val POST_REDRAFT = "post_redraft"
internal const val PHOTO_DATA = "photo_data"
}
private lateinit var accounts: List<UserDatabaseEntity>
private var selectedAccount: Int = -1
private lateinit var menu: Menu
private var user: UserDatabaseEntity? = null
private lateinit var instance: InstanceDatabaseEntity
@ -69,8 +48,10 @@ class PostSubmissionActivity : BaseThemedWithoutBarActivity() {
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.add_details)
user = db.userDao().getActiveUser()
accounts = db.userDao().getAll()
instance = user?.run {
db.instanceDao().getAll().first { instanceDatabaseEntity ->
@ -83,8 +64,7 @@ class PostSubmissionActivity : BaseThemedWithoutBarActivity() {
val _model: PostSubmissionViewModel by viewModels {
PostSubmissionViewModelFactory(
application,
photoData!!,
instance
photoData!!
)
}
model = _model
@ -112,6 +92,8 @@ class PostSubmissionActivity : BaseThemedWithoutBarActivity() {
binding.uploadErrorTextExplanation.visibility =
if (uiState.uploadErrorExplanationVisible) VISIBLE else INVISIBLE
selectedAccount = accounts.indexOf(uiState.chosenAccount)
binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
}
}
@ -120,6 +102,10 @@ class PostSubmissionActivity : BaseThemedWithoutBarActivity() {
model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text)
}
binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked ->
model.updateNSFW(isChecked)
}
val existingDescription: String? = intent.getStringExtra(PICTURE_DESCRIPTION)
binding.newPostDescriptionInputField.setText(
@ -130,8 +116,7 @@ class PostSubmissionActivity : BaseThemedWithoutBarActivity() {
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
val firstPostImage = photoData!![0]
setSquareImageFromURL(View(applicationContext), firstPostImage.imageUri.toString(), binding.postPreview)
setSquareImageFromURL(View(applicationContext), photoData!![0].imageUri.toString(), binding.postPreview)
// get the description and send the post
binding.postCreationSendButton.setOnClickListener {
if (validatePost()) model.upload()
@ -151,21 +136,30 @@ class PostSubmissionActivity : BaseThemedWithoutBarActivity() {
}
}
override fun onBackPressed() {
val redraft = intent.getBooleanExtra(POST_REDRAFT, false)
if (redraft) {
val builder = AlertDialog.Builder(binding.root.context)
builder.apply {
setMessage(R.string.redraft_dialog_cancel)
setPositiveButton(android.R.string.ok) { _, _ ->
super.onBackPressed()
}
setNegativeButton(android.R.string.cancel) { _, _ -> }
show()
override fun onCreateOptionsMenu(newMenu: Menu): Boolean {
menuInflater.inflate(R.menu.post_submission_account_menu, newMenu)
menu = newMenu
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId){
R.id.action_switch_accounts -> {
AlertDialog.Builder(this).apply {
setIcon(R.drawable.material_drawer_ico_account)
setTitle(R.string.switch_accounts)
setSingleChoiceItems(accounts.map { it.username + " (${it.fullHandle})" }.toTypedArray(), selectedAccount) { dialog, which ->
if(selectedAccount != which){
model.chooseAccount(accounts[which])
}
dialog.dismiss()
}
setNegativeButton(android.R.string.cancel) { _, _ -> }
}.show()
return true
}
} else {
super.onBackPressed()
}
return super.onOptionsItemSelected(item)
}
private fun validatePost(): Boolean {

View File

@ -1,15 +1,11 @@
package org.pixeldroid.app.postCreation
import android.app.Application
import android.content.ClipData
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.exifinterface.media.ExifInterface
import androidx.lifecycle.*
import androidx.preference.PreferenceManager
@ -25,11 +21,9 @@ import kotlinx.coroutines.launch
import okhttp3.MultipartBody
import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity.RelativeCropPosition
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.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import org.pixeldroid.app.utils.fileExtension
import org.pixeldroid.app.utils.getMimeType
@ -39,7 +33,6 @@ import java.io.FileNotFoundException
import java.io.IOException
import java.net.URI
import javax.inject.Inject
import kotlin.math.ceil
// Models the UI state for the PostCreationActivity
@ -49,6 +42,9 @@ data class PostSubmissionActivityUiState(
val postCreationSendButtonEnabled: Boolean = true,
val newPostDescriptionText: String = "",
val nsfw: Boolean = false,
val chosenAccount: UserDatabaseEntity? = null,
val uploadProgressBarVisible: Boolean = false,
val uploadProgress: Int = 0,
@ -56,19 +52,21 @@ data class PostSubmissionActivityUiState(
val uploadErrorVisible: Boolean = false,
val uploadErrorExplanationText: String = "",
val uploadErrorExplanationVisible: Boolean = false,
)
)
class PostSubmissionViewModel(application: Application, clipdata: ClipData? = null, val instance: InstanceDatabaseEntity? = null) : AndroidViewModel(application) {
class PostSubmissionViewModel(application: Application, photodata: ArrayList<PhotoData>? = null) : AndroidViewModel(application) {
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
MutableLiveData<MutableList<PhotoData>>().also {
it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) }
if (photodata != null) {
it.value = photodata.toMutableList()
}
}
}
@Inject
lateinit var apiHolder: PixelfedAPIHolder
private val _uiState: MutableStateFlow<PostCreationActivityUiState>
private val _uiState: MutableStateFlow<PostSubmissionActivityUiState>
init {
(application as PixelDroidApplication).getAppComponent().inject(this)
@ -76,10 +74,10 @@ class PostSubmissionViewModel(application: Application, clipdata: ClipData? = nu
PreferenceManager.getDefaultSharedPreferences(application)
val initialDescription = sharedPreferences.getString("prefill_description", "") ?: ""
_uiState = MutableStateFlow(PostCreationActivityUiState(newPostDescriptionText = initialDescription))
_uiState = MutableStateFlow(PostSubmissionActivityUiState(newPostDescriptionText = initialDescription))
}
val uiState: StateFlow<PostCreationActivityUiState> = _uiState
val uiState: StateFlow<PostSubmissionActivityUiState> = _uiState
// Map photoData indexes to FFmpeg Session IDs
private val sessionMap: MutableMap<Uri, Long> = mutableMapOf()
@ -94,126 +92,10 @@ class PostSubmissionViewModel(application: Application, clipdata: ClipData? = nu
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).let {
val sizeAndVideoPair: Pair<Long, Boolean> =
getSizeAndVideoValidate(it.uri, (previousList?.size ?: 0) + dataToAdd.size + 1)
dataToAdd.add(PhotoData(imageUri = it.uri, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second, imageDescription = it.text?.toString()))
}
}
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 = uri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
val isVideo = type.startsWith("video/")
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(uri: Uri, progress: Int?, stabilizationFirstPass: Boolean = false, error: Boolean = false) {
photoData.value?.indexOfFirst { it.imageUri == uri }?.let { position ->
photoData.value?.set(position,
photoData.value!![position].copy(
videoEncodeProgress = progress,
videoEncodeStabilizationFirstPass = stabilizationFirstPass,
videoEncodeError = error,
)
)
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
@ -224,9 +106,6 @@ class PostSubmissionViewModel(application: Application, clipdata: ClipData? = nu
_uiState.update { currentUiState ->
currentUiState.copy(
postCreationSendButtonEnabled = false,
addPhotoButtonEnabled = false,
editPhotoButtonEnabled = false,
removePhotoButtonEnabled = false,
uploadCompletedTextviewVisible = false,
uploadErrorVisible = false,
uploadProgressBarVisible = true
@ -300,9 +179,14 @@ class PostSubmissionViewModel(application: Application, clipdata: ClipData? = nu
val description = data.imageDescription?.let { MultipartBody.Part.createFormData("description", it) }
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
//Ugly temporary account switching, but it works well enough for now
val api = uiState.value.chosenAccount?.let {
apiHolder.setToCurrentUser(it)
} ?: apiHolder.api ?: apiHolder.setToCurrentUser()
val inter = api.mediaUpload(description, requestBody.parts[0])
apiHolder.api = null
postSub = inter
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@ -349,6 +233,10 @@ class PostSubmissionViewModel(application: Application, clipdata: ClipData? = nu
private fun post() {
val description = uiState.value.newPostDescriptionText
//TODO investigate why this works but booleans don't
val nsfw = if(uiState.value.nsfw) 1 else 0
_uiState.update { currentUiState ->
currentUiState.copy(
postCreationSendButtonEnabled = false
@ -356,11 +244,15 @@ class PostSubmissionViewModel(application: Application, clipdata: ClipData? = nu
}
viewModelScope.launch {
try {
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
//Ugly temporary account switching, but it works well enough for now
val api = uiState.value.chosenAccount?.let {
apiHolder.setToCurrentUser(it)
} ?: apiHolder.api ?: apiHolder.setToCurrentUser()
api.postStatus(
statusText = description,
media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList()
media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList(),
sensitive = nsfw
)
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_success),
Toast.LENGTH_SHORT).show()
@ -386,6 +278,8 @@ class PostSubmissionViewModel(application: Application, clipdata: ClipData? = nu
postCreationSendButtonEnabled = true
)
}
} finally {
apiHolder.api = null
}
}
}
@ -400,16 +294,21 @@ class PostSubmissionViewModel(application: Application, clipdata: ClipData? = nu
override fun onCleared() {
super.onCleared()
VideoEditActivity.cancelEncoding()
tempFiles.forEach {
it.delete()
}
}
fun updateNSFW(checked: Boolean) { _uiState.update { it.copy(nsfw = checked) } }
fun chooseAccount(which: UserDatabaseEntity) {
_uiState.update { it.copy(chosenAccount = which) }
}
}
class PostSubmissionViewModelFactory(val application: Application, val photoData: ArrayList<PhotoData>, val instance: InstanceDatabaseEntity) : ViewModelProvider.Factory {
class PostSubmissionViewModelFactory(val application: Application, val photoData: ArrayList<PhotoData>) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java, ArrayList::class.java, InstanceDatabaseEntity::class.java).newInstance(application, photoData, instance)
return modelClass.getConstructor(Application::class.java, ArrayList::class.java).newInstance(application, photoData)
}
}

View File

@ -153,18 +153,18 @@ interface PixelfedAPI {
@FormUrlEncoded
@POST("/api/v1/statuses")
suspend fun postStatus(
@Field("status") statusText : String,
@Field("in_reply_to_id") in_reply_to_id : String? = null,
@Field("media_ids[]") media_ids : List<String> = emptyList(),
@Field("poll[options][]") poll_options : List<String>? = null,
@Field("poll[expires_in]") poll_expires : List<String>? = null,
@Field("poll[multiple]") poll_multiple : List<String>? = null,
@Field("poll[hide_totals]") poll_hideTotals : List<String>? = null,
@Field("sensitive") sensitive : Boolean? = null,
@Field("spoiler_text") spoiler_text : String? = null,
@Field("visibility") visibility : String = "public",
@Field("scheduled_at") scheduled_at : String? = null,
@Field("language") language : String? = null
@Field("status") statusText: String,
@Field("in_reply_to_id") in_reply_to_id: String? = null,
@Field("media_ids[]") media_ids: List<String> = emptyList(),
@Field("poll[options][]") poll_options: List<String>? = null,
@Field("poll[expires_in]") poll_expires: List<String>? = null,
@Field("poll[multiple]") poll_multiple: List<String>? = null,
@Field("poll[hide_totals]") poll_hideTotals: List<String>? = null,
@Field("sensitive") sensitive: Int? = null,
@Field("spoiler_text") spoiler_text: String? = null,
@Field("visibility") visibility: String = "public",
@Field("scheduled_at") scheduled_at: String? = null,
@Field("language") language: String? = null
) : Status
@DELETE("/api/v1/statuses/{id}")

View File

@ -11,27 +11,16 @@
android:layout_width="match_parent"
android:layout_height="0dp"
app:showCaption="true"
app:layout_constraintBottom_toBottomOf="@+id/uploadProgressBar"
app:layout_constraintBottom_toTopOf="@+id/buttonConstraints"
app:layout_constraintTop_toTopOf="parent"/>
<ProgressBar
android:id="@+id/uploadProgressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@id/buttonConstraints"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/buttonConstraints"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/carousel">
app:layout_constraintEnd_toEndOf="parent">
<Button
android:id="@+id/post_creation_send_button"
@ -44,17 +33,6 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/submitting_progress_bar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout

View File

@ -7,15 +7,13 @@
tools:context=".postCreation.PostSubmissionActivity">
<ImageView
android:id="@+id/profilePictureImageView"
android:id="@+id/post_preview"
android:layout_margin="20dp"
android:layout_width="88dp"
android:layout_height="88dp"
android:layout_marginStart="20dp"
android:layout_marginTop="6dp"
android:contentDescription="@string/profile_picture"
android:contentDescription="@string/post_preview"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
app:layout_constraintTop_toTopOf="parent"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/upload_error"
@ -71,14 +69,6 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/post_preview"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/buttonConstraints"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/upload_completed_textview"
@ -91,7 +81,7 @@
android:visibility="invisible"
app:drawableStartCompat="@drawable/cloud_done_24"
app:drawableTint="@android:color/holo_green_light"
app:layout_constraintBottom_toTopOf="@id/postTextInputLayout"
app:layout_constraintBottom_toTopOf="@+id/buttonConstraints"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible" />
@ -99,10 +89,9 @@
android:id="@+id/buttonConstraints"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_margin="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/postTextInputLayout">
app:layout_constraintEnd_toEndOf="parent">
<Button
android:id="@+id/post_creation_send_button"
@ -134,7 +123,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@id/postTextInputLayout"
app:layout_constraintBottom_toBottomOf="@+id/upload_completed_textview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
@ -143,13 +132,14 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/description"
app:counterEnabled="true"
android:paddingStart="15dp"
android:paddingTop="8dp"
android:paddingEnd="15dp"
app:counterEnabled="true"
app:errorEnabled="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/buttonConstraints"
app:layout_constraintStart_toStartOf="parent">
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/post_preview">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/new_post_description_input_field"
@ -160,4 +150,27 @@
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/nsfwSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="@+id/post_preview"
app:layout_constraintStart_toEndOf="@+id/post_preview"
app:layout_constraintTop_toTopOf="@+id/post_preview" />
<TextView
android:id="@+id/privateTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/contains_nsfw"
android:textStyle="bold"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="@+id/nsfwSwitch"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/nsfwSwitch"
app:layout_constraintTop_toTopOf="@+id/nsfwSwitch" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_switch_accounts"
android:orderInCategory="100"
android:title="@string/switch_accounts"
android:icon="@drawable/material_drawer_ico_account"
app:showAsAction="ifRoom"/>
</menu>

View File

@ -103,7 +103,8 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="upload_post_error">Post upload error</string>
<string name="description">Description…</string>
<string name="post">Post</string>
<string name="upload_next_step">Upload and go to next step</string>
<string name="upload_next_step">Next step</string>
<string name="add_details">Add some details</string>
<string name="add_photo">Add a photo</string>
<string name="post_image">One of the images in the post</string>
<string name="switch_to_grid">Switch to grid view</string>
@ -323,4 +324,6 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="profile_saved">Changes saved!</string>
<string name="error_profile">Something went wrong. Tap to retry</string>
<string name="change_profile_picture">Change your profile picture</string>
<string name="contains_nsfw">Contains NSFW media</string>
<string name="switch_accounts">Switch accounts</string>
</resources>