Merge branch 'fix_redraft' into 'master'
Adapt redraft to new PostSubmissionActivity See merge request pixeldroid/PixelDroid!498
This commit is contained in:
commit
552b85c59d
@ -56,6 +56,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".postCreation.PostCreationActivity"
|
android:name=".postCreation.PostCreationActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:theme="@style/AppTheme.NoActionBar">
|
android:theme="@style/AppTheme.NoActionBar">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
@ -67,9 +68,6 @@
|
|||||||
<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"
|
||||||
|
@ -1,44 +1,13 @@
|
|||||||
package org.pixeldroid.app.postCreation
|
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.os.*
|
||||||
import android.provider.MediaStore
|
import androidx.navigation.NavController
|
||||||
import android.util.Log
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import android.view.View
|
|
||||||
import android.view.View.INVISIBLE
|
|
||||||
import android.view.View.VISIBLE
|
|
||||||
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.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.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.carousel.CarouselItem
|
|
||||||
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
|
||||||
import org.pixeldroid.app.utils.fileExtension
|
|
||||||
import org.pixeldroid.app.utils.getMimeType
|
|
||||||
import java.io.File
|
|
||||||
import java.io.OutputStream
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
const val TAG = "Post Creation Activity"
|
const val TAG = "Post Creation Activity"
|
||||||
|
|
||||||
@ -46,8 +15,9 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal const val PICTURE_DESCRIPTION = "picture_description"
|
internal const val PICTURE_DESCRIPTION = "picture_description"
|
||||||
internal const val TEMP_FILES = "temp_files"
|
|
||||||
internal const val POST_REDRAFT = "post_redraft"
|
internal const val POST_REDRAFT = "post_redraft"
|
||||||
|
internal const val POST_NSFW = "post_nsfw"
|
||||||
|
internal const val TEMP_FILES = "temp_files"
|
||||||
}
|
}
|
||||||
|
|
||||||
private var user: UserDatabaseEntity? = null
|
private var user: UserDatabaseEntity? = null
|
||||||
@ -55,13 +25,10 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
|||||||
|
|
||||||
private lateinit var binding: ActivityPostCreationBinding
|
private lateinit var binding: ActivityPostCreationBinding
|
||||||
|
|
||||||
private lateinit var model: PostCreationViewModel
|
private lateinit var navController: NavController
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityPostCreationBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
user = db.userDao().getActiveUser()
|
user = db.userDao().getActiveUser()
|
||||||
|
|
||||||
@ -71,231 +38,16 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
|||||||
}
|
}
|
||||||
} ?: InstanceDatabaseEntity("", "")
|
} ?: InstanceDatabaseEntity("", "")
|
||||||
|
|
||||||
val _model: PostCreationViewModel by viewModels {
|
binding = ActivityPostCreationBinding.inflate(layoutInflater)
|
||||||
PostCreationViewModelFactory(
|
setContentView(binding.root)
|
||||||
application,
|
val navHostFragment =
|
||||||
intent.clipData!!,
|
supportFragmentManager.findFragmentById(R.id.postCreationContainer) as NavHostFragment
|
||||||
instance
|
navController = navHostFragment.navController
|
||||||
)
|
navController.setGraph(R.navigation.post_creation_graph)
|
||||||
}
|
|
||||||
model = _model
|
|
||||||
|
|
||||||
model.getPhotoData().observe(this) { 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 {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
val redraft = intent.getBooleanExtra(POST_REDRAFT, false)
|
return navController.navigateUp() || super.onSupportNavigateUp()
|
||||||
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 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<OutputStream, String> {
|
|
||||||
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<Intent> = 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)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -0,0 +1,310 @@
|
|||||||
|
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
|
||||||
|
) { tried_path, uri ->
|
||||||
|
if (uri == null) {
|
||||||
|
Log.e(
|
||||||
|
"NEW IMAGE SCAN FAILED",
|
||||||
|
"Tried to scan $tried_path, but it failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Snackbar.make(
|
||||||
|
button, getString(R.string.save_image_success),
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOutputFile(uri: Uri): Pair<OutputStream, String> {
|
||||||
|
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 {
|
||||||
|
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<Intent> = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()){
|
||||||
|
result: ActivityResult? ->
|
||||||
|
if (result?.resultCode == Activity.RESULT_OK && result.data != null) {
|
||||||
|
val position: Int = result.data!!.getIntExtra(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,14 +2,16 @@ 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.os.Parcelable
|
||||||
import android.provider.OpenableColumns
|
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.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
@ -17,18 +19,32 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.preference.PreferenceManager
|
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.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 kotlinx.parcelize.Parcelize
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import org.pixeldroid.app.MainActivity
|
||||||
import org.pixeldroid.app.R
|
import org.pixeldroid.app.R
|
||||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
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.InstanceDatabaseEntity
|
||||||
|
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||||
|
import org.pixeldroid.app.utils.fileExtension
|
||||||
import org.pixeldroid.app.utils.getMimeType
|
import org.pixeldroid.app.utils.getMimeType
|
||||||
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
|
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
|
||||||
|
import retrofit2.HttpException
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.URI
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.collections.MutableList
|
import kotlin.collections.MutableList
|
||||||
@ -57,7 +73,19 @@ data class PostCreationActivityUiState(
|
|||||||
|
|
||||||
val isCarousel: Boolean = true,
|
val isCarousel: Boolean = true,
|
||||||
|
|
||||||
|
val postCreationSendButtonEnabled: Boolean = true,
|
||||||
|
|
||||||
val newPostDescriptionText: String = "",
|
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
|
@Parcelize
|
||||||
@ -74,7 +102,13 @@ data class PhotoData(
|
|||||||
var videoEncodeError: Boolean = false,
|
var videoEncodeError: Boolean = false,
|
||||||
) : Parcelable
|
) : 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,
|
||||||
|
existingDescription: String? = null,
|
||||||
|
existingNSFW: Boolean = false
|
||||||
|
) : 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()) }
|
||||||
@ -90,9 +124,12 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
|||||||
(application as PixelDroidApplication).getAppComponent().inject(this)
|
(application as PixelDroidApplication).getAppComponent().inject(this)
|
||||||
val sharedPreferences =
|
val sharedPreferences =
|
||||||
PreferenceManager.getDefaultSharedPreferences(application)
|
PreferenceManager.getDefaultSharedPreferences(application)
|
||||||
val initialDescription = sharedPreferences.getString("prefill_description", "") ?: ""
|
val templateDescription = sharedPreferences.getString("prefill_description", "") ?: ""
|
||||||
|
|
||||||
_uiState = MutableStateFlow(PostCreationActivityUiState(newPostDescriptionText = initialDescription))
|
_uiState = MutableStateFlow(PostCreationActivityUiState(
|
||||||
|
newPostDescriptionText = existingDescription ?: templateDescription,
|
||||||
|
nsfw = existingNSFW
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
val uiState: StateFlow<PostCreationActivityUiState> = _uiState
|
val uiState: StateFlow<PostCreationActivityUiState> = _uiState
|
||||||
@ -169,7 +206,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
|||||||
val type = uri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
|
val type = uri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
|
||||||
val isVideo = type.startsWith("video/")
|
val isVideo = type.startsWith("video/")
|
||||||
|
|
||||||
if(isVideo && !instance!!.videoEnabled){
|
if (isVideo && !instance!!.videoEnabled) {
|
||||||
_uiState.update { currentUiState ->
|
_uiState.update { currentUiState ->
|
||||||
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.video_not_supported))
|
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.video_not_supported))
|
||||||
}
|
}
|
||||||
@ -203,15 +240,6 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
|||||||
photoData.value = photoData.value
|
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) })
|
|
||||||
ContextCompat.startActivity(context, intent, 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) {
|
||||||
@ -260,7 +288,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
|||||||
private fun videoEncodeProgress(originalUri: Uri, progress: Int, firstPass: Boolean, outputVideoPath: Uri?, error: Boolean){
|
private fun videoEncodeProgress(originalUri: Uri, progress: Int, firstPass: Boolean, outputVideoPath: Uri?, error: Boolean){
|
||||||
photoData.value?.indexOfFirst { it.imageUri == originalUri }?.let { position ->
|
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
|
// If outputVideoPath is not null, it means the video is done and we can change Uris
|
||||||
val (size, _) = getSizeAndVideoValidate(outputVideoPath, position)
|
val (size, _) = getSizeAndVideoValidate(outputVideoPath, position)
|
||||||
|
|
||||||
@ -308,7 +336,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun registerNewFFmpegSession(position: Uri, sessionId: Long) {
|
private fun registerNewFFmpegSession(position: Uri, sessionId: Long) {
|
||||||
sessionMap[position] = sessionId
|
sessionMap[position] = sessionId
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,10 +347,213 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity) : ViewModelProvider.Factory {
|
fun resetUploadStatus() {
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
photoData.value = photoData.value?.map { it.copy(uploadId = null, progress = null) }?.toMutableList()
|
||||||
return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java).newInstance(application, clipdata, instance)
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads the images that are in the [photoData] array.
|
||||||
|
* Keeps track of them in the [PhotoData.progress] (for the upload progress), and the
|
||||||
|
* [PhotoData.uploadId] (for the list of ids of the uploads).
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalUnsignedTypes::class)
|
||||||
|
fun upload() {
|
||||||
|
_uiState.update { currentUiState ->
|
||||||
|
currentUiState.copy(
|
||||||
|
postCreationSendButtonEnabled = false,
|
||||||
|
uploadCompletedTextviewVisible = false,
|
||||||
|
uploadErrorVisible = false,
|
||||||
|
uploadProgressBarVisible = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (data: PhotoData in getPhotoData().value ?: emptyList()) {
|
||||||
|
val extension = data.imageUri.fileExtension(getApplication<PixelDroidApplication>().contentResolver)
|
||||||
|
|
||||||
|
val strippedImage = File.createTempFile("temp_img", ".$extension", getApplication<PixelDroidApplication>().cacheDir)
|
||||||
|
|
||||||
|
val imageUri = data.imageUri
|
||||||
|
|
||||||
|
val (strippedOrNot, size) = try {
|
||||||
|
val orientation = ExifInterface(getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!).getAttributeInt(
|
||||||
|
ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
||||||
|
|
||||||
|
stripMetadata(imageUri, strippedImage, getApplication<PixelDroidApplication>().contentResolver)
|
||||||
|
|
||||||
|
// Restore EXIF orientation
|
||||||
|
val exifInterface = ExifInterface(strippedImage)
|
||||||
|
exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, orientation.toString())
|
||||||
|
exifInterface.saveAttributes()
|
||||||
|
|
||||||
|
Pair(strippedImage.inputStream(), strippedImage.length())
|
||||||
|
} catch (e: UnsupportedFileFormatException){
|
||||||
|
strippedImage.delete()
|
||||||
|
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
|
||||||
|
val imageInputStream = try {
|
||||||
|
getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!
|
||||||
|
} catch (e: FileNotFoundException){
|
||||||
|
_uiState.update { currentUiState ->
|
||||||
|
currentUiState.copy(
|
||||||
|
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
|
||||||
|
data.imageUri)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Pair(imageInputStream, data.size)
|
||||||
|
} catch (e: IOException){
|
||||||
|
strippedImage.delete()
|
||||||
|
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
|
||||||
|
_uiState.update { currentUiState ->
|
||||||
|
currentUiState.copy(
|
||||||
|
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
|
||||||
|
data.imageUri)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val type = data.imageUri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
|
||||||
|
val imagePart = ProgressRequestBody(strippedOrNot, size, type)
|
||||||
|
val requestBody = MultipartBody.Builder()
|
||||||
|
.setType(MultipartBody.FORM)
|
||||||
|
.addFormDataPart("file", System.currentTimeMillis().toString(), imagePart)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val sub = imagePart.progressSubject
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe { percentage ->
|
||||||
|
data.progress = percentage.toInt()
|
||||||
|
_uiState.update { currentUiState ->
|
||||||
|
currentUiState.copy(
|
||||||
|
uploadProgress = getPhotoData().value!!.sumOf { it.progress ?: 0 } / getPhotoData().value!!.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var postSub: Disposable? = null
|
||||||
|
|
||||||
|
val description = data.imageDescription?.let { MultipartBody.Part.createFormData("description", it) }
|
||||||
|
|
||||||
|
// Ugly temporary account switching, but it works well enough for now
|
||||||
|
val api = uiState.value.chosenAccount?.let {
|
||||||
|
apiHolder.setToCurrentUser(it)
|
||||||
|
} ?: apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||||
|
|
||||||
|
val inter = api.mediaUpload(description, requestBody.parts[0])
|
||||||
|
|
||||||
|
apiHolder.api = null
|
||||||
|
postSub = inter
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ attachment: Attachment ->
|
||||||
|
data.progress = 0
|
||||||
|
data.uploadId = attachment.id!!
|
||||||
|
},
|
||||||
|
{ e: Throwable ->
|
||||||
|
_uiState.update { currentUiState ->
|
||||||
|
currentUiState.copy(
|
||||||
|
uploadErrorVisible = true,
|
||||||
|
uploadErrorExplanationText = if(e is HttpException){
|
||||||
|
getApplication<PixelDroidApplication>().getString(R.string.upload_error, e.code())
|
||||||
|
} else "",
|
||||||
|
uploadErrorExplanationVisible = e is HttpException,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
strippedImage.delete()
|
||||||
|
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
|
||||||
|
e.printStackTrace()
|
||||||
|
postSub?.dispose()
|
||||||
|
sub.dispose()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
strippedImage.delete()
|
||||||
|
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
|
||||||
|
data.progress = 100
|
||||||
|
if (getPhotoData().value!!.all { it.progress == 100 && it.uploadId != null }) {
|
||||||
|
_uiState.update { currentUiState ->
|
||||||
|
currentUiState.copy(
|
||||||
|
uploadProgressBarVisible = false,
|
||||||
|
uploadCompletedTextviewVisible = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
post()
|
||||||
|
}
|
||||||
|
postSub?.dispose()
|
||||||
|
sub.dispose()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun post() {
|
||||||
|
val description = uiState.value.newPostDescriptionText
|
||||||
|
|
||||||
|
// TODO: investigate why this works but booleans don't
|
||||||
|
val nsfw = if (uiState.value.nsfw) 1 else 0
|
||||||
|
|
||||||
|
_uiState.update { currentUiState ->
|
||||||
|
currentUiState.copy(
|
||||||
|
postCreationSendButtonEnabled = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
//Ugly temporary account switching, but it works well enough for now
|
||||||
|
val api = uiState.value.chosenAccount?.let {
|
||||||
|
apiHolder.setToCurrentUser(it)
|
||||||
|
} ?: apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||||
|
|
||||||
|
api.postStatus(
|
||||||
|
statusText = description,
|
||||||
|
media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList(),
|
||||||
|
sensitive = nsfw
|
||||||
|
)
|
||||||
|
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_success),
|
||||||
|
Toast.LENGTH_SHORT).show()
|
||||||
|
val intent = Intent(getApplication(), MainActivity::class.java)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
//TODO make the activity launch this instead (and surrounding toasts too)
|
||||||
|
getApplication<PixelDroidApplication>().startActivity(intent)
|
||||||
|
} catch (exception: 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} 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 {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java, String::class.java, Boolean::class.java).newInstance(application, clipdata, instance, existingDescription, existingNSFW)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,188 +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"
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var accounts: List<UserDatabaseEntity>
|
|
||||||
private var selectedAccount: Int = -1
|
|
||||||
private lateinit var menu: Menu
|
|
||||||
private var user: UserDatabaseEntity? = null
|
|
||||||
private lateinit var instance: InstanceDatabaseEntity
|
|
||||||
|
|
||||||
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<PhotoData>(PHOTO_DATA) as ArrayList<PhotoData>?
|
|
||||||
|
|
||||||
val _model: PostSubmissionViewModel by viewModels {
|
|
||||||
PostSubmissionViewModelFactory(
|
|
||||||
application,
|
|
||||||
photoData!!
|
|
||||||
)
|
|
||||||
}
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,192 @@
|
|||||||
|
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<UserDatabaseEntity>
|
||||||
|
private var selectedAccount: Int = -1
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -54,7 +54,7 @@ data class PostSubmissionActivityUiState(
|
|||||||
val uploadErrorExplanationVisible: Boolean = false,
|
val uploadErrorExplanationVisible: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
class PostSubmissionViewModel(application: Application, photodata: ArrayList<PhotoData>? = null) : AndroidViewModel(application) {
|
class PostSubmissionViewModel(application: Application, photodata: ArrayList<PhotoData>? = null, val existingDescription: String? = 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 {
|
||||||
if (photodata != null) {
|
if (photodata != null) {
|
||||||
@ -74,7 +74,7 @@ class PostSubmissionViewModel(application: Application, photodata: ArrayList<Pho
|
|||||||
PreferenceManager.getDefaultSharedPreferences(application)
|
PreferenceManager.getDefaultSharedPreferences(application)
|
||||||
val initialDescription = sharedPreferences.getString("prefill_description", "") ?: ""
|
val initialDescription = sharedPreferences.getString("prefill_description", "") ?: ""
|
||||||
|
|
||||||
_uiState = MutableStateFlow(PostSubmissionActivityUiState(newPostDescriptionText = initialDescription))
|
_uiState = MutableStateFlow(PostSubmissionActivityUiState(newPostDescriptionText = existingDescription ?: initialDescription))
|
||||||
}
|
}
|
||||||
|
|
||||||
val uiState: StateFlow<PostSubmissionActivityUiState> = _uiState
|
val uiState: StateFlow<PostSubmissionActivityUiState> = _uiState
|
||||||
@ -235,7 +235,7 @@ class PostSubmissionViewModel(application: Application, photodata: ArrayList<Pho
|
|||||||
val description = uiState.value.newPostDescriptionText
|
val description = uiState.value.newPostDescriptionText
|
||||||
|
|
||||||
//TODO investigate why this works but booleans don't
|
//TODO investigate why this works but booleans don't
|
||||||
val nsfw = if(uiState.value.nsfw) 1 else 0
|
val nsfw = if (uiState.value.nsfw) 1 else 0
|
||||||
|
|
||||||
_uiState.update { currentUiState ->
|
_uiState.update { currentUiState ->
|
||||||
currentUiState.copy(
|
currentUiState.copy(
|
||||||
@ -307,8 +307,8 @@ class PostSubmissionViewModel(application: Application, photodata: ArrayList<Pho
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PostSubmissionViewModelFactory(val application: Application, val photoData: ArrayList<PhotoData>) : ViewModelProvider.Factory {
|
class PostSubmissionViewModelFactory(val application: Application, val photoData: ArrayList<PhotoData>, val existingDescription: String?) : 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, ArrayList::class.java).newInstance(application, photoData)
|
return modelClass.getConstructor(Application::class.java, ArrayList::class.java, String::class.java).newInstance(application, photoData, existingDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -81,6 +81,7 @@ class CameraFragment : BaseFragment() {
|
|||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
inActivity = arguments?.getBoolean("CameraActivity") ?: false
|
inActivity = arguments?.getBoolean("CameraActivity") ?: false
|
||||||
|
|
||||||
binding = FragmentCameraBinding.inflate(layoutInflater)
|
binding = FragmentCameraBinding.inflate(layoutInflater)
|
||||||
|
@ -8,7 +8,6 @@ import android.content.Intent
|
|||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.graphics.drawable.AnimatedVectorDrawable
|
import android.graphics.drawable.AnimatedVectorDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.location.GnssAntennaInfo.Listener
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
@ -479,24 +478,25 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||||||
val postDescription = status?.content ?: ""
|
val postDescription = status?.content ?: ""
|
||||||
val postAttachments =
|
val postAttachments =
|
||||||
status?.media_attachments!! // Catch possible exception from !! (?)
|
status?.media_attachments!! // Catch possible exception from !! (?)
|
||||||
val imageUris: MutableList<Uri> = mutableListOf()
|
val postNSFW = status?.sensitive
|
||||||
val imageNames: MutableList<String> = mutableListOf()
|
|
||||||
val imageDescriptions: MutableList<String> =
|
|
||||||
mutableListOf()
|
|
||||||
|
|
||||||
for (currentAttachment in postAttachments) {
|
val imageUriStrings = postAttachments.map { postAttachment ->
|
||||||
val imageUri = currentAttachment.url ?: ""
|
postAttachment.url ?: ""
|
||||||
val imageName =
|
}
|
||||||
Uri.parse(imageUri).lastPathSegment.toString()
|
val imageNames = imageUriStrings.map { imageUriString ->
|
||||||
val imageDescription =
|
Uri.parse(imageUriString).lastPathSegment.toString()
|
||||||
currentAttachment.description ?: ""
|
}
|
||||||
val downloadedFile =
|
val downloadedFiles = imageNames.map { imageName ->
|
||||||
File(context.cacheDir, imageName)
|
File(context.cacheDir, imageName)
|
||||||
val downloadedUri = Uri.fromFile(downloadedFile)
|
}
|
||||||
|
val imageUris = downloadedFiles.map { downloadedFile ->
|
||||||
imageUris.add(downloadedUri)
|
Uri.fromFile(downloadedFile)
|
||||||
imageNames.add(imageName)
|
}
|
||||||
imageDescriptions.add(imageDescription)
|
val imageDescriptions = postAttachments.map { postAttachment ->
|
||||||
|
fromHtml(postAttachment.description ?: "").toString()
|
||||||
|
}
|
||||||
|
val downloadRequests: List<Request> = imageUriStrings.map { imageUriString ->
|
||||||
|
Request.Builder().url(imageUriString).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
val counter = AtomicInteger(0)
|
val counter = AtomicInteger(0)
|
||||||
@ -506,6 +506,11 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||||||
// Wait for all outstanding downloads to finish
|
// Wait for all outstanding downloads to finish
|
||||||
if (counter.incrementAndGet() == imageUris.size) {
|
if (counter.incrementAndGet() == imageUris.size) {
|
||||||
if (allFilesExist(imageNames)) {
|
if (allFilesExist(imageNames)) {
|
||||||
|
// Delete original post
|
||||||
|
lifecycleScope.launch {
|
||||||
|
deletePost(apiHolder.api ?: apiHolder.setToCurrentUser(), db)
|
||||||
|
}
|
||||||
|
|
||||||
val counterInt = counter.get()
|
val counterInt = counter.get()
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
binding.root.context,
|
binding.root.context,
|
||||||
@ -518,17 +523,9 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||||||
).show()
|
).show()
|
||||||
// Pass downloaded images to new post creation activity
|
// Pass downloaded images to new post creation activity
|
||||||
intent.apply {
|
intent.apply {
|
||||||
assert(imageUris.size == imageDescriptions.size)
|
imageUris.zip(imageDescriptions).map { (imageUri, imageDescription) ->
|
||||||
|
ClipData.Item(imageDescription, null, imageUri)
|
||||||
for (i in 0 until imageUris.size) {
|
}.forEach { imageItem ->
|
||||||
val imageUri = imageUris[i]
|
|
||||||
val imageDescription =
|
|
||||||
fromHtml(imageDescriptions[i]).toString()
|
|
||||||
val imageItem = ClipData.Item(
|
|
||||||
imageDescription,
|
|
||||||
null,
|
|
||||||
imageUri
|
|
||||||
)
|
|
||||||
if (clipData == null) {
|
if (clipData == null) {
|
||||||
clipData = ClipData(
|
clipData = ClipData(
|
||||||
"",
|
"",
|
||||||
@ -539,7 +536,6 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||||||
clipData!!.addItem(imageItem)
|
clipData!!.addItem(imageItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
@ -559,6 +555,10 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||||||
PostCreationActivity.POST_REDRAFT,
|
PostCreationActivity.POST_REDRAFT,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
intent.putExtra(
|
||||||
|
PostCreationActivity.POST_NSFW,
|
||||||
|
postNSFW
|
||||||
|
)
|
||||||
|
|
||||||
// Launch post creation activity
|
// Launch post creation activity
|
||||||
binding.root.context.startActivity(intent)
|
binding.root.context.startActivity(intent)
|
||||||
@ -576,15 +576,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Iterate through all pictures of the original post
|
// Iterate through all pictures of the original post
|
||||||
for (currentAttachment in postAttachments) {
|
downloadRequests.zip(downloadedFiles).forEach { (downloadRequest, downloadedFile) ->
|
||||||
val imageUri = currentAttachment.url ?: ""
|
|
||||||
val imageName =
|
|
||||||
Uri.parse(imageUri).lastPathSegment.toString()
|
|
||||||
val downloadedFile =
|
|
||||||
File(context.cacheDir, imageName)
|
|
||||||
val downloadRequest: Request =
|
|
||||||
Request.Builder().url(imageUri).build()
|
|
||||||
|
|
||||||
// Check whether image is in cache directory already (maybe rather do so using Glide in the future?)
|
// Check whether image is in cache directory already (maybe rather do so using Glide in the future?)
|
||||||
if (!downloadedFile.exists()) {
|
if (!downloadedFile.exists()) {
|
||||||
OkHttpClient().newCall(downloadRequest)
|
OkHttpClient().newCall(downloadRequest)
|
||||||
@ -619,7 +611,6 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||||||
continuation()
|
continuation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (exception: HttpException) {
|
} catch (exception: HttpException) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
binding.root.context,
|
binding.root.context,
|
||||||
@ -636,13 +627,6 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete original post
|
|
||||||
deletePost(
|
|
||||||
apiHolder.api ?: apiHolder.setToCurrentUser(),
|
|
||||||
db
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
@ -831,14 +815,10 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun allFilesExist(listOfNames: MutableList<String>): Boolean {
|
private fun allFilesExist(listOfNames: List<String>): Boolean {
|
||||||
for (name in listOfNames) {
|
return listOfNames.all {
|
||||||
val file = File(binding.root.context.cacheDir, name)
|
File(binding.root.context.cacheDir, it).exists()
|
||||||
if (!file.exists()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
5
app/src/main/res/drawable/switch_account.xml
Normal file
5
app/src/main/res/drawable/switch_account.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="?attr/colorOnBackground" android:pathData="M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM14,4c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM20,16L8,16v-1.5c0,-1.99 4,-3 6,-3s6,1.01 6,3L20,16z"/>
|
||||||
|
</vector>
|
@ -6,96 +6,16 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".postCreation.PostCreationActivity">
|
tools:context=".postCreation.PostCreationActivity">
|
||||||
|
|
||||||
<org.pixeldroid.app.postCreation.carousel.ImageCarousel
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:id="@+id/carousel"
|
android:id="@+id/postCreationContainer"
|
||||||
|
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="match_parent"
|
||||||
app:showCaption="true"
|
app:defaultNavHost="true"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/buttonConstraints"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"/>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:id="@+id/buttonConstraints"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent">
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
<Button
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:id="@+id/post_creation_send_button"
|
app:navGraph="@navigation/post_creation_graph" />
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:enabled="true"
|
|
||||||
android:text="@string/upload_next_step"
|
|
||||||
android:visibility="visible"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:id="@+id/toolbarPostCreation"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="#40000000"
|
|
||||||
android:minHeight="?attr/actionBarSize"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
|
||||||
android:id="@+id/savePhotoButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="30dp"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="@string/save_to_gallery"
|
|
||||||
android:tooltipText='@string/save_to_gallery'
|
|
||||||
android:src="@drawable/download_file_30dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
|
||||||
android:id="@+id/removePhotoButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="30dp"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="@string/delete"
|
|
||||||
android:tooltipText='@string/delete'
|
|
||||||
android:src="@drawable/delete_30dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/savePhotoButton"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
|
||||||
android:id="@+id/editPhotoButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="30dp"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="@string/edit"
|
|
||||||
android:tooltipText='@string/edit'
|
|
||||||
android:src="@drawable/ic_baseline_edit_30"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/removePhotoButton"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
|
||||||
android:id="@+id/addPhotoButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="30dp"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="@string/add_photo"
|
|
||||||
android:tooltipText='@string/add_photo'
|
|
||||||
android:src="@drawable/add_photo_button"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
102
app/src/main/res/layout/fragment_post_creation.xml
Normal file
102
app/src/main/res/layout/fragment_post_creation.xml
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?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.PostCreationFragment"
|
||||||
|
android:id="@+id/postCreationFragment" >
|
||||||
|
|
||||||
|
<org.pixeldroid.app.postCreation.carousel.ImageCarousel
|
||||||
|
android:id="@+id/carousel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:showCaption="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/buttonConstraints"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/buttonConstraints"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/post_creation_send_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:enabled="true"
|
||||||
|
android:text="@string/upload_next_step"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/toolbarPostCreation"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="#40000000"
|
||||||
|
android:minHeight="?attr/actionBarSize"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
android:id="@+id/savePhotoButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/save_to_gallery"
|
||||||
|
android:tooltipText='@string/save_to_gallery'
|
||||||
|
android:src="@drawable/download_file_30dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
android:id="@+id/removePhotoButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/delete"
|
||||||
|
android:tooltipText='@string/delete'
|
||||||
|
android:src="@drawable/delete_30dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/savePhotoButton"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
android:id="@+id/editPhotoButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/edit"
|
||||||
|
android:tooltipText='@string/edit'
|
||||||
|
android:src="@drawable/ic_baseline_edit_30"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/removePhotoButton"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
android:id="@+id/addPhotoButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="30dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/add_photo"
|
||||||
|
android:tooltipText='@string/add_photo'
|
||||||
|
android:src="@drawable/add_photo_button"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -4,7 +4,18 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".postCreation.PostSubmissionActivity">
|
tools:context=".postCreation.PostSubmissionFragment"
|
||||||
|
android:id="@+id/postSubmissionFragment" >
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/top_bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="?attr/actionBarSize"
|
||||||
|
android:theme="?attr/actionBarTheme"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/post_preview"
|
android:id="@+id/post_preview"
|
||||||
@ -13,7 +24,7 @@
|
|||||||
android:layout_height="88dp"
|
android:layout_height="88dp"
|
||||||
android:contentDescription="@string/post_preview"
|
android:contentDescription="@string/post_preview"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"/>
|
app:layout_constraintTop_toBottomOf="@id/top_bar"/>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/upload_error"
|
android:id="@+id/upload_error"
|
||||||
@ -24,7 +35,7 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toBottomOf="@id/top_bar"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -35,25 +46,25 @@
|
|||||||
android:text="@string/media_upload_failed"
|
android:text="@string/media_upload_failed"
|
||||||
android:textColor="?attr/colorOnError"
|
android:textColor="?attr/colorOnError"
|
||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
|
app:drawableStartCompat="@drawable/cloud_off_24"
|
||||||
|
app:drawableTint="?attr/colorOnError"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
app:drawableStartCompat="@drawable/cloud_off_24"
|
|
||||||
app:drawableTint="?attr/colorOnError" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/upload_error_text_explanation"
|
android:id="@+id/upload_error_text_explanation"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/colorError"
|
android:background="?attr/colorError"
|
||||||
tools:text="Error code returned by server: 413"
|
|
||||||
android:textColor="?attr/colorOnError"
|
android:textColor="?attr/colorOnError"
|
||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_view"
|
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_view"
|
||||||
|
tools:text="Error code returned by server: 413"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -65,8 +76,6 @@
|
|||||||
app:layout_constraintHorizontal_bias="0.498"
|
app:layout_constraintHorizontal_bias="0.498"
|
||||||
app:layout_constraintStart_toStartOf="@id/upload_error_text_view"
|
app:layout_constraintStart_toStartOf="@id/upload_error_text_view"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_explanation" />
|
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_explanation" />
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
@ -6,6 +6,6 @@
|
|||||||
android:id="@+id/action_switch_accounts"
|
android:id="@+id/action_switch_accounts"
|
||||||
android:orderInCategory="100"
|
android:orderInCategory="100"
|
||||||
android:title="@string/switch_accounts"
|
android:title="@string/switch_accounts"
|
||||||
android:icon="@drawable/material_drawer_ico_account"
|
android:icon="@drawable/switch_account"
|
||||||
app:showAsAction="ifRoom"/>
|
app:showAsAction="ifRoom"/>
|
||||||
</menu>
|
</menu>
|
25
app/src/main/res/navigation/post_creation_graph.xml
Normal file
25
app/src/main/res/navigation/post_creation_graph.xml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<navigation 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:id="@+id/post_creation_graph"
|
||||||
|
app:startDestination="@id/postCreationFragment" >
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/postCreationFragment"
|
||||||
|
android:name="org.pixeldroid.app.postCreation.PostCreationFragment"
|
||||||
|
tools:layout="@layout/fragment_post_creation" >
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_postCreationFragment_to_postSubmissionFragment"
|
||||||
|
app:destination="@id/postSubmissionFragment" />
|
||||||
|
</fragment>
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/postSubmissionFragment"
|
||||||
|
android:name="org.pixeldroid.app.postCreation.PostSubmissionFragment"
|
||||||
|
android:label="@string/add_details"
|
||||||
|
tools:layout="@layout/fragment_post_submission" >
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_postSubmissionFragment_to_postCreationFragment"
|
||||||
|
app:destination="@id/postCreationFragment" />
|
||||||
|
</fragment>
|
||||||
|
</navigation>
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user