diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8d057857..915bc2c3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -67,9 +67,6 @@
-
-
- // update UI
- binding.carousel.addData(
- newPhotoData.map {
- CarouselItem(
- it.imageUri, it.imageDescription, it.video,
- it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass,
- it.videoEncodeComplete, it.videoEncodeError,
- )
- }
- )
- }
-
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
- model.uiState.collect { uiState ->
- uiState.userMessage?.let {
- AlertDialog.Builder(binding.root.context).apply {
- setMessage(it)
- setNegativeButton(android.R.string.ok) { _, _ -> }
- }.show()
-
- // Notify the ViewModel the message is displayed
- model.userMessageShown()
- }
- binding.addPhotoButton.isEnabled = uiState.addPhotoButtonEnabled
- binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled
- binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled
- binding.toolbarPostCreation.visibility =
- if (uiState.isCarousel) VISIBLE else INVISIBLE
- binding.carousel.layoutCarousel = uiState.isCarousel
- }
- }
- }
-
- binding.carousel.apply {
- layoutCarouselCallback = { model.becameCarousel(it)}
- maxEntries = instance.albumLimit
- addPhotoButtonCallback = {
- addPhoto()
- }
- updateDescriptionCallback = { position: Int, description: String ->
- model.updateDescription(position, description)
- }
- }
- // get the description and send the post
- binding.postCreationSendButton.setOnClickListener {
- if (validatePost() && model.isNotEmpty()) {
- model.nextStep(binding.root.context)
- }
- }
-
- binding.editPhotoButton.setOnClickListener {
- binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
- edit(currentPosition)
- }
- }
-
- binding.addPhotoButton.setOnClickListener {
- addPhoto()
- }
-
- binding.savePhotoButton.setOnClickListener {
- binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
- savePicture(it, currentPosition)
- }
- }
-
- binding.removePhotoButton.setOnClickListener {
- binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
- model.removeAt(currentPosition)
- model.cancelEncode(currentPosition)
- }
- }
-
- // 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)
- }
-
- onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- 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) { _, _ ->
- finish()
- }
- setNegativeButton(android.R.string.cancel) { _, _ -> }
- show()
- }
- } else {
- finish()
- }
- }
- })
+ binding = ActivityPostCreationBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.postCreationContainer) as NavHostFragment
+ navController = navHostFragment.navController
+ navController.setGraph(R.navigation.post_creation_graph)
}
- private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- if (result.resultCode == Activity.RESULT_OK && result.data?.clipData != null) {
- result.data?.clipData?.let {
- model.setImages(model.addPossibleImages(it))
- }
- } else if (result.resultCode != Activity.RESULT_CANCELED) {
- Toast.makeText(applicationContext, R.string.add_images_error, Toast.LENGTH_SHORT).show()
- }
+ override fun onSupportNavigateUp(): Boolean {
+ return navController.navigateUp() || super.onSupportNavigateUp()
}
- private fun addPhoto(){
- addPhotoResultContract.launch(
- Intent(this, CameraActivity::class.java)
- )
- }
-
- private fun savePicture(button: View, currentPosition: Int) {
- val originalUri = model.getPhotoData().value!![currentPosition].imageUri
-
- val pair = getOutputFile(originalUri)
- val outputStream: OutputStream = pair.first
- val path: String = pair.second
-
- contentResolver.openInputStream(originalUri)!!.use { input ->
- outputStream.use { output ->
- input.copyTo(output)
- }
- }
-
- if(path.startsWith("file")) {
- MediaScannerConnection.scanFile(
- this,
- arrayOf(path.toUri().toFile().absolutePath),
- null
- ) { path, uri ->
- if (uri == null) {
- Log.e(
- "NEW IMAGE SCAN FAILED",
- "Tried to scan $path, but it failed"
- )
- }
- }
- }
- Snackbar.make(
- button, getString(R.string.save_image_success),
- Snackbar.LENGTH_LONG
- ).show()
- }
-
- private fun getOutputFile(uri: Uri): Pair {
- val extension = uri.fileExtension(contentResolver)
-
- val name = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
- .format(System.currentTimeMillis()) + ".$extension"
-
- val outputStream: OutputStream
- val path: String
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- val resolver: ContentResolver = contentResolver
- val type = uri.getMimeType(contentResolver)
- val contentValues = ContentValues()
- contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
- contentValues.put(MediaStore.MediaColumns.MIME_TYPE, type)
- contentValues.put(
- MediaStore.MediaColumns.RELATIVE_PATH,
- Environment.DIRECTORY_PICTURES
- )
- val store =
- if (type.startsWith("video")) MediaStore.Video.Media.EXTERNAL_CONTENT_URI
- else MediaStore.Images.Media.EXTERNAL_CONTENT_URI
- val imageUri: Uri = resolver.insert(store, contentValues)!!
- path = imageUri.toString()
- outputStream = resolver.openOutputStream(imageUri)!!
- } else {
- @Suppress("DEPRECATION") val imagesDir =
- Environment.getExternalStoragePublicDirectory(getString(R.string.app_name))
- imagesDir.mkdir()
- val file = File(imagesDir, name)
- path = Uri.fromFile(file).toString()
- outputStream = file.outputStream()
- }
- return Pair(outputStream, path)
- }
-
-
- private fun validatePost(): Boolean {
- if(model.getPhotoData().value?.all { !it.video || it.videoEncodeComplete } == false){
- AlertDialog.Builder(this).apply {
- setMessage(R.string.still_encoding)
- setNegativeButton(android.R.string.ok) { _, _ -> }
- }.show()
- return false
- }
- return true
- }
-
- private val editResultContract: ActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){
- result: ActivityResult? ->
- if (result?.resultCode == Activity.RESULT_OK && result.data != null) {
- val position: Int = result.data!!.getIntExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_POSITION, 0)
- model.modifyAt(position, result.data!!)
- ?: Toast.makeText(applicationContext, R.string.error_editing, Toast.LENGTH_SHORT).show()
- } else if(result?.resultCode != Activity.RESULT_CANCELED){
- Toast.makeText(applicationContext, R.string.error_editing, Toast.LENGTH_SHORT).show()
- }
- }
-
- private fun edit(position: Int) {
- val intent = Intent(
- this,
- if(model.getPhotoData().value!![position].video) org.pixeldroid.media_editor.photoEdit.VideoEditActivity::class.java else org.pixeldroid.media_editor.photoEdit.PhotoEditActivity::class.java
- )
- .putExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_URI, model.getPhotoData().value!![position].imageUri)
- .putExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_POSITION, position)
-
- editResultContract.launch(intent)
-
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt
new file mode 100644
index 00000000..e0d722b8
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt
@@ -0,0 +1,311 @@
+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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.net.toFile
+import androidx.core.net.toUri
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.coroutines.launch
+import org.pixeldroid.app.R
+import org.pixeldroid.app.databinding.FragmentPostCreationBinding
+import org.pixeldroid.app.postCreation.camera.CameraActivity
+import org.pixeldroid.app.postCreation.carousel.CarouselItem
+import org.pixeldroid.app.utils.BaseFragment
+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.media_editor.photoEdit.PhotoEditActivity
+import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
+import java.io.File
+import java.io.OutputStream
+import java.text.SimpleDateFormat
+import java.util.*
+
+
+class PostCreationFragment : BaseFragment() {
+
+ private var user: UserDatabaseEntity? = null
+ private var instance: InstanceDatabaseEntity = InstanceDatabaseEntity("", "")
+
+ private lateinit var binding: FragmentPostCreationBinding
+ private lateinit var model: PostCreationViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ super.onCreateView(inflater, container, savedInstanceState)
+
+ // Inflate the layout for this fragment
+ binding = FragmentPostCreationBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ user = db.userDao().getActiveUser()
+
+ instance = user?.run {
+ db.instanceDao().getAll().first { instanceDatabaseEntity ->
+ instanceDatabaseEntity.uri.contains(instance_uri)
+ }
+ } ?: InstanceDatabaseEntity("", "")
+
+ val _model: PostCreationViewModel by activityViewModels {
+ PostCreationViewModelFactory(
+ requireActivity().application,
+ requireActivity().intent.clipData!!,
+ instance,
+ requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
+ requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false)
+ )
+ }
+ model = _model
+
+ model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData ->
+ // update UI
+ binding.carousel.addData(
+ newPhotoData.map {
+ CarouselItem(
+ it.imageUri, it.imageDescription, it.video,
+ it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass,
+ it.videoEncodeComplete, it.videoEncodeError,
+ )
+ }
+ )
+ }
+
+ lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ model.uiState.collect { uiState ->
+ uiState.userMessage?.let {
+ AlertDialog.Builder(binding.root.context).apply {
+ setMessage(it)
+ setNegativeButton(android.R.string.ok) { _, _ -> }
+ }.show()
+
+ // Notify the ViewModel the message is displayed
+ model.userMessageShown()
+ }
+ binding.addPhotoButton.isEnabled = uiState.addPhotoButtonEnabled
+ binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled
+ binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled
+ binding.toolbarPostCreation.visibility =
+ if (uiState.isCarousel) View.VISIBLE else View.INVISIBLE
+ binding.carousel.layoutCarousel = uiState.isCarousel
+ }
+ }
+ }
+
+ binding.carousel.apply {
+ layoutCarouselCallback = { model.becameCarousel(it)}
+ maxEntries = instance.albumLimit
+ addPhotoButtonCallback = {
+ addPhoto()
+ }
+ updateDescriptionCallback = { position: Int, description: String ->
+ model.updateDescription(position, description)
+ }
+ }
+ // get the description and send the post
+ binding.postCreationSendButton.setOnClickListener {
+ if (validatePost() && model.isNotEmpty()) {
+ findNavController().navigate(R.id.action_postCreationFragment_to_postSubmissionFragment)
+ }
+ }
+
+ binding.editPhotoButton.setOnClickListener {
+ binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
+ edit(currentPosition)
+ }
+ }
+
+ binding.addPhotoButton.setOnClickListener {
+ addPhoto()
+ }
+
+ binding.savePhotoButton.setOnClickListener {
+ binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
+ savePicture(it, currentPosition)
+ }
+ }
+
+ binding.removePhotoButton.setOnClickListener {
+ binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
+ model.removeAt(currentPosition)
+ model.cancelEncode(currentPosition)
+ }
+ }
+
+ // Clean up temporary files, if any
+ val tempFiles = requireActivity().intent.getStringArrayExtra(PostCreationActivity.TEMP_FILES)
+ tempFiles?.asList()?.forEach {
+ val file = File(binding.root.context.cacheDir, it)
+ model.trackTempFile(file)
+ }
+
+ // Handle back pressed button
+ requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ val redraft = requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_REDRAFT, false)
+ if (redraft) {
+ val builder = AlertDialog.Builder(binding.root.context)
+ builder.apply {
+ setMessage(R.string.redraft_dialog_cancel)
+ setPositiveButton(android.R.string.ok) { _, _ ->
+ requireActivity().finish()
+ }
+ setNegativeButton(android.R.string.cancel) { _, _ -> }
+ show()
+ }
+ } else {
+ requireActivity().finish()
+ }
+ }
+ })
+ }
+
+ private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == Activity.RESULT_OK && result.data?.clipData != null) {
+ result.data?.clipData?.let {
+ model.setImages(model.addPossibleImages(it))
+ }
+ } else if (result.resultCode != Activity.RESULT_CANCELED) {
+ Toast.makeText(requireActivity(), R.string.add_images_error, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ private fun addPhoto(){
+ addPhotoResultContract.launch(
+ Intent(requireActivity(), CameraActivity::class.java)
+ )
+ }
+
+ private fun savePicture(button: View, currentPosition: Int) {
+ val originalUri = model.getPhotoData().value!![currentPosition].imageUri
+
+ val pair = getOutputFile(originalUri)
+ val outputStream: OutputStream = pair.first
+ val path: String = pair.second
+
+ requireActivity().contentResolver.openInputStream(originalUri)!!.use { input ->
+ outputStream.use { output ->
+ input.copyTo(output)
+ }
+ }
+
+ if(path.startsWith("file")) {
+ MediaScannerConnection.scanFile(
+ requireActivity(),
+ arrayOf(path.toUri().toFile().absolutePath),
+ null
+ ) { path, uri ->
+ if (uri == null) {
+ Log.e(
+ "NEW IMAGE SCAN FAILED",
+ "Tried to scan $path, but it failed"
+ )
+ }
+ }
+ }
+ Snackbar.make(
+ button, getString(R.string.save_image_success),
+ Snackbar.LENGTH_LONG
+ ).show()
+ }
+
+ private fun getOutputFile(uri: Uri): Pair {
+ val extension = uri.fileExtension(requireActivity().contentResolver)
+
+ val name = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
+ .format(System.currentTimeMillis()) + ".$extension"
+
+ val outputStream: OutputStream
+ val path: String
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val resolver: ContentResolver = requireActivity().contentResolver
+ val type = uri.getMimeType(requireActivity().contentResolver)
+ val contentValues = ContentValues()
+ contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
+ contentValues.put(MediaStore.MediaColumns.MIME_TYPE, type)
+ contentValues.put(
+ MediaStore.MediaColumns.RELATIVE_PATH,
+ Environment.DIRECTORY_PICTURES
+ )
+ val store =
+ if (type.startsWith("video")) MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+ else MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+ val imageUri: Uri = resolver.insert(store, contentValues)!!
+ path = imageUri.toString()
+ outputStream = resolver.openOutputStream(imageUri)!!
+ } else {
+ @Suppress("DEPRECATION") val imagesDir =
+ Environment.getExternalStoragePublicDirectory(getString(R.string.app_name))
+ imagesDir.mkdir()
+ val file = File(imagesDir, name)
+ path = Uri.fromFile(file).toString()
+ outputStream = file.outputStream()
+ }
+ return Pair(outputStream, path)
+ }
+
+
+ private fun validatePost(): Boolean {
+ if (model.getPhotoData().value?.all { !it.video || it.videoEncodeComplete } == false) {
+ AlertDialog.Builder(requireActivity()).apply {
+ setMessage(R.string.still_encoding)
+ setNegativeButton(android.R.string.ok) { _, _ -> }
+ }.show()
+ return false
+ }
+ return true
+ }
+
+ private val editResultContract: ActivityResultLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()){
+ result: ActivityResult? ->
+ if (result?.resultCode == Activity.RESULT_OK && result.data != null) {
+ val position: Int = result.data!!.getIntExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_POSITION, 0)
+ model.modifyAt(position, result.data!!)
+ ?: Toast.makeText(requireActivity(), R.string.error_editing, Toast.LENGTH_SHORT).show()
+ } else if(result?.resultCode != Activity.RESULT_CANCELED){
+ Toast.makeText(requireActivity(), R.string.error_editing, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ private fun edit(position: Int) {
+ val intent = Intent(
+ requireActivity(),
+ if (model.getPhotoData().value!![position].video) VideoEditActivity::class.java else PhotoEditActivity::class.java
+ )
+ .putExtra(PhotoEditActivity.PICTURE_URI, model.getPhotoData().value!![position].imageUri)
+ .putExtra(PhotoEditActivity.PICTURE_POSITION, position)
+
+ editResultContract.launch(intent)
+ }
+}
+
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
index dcb5c6a7..4f9c214f 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
@@ -2,14 +2,16 @@ package org.pixeldroid.app.postCreation
import android.app.Application
import android.content.ClipData
-import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Parcelable
import android.provider.OpenableColumns
-import androidx.core.content.ContextCompat
+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.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@@ -17,18 +19,32 @@ 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.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
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
+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.collections.ArrayList
import kotlin.collections.MutableList
@@ -57,7 +73,19 @@ data class PostCreationActivityUiState(
val isCarousel: Boolean = true,
+ 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,
)
@Parcelize
@@ -92,7 +120,10 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
PreferenceManager.getDefaultSharedPreferences(application)
val initialDescription = sharedPreferences.getString("prefill_description", "") ?: ""
- _uiState = MutableStateFlow(PostCreationActivityUiState(newPostDescriptionText = existingDescription ?: initialDescription))
+ _uiState = MutableStateFlow(PostCreationActivityUiState(
+ newPostDescriptionText = existingDescription ?: initialDescription,
+ nsfw = existingNSFW
+ ))
}
val uiState: StateFlow = _uiState
@@ -169,7 +200,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
val type = uri.getMimeType(getApplication().contentResolver)
val isVideo = type.startsWith("video/")
- if(isVideo && !instance!!.videoEnabled){
+ if (isVideo && !instance!!.videoEnabled) {
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = getApplication().getString(R.string.video_not_supported))
}
@@ -203,17 +234,6 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
photoData.value = photoData.value
}
- /**
- * Next step
- */
- fun nextStep(context: Context) {
- val intent = Intent(context, PostSubmissionActivity::class.java)
- intent.putExtra(PostSubmissionActivity.PHOTO_DATA, getPhotoData().value?.let { ArrayList(it) })
- intent.putExtra(PostSubmissionActivity.PICTURE_DESCRIPTION, existingDescription)
- intent.putExtra(PostSubmissionActivity.POST_NSFW, existingNSFW)
- ContextCompat.startActivity(context, intent, null)
- }
-
fun modifyAt(position: Int, data: Intent): Unit? {
val result: PhotoData = photoData.value?.getOrNull(position)?.run {
if (video) {
@@ -262,7 +282,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
private fun videoEncodeProgress(originalUri: Uri, progress: Int, firstPass: Boolean, outputVideoPath: Uri?, error: Boolean){
photoData.value?.indexOfFirst { it.imageUri == originalUri }?.let { position ->
- if(outputVideoPath != null){
+ if (outputVideoPath != null) {
// If outputVideoPath is not null, it means the video is done and we can change Uris
val (size, _) = getSizeAndVideoValidate(outputVideoPath, position)
@@ -310,7 +330,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
}
}
- fun registerNewFFmpegSession(position: Uri, sessionId: Long) {
+ private fun registerNewFFmpegSession(position: Uri, sessionId: Long) {
sessionMap[position] = sessionId
}
@@ -321,6 +341,209 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
)
}
}
+
+ 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().contentResolver)
+
+ val strippedImage = File.createTempFile("temp_img", ".$extension", getApplication().cacheDir)
+
+ val imageUri = data.imageUri
+
+ val (strippedOrNot, size) = try {
+ val orientation = ExifInterface(getApplication().contentResolver.openInputStream(imageUri)!!).getAttributeInt(
+ ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
+
+ stripMetadata(imageUri, strippedImage, getApplication().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().contentResolver.openInputStream(imageUri)!!
+ } catch (e: FileNotFoundException){
+ _uiState.update { currentUiState ->
+ currentUiState.copy(
+ userMessage = getApplication().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().getString(R.string.file_not_found,
+ data.imageUri)
+ )
+ }
+ return
+ }
+
+ val type = data.imageUri.getMimeType(getApplication().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().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().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().startActivity(intent)
+ } catch (exception: IOException) {
+ Toast.makeText(getApplication(), getApplication().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().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
+ )
+ }
+ } finally {
+ apiHolder.api = null
+ }
+ }
+ }
+
+ fun newPostDescriptionChanged(text: Editable?) {
+ _uiState.update { it.copy(newPostDescriptionText = text.toString()) }
+ }
+
+ fun updateNSFW(checked: Boolean) { _uiState.update { it.copy(nsfw = checked) } }
+
+ fun chooseAccount(which: UserDatabaseEntity) {
+ _uiState.update { it.copy(chosenAccount = which) }
+ }
}
class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity, val existingDescription: String?, val existingNSFW: Boolean) : ViewModelProvider.Factory {
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionActivity.kt
deleted file mode 100644
index 76ce02f3..00000000
--- a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionActivity.kt
+++ /dev/null
@@ -1,193 +0,0 @@
-package org.pixeldroid.app.postCreation
-
-import android.app.AlertDialog
-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 androidx.activity.viewModels
-import androidx.core.widget.doAfterTextChanged
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import kotlinx.coroutines.launch
-import org.pixeldroid.app.R
-import org.pixeldroid.app.databinding.ActivityPostSubmissionBinding
-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.setSquareImageFromURL
-import java.io.File
-
-
-class PostSubmissionActivity : BaseThemedWithoutBarActivity() {
-
- companion object {
- internal const val PICTURE_DESCRIPTION = "picture_description"
- internal const val PHOTO_DATA = "photo_data"
- internal const val POST_NSFW = "post_nsfw"
-}
-
- private lateinit var accounts: List
- private var selectedAccount: Int = -1
- private lateinit var menu: Menu
- 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)
- supportActionBar?.setTitle(R.string.add_details)
-
- user = db.userDao().getActiveUser()
- accounts = db.userDao().getAll()
-
- instance = user?.run {
- db.instanceDao().getAll().first { instanceDatabaseEntity ->
- instanceDatabaseEntity.uri.contains(instance_uri)
- }
- } ?: InstanceDatabaseEntity("", "")
-
- val photoData = intent.getParcelableArrayListExtra(PHOTO_DATA) as ArrayList?
-
- val _model: PostSubmissionViewModel by viewModels {
- PostSubmissionViewModelFactory(
- application,
- photoData!!,
- intent.getStringExtra(PICTURE_DESCRIPTION)
- )
- }
- model = _model
-
- val sensitive = intent.getBooleanExtra(POST_NSFW, false)
- model.updateNSFW(sensitive)
- binding.nsfwSwitch.isChecked = sensitive
-
- 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
-
- selectedAccount = accounts.indexOf(uiState.chosenAccount)
-
- binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
- }
- }
- }
- binding.newPostDescriptionInputField.doAfterTextChanged {
- model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text)
- }
-
- binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked ->
- model.updateNSFW(isChecked)
- }
-
- 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
-
- setSquareImageFromURL(View(applicationContext), photoData!![0].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 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
- }
- }
- return super.onOptionsItemSelected(item)
- }
-
- 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
- }
-
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt
new file mode 100644
index 00000000..371f41e3
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt
@@ -0,0 +1,194 @@
+package org.pixeldroid.app.postCreation
+
+import android.app.AlertDialog
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.OnBackPressedCallback
+import androidx.core.view.MenuProvider
+import androidx.core.widget.doAfterTextChanged
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.fragment.findNavController
+import androidx.navigation.ui.setupWithNavController
+import kotlinx.coroutines.launch
+import org.pixeldroid.app.R
+import org.pixeldroid.app.databinding.FragmentPostSubmissionBinding
+import org.pixeldroid.app.utils.BaseFragment
+import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
+import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
+import org.pixeldroid.app.utils.setSquareImageFromURL
+
+
+class PostSubmissionFragment : BaseFragment() {
+
+ private lateinit var accounts: List
+ private var selectedAccount: Int = -1
+// private lateinit var menu: Menu
+
+ private var user: UserDatabaseEntity? = null
+ private lateinit var instance: InstanceDatabaseEntity
+
+ private lateinit var binding: FragmentPostSubmissionBinding
+ private lateinit var model: PostCreationViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ super.onCreateView(inflater, container, savedInstanceState)
+
+ // Inflate the layout for this fragment
+ binding = FragmentPostSubmissionBinding.inflate(layoutInflater)
+// setHasOptionsMenu(true)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.topBar.setupWithNavController(findNavController())
+
+ user = db.userDao().getActiveUser()
+ accounts = db.userDao().getAll()
+
+ instance = user?.run {
+ db.instanceDao().getAll().first { instanceDatabaseEntity ->
+ instanceDatabaseEntity.uri.contains(instance_uri)
+ }
+ } ?: InstanceDatabaseEntity("", "")
+
+ val _model: PostCreationViewModel by activityViewModels {
+ PostCreationViewModelFactory(
+ requireActivity().application,
+ requireActivity().intent.clipData!!,
+ instance,
+ requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
+ requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false)
+ )
+ }
+ model = _model
+
+ // Display the values from the view model
+ binding.nsfwSwitch.isChecked = model.uiState.value.nsfw
+ binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText)
+
+ lifecycleScope.launch {
+ viewLifecycleOwner.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) View.VISIBLE else View.INVISIBLE
+ binding.uploadProgressBar.progress = uiState.uploadProgress
+ binding.uploadCompletedTextview.visibility =
+ if (uiState.uploadCompletedTextviewVisible) View.VISIBLE else View.INVISIBLE
+ binding.uploadError.visibility =
+ if (uiState.uploadErrorVisible) View.VISIBLE else View.INVISIBLE
+ binding.uploadErrorTextExplanation.visibility =
+ if (uiState.uploadErrorExplanationVisible) View.VISIBLE else View.INVISIBLE
+
+ selectedAccount = accounts.indexOf(uiState.chosenAccount)
+
+ binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
+ }
+ }
+ }
+
+ binding.newPostDescriptionInputField.doAfterTextChanged {
+ model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text)
+ }
+
+ binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked ->
+ model.updateNSFW(isChecked)
+ }
+
+ binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
+
+ setSquareImageFromURL(View(requireActivity()), model.getPhotoData()!!.value?.get(0)?.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()
+ }
+
+ // Handle back pressed button
+ requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ findNavController().navigate(R.id.action_postSubmissionFragment_to_postCreationFragment)
+ }
+ })
+
+ binding.topBar.addMenuProvider(object: MenuProvider {
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ // Add menu items here
+ menuInflater.inflate(R.menu.post_submission_account_menu, menu)
+ }
+
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ // Handle the menu selection
+ return when (menuItem.itemId) {
+ R.id.action_switch_accounts -> {
+ AlertDialog.Builder(requireActivity()).apply {
+ setIcon(R.drawable.switch_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 -> false
+ }
+ }
+ }, viewLifecycleOwner, Lifecycle.State.RESUMED)
+ }
+
+ 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 = View.GONE
+ binding.postCreationSendButton.visibility = View.VISIBLE
+ } else {
+ binding.postingProgressBar.visibility = View.VISIBLE
+ binding.postCreationSendButton.visibility = View.GONE
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt
index 51bf07b0..715c4ea1 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt
@@ -81,6 +81,7 @@ class CameraFragment : BaseFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
+ super.onCreateView(inflater, container, savedInstanceState)
inActivity = arguments?.getBoolean("CameraActivity") ?: false
binding = FragmentCameraBinding.inflate(layoutInflater)
diff --git a/app/src/main/res/drawable/switch_account.xml b/app/src/main/res/drawable/switch_account.xml
new file mode 100644
index 00000000..ace366b2
--- /dev/null
+++ b/app/src/main/res/drawable/switch_account.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_post_creation.xml b/app/src/main/res/layout/activity_post_creation.xml
index 57d5def9..98f621cc 100644
--- a/app/src/main/res/layout/activity_post_creation.xml
+++ b/app/src/main/res/layout/activity_post_creation.xml
@@ -6,96 +6,16 @@
android:layout_height="match_parent"
tools:context=".postCreation.PostCreationActivity">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:navGraph="@navigation/post_creation_graph" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_post_creation.xml b/app/src/main/res/layout/fragment_post_creation.xml
new file mode 100644
index 00000000..94317989
--- /dev/null
+++ b/app/src/main/res/layout/fragment_post_creation.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_post_submission.xml b/app/src/main/res/layout/fragment_post_submission.xml
similarity index 89%
rename from app/src/main/res/layout/activity_post_submission.xml
rename to app/src/main/res/layout/fragment_post_submission.xml
index dec6867b..47044b6b 100644
--- a/app/src/main/res/layout/activity_post_submission.xml
+++ b/app/src/main/res/layout/fragment_post_submission.xml
@@ -4,7 +4,19 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".postCreation.PostSubmissionActivity">
+ tools:context=".postCreation.PostSubmissionFragment"
+ android:id="@+id/postSubmissionFragment" >
+
+
+ app:layout_constraintTop_toBottomOf="@id/top_bar"/>
+ app:layout_constraintTop_toTopOf="parent" />
-
-
diff --git a/app/src/main/res/menu/post_submission_account_menu.xml b/app/src/main/res/menu/post_submission_account_menu.xml
index 4841e6bb..8b5c395d 100644
--- a/app/src/main/res/menu/post_submission_account_menu.xml
+++ b/app/src/main/res/menu/post_submission_account_menu.xml
@@ -6,6 +6,6 @@
android:id="@+id/action_switch_accounts"
android:orderInCategory="100"
android:title="@string/switch_accounts"
- android:icon="@drawable/material_drawer_ico_account"
+ android:icon="@drawable/switch_account"
app:showAsAction="ifRoom"/>
\ No newline at end of file
diff --git a/app/src/main/res/navigation/post_creation_graph.xml b/app/src/main/res/navigation/post_creation_graph.xml
new file mode 100644
index 00000000..0ae88742
--- /dev/null
+++ b/app/src/main/res/navigation/post_creation_graph.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file