Update media editor and remove unused file
This commit is contained in:
parent
713571e4dc
commit
04244bacff
|
@ -154,14 +154,14 @@ dependencies {
|
||||||
implementation "androidx.annotation:annotation:1.5.0"
|
implementation "androidx.annotation:annotation:1.5.0"
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
implementation "androidx.activity:activity-ktx:1.6.1"
|
implementation "androidx.activity:activity-ktx:1.6.1"
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.5.4'
|
implementation 'androidx.fragment:fragment-ktx:1.5.5'
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.7.1'
|
implementation 'androidx.work:work-runtime-ktx:2.7.1'
|
||||||
implementation 'androidx.media2:media2-widget:1.2.1'
|
implementation 'androidx.media2:media2-widget:1.2.1'
|
||||||
implementation 'androidx.media2:media2-player:1.2.1'
|
implementation 'androidx.media2:media2-player:1.2.1'
|
||||||
|
|
||||||
|
|
||||||
// Use the most recent version of CameraX
|
// Use the most recent version of CameraX
|
||||||
def cameraX_version = '1.1.0'
|
def cameraX_version = '1.2.0'
|
||||||
implementation "androidx.camera:camera-core:$cameraX_version"
|
implementation "androidx.camera:camera-core:$cameraX_version"
|
||||||
implementation "androidx.camera:camera-camera2:$cameraX_version"
|
implementation "androidx.camera:camera-camera2:$cameraX_version"
|
||||||
// CameraX Lifecycle library
|
// CameraX Lifecycle library
|
||||||
|
@ -201,7 +201,7 @@ dependencies {
|
||||||
implementation 'com.github.connyduck:sparkbutton:4.1.0'
|
implementation 'com.github.connyduck:sparkbutton:4.1.0'
|
||||||
|
|
||||||
|
|
||||||
implementation 'org.pixeldroid.pixeldroid:android-media-editor:1.2'
|
implementation 'org.pixeldroid.pixeldroid:android-media-editor:1.4'
|
||||||
implementation project(path: ':scrambler')
|
implementation project(path: ':scrambler')
|
||||||
|
|
||||||
implementation('com.github.bumptech.glide:glide:4.14.2') {
|
implementation('com.github.bumptech.glide:glide:4.14.2') {
|
||||||
|
@ -254,13 +254,13 @@ dependencies {
|
||||||
|
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
|
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
|
||||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
androidTestImplementation 'androidx.test:runner:1.5.1'
|
||||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
|
||||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
androidTestImplementation 'androidx.test:runner:1.5.1'
|
||||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.0'
|
||||||
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
|
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
|
||||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.2'
|
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.2'
|
||||||
|
|
||||||
|
|
|
@ -1,314 +0,0 @@
|
||||||
package org.pixeldroid.app.postCreation
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.text.Editable
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.exifinterface.media.ExifInterface
|
|
||||||
import androidx.lifecycle.*
|
|
||||||
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 okhttp3.MultipartBody
|
|
||||||
import org.pixeldroid.app.MainActivity
|
|
||||||
import org.pixeldroid.app.R
|
|
||||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
|
||||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
|
||||||
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
|
|
||||||
import retrofit2.HttpException
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.IOException
|
|
||||||
import java.net.URI
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
|
||||||
// Models the UI state for the PostCreationActivity
|
|
||||||
data class PostSubmissionActivityUiState(
|
|
||||||
val userMessage: String? = null,
|
|
||||||
|
|
||||||
val postCreationSendButtonEnabled: Boolean = true,
|
|
||||||
|
|
||||||
val newPostDescriptionText: String = "",
|
|
||||||
val nsfw: Boolean = false,
|
|
||||||
|
|
||||||
val chosenAccount: UserDatabaseEntity? = null,
|
|
||||||
|
|
||||||
val uploadProgressBarVisible: Boolean = false,
|
|
||||||
val uploadProgress: Int = 0,
|
|
||||||
val uploadCompletedTextviewVisible: Boolean = false,
|
|
||||||
val uploadErrorVisible: Boolean = false,
|
|
||||||
val uploadErrorExplanationText: String = "",
|
|
||||||
val uploadErrorExplanationVisible: Boolean = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
class PostSubmissionViewModel(application: Application, photodata: ArrayList<PhotoData>? = null, val existingDescription: String? = null) : AndroidViewModel(application) {
|
|
||||||
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
|
|
||||||
MutableLiveData<MutableList<PhotoData>>().also {
|
|
||||||
if (photodata != null) {
|
|
||||||
it.value = photodata.toMutableList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var apiHolder: PixelfedAPIHolder
|
|
||||||
|
|
||||||
private val _uiState: MutableStateFlow<PostSubmissionActivityUiState>
|
|
||||||
|
|
||||||
init {
|
|
||||||
(application as PixelDroidApplication).getAppComponent().inject(this)
|
|
||||||
val sharedPreferences =
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(application)
|
|
||||||
val initialDescription = sharedPreferences.getString("prefill_description", "") ?: ""
|
|
||||||
|
|
||||||
_uiState = MutableStateFlow(PostSubmissionActivityUiState(newPostDescriptionText = existingDescription ?: initialDescription))
|
|
||||||
}
|
|
||||||
|
|
||||||
val uiState: StateFlow<PostSubmissionActivityUiState> = _uiState
|
|
||||||
|
|
||||||
// Map photoData indexes to FFmpeg Session IDs
|
|
||||||
private val sessionMap: MutableMap<Uri, 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()
|
|
||||||
|
|
||||||
fun userMessageShown() {
|
|
||||||
_uiState.update { currentUiState ->
|
|
||||||
currentUiState.copy(userMessage = null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPhotoData(): LiveData<MutableList<PhotoData>> = photoData
|
|
||||||
|
|
||||||
fun resetUploadStatus() {
|
|
||||||
photoData.value = photoData.value?.map { it.copy(uploadId = null, progress = null) }?.toMutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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).
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalUnsignedTypes::class)
|
|
||||||
fun upload() {
|
|
||||||
_uiState.update { currentUiState ->
|
|
||||||
currentUiState.copy(
|
|
||||||
postCreationSendButtonEnabled = false,
|
|
||||||
uploadCompletedTextviewVisible = false,
|
|
||||||
uploadErrorVisible = false,
|
|
||||||
uploadProgressBarVisible = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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) }
|
|
||||||
|
|
||||||
//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())
|
|
||||||
.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
|
|
||||||
|
|
||||||
//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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
//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(),
|
|
||||||
sensitive = nsfw
|
|
||||||
)
|
|
||||||
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: 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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}finally {
|
|
||||||
apiHolder.api = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun newPostDescriptionChanged(text: Editable?) {
|
|
||||||
_uiState.update { it.copy(newPostDescriptionText = text.toString()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun trackTempFile(file: File) {
|
|
||||||
tempFiles.add(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
super.onCleared()
|
|
||||||
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 existingDescription: String?) : ViewModelProvider.Factory {
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
|
||||||
return modelClass.getConstructor(Application::class.java, ArrayList::class.java, String::class.java).newInstance(application, photoData, existingDescription)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,7 +9,6 @@ import org.pixeldroid.app.utils.BaseFragment
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
import org.pixeldroid.app.postCreation.PostCreationViewModel
|
import org.pixeldroid.app.postCreation.PostCreationViewModel
|
||||||
import org.pixeldroid.app.profile.EditProfileViewModel
|
import org.pixeldroid.app.profile.EditProfileViewModel
|
||||||
import org.pixeldroid.app.postCreation.PostSubmissionViewModel
|
|
||||||
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker
|
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@ -23,7 +22,6 @@ interface ApplicationComponent {
|
||||||
fun inject(notificationsWorker: NotificationsWorker)
|
fun inject(notificationsWorker: NotificationsWorker)
|
||||||
fun inject(postCreationViewModel: PostCreationViewModel)
|
fun inject(postCreationViewModel: PostCreationViewModel)
|
||||||
fun inject(editProfileViewModel: EditProfileViewModel)
|
fun inject(editProfileViewModel: EditProfileViewModel)
|
||||||
fun inject(postSubmissionViewModel: PostSubmissionViewModel)
|
|
||||||
|
|
||||||
val context: Context?
|
val context: Context?
|
||||||
val application: Application?
|
val application: Application?
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue