Restructure post creation activity into two fragments
This commit is contained in:
parent
ba26871572
commit
4f3020e0be
|
@ -67,9 +67,6 @@
|
|||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".postCreation.PostSubmissionActivity">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".profile.FollowsActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
|
|
|
@ -1,45 +1,13 @@
|
|||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Intent
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.*
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
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 androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
|
||||
import org.pixeldroid.app.postCreation.camera.CameraActivity
|
||||
import org.pixeldroid.app.postCreation.carousel.CarouselItem
|
||||
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.fileExtension
|
||||
import org.pixeldroid.app.utils.getMimeType
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
const val TAG = "Post Creation Activity"
|
||||
|
||||
|
@ -47,9 +15,9 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
|||
|
||||
companion object {
|
||||
internal const val PICTURE_DESCRIPTION = "picture_description"
|
||||
internal const val TEMP_FILES = "temp_files"
|
||||
internal const val POST_REDRAFT = "post_redraft"
|
||||
internal const val POST_NSFW = "post_nsfw"
|
||||
internal const val TEMP_FILES = "temp_files"
|
||||
}
|
||||
|
||||
private var user: UserDatabaseEntity? = null
|
||||
|
@ -57,13 +25,10 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
|||
|
||||
private lateinit var binding: ActivityPostCreationBinding
|
||||
|
||||
private lateinit var model: PostCreationViewModel
|
||||
|
||||
private lateinit var navController: NavController
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityPostCreationBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
user = db.userDao().getActiveUser()
|
||||
|
||||
|
@ -73,235 +38,16 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
|||
}
|
||||
} ?: InstanceDatabaseEntity("", "")
|
||||
|
||||
val _model: PostCreationViewModel by viewModels {
|
||||
PostCreationViewModelFactory(
|
||||
application,
|
||||
intent.clipData!!,
|
||||
instance,
|
||||
intent.getStringExtra(PICTURE_DESCRIPTION),
|
||||
intent.getBooleanExtra(POST_NSFW, false)
|
||||
)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
val redraft = intent.getBooleanExtra(POST_REDRAFT, false)
|
||||
if (redraft) {
|
||||
val builder = AlertDialog.Builder(binding.root.context)
|
||||
builder.apply {
|
||||
setMessage(R.string.redraft_dialog_cancel)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
finish()
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
show()
|
||||
}
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
binding = ActivityPostCreationBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val navHostFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.postCreationContainer) as NavHostFragment
|
||||
navController = navHostFragment.navController
|
||||
navController.setGraph(R.navigation.post_creation_graph)
|
||||
}
|
||||
|
||||
private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK && result.data?.clipData != null) {
|
||||
result.data?.clipData?.let {
|
||||
model.setImages(model.addPossibleImages(it))
|
||||
}
|
||||
} else if (result.resultCode != Activity.RESULT_CANCELED) {
|
||||
Toast.makeText(applicationContext, R.string.add_images_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
return navController.navigateUp() || super.onSupportNavigateUp()
|
||||
}
|
||||
|
||||
private fun addPhoto(){
|
||||
addPhotoResultContract.launch(
|
||||
Intent(this, CameraActivity::class.java)
|
||||
)
|
||||
}
|
||||
|
||||
private fun savePicture(button: View, currentPosition: Int) {
|
||||
val originalUri = model.getPhotoData().value!![currentPosition].imageUri
|
||||
|
||||
val pair = getOutputFile(originalUri)
|
||||
val outputStream: OutputStream = pair.first
|
||||
val path: String = pair.second
|
||||
|
||||
contentResolver.openInputStream(originalUri)!!.use { input ->
|
||||
outputStream.use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
if(path.startsWith("file")) {
|
||||
MediaScannerConnection.scanFile(
|
||||
this,
|
||||
arrayOf(path.toUri().toFile().absolutePath),
|
||||
null
|
||||
) { path, uri ->
|
||||
if (uri == null) {
|
||||
Log.e(
|
||||
"NEW IMAGE SCAN FAILED",
|
||||
"Tried to scan $path, but it failed"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Snackbar.make(
|
||||
button, getString(R.string.save_image_success),
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun getOutputFile(uri: Uri): Pair<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,311 @@
|
|||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Intent
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.*
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.FragmentPostCreationBinding
|
||||
import org.pixeldroid.app.postCreation.camera.CameraActivity
|
||||
import org.pixeldroid.app.postCreation.carousel.CarouselItem
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.fileExtension
|
||||
import org.pixeldroid.app.utils.getMimeType
|
||||
import org.pixeldroid.media_editor.photoEdit.PhotoEditActivity
|
||||
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
||||
class PostCreationFragment : BaseFragment() {
|
||||
|
||||
private var user: UserDatabaseEntity? = null
|
||||
private var instance: InstanceDatabaseEntity = InstanceDatabaseEntity("", "")
|
||||
|
||||
private lateinit var binding: FragmentPostCreationBinding
|
||||
private lateinit var model: PostCreationViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// Inflate the layout for this fragment
|
||||
binding = FragmentPostCreationBinding.inflate(layoutInflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
user = db.userDao().getActiveUser()
|
||||
|
||||
instance = user?.run {
|
||||
db.instanceDao().getAll().first { instanceDatabaseEntity ->
|
||||
instanceDatabaseEntity.uri.contains(instance_uri)
|
||||
}
|
||||
} ?: InstanceDatabaseEntity("", "")
|
||||
|
||||
val _model: PostCreationViewModel by activityViewModels {
|
||||
PostCreationViewModelFactory(
|
||||
requireActivity().application,
|
||||
requireActivity().intent.clipData!!,
|
||||
instance,
|
||||
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
|
||||
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false)
|
||||
)
|
||||
}
|
||||
model = _model
|
||||
|
||||
model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData ->
|
||||
// update UI
|
||||
binding.carousel.addData(
|
||||
newPhotoData.map {
|
||||
CarouselItem(
|
||||
it.imageUri, it.imageDescription, it.video,
|
||||
it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass,
|
||||
it.videoEncodeComplete, it.videoEncodeError,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.uiState.collect { uiState ->
|
||||
uiState.userMessage?.let {
|
||||
AlertDialog.Builder(binding.root.context).apply {
|
||||
setMessage(it)
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
|
||||
// Notify the ViewModel the message is displayed
|
||||
model.userMessageShown()
|
||||
}
|
||||
binding.addPhotoButton.isEnabled = uiState.addPhotoButtonEnabled
|
||||
binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled
|
||||
binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled
|
||||
binding.toolbarPostCreation.visibility =
|
||||
if (uiState.isCarousel) View.VISIBLE else View.INVISIBLE
|
||||
binding.carousel.layoutCarousel = uiState.isCarousel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.carousel.apply {
|
||||
layoutCarouselCallback = { model.becameCarousel(it)}
|
||||
maxEntries = instance.albumLimit
|
||||
addPhotoButtonCallback = {
|
||||
addPhoto()
|
||||
}
|
||||
updateDescriptionCallback = { position: Int, description: String ->
|
||||
model.updateDescription(position, description)
|
||||
}
|
||||
}
|
||||
// get the description and send the post
|
||||
binding.postCreationSendButton.setOnClickListener {
|
||||
if (validatePost() && model.isNotEmpty()) {
|
||||
findNavController().navigate(R.id.action_postCreationFragment_to_postSubmissionFragment)
|
||||
}
|
||||
}
|
||||
|
||||
binding.editPhotoButton.setOnClickListener {
|
||||
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
||||
edit(currentPosition)
|
||||
}
|
||||
}
|
||||
|
||||
binding.addPhotoButton.setOnClickListener {
|
||||
addPhoto()
|
||||
}
|
||||
|
||||
binding.savePhotoButton.setOnClickListener {
|
||||
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
||||
savePicture(it, currentPosition)
|
||||
}
|
||||
}
|
||||
|
||||
binding.removePhotoButton.setOnClickListener {
|
||||
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
||||
model.removeAt(currentPosition)
|
||||
model.cancelEncode(currentPosition)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up temporary files, if any
|
||||
val tempFiles = requireActivity().intent.getStringArrayExtra(PostCreationActivity.TEMP_FILES)
|
||||
tempFiles?.asList()?.forEach {
|
||||
val file = File(binding.root.context.cacheDir, it)
|
||||
model.trackTempFile(file)
|
||||
}
|
||||
|
||||
// Handle back pressed button
|
||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
val redraft = requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_REDRAFT, false)
|
||||
if (redraft) {
|
||||
val builder = AlertDialog.Builder(binding.root.context)
|
||||
builder.apply {
|
||||
setMessage(R.string.redraft_dialog_cancel)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
requireActivity().finish()
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
show()
|
||||
}
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK && result.data?.clipData != null) {
|
||||
result.data?.clipData?.let {
|
||||
model.setImages(model.addPossibleImages(it))
|
||||
}
|
||||
} else if (result.resultCode != Activity.RESULT_CANCELED) {
|
||||
Toast.makeText(requireActivity(), R.string.add_images_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addPhoto(){
|
||||
addPhotoResultContract.launch(
|
||||
Intent(requireActivity(), CameraActivity::class.java)
|
||||
)
|
||||
}
|
||||
|
||||
private fun savePicture(button: View, currentPosition: Int) {
|
||||
val originalUri = model.getPhotoData().value!![currentPosition].imageUri
|
||||
|
||||
val pair = getOutputFile(originalUri)
|
||||
val outputStream: OutputStream = pair.first
|
||||
val path: String = pair.second
|
||||
|
||||
requireActivity().contentResolver.openInputStream(originalUri)!!.use { input ->
|
||||
outputStream.use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
if(path.startsWith("file")) {
|
||||
MediaScannerConnection.scanFile(
|
||||
requireActivity(),
|
||||
arrayOf(path.toUri().toFile().absolutePath),
|
||||
null
|
||||
) { path, uri ->
|
||||
if (uri == null) {
|
||||
Log.e(
|
||||
"NEW IMAGE SCAN FAILED",
|
||||
"Tried to scan $path, but it failed"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Snackbar.make(
|
||||
button, getString(R.string.save_image_success),
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun getOutputFile(uri: Uri): Pair<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 {
|
||||
@Suppress("DEPRECATION") val imagesDir =
|
||||
Environment.getExternalStoragePublicDirectory(getString(R.string.app_name))
|
||||
imagesDir.mkdir()
|
||||
val file = File(imagesDir, name)
|
||||
path = Uri.fromFile(file).toString()
|
||||
outputStream = file.outputStream()
|
||||
}
|
||||
return Pair(outputStream, path)
|
||||
}
|
||||
|
||||
|
||||
private fun validatePost(): Boolean {
|
||||
if (model.getPhotoData().value?.all { !it.video || it.videoEncodeComplete } == false) {
|
||||
AlertDialog.Builder(requireActivity()).apply {
|
||||
setMessage(R.string.still_encoding)
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private val editResultContract: ActivityResultLauncher<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(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.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.core.content.ContextCompat
|
||||
import android.text.Editable
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
@ -17,18 +19,32 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException
|
||||
import com.jarsilio.android.scrambler.stripMetadata
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import okhttp3.MultipartBody
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import org.pixeldroid.app.utils.fileExtension
|
||||
import org.pixeldroid.app.utils.getMimeType
|
||||
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
|
||||
import retrofit2.HttpException
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.MutableList
|
||||
|
@ -57,7 +73,19 @@ data class PostCreationActivityUiState(
|
|||
|
||||
val isCarousel: Boolean = true,
|
||||
|
||||
val postCreationSendButtonEnabled: Boolean = true,
|
||||
|
||||
val newPostDescriptionText: String = "",
|
||||
val nsfw: Boolean = false,
|
||||
|
||||
val chosenAccount: UserDatabaseEntity? = null,
|
||||
|
||||
val uploadProgressBarVisible: Boolean = false,
|
||||
val uploadProgress: Int = 0,
|
||||
val uploadCompletedTextviewVisible: Boolean = false,
|
||||
val uploadErrorVisible: Boolean = false,
|
||||
val uploadErrorExplanationText: String = "",
|
||||
val uploadErrorExplanationVisible: Boolean = false,
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
|
@ -92,7 +120,10 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
|||
PreferenceManager.getDefaultSharedPreferences(application)
|
||||
val initialDescription = sharedPreferences.getString("prefill_description", "") ?: ""
|
||||
|
||||
_uiState = MutableStateFlow(PostCreationActivityUiState(newPostDescriptionText = existingDescription ?: initialDescription))
|
||||
_uiState = MutableStateFlow(PostCreationActivityUiState(
|
||||
newPostDescriptionText = existingDescription ?: initialDescription,
|
||||
nsfw = existingNSFW
|
||||
))
|
||||
}
|
||||
|
||||
val uiState: StateFlow<PostCreationActivityUiState> = _uiState
|
||||
|
@ -169,7 +200,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
|||
val type = uri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
|
||||
val isVideo = type.startsWith("video/")
|
||||
|
||||
if(isVideo && !instance!!.videoEnabled){
|
||||
if (isVideo && !instance!!.videoEnabled) {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.video_not_supported))
|
||||
}
|
||||
|
@ -203,17 +234,6 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
|||
photoData.value = photoData.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Next step
|
||||
*/
|
||||
fun nextStep(context: Context) {
|
||||
val intent = Intent(context, PostSubmissionActivity::class.java)
|
||||
intent.putExtra(PostSubmissionActivity.PHOTO_DATA, getPhotoData().value?.let { ArrayList(it) })
|
||||
intent.putExtra(PostSubmissionActivity.PICTURE_DESCRIPTION, existingDescription)
|
||||
intent.putExtra(PostSubmissionActivity.POST_NSFW, existingNSFW)
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
}
|
||||
|
||||
fun modifyAt(position: Int, data: Intent): Unit? {
|
||||
val result: PhotoData = photoData.value?.getOrNull(position)?.run {
|
||||
if (video) {
|
||||
|
@ -262,7 +282,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
|||
private fun videoEncodeProgress(originalUri: Uri, progress: Int, firstPass: Boolean, outputVideoPath: Uri?, error: Boolean){
|
||||
photoData.value?.indexOfFirst { it.imageUri == originalUri }?.let { position ->
|
||||
|
||||
if(outputVideoPath != null){
|
||||
if (outputVideoPath != null) {
|
||||
// If outputVideoPath is not null, it means the video is done and we can change Uris
|
||||
val (size, _) = getSizeAndVideoValidate(outputVideoPath, position)
|
||||
|
||||
|
@ -310,7 +330,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
|||
}
|
||||
}
|
||||
|
||||
fun registerNewFFmpegSession(position: Uri, sessionId: Long) {
|
||||
private fun registerNewFFmpegSession(position: Uri, sessionId: Long) {
|
||||
sessionMap[position] = sessionId
|
||||
}
|
||||
|
||||
|
@ -321,6 +341,209 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetUploadStatus() {
|
||||
photoData.value = photoData.value?.map { it.copy(uploadId = null, progress = null) }?.toMutableList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the images that are in the [photoData] array.
|
||||
* Keeps track of them in the [PhotoData.progress] (for the upload progress), and the
|
||||
* [PhotoData.uploadId] (for the list of ids of the uploads).
|
||||
*/
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
fun upload() {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
postCreationSendButtonEnabled = false,
|
||||
uploadCompletedTextviewVisible = false,
|
||||
uploadErrorVisible = false,
|
||||
uploadProgressBarVisible = true
|
||||
)
|
||||
}
|
||||
|
||||
for (data: PhotoData in getPhotoData().value ?: emptyList()) {
|
||||
val extension = data.imageUri.fileExtension(getApplication<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 {
|
||||
|
|
|
@ -1,193 +0,0 @@
|
|||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityPostSubmissionBinding
|
||||
import org.pixeldroid.app.postCreation.PostCreationActivity.Companion.TEMP_FILES
|
||||
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.setSquareImageFromURL
|
||||
import java.io.File
|
||||
|
||||
|
||||
class PostSubmissionActivity : BaseThemedWithoutBarActivity() {
|
||||
|
||||
companion object {
|
||||
internal const val PICTURE_DESCRIPTION = "picture_description"
|
||||
internal const val PHOTO_DATA = "photo_data"
|
||||
internal const val POST_NSFW = "post_nsfw"
|
||||
}
|
||||
|
||||
private lateinit var accounts: List<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!!,
|
||||
intent.getStringExtra(PICTURE_DESCRIPTION)
|
||||
)
|
||||
}
|
||||
model = _model
|
||||
|
||||
val sensitive = intent.getBooleanExtra(POST_NSFW, false)
|
||||
model.updateNSFW(sensitive)
|
||||
binding.nsfwSwitch.isChecked = sensitive
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.uiState.collect { uiState ->
|
||||
uiState.userMessage?.let {
|
||||
AlertDialog.Builder(binding.root.context).apply {
|
||||
setMessage(it)
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
|
||||
// Notify the ViewModel the message is displayed
|
||||
model.userMessageShown()
|
||||
}
|
||||
enableButton(uiState.postCreationSendButtonEnabled)
|
||||
binding.uploadProgressBar.visibility =
|
||||
if (uiState.uploadProgressBarVisible) VISIBLE else INVISIBLE
|
||||
binding.uploadProgressBar.progress = uiState.uploadProgress
|
||||
binding.uploadCompletedTextview.visibility =
|
||||
if (uiState.uploadCompletedTextviewVisible) VISIBLE else INVISIBLE
|
||||
binding.uploadError.visibility =
|
||||
if (uiState.uploadErrorVisible) VISIBLE else INVISIBLE
|
||||
binding.uploadErrorTextExplanation.visibility =
|
||||
if (uiState.uploadErrorExplanationVisible) VISIBLE else INVISIBLE
|
||||
|
||||
selectedAccount = accounts.indexOf(uiState.chosenAccount)
|
||||
|
||||
binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.newPostDescriptionInputField.doAfterTextChanged {
|
||||
model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text)
|
||||
}
|
||||
|
||||
binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
model.updateNSFW(isChecked)
|
||||
}
|
||||
|
||||
val existingDescription: String? = intent.getStringExtra(PICTURE_DESCRIPTION)
|
||||
|
||||
binding.newPostDescriptionInputField.setText(
|
||||
// Set description from redraft if any, otherwise from the template
|
||||
existingDescription ?: model.uiState.value.newPostDescriptionText
|
||||
)
|
||||
|
||||
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
|
||||
|
||||
setSquareImageFromURL(View(applicationContext), photoData!![0].imageUri.toString(), binding.postPreview)
|
||||
// get the description and send the post
|
||||
binding.postCreationSendButton.setOnClickListener {
|
||||
if (validatePost()) model.upload()
|
||||
}
|
||||
|
||||
// Button to retry image upload when it fails
|
||||
binding.retryUploadButton.setOnClickListener {
|
||||
model.resetUploadStatus()
|
||||
model.upload()
|
||||
}
|
||||
|
||||
// Clean up temporary files, if any
|
||||
val tempFiles = intent.getStringArrayExtra(TEMP_FILES)
|
||||
tempFiles?.asList()?.forEach {
|
||||
val file = File(binding.root.context.cacheDir, it)
|
||||
model.trackTempFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(newMenu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.post_submission_account_menu, newMenu)
|
||||
menu = newMenu
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId){
|
||||
R.id.action_switch_accounts -> {
|
||||
AlertDialog.Builder(this).apply {
|
||||
setIcon(R.drawable.material_drawer_ico_account)
|
||||
setTitle(R.string.switch_accounts)
|
||||
setSingleChoiceItems(accounts.map { it.username + " (${it.fullHandle})" }.toTypedArray(), selectedAccount) { dialog, which ->
|
||||
if(selectedAccount != which){
|
||||
model.chooseAccount(accounts[which])
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
}.show()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun validatePost(): Boolean {
|
||||
binding.postTextInputLayout.run {
|
||||
val content = editText?.length() ?: 0
|
||||
if (content > counterMaxLength) {
|
||||
// error, too many characters
|
||||
error = resources.getQuantityString(R.plurals.description_max_characters, counterMaxLength, counterMaxLength)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun enableButton(enable: Boolean = true){
|
||||
binding.postCreationSendButton.isEnabled = enable
|
||||
if(enable){
|
||||
binding.postingProgressBar.visibility = GONE
|
||||
binding.postCreationSendButton.visibility = VISIBLE
|
||||
} else {
|
||||
binding.postingProgressBar.visibility = VISIBLE
|
||||
binding.postCreationSendButton.visibility = GONE
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.FragmentPostSubmissionBinding
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.setSquareImageFromURL
|
||||
|
||||
|
||||
class PostSubmissionFragment : BaseFragment() {
|
||||
|
||||
private lateinit var accounts: List<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: FragmentPostSubmissionBinding
|
||||
private lateinit var model: PostCreationViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// Inflate the layout for this fragment
|
||||
binding = FragmentPostSubmissionBinding.inflate(layoutInflater)
|
||||
// setHasOptionsMenu(true)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.topBar.setupWithNavController(findNavController())
|
||||
|
||||
user = db.userDao().getActiveUser()
|
||||
accounts = db.userDao().getAll()
|
||||
|
||||
instance = user?.run {
|
||||
db.instanceDao().getAll().first { instanceDatabaseEntity ->
|
||||
instanceDatabaseEntity.uri.contains(instance_uri)
|
||||
}
|
||||
} ?: InstanceDatabaseEntity("", "")
|
||||
|
||||
val _model: PostCreationViewModel by activityViewModels {
|
||||
PostCreationViewModelFactory(
|
||||
requireActivity().application,
|
||||
requireActivity().intent.clipData!!,
|
||||
instance,
|
||||
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
|
||||
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false)
|
||||
)
|
||||
}
|
||||
model = _model
|
||||
|
||||
// Display the values from the view model
|
||||
binding.nsfwSwitch.isChecked = model.uiState.value.nsfw
|
||||
binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText)
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.uiState.collect { uiState ->
|
||||
uiState.userMessage?.let {
|
||||
AlertDialog.Builder(binding.root.context).apply {
|
||||
setMessage(it)
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
|
||||
// Notify the ViewModel the message is displayed
|
||||
model.userMessageShown()
|
||||
}
|
||||
enableButton(uiState.postCreationSendButtonEnabled)
|
||||
binding.uploadProgressBar.visibility =
|
||||
if (uiState.uploadProgressBarVisible) View.VISIBLE else View.INVISIBLE
|
||||
binding.uploadProgressBar.progress = uiState.uploadProgress
|
||||
binding.uploadCompletedTextview.visibility =
|
||||
if (uiState.uploadCompletedTextviewVisible) View.VISIBLE else View.INVISIBLE
|
||||
binding.uploadError.visibility =
|
||||
if (uiState.uploadErrorVisible) View.VISIBLE else View.INVISIBLE
|
||||
binding.uploadErrorTextExplanation.visibility =
|
||||
if (uiState.uploadErrorExplanationVisible) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
selectedAccount = accounts.indexOf(uiState.chosenAccount)
|
||||
|
||||
binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.newPostDescriptionInputField.doAfterTextChanged {
|
||||
model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text)
|
||||
}
|
||||
|
||||
binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
model.updateNSFW(isChecked)
|
||||
}
|
||||
|
||||
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
|
||||
|
||||
setSquareImageFromURL(View(requireActivity()), model.getPhotoData()!!.value?.get(0)?.imageUri.toString(), binding.postPreview)
|
||||
|
||||
// Get the description and send the post
|
||||
binding.postCreationSendButton.setOnClickListener {
|
||||
if (validatePost()) model.upload()
|
||||
}
|
||||
|
||||
// Button to retry image upload when it fails
|
||||
binding.retryUploadButton.setOnClickListener {
|
||||
model.resetUploadStatus()
|
||||
model.upload()
|
||||
}
|
||||
|
||||
// Handle back pressed button
|
||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
findNavController().navigate(R.id.action_postSubmissionFragment_to_postCreationFragment)
|
||||
}
|
||||
})
|
||||
|
||||
binding.topBar.addMenuProvider(object: MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
// Add menu items here
|
||||
menuInflater.inflate(R.menu.post_submission_account_menu, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
// Handle the menu selection
|
||||
return when (menuItem.itemId) {
|
||||
R.id.action_switch_accounts -> {
|
||||
AlertDialog.Builder(requireActivity()).apply {
|
||||
setIcon(R.drawable.switch_account)
|
||||
setTitle(R.string.switch_accounts)
|
||||
setSingleChoiceItems(accounts.map { it.username + " (${it.fullHandle})" }.toTypedArray(), selectedAccount) { dialog, which ->
|
||||
if (selectedAccount != which) {
|
||||
model.chooseAccount(accounts[which])
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
}.show()
|
||||
return true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
}
|
||||
|
||||
private fun validatePost(): Boolean {
|
||||
binding.postTextInputLayout.run {
|
||||
val content = editText?.length() ?: 0
|
||||
if (content > counterMaxLength) {
|
||||
// error, too many characters
|
||||
error = resources.getQuantityString(R.plurals.description_max_characters, counterMaxLength, counterMaxLength)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun enableButton(enable: Boolean = true){
|
||||
binding.postCreationSendButton.isEnabled = enable
|
||||
if(enable){
|
||||
binding.postingProgressBar.visibility = View.GONE
|
||||
binding.postCreationSendButton.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.postingProgressBar.visibility = View.VISIBLE
|
||||
binding.postCreationSendButton.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -81,6 +81,7 @@ class CameraFragment : BaseFragment() {
|
|||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
inActivity = arguments?.getBoolean("CameraActivity") ?: false
|
||||
|
||||
binding = FragmentCameraBinding.inflate(layoutInflater)
|
||||
|
|
|
@ -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"
|
||||
tools:context=".postCreation.PostCreationActivity">
|
||||
|
||||
<org.pixeldroid.app.postCreation.carousel.ImageCarousel
|
||||
android:id="@+id/carousel"
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/postCreationContainer"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
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"
|
||||
android:layout_height="match_parent"
|
||||
app:defaultNavHost="true"
|
||||
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>
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:navGraph="@navigation/post_creation_graph" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -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,19 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".postCreation.PostSubmissionActivity">
|
||||
tools:context=".postCreation.PostSubmissionFragment"
|
||||
android:id="@+id/postSubmissionFragment" >
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/top_bar"
|
||||
android:layout_width="409dp"
|
||||
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"
|
||||
app:layout_constraintBottom_toTopOf="@id/post_preview" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/post_preview"
|
||||
|
@ -13,7 +25,7 @@
|
|||
android:layout_height="88dp"
|
||||
android:contentDescription="@string/post_preview"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
app:layout_constraintTop_toBottomOf="@id/top_bar"/>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/upload_error"
|
||||
|
@ -24,7 +36,7 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/top_bar"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
|
@ -35,25 +47,25 @@
|
|||
android:text="@string/media_upload_failed"
|
||||
android:textColor="?attr/colorOnError"
|
||||
android:textSize="20sp"
|
||||
app:drawableStartCompat="@drawable/cloud_off_24"
|
||||
app:drawableTint="?attr/colorOnError"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:drawableStartCompat="@drawable/cloud_off_24"
|
||||
app:drawableTint="?attr/colorOnError" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/upload_error_text_explanation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorError"
|
||||
tools:text="Error code returned by server: 413"
|
||||
android:textColor="?attr/colorOnError"
|
||||
android:textSize="20sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_view"
|
||||
tools:text="Error code returned by server: 413"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
|
@ -65,8 +77,6 @@
|
|||
app:layout_constraintHorizontal_bias="0.498"
|
||||
app:layout_constraintStart_toStartOf="@id/upload_error_text_view"
|
||||
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_explanation" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
@ -6,6 +6,6 @@
|
|||
android:id="@+id/action_switch_accounts"
|
||||
android:orderInCategory="100"
|
||||
android:title="@string/switch_accounts"
|
||||
android:icon="@drawable/material_drawer_ico_account"
|
||||
android:icon="@drawable/switch_account"
|
||||
app:showAsAction="ifRoom"/>
|
||||
</menu>
|
|
@ -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>
|
Loading…
Reference in New Issue