Start post submission activity
This commit is contained in:
parent
2d712ed395
commit
851d95bf0f
|
@ -8,6 +8,7 @@ apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'jacoco'
|
apply plugin: 'jacoco'
|
||||||
|
apply plugin: "kotlin-parcelize"
|
||||||
|
|
||||||
|
|
||||||
// Force latest version of Jacoco, initially done to resolve https://github.com/jacoco/jacoco/issues/1155
|
// Force latest version of Jacoco, initially done to resolve https://github.com/jacoco/jacoco/issues/1155
|
||||||
|
|
|
@ -72,6 +72,9 @@
|
||||||
<data android:mimeType="video/*" />
|
<data android:mimeType="video/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".postCreation.PostSubmissionActivity">
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".profile.FollowsActivity"
|
android:name=".profile.FollowsActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
|
|
|
@ -19,6 +19,7 @@ import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.widget.doAfterTextChanged
|
import androidx.core.widget.doAfterTextChanged
|
||||||
|
@ -32,6 +33,7 @@ import org.pixeldroid.app.R
|
||||||
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
|
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
|
||||||
import org.pixeldroid.app.postCreation.camera.CameraActivity
|
import org.pixeldroid.app.postCreation.camera.CameraActivity
|
||||||
import org.pixeldroid.app.postCreation.carousel.CarouselItem
|
import org.pixeldroid.app.postCreation.carousel.CarouselItem
|
||||||
|
import org.pixeldroid.app.searchDiscover.TrendingActivity
|
||||||
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
|
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
|
||||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||||
|
@ -112,37 +114,14 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
||||||
binding.uploadProgressBar.visibility =
|
binding.uploadProgressBar.visibility =
|
||||||
if (uiState.uploadProgressBarVisible) VISIBLE else INVISIBLE
|
if (uiState.uploadProgressBarVisible) VISIBLE else INVISIBLE
|
||||||
binding.uploadProgressBar.progress = uiState.uploadProgress
|
binding.uploadProgressBar.progress = uiState.uploadProgress
|
||||||
binding.uploadCompletedTextview.visibility =
|
|
||||||
if (uiState.uploadCompletedTextviewVisible) VISIBLE else INVISIBLE
|
|
||||||
binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled
|
binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled
|
||||||
binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled
|
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 =
|
binding.toolbarPostCreation.visibility =
|
||||||
if (uiState.isCarousel) VISIBLE else INVISIBLE
|
if (uiState.isCarousel) VISIBLE else INVISIBLE
|
||||||
binding.carousel.layoutCarousel = uiState.isCarousel
|
binding.carousel.layoutCarousel = uiState.isCarousel
|
||||||
|
|
||||||
|
|
||||||
binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.newPostDescriptionInputField.doAfterTextChanged {
|
|
||||||
model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text)
|
|
||||||
}
|
|
||||||
|
|
||||||
val existingDescription: String? = intent.getStringExtra(PICTURE_DESCRIPTION)
|
|
||||||
|
|
||||||
binding.newPostDescriptionInputField.setText(
|
|
||||||
// Set description from redraft if any, otherwise from the template
|
|
||||||
existingDescription ?: model.uiState.value.newPostDescriptionText
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
|
|
||||||
|
|
||||||
binding.carousel.apply {
|
binding.carousel.apply {
|
||||||
layoutCarouselCallback = { model.becameCarousel(it)}
|
layoutCarouselCallback = { model.becameCarousel(it)}
|
||||||
|
@ -156,13 +135,9 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
||||||
}
|
}
|
||||||
// get the description and send the post
|
// get the description and send the post
|
||||||
binding.postCreationSendButton.setOnClickListener {
|
binding.postCreationSendButton.setOnClickListener {
|
||||||
if (validatePost() && model.isNotEmpty()) model.upload()
|
if (validatePost() && model.isNotEmpty()) {
|
||||||
}
|
model.upload(it.context, binding.root.context)
|
||||||
|
}
|
||||||
// Button to retry image upload when it fails
|
|
||||||
binding.retryUploadButton.setOnClickListener {
|
|
||||||
model.resetUploadStatus()
|
|
||||||
model.upload()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.editPhotoButton.setOnClickListener {
|
binding.editPhotoButton.setOnClickListener {
|
||||||
|
@ -181,7 +156,6 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
binding.removePhotoButton.setOnClickListener {
|
binding.removePhotoButton.setOnClickListener {
|
||||||
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
||||||
model.removeAt(currentPosition)
|
model.removeAt(currentPosition)
|
||||||
|
@ -300,14 +274,6 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
||||||
|
|
||||||
|
|
||||||
private fun validatePost(): Boolean {
|
private fun validatePost(): Boolean {
|
||||||
binding.postTextInputLayout.run {
|
|
||||||
val content = editText?.length() ?: 0
|
|
||||||
if (content > counterMaxLength) {
|
|
||||||
// error, too many characters
|
|
||||||
error = resources.getQuantityString(R.plurals.description_max_characters, counterMaxLength, counterMaxLength)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(model.getPhotoData().value?.all { it.videoEncodeProgress == null } == false){
|
if(model.getPhotoData().value?.all { it.videoEncodeProgress == null } == false){
|
||||||
AlertDialog.Builder(this).apply {
|
AlertDialog.Builder(this).apply {
|
||||||
setMessage(R.string.still_encoding)
|
setMessage(R.string.still_encoding)
|
||||||
|
@ -321,10 +287,10 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
||||||
private fun enableButton(enable: Boolean = true){
|
private fun enableButton(enable: Boolean = true){
|
||||||
binding.postCreationSendButton.isEnabled = enable
|
binding.postCreationSendButton.isEnabled = enable
|
||||||
if(enable){
|
if(enable){
|
||||||
binding.postingProgressBar.visibility = GONE
|
binding.submittingProgressBar.visibility = GONE
|
||||||
binding.postCreationSendButton.visibility = VISIBLE
|
binding.postCreationSendButton.visibility = VISIBLE
|
||||||
} else {
|
} else {
|
||||||
binding.postingProgressBar.visibility = VISIBLE
|
binding.submittingProgressBar.visibility = VISIBLE
|
||||||
binding.postCreationSendButton.visibility = GONE
|
binding.postCreationSendButton.visibility = GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,15 @@ package org.pixeldroid.app.postCreation
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Parcelable
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
|
@ -22,6 +25,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import org.pixeldroid.app.MainActivity
|
import org.pixeldroid.app.MainActivity
|
||||||
import org.pixeldroid.app.R
|
import org.pixeldroid.app.R
|
||||||
|
@ -37,6 +41,7 @@ import retrofit2.HttpException
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.io.Serializable
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
@ -61,8 +66,9 @@ data class PostCreationActivityUiState(
|
||||||
val uploadErrorVisible: Boolean = false,
|
val uploadErrorVisible: Boolean = false,
|
||||||
val uploadErrorExplanationText: String = "",
|
val uploadErrorExplanationText: String = "",
|
||||||
val uploadErrorExplanationVisible: Boolean = false,
|
val uploadErrorExplanationVisible: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
data class PhotoData(
|
data class PhotoData(
|
||||||
var imageUri: Uri,
|
var imageUri: Uri,
|
||||||
var size: Long,
|
var size: Long,
|
||||||
|
@ -74,12 +80,12 @@ data class PhotoData(
|
||||||
var videoEncodeStabilizationFirstPass: Boolean? = null,
|
var videoEncodeStabilizationFirstPass: Boolean? = null,
|
||||||
var videoEncodeComplete: Boolean = false,
|
var videoEncodeComplete: Boolean = false,
|
||||||
var videoEncodeError: Boolean = false,
|
var videoEncodeError: Boolean = false,
|
||||||
)
|
) : Parcelable
|
||||||
|
|
||||||
class PostCreationViewModel(application: Application, clipdata: ClipData? = null, val instance: InstanceDatabaseEntity? = null) : AndroidViewModel(application) {
|
class PostCreationViewModel(application: Application, clipdata: ClipData? = null, val instance: InstanceDatabaseEntity? = null) : AndroidViewModel(application) {
|
||||||
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
|
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
|
||||||
MutableLiveData<MutableList<PhotoData>>().also {
|
MutableLiveData<MutableList<PhotoData>>().also {
|
||||||
it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) }
|
it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,7 +244,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
||||||
* [PhotoData.uploadId] (for the list of ids of the uploads).
|
* [PhotoData.uploadId] (for the list of ids of the uploads).
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalUnsignedTypes::class)
|
@OptIn(ExperimentalUnsignedTypes::class)
|
||||||
fun upload() {
|
fun upload(context: Context, bindingContext: Context) {
|
||||||
_uiState.update { currentUiState ->
|
_uiState.update { currentUiState ->
|
||||||
currentUiState.copy(
|
currentUiState.copy(
|
||||||
postCreationSendButtonEnabled = false,
|
postCreationSendButtonEnabled = false,
|
||||||
|
@ -251,6 +257,10 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()) {
|
for (data: PhotoData in getPhotoData().value ?: emptyList()) {
|
||||||
val extension = data.imageUri.fileExtension(getApplication<PixelDroidApplication>().contentResolver)
|
val extension = data.imageUri.fileExtension(getApplication<PixelDroidApplication>().contentResolver)
|
||||||
|
|
||||||
|
@ -409,7 +419,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun modifyAt(position: Int, data: Intent): Unit? {
|
fun modifyAt(position: Int, data: Intent): Unit? {
|
||||||
val result: PhotoData = photoData.value?.getOrNull(position)?.run {
|
val result: PhotoData = photoData.value?.getOrNull(position)?.run {
|
||||||
if (video) {
|
if (video) {
|
||||||
val modified: Boolean = data.getBooleanExtra(VideoEditActivity.MODIFIED, false)
|
val modified: Boolean = data.getBooleanExtra(VideoEditActivity.MODIFIED, false)
|
||||||
if(modified){
|
if(modified){
|
||||||
|
@ -446,10 +456,6 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
||||||
return Unit
|
return Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newPostDescriptionChanged(text: Editable?) {
|
|
||||||
_uiState.update { it.copy(newPostDescriptionText = text.toString()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param originalUri the Uri of the file you sent to be edited
|
* @param originalUri the Uri of the file you sent to be edited
|
||||||
* @param progress percentage of (this pass of) encoding that is done
|
* @param progress percentage of (this pass of) encoding that is done
|
||||||
|
@ -520,11 +526,10 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity) : ViewModelProvider.Factory {
|
class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity) : ViewModelProvider.Factory {
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
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)
|
return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java).newInstance(application, clipdata, instance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
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.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.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.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 var user: UserDatabaseEntity? = null
|
||||||
|
private lateinit var instance: InstanceDatabaseEntity
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityPostSubmissionBinding
|
||||||
|
|
||||||
|
private lateinit var model: PostSubmissionViewModel
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityPostSubmissionBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
|
user = db.userDao().getActiveUser()
|
||||||
|
|
||||||
|
instance = user?.run {
|
||||||
|
db.instanceDao().getAll().first { instanceDatabaseEntity ->
|
||||||
|
instanceDatabaseEntity.uri.contains(instance_uri)
|
||||||
|
}
|
||||||
|
} ?: InstanceDatabaseEntity("", "")
|
||||||
|
|
||||||
|
val photoData = intent.getParcelableArrayListExtra<PhotoData>(PHOTO_DATA) as ArrayList<PhotoData>?
|
||||||
|
|
||||||
|
val _model: PostSubmissionViewModel by viewModels {
|
||||||
|
PostSubmissionViewModelFactory(
|
||||||
|
application,
|
||||||
|
photoData!!,
|
||||||
|
instance
|
||||||
|
)
|
||||||
|
}
|
||||||
|
model = _model
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
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.uploadError.visibility =
|
||||||
|
if (uiState.uploadErrorVisible) VISIBLE else INVISIBLE
|
||||||
|
binding.uploadErrorTextExplanation.visibility =
|
||||||
|
if (uiState.uploadErrorExplanationVisible) VISIBLE else INVISIBLE
|
||||||
|
|
||||||
|
binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.newPostDescriptionInputField.doAfterTextChanged {
|
||||||
|
model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
val existingDescription: String? = intent.getStringExtra(PICTURE_DESCRIPTION)
|
||||||
|
|
||||||
|
binding.newPostDescriptionInputField.setText(
|
||||||
|
// Set description from redraft if any, otherwise from the template
|
||||||
|
existingDescription ?: model.uiState.value.newPostDescriptionText
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
|
||||||
|
|
||||||
|
val firstPostImage = photoData!![0]
|
||||||
|
setSquareImageFromURL(View(applicationContext), firstPostImage.imageUri.toString(), binding.postPreview)
|
||||||
|
// get the description and send the post
|
||||||
|
binding.postCreationSendButton.setOnClickListener {
|
||||||
|
if (validatePost()) model.upload()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button to retry image upload when it fails
|
||||||
|
binding.retryUploadButton.setOnClickListener {
|
||||||
|
model.resetUploadStatus()
|
||||||
|
model.upload()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up temporary files, if any
|
||||||
|
val tempFiles = intent.getStringArrayExtra(TEMP_FILES)
|
||||||
|
tempFiles?.asList()?.forEach {
|
||||||
|
val file = File(binding.root.context.cacheDir, it)
|
||||||
|
model.trackTempFile(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validatePost(): Boolean {
|
||||||
|
binding.postTextInputLayout.run {
|
||||||
|
val content = editText?.length() ?: 0
|
||||||
|
if (content > counterMaxLength) {
|
||||||
|
// error, too many characters
|
||||||
|
error = resources.getQuantityString(R.plurals.description_max_characters, counterMaxLength, counterMaxLength)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enableButton(enable: Boolean = true){
|
||||||
|
binding.postCreationSendButton.isEnabled = enable
|
||||||
|
if(enable){
|
||||||
|
binding.postingProgressBar.visibility = GONE
|
||||||
|
binding.postCreationSendButton.visibility = VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.postingProgressBar.visibility = VISIBLE
|
||||||
|
binding.postCreationSendButton.visibility = GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,415 @@
|
||||||
|
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
|
||||||
|
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.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 java.io.File
|
||||||
|
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
|
||||||
|
data class PostSubmissionActivityUiState(
|
||||||
|
val userMessage: String? = null,
|
||||||
|
|
||||||
|
val postCreationSendButtonEnabled: 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
class PostSubmissionViewModel(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()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var apiHolder: PixelfedAPIHolder
|
||||||
|
|
||||||
|
private val _uiState: MutableStateFlow<PostCreationActivityUiState>
|
||||||
|
|
||||||
|
init {
|
||||||
|
(application as PixelDroidApplication).getAppComponent().inject(this)
|
||||||
|
val sharedPreferences =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(application)
|
||||||
|
val initialDescription = sharedPreferences.getString("prefill_description", "") ?: ""
|
||||||
|
|
||||||
|
_uiState = MutableStateFlow(PostCreationActivityUiState(newPostDescriptionText = initialDescription))
|
||||||
|
}
|
||||||
|
|
||||||
|
val uiState: StateFlow<PostCreationActivityUiState> = _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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* [PhotoData.uploadId] (for the list of ids of the uploads).
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalUnsignedTypes::class)
|
||||||
|
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 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun newPostDescriptionChanged(text: Editable?) {
|
||||||
|
_uiState.update { it.copy(newPostDescriptionText = text.toString()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun trackTempFile(file: File) {
|
||||||
|
tempFiles.add(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
VideoEditActivity.cancelEncoding()
|
||||||
|
tempFiles.forEach {
|
||||||
|
it.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PostSubmissionViewModelFactory(val application: Application, val photoData: ArrayList<PhotoData>, val instance: InstanceDatabaseEntity) : 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ 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
|
||||||
|
|
||||||
|
@ -22,6 +23,7 @@ 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?
|
||||||
|
|
|
@ -6,60 +6,6 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".postCreation.PostCreationActivity">
|
tools:context=".postCreation.PostCreationActivity">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:id="@+id/upload_error"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:elevation="2dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:visibility="visible">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/upload_error_text_view"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/colorError"
|
|
||||||
android:text="@string/media_upload_failed"
|
|
||||||
android:textColor="?attr/colorOnError"
|
|
||||||
android:textSize="20sp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:drawableStartCompat="@drawable/cloud_off_24"
|
|
||||||
app:drawableTint="?attr/colorOnError" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/upload_error_text_explanation"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/colorError"
|
|
||||||
tools:text="Error code returned by server: 413"
|
|
||||||
android:textColor="?attr/colorOnError"
|
|
||||||
android:textSize="20sp"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_view"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/retry_upload_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/retry"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/upload_error_text_view"
|
|
||||||
app:layout_constraintHorizontal_bias="0.498"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/upload_error_text_view"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_explanation" />
|
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<org.pixeldroid.app.postCreation.carousel.ImageCarousel
|
<org.pixeldroid.app.postCreation.carousel.ImageCarousel
|
||||||
android:id="@+id/carousel"
|
android:id="@+id/carousel"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -68,20 +14,15 @@
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/uploadProgressBar"
|
app:layout_constraintBottom_toBottomOf="@+id/uploadProgressBar"
|
||||||
app:layout_constraintTop_toTopOf="parent"/>
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
<TextView
|
<ProgressBar
|
||||||
android:id="@+id/upload_completed_textview"
|
android:id="@+id/uploadProgressBar"
|
||||||
android:layout_width="wrap_content"
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
android:layout_height="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/media_upload_completed"
|
|
||||||
android:textColor="@android:color/holo_green_light"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:visibility="invisible"
|
android:visibility="invisible"
|
||||||
app:drawableStartCompat="@drawable/cloud_done_24"
|
app:layout_constraintBottom_toTopOf="@id/buttonConstraints"
|
||||||
app:drawableTint="@android:color/holo_green_light"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/postTextInputLayout"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
tools:visibility="visible" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/buttonConstraints"
|
android:id="@+id/buttonConstraints"
|
||||||
|
@ -90,14 +31,14 @@
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/postTextInputLayout">
|
app:layout_constraintTop_toBottomOf="@id/carousel">
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/post_creation_send_button"
|
android:id="@+id/post_creation_send_button"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:text="@string/post"
|
android:text="@string/upload_next_step"
|
||||||
android:visibility="visible"
|
android:visibility="visible"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
@ -105,7 +46,7 @@
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/posting_progress_bar"
|
android:id="@+id/submitting_progress_bar"
|
||||||
style="?android:attr/progressBarStyle"
|
style="?android:attr/progressBarStyle"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -116,39 +57,6 @@
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<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/postTextInputLayout"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
android:id="@+id/postTextInputLayout"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/description"
|
|
||||||
app:counterEnabled="true"
|
|
||||||
android:paddingStart="15dp"
|
|
||||||
android:paddingEnd="15dp"
|
|
||||||
app:errorEnabled="true"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/buttonConstraints"
|
|
||||||
app:layout_constraintStart_toStartOf="parent">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/new_post_description_input_field"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:ems="10"
|
|
||||||
android:inputType="textMultiLine" />
|
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/toolbarPostCreation"
|
android:id="@+id/toolbarPostCreation"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -198,7 +106,6 @@
|
||||||
app:layout_constraintStart_toEndOf="@+id/removePhotoButton"
|
app:layout_constraintStart_toEndOf="@+id/removePhotoButton"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
android:id="@+id/addPhotoButton"
|
android:id="@+id/addPhotoButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".postCreation.PostSubmissionActivity">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/profilePictureImageView"
|
||||||
|
android:layout_width="88dp"
|
||||||
|
android:layout_height="88dp"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:contentDescription="@string/profile_picture"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:srcCompat="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/upload_error"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:elevation="2dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/upload_error_text_view"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/colorError"
|
||||||
|
android:text="@string/media_upload_failed"
|
||||||
|
android:textColor="?attr/colorOnError"
|
||||||
|
android:textSize="20sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:drawableStartCompat="@drawable/cloud_off_24"
|
||||||
|
app:drawableTint="?attr/colorOnError" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/upload_error_text_explanation"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/colorError"
|
||||||
|
tools:text="Error code returned by server: 413"
|
||||||
|
android:textColor="?attr/colorOnError"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_view"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/retry_upload_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/retry"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/upload_error_text_view"
|
||||||
|
app:layout_constraintHorizontal_bias="0.498"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/upload_error_text_view"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_explanation" />
|
||||||
|
|
||||||
|
|
||||||
|
</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"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="@string/media_upload_completed"
|
||||||
|
android:textColor="@android:color/holo_green_light"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:drawableStartCompat="@drawable/cloud_done_24"
|
||||||
|
app:drawableTint="@android:color/holo_green_light"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/postTextInputLayout"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<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_toTopOf="@id/postTextInputLayout">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/post_creation_send_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:enabled="true"
|
||||||
|
android:text="@string/post"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/posting_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>
|
||||||
|
|
||||||
|
<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/postTextInputLayout"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/postTextInputLayout"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/description"
|
||||||
|
app:counterEnabled="true"
|
||||||
|
android:paddingStart="15dp"
|
||||||
|
android:paddingEnd="15dp"
|
||||||
|
app:errorEnabled="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/buttonConstraints"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/new_post_description_input_field"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:ems="10"
|
||||||
|
android:inputType="textMultiLine" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -103,6 +103,7 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
|
||||||
<string name="upload_post_error">Post upload error</string>
|
<string name="upload_post_error">Post upload error</string>
|
||||||
<string name="description">Description…</string>
|
<string name="description">Description…</string>
|
||||||
<string name="post">Post</string>
|
<string name="post">Post</string>
|
||||||
|
<string name="upload_next_step">Upload and go to next step</string>
|
||||||
<string name="add_photo">Add a photo</string>
|
<string name="add_photo">Add a photo</string>
|
||||||
<string name="post_image">One of the images in the post</string>
|
<string name="post_image">One of the images in the post</string>
|
||||||
<string name="switch_to_grid">Switch to grid view</string>
|
<string name="switch_to_grid">Switch to grid view</string>
|
||||||
|
|
Loading…
Reference in New Issue