diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cb465e7b..214cbc4a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -68,6 +68,7 @@
@@ -79,9 +80,6 @@
-
-
- // update UI
- binding.carousel.addData(
- newPhotoData.map {
- CarouselItem(
- it.imageUri, it.imageDescription, it.video,
- it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass,
- it.videoEncodeComplete, it.videoEncodeError,
- )
- }
- )
- }
-
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
- model.uiState.collect { uiState ->
- uiState.userMessage?.let {
- AlertDialog.Builder(binding.root.context).apply {
- setMessage(it)
- setNegativeButton(android.R.string.ok) { _, _ -> }
- }.show()
-
- // Notify the ViewModel the message is displayed
- model.userMessageShown()
- }
- binding.addPhotoButton.isEnabled = uiState.addPhotoButtonEnabled
- binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled
- binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled
- binding.toolbarPostCreation.visibility =
- if (uiState.isCarousel) VISIBLE else INVISIBLE
- binding.carousel.layoutCarousel = uiState.isCarousel
- }
- }
- }
-
- binding.carousel.apply {
- layoutCarouselCallback = { model.becameCarousel(it)}
- maxEntries = instance.albumLimit
- addPhotoButtonCallback = {
- addPhoto()
- }
- updateDescriptionCallback = { position: Int, description: String ->
- model.updateDescription(position, description)
- }
- }
- // get the description and send the post
- binding.postCreationSendButton.setOnClickListener {
- if (validatePost() && model.isNotEmpty()) {
- model.nextStep(binding.root.context)
- }
- }
-
- binding.editPhotoButton.setOnClickListener {
- binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
- edit(currentPosition)
- }
- }
-
- binding.addPhotoButton.setOnClickListener {
- addPhoto()
- }
-
- binding.savePhotoButton.setOnClickListener {
- binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
- savePicture(it, currentPosition)
- }
- }
-
- binding.removePhotoButton.setOnClickListener {
- binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
- model.removeAt(currentPosition)
- model.cancelEncode(currentPosition)
- }
- }
-
- // Clean up temporary files, if any
- val tempFiles = intent.getStringArrayExtra(TEMP_FILES)
- tempFiles?.asList()?.forEach {
- val file = File(binding.root.context.cacheDir, it)
- model.trackTempFile(file)
- }
+ 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)
}
- override fun onBackPressed() {
- val redraft = intent.getBooleanExtra(POST_REDRAFT, false)
- if (redraft) {
- val builder = AlertDialog.Builder(binding.root.context)
- builder.apply {
- setMessage(R.string.redraft_dialog_cancel)
- setPositiveButton(android.R.string.ok) { _, _ ->
- super.onBackPressed()
- }
- setNegativeButton(android.R.string.cancel) { _, _ -> }
- show()
- }
- } else {
- super.onBackPressed()
- }
+ override fun onSupportNavigateUp(): Boolean {
+ return navController.navigateUp() || super.onSupportNavigateUp()
}
- 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 {
- val extension = uri.fileExtension(contentResolver)
-
- val name = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
- .format(System.currentTimeMillis()) + ".$extension"
-
- val outputStream: OutputStream
- val path: String
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- val resolver: ContentResolver = contentResolver
- val type = uri.getMimeType(contentResolver)
- val contentValues = ContentValues()
- contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
- contentValues.put(MediaStore.MediaColumns.MIME_TYPE, type)
- contentValues.put(
- MediaStore.MediaColumns.RELATIVE_PATH,
- Environment.DIRECTORY_PICTURES
- )
- val store =
- if (type.startsWith("video")) MediaStore.Video.Media.EXTERNAL_CONTENT_URI
- else MediaStore.Images.Media.EXTERNAL_CONTENT_URI
- val imageUri: Uri = resolver.insert(store, contentValues)!!
- path = imageUri.toString()
- outputStream = resolver.openOutputStream(imageUri)!!
- } else {
- @Suppress("DEPRECATION") val imagesDir =
- Environment.getExternalStoragePublicDirectory(getString(R.string.app_name))
- imagesDir.mkdir()
- val file = File(imagesDir, name)
- path = Uri.fromFile(file).toString()
- outputStream = file.outputStream()
- }
- return Pair(outputStream, path)
- }
-
-
- private fun validatePost(): Boolean {
- if(model.getPhotoData().value?.all { !it.video || it.videoEncodeComplete } == false){
- AlertDialog.Builder(this).apply {
- setMessage(R.string.still_encoding)
- setNegativeButton(android.R.string.ok) { _, _ -> }
- }.show()
- return false
- }
- return true
- }
-
- private val editResultContract: ActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){
- result: ActivityResult? ->
- if (result?.resultCode == Activity.RESULT_OK && result.data != null) {
- val position: Int = result.data!!.getIntExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_POSITION, 0)
- model.modifyAt(position, result.data!!)
- ?: Toast.makeText(applicationContext, R.string.error_editing, Toast.LENGTH_SHORT).show()
- } else if(result?.resultCode != Activity.RESULT_CANCELED){
- Toast.makeText(applicationContext, R.string.error_editing, Toast.LENGTH_SHORT).show()
- }
- }
-
- private fun edit(position: Int) {
- val intent = Intent(
- this,
- if(model.getPhotoData().value!![position].video) org.pixeldroid.media_editor.photoEdit.VideoEditActivity::class.java else org.pixeldroid.media_editor.photoEdit.PhotoEditActivity::class.java
- )
- .putExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_URI, model.getPhotoData().value!![position].imageUri)
- .putExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_POSITION, position)
-
- editResultContract.launch(intent)
-
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt
new file mode 100644
index 00000000..4d24cdbc
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt
@@ -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 {
+ 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 = 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)
+ }
+}
+
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
index 47d54d9c..7ed6e672 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
@@ -2,14 +2,16 @@ package org.pixeldroid.app.postCreation
import android.app.Application
import android.content.ClipData
-import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Parcelable
import android.provider.OpenableColumns
-import androidx.core.content.ContextCompat
+import android.text.Editable
+import android.util.Log
+import android.widget.Toast
import androidx.core.net.toFile
import androidx.core.net.toUri
+import androidx.exifinterface.media.ExifInterface
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@@ -17,18 +19,32 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager
+import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException
+import com.jarsilio.android.scrambler.stripMetadata
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.disposables.Disposable
+import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
+import okhttp3.MultipartBody
+import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R
import org.pixeldroid.app.utils.PixelDroidApplication
+import org.pixeldroid.app.utils.api.objects.Attachment
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
+import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
+import org.pixeldroid.app.utils.fileExtension
import org.pixeldroid.app.utils.getMimeType
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
+import retrofit2.HttpException
import java.io.File
+import java.io.FileNotFoundException
+import java.io.IOException
+import java.net.URI
import javax.inject.Inject
import kotlin.collections.ArrayList
import kotlin.collections.MutableList
@@ -57,7 +73,19 @@ data class PostCreationActivityUiState(
val isCarousel: Boolean = true,
+ val postCreationSendButtonEnabled: Boolean = true,
+
val newPostDescriptionText: String = "",
+ val nsfw: Boolean = false,
+
+ val chosenAccount: UserDatabaseEntity? = null,
+
+ val uploadProgressBarVisible: Boolean = false,
+ val uploadProgress: Int = 0,
+ val uploadCompletedTextviewVisible: Boolean = false,
+ val uploadErrorVisible: Boolean = false,
+ val uploadErrorExplanationText: String = "",
+ val uploadErrorExplanationVisible: Boolean = false,
)
@Parcelize
@@ -74,7 +102,13 @@ data class PhotoData(
var videoEncodeError: Boolean = false,
) : Parcelable
-class PostCreationViewModel(application: Application, clipdata: ClipData? = null, val instance: InstanceDatabaseEntity? = null) : AndroidViewModel(application) {
+class PostCreationViewModel(
+ application: Application,
+ clipdata: ClipData? = null,
+ val instance: InstanceDatabaseEntity? = null,
+ existingDescription: String? = null,
+ existingNSFW: Boolean = false
+) : AndroidViewModel(application) {
private val photoData: MutableLiveData> by lazy {
MutableLiveData>().also {
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)
val sharedPreferences =
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 = _uiState
@@ -169,7 +206,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
val type = uri.getMimeType(getApplication().contentResolver)
val isVideo = type.startsWith("video/")
- if(isVideo && !instance!!.videoEnabled){
+ if (isVideo && !instance!!.videoEnabled) {
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = getApplication().getString(R.string.video_not_supported))
}
@@ -203,15 +240,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) })
- ContextCompat.startActivity(context, intent, null)
- }
-
fun modifyAt(position: Int, data: Intent): Unit? {
val result: PhotoData = photoData.value?.getOrNull(position)?.run {
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){
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)
@@ -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
}
@@ -319,10 +347,213 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
)
}
}
-}
-class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity) : ViewModelProvider.Factory {
- override fun create(modelClass: Class): T {
- return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java).newInstance(application, clipdata, instance)
+ fun resetUploadStatus() {
+ photoData.value = photoData.value?.map { it.copy(uploadId = null, progress = null) }?.toMutableList()
+ }
+
+ /**
+ * Uploads the images that are in the [photoData] array.
+ * Keeps track of them in the [PhotoData.progress] (for the upload progress), and the
+ * [PhotoData.uploadId] (for the list of ids of the uploads).
+ */
+ @OptIn(ExperimentalUnsignedTypes::class)
+ fun upload() {
+ _uiState.update { currentUiState ->
+ currentUiState.copy(
+ postCreationSendButtonEnabled = false,
+ uploadCompletedTextviewVisible = false,
+ uploadErrorVisible = false,
+ uploadProgressBarVisible = true
+ )
+ }
+
+ for (data: PhotoData in getPhotoData().value ?: emptyList()) {
+ val extension = data.imageUri.fileExtension(getApplication().contentResolver)
+
+ val strippedImage = File.createTempFile("temp_img", ".$extension", getApplication().cacheDir)
+
+ val imageUri = data.imageUri
+
+ val (strippedOrNot, size) = try {
+ val orientation = ExifInterface(getApplication().contentResolver.openInputStream(imageUri)!!).getAttributeInt(
+ ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
+
+ stripMetadata(imageUri, strippedImage, getApplication().contentResolver)
+
+ // Restore EXIF orientation
+ val exifInterface = ExifInterface(strippedImage)
+ exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, orientation.toString())
+ exifInterface.saveAttributes()
+
+ Pair(strippedImage.inputStream(), strippedImage.length())
+ } catch (e: UnsupportedFileFormatException){
+ strippedImage.delete()
+ if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
+ val imageInputStream = try {
+ getApplication().contentResolver.openInputStream(imageUri)!!
+ } catch (e: FileNotFoundException){
+ _uiState.update { currentUiState ->
+ currentUiState.copy(
+ userMessage = getApplication().getString(R.string.file_not_found,
+ data.imageUri)
+ )
+ }
+ return
+ }
+ Pair(imageInputStream, data.size)
+ } catch (e: IOException){
+ strippedImage.delete()
+ if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
+ _uiState.update { currentUiState ->
+ currentUiState.copy(
+ userMessage = getApplication().getString(R.string.file_not_found,
+ data.imageUri)
+ )
+ }
+ return
+ }
+
+ val type = data.imageUri.getMimeType(getApplication().contentResolver)
+ val imagePart = ProgressRequestBody(strippedOrNot, size, type)
+ val requestBody = MultipartBody.Builder()
+ .setType(MultipartBody.FORM)
+ .addFormDataPart("file", System.currentTimeMillis().toString(), imagePart)
+ .build()
+
+ val sub = imagePart.progressSubject
+ .subscribeOn(Schedulers.io())
+ .subscribe { percentage ->
+ data.progress = percentage.toInt()
+ _uiState.update { currentUiState ->
+ currentUiState.copy(
+ uploadProgress = getPhotoData().value!!.sumOf { it.progress ?: 0 } / getPhotoData().value!!.size
+ )
+ }
+ }
+
+ var postSub: Disposable? = null
+
+ val description = data.imageDescription?.let { MultipartBody.Part.createFormData("description", it) }
+
+ // Ugly temporary account switching, but it works well enough for now
+ val api = uiState.value.chosenAccount?.let {
+ apiHolder.setToCurrentUser(it)
+ } ?: apiHolder.api ?: apiHolder.setToCurrentUser()
+
+ val inter = api.mediaUpload(description, requestBody.parts[0])
+
+ apiHolder.api = null
+ postSub = inter
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ { attachment: Attachment ->
+ data.progress = 0
+ data.uploadId = attachment.id!!
+ },
+ { e: Throwable ->
+ _uiState.update { currentUiState ->
+ currentUiState.copy(
+ uploadErrorVisible = true,
+ uploadErrorExplanationText = if(e is HttpException){
+ getApplication().getString(R.string.upload_error, e.code())
+ } else "",
+ uploadErrorExplanationVisible = e is HttpException,
+ )
+ }
+ strippedImage.delete()
+ if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
+ e.printStackTrace()
+ postSub?.dispose()
+ sub.dispose()
+ },
+ {
+ strippedImage.delete()
+ if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
+ data.progress = 100
+ if (getPhotoData().value!!.all { it.progress == 100 && it.uploadId != null }) {
+ _uiState.update { currentUiState ->
+ currentUiState.copy(
+ uploadProgressBarVisible = false,
+ uploadCompletedTextviewVisible = true
+ )
+ }
+ post()
+ }
+ postSub?.dispose()
+ sub.dispose()
+ }
+ )
+ }
+ }
+
+ private fun post() {
+ val description = uiState.value.newPostDescriptionText
+
+ // TODO: investigate why this works but booleans don't
+ val nsfw = if (uiState.value.nsfw) 1 else 0
+
+ _uiState.update { currentUiState ->
+ currentUiState.copy(
+ postCreationSendButtonEnabled = false
+ )
+ }
+ viewModelScope.launch {
+ try {
+ //Ugly temporary account switching, but it works well enough for now
+ val api = uiState.value.chosenAccount?.let {
+ apiHolder.setToCurrentUser(it)
+ } ?: apiHolder.api ?: apiHolder.setToCurrentUser()
+
+ api.postStatus(
+ statusText = description,
+ media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList(),
+ sensitive = nsfw
+ )
+ Toast.makeText(getApplication(), getApplication().getString(R.string.upload_post_success),
+ Toast.LENGTH_SHORT).show()
+ val intent = Intent(getApplication(), MainActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ //TODO make the activity launch this instead (and surrounding toasts too)
+ getApplication().startActivity(intent)
+ } catch (exception: IOException) {
+ Toast.makeText(getApplication(), getApplication().getString(R.string.upload_post_error),
+ Toast.LENGTH_SHORT).show()
+ Log.e(TAG, exception.toString())
+ _uiState.update { currentUiState ->
+ currentUiState.copy(
+ postCreationSendButtonEnabled = true
+ )
+ }
+ } catch (exception: HttpException) {
+ Toast.makeText(getApplication(), getApplication().getString(R.string.upload_post_failed),
+ Toast.LENGTH_SHORT).show()
+ Log.e(TAG, exception.response().toString() + exception.message().toString())
+ _uiState.update { currentUiState ->
+ currentUiState.copy(
+ postCreationSendButtonEnabled = true
+ )
+ }
+ } finally {
+ apiHolder.api = null
+ }
+ }
+ }
+
+ fun newPostDescriptionChanged(text: Editable?) {
+ _uiState.update { it.copy(newPostDescriptionText = text.toString()) }
+ }
+
+ fun updateNSFW(checked: Boolean) { _uiState.update { it.copy(nsfw = checked) } }
+
+ fun chooseAccount(which: UserDatabaseEntity) {
+ _uiState.update { it.copy(chosenAccount = which) }
+ }
+}
+
+class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity, val existingDescription: String?, val existingNSFW: Boolean) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): 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)
}
}
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionActivity.kt
deleted file mode 100644
index 6ead56d7..00000000
--- a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionActivity.kt
+++ /dev/null
@@ -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
- private var selectedAccount: Int = -1
- private lateinit var menu: Menu
- private var user: UserDatabaseEntity? = null
- private lateinit var instance: InstanceDatabaseEntity
-
- private lateinit var binding: ActivityPostSubmissionBinding
-
- private lateinit var model: PostSubmissionViewModel
-
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityPostSubmissionBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- supportActionBar?.setTitle(R.string.add_details)
-
- user = db.userDao().getActiveUser()
- accounts = db.userDao().getAll()
-
- instance = user?.run {
- db.instanceDao().getAll().first { instanceDatabaseEntity ->
- instanceDatabaseEntity.uri.contains(instance_uri)
- }
- } ?: InstanceDatabaseEntity("", "")
-
- val photoData = intent.getParcelableArrayListExtra(PHOTO_DATA) as ArrayList?
-
- val _model: PostSubmissionViewModel by viewModels {
- PostSubmissionViewModelFactory(
- application,
- photoData!!
- )
- }
- 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
- }
-
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt
new file mode 100644
index 00000000..bda9bac7
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt
@@ -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
+ 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
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionViewModel.kt
index 816046f1..aab0f2f2 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionViewModel.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionViewModel.kt
@@ -54,7 +54,7 @@ data class PostSubmissionActivityUiState(
val uploadErrorExplanationVisible: Boolean = false,
)
-class PostSubmissionViewModel(application: Application, photodata: ArrayList? = null) : AndroidViewModel(application) {
+class PostSubmissionViewModel(application: Application, photodata: ArrayList? = null, val existingDescription: String? = null) : AndroidViewModel(application) {
private val photoData: MutableLiveData> by lazy {
MutableLiveData>().also {
if (photodata != null) {
@@ -74,7 +74,7 @@ class PostSubmissionViewModel(application: Application, photodata: ArrayList = _uiState
@@ -235,7 +235,7 @@ class PostSubmissionViewModel(application: Application, photodata: ArrayList
currentUiState.copy(
@@ -307,8 +307,8 @@ class PostSubmissionViewModel(application: Application, photodata: ArrayList) : ViewModelProvider.Factory {
+class PostSubmissionViewModelFactory(val application: Application, val photoData: ArrayList, val existingDescription: String?) : ViewModelProvider.Factory {
override fun create(modelClass: Class): 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)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt
index 51bf07b0..715c4ea1 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt
@@ -81,6 +81,7 @@ class CameraFragment : BaseFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
+ super.onCreateView(inflater, container, savedInstanceState)
inActivity = arguments?.getBoolean("CameraActivity") ?: false
binding = FragmentCameraBinding.inflate(layoutInflater)
diff --git a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt
index 8fbc370a..1476b124 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt
@@ -8,7 +8,6 @@ import android.content.Intent
import android.graphics.Typeface
import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
-import android.location.GnssAntennaInfo.Listener
import android.net.Uri
import android.os.Looper
import android.text.method.LinkMovementMethod
@@ -479,24 +478,25 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
val postDescription = status?.content ?: ""
val postAttachments =
status?.media_attachments!! // Catch possible exception from !! (?)
- val imageUris: MutableList = mutableListOf()
- val imageNames: MutableList = mutableListOf()
- val imageDescriptions: MutableList =
- mutableListOf()
+ val postNSFW = status?.sensitive
- for (currentAttachment in postAttachments) {
- val imageUri = currentAttachment.url ?: ""
- val imageName =
- Uri.parse(imageUri).lastPathSegment.toString()
- val imageDescription =
- currentAttachment.description ?: ""
- val downloadedFile =
- File(context.cacheDir, imageName)
- val downloadedUri = Uri.fromFile(downloadedFile)
-
- imageUris.add(downloadedUri)
- imageNames.add(imageName)
- imageDescriptions.add(imageDescription)
+ val imageUriStrings = postAttachments.map { postAttachment ->
+ postAttachment.url ?: ""
+ }
+ val imageNames = imageUriStrings.map { imageUriString ->
+ Uri.parse(imageUriString).lastPathSegment.toString()
+ }
+ val downloadedFiles = imageNames.map { imageName ->
+ File(context.cacheDir, imageName)
+ }
+ val imageUris = downloadedFiles.map { downloadedFile ->
+ Uri.fromFile(downloadedFile)
+ }
+ val imageDescriptions = postAttachments.map { postAttachment ->
+ fromHtml(postAttachment.description ?: "").toString()
+ }
+ val downloadRequests: List = imageUriStrings.map { imageUriString ->
+ Request.Builder().url(imageUriString).build()
}
val counter = AtomicInteger(0)
@@ -506,6 +506,11 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
// Wait for all outstanding downloads to finish
if (counter.incrementAndGet() == imageUris.size) {
if (allFilesExist(imageNames)) {
+ // Delete original post
+ lifecycleScope.launch {
+ deletePost(apiHolder.api ?: apiHolder.setToCurrentUser(), db)
+ }
+
val counterInt = counter.get()
Toast.makeText(
binding.root.context,
@@ -518,17 +523,9 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
).show()
// Pass downloaded images to new post creation activity
intent.apply {
- assert(imageUris.size == imageDescriptions.size)
-
- for (i in 0 until imageUris.size) {
- val imageUri = imageUris[i]
- val imageDescription =
- fromHtml(imageDescriptions[i]).toString()
- val imageItem = ClipData.Item(
- imageDescription,
- null,
- imageUri
- )
+ imageUris.zip(imageDescriptions).map { (imageUri, imageDescription) ->
+ ClipData.Item(imageDescription, null, imageUri)
+ }.forEach { imageItem ->
if (clipData == null) {
clipData = ClipData(
"",
@@ -539,7 +536,6 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
clipData!!.addItem(imageItem)
}
}
-
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
@@ -559,6 +555,10 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
PostCreationActivity.POST_REDRAFT,
true
)
+ intent.putExtra(
+ PostCreationActivity.POST_NSFW,
+ postNSFW
+ )
// Launch post creation activity
binding.root.context.startActivity(intent)
@@ -576,15 +576,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
}
// Iterate through all pictures of the original post
- for (currentAttachment in postAttachments) {
- 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()
-
+ downloadRequests.zip(downloadedFiles).forEach { (downloadRequest, downloadedFile) ->
// Check whether image is in cache directory already (maybe rather do so using Glide in the future?)
if (!downloadedFile.exists()) {
OkHttpClient().newCall(downloadRequest)
@@ -619,7 +611,6 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
continuation()
}
}
-
} catch (exception: HttpException) {
Toast.makeText(
binding.root.context,
@@ -636,13 +627,6 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
Toast.LENGTH_SHORT
).show()
}
-
- // Delete original post
- deletePost(
- apiHolder.api ?: apiHolder.setToCurrentUser(),
- db
- )
-
}
}
setNegativeButton(android.R.string.cancel) { _, _ -> }
@@ -831,14 +815,10 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
}
}
- private fun allFilesExist(listOfNames: MutableList): Boolean {
- for (name in listOfNames) {
- val file = File(binding.root.context.cacheDir, name)
- if (!file.exists()) {
- return false
- }
+ private fun allFilesExist(listOfNames: List): Boolean {
+ return listOfNames.all {
+ File(binding.root.context.cacheDir, it).exists()
}
- return true
}
companion object {
diff --git a/app/src/main/java/org/pixeldroid/app/profile/ProfilePostsRecyclerViewAdapter.kt b/app/src/main/java/org/pixeldroid/app/profile/ProfilePostsRecyclerViewAdapter.kt
index 5a33a222..9e449652 100644
--- a/app/src/main/java/org/pixeldroid/app/profile/ProfilePostsRecyclerViewAdapter.kt
+++ b/app/src/main/java/org/pixeldroid/app/profile/ProfilePostsRecyclerViewAdapter.kt
@@ -1,12 +1,22 @@
package org.pixeldroid.app.profile
-import android.view.View
+import android.view.LayoutInflater
+import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
-import org.pixeldroid.app.R
+import org.pixeldroid.app.databinding.FragmentProfilePostsBinding
-class ProfilePostViewHolder(val postView: View) : RecyclerView.ViewHolder(postView) {
- val postPreview: ImageView = postView.findViewById(R.id.postPreview)
- val albumIcon: ImageView = postView.findViewById(R.id.albumIcon)
- val videoIcon: ImageView = postView.findViewById(R.id.albumIcon)
+class ProfilePostViewHolder(val postView: FragmentProfilePostsBinding) : RecyclerView.ViewHolder(postView.root) {
+ val postPreview: ImageView = postView.postPreview
+ val albumIcon: ImageView = postView.albumIcon
+ val videoIcon: ImageView = postView.videoIcon
+
+ companion object {
+ fun create(parent: ViewGroup): ProfilePostViewHolder {
+ val itemBinding = FragmentProfilePostsBinding.inflate(
+ LayoutInflater.from(parent.context), parent, false
+ )
+ return ProfilePostViewHolder(itemBinding)
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/searchDiscover/TrendingActivity.kt b/app/src/main/java/org/pixeldroid/app/searchDiscover/TrendingActivity.kt
index e05d9eeb..44f474ff 100644
--- a/app/src/main/java/org/pixeldroid/app/searchDiscover/TrendingActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/searchDiscover/TrendingActivity.kt
@@ -1,8 +1,8 @@
package org.pixeldroid.app.searchDiscover
+import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
-import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
@@ -19,6 +19,7 @@ import org.pixeldroid.app.utils.BaseThemedWithBarActivity
import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.Attachment
+import org.pixeldroid.app.utils.api.objects.FeedContent
import org.pixeldroid.app.utils.api.objects.Status
import org.pixeldroid.app.utils.api.objects.Tag
import org.pixeldroid.app.utils.setSquareImageFromURL
@@ -27,44 +28,41 @@ import java.io.IOException
class TrendingActivity : BaseThemedWithBarActivity() {
- private lateinit var api: PixelfedAPI
private lateinit var binding: ActivityTrendingBinding
- private lateinit var recycler : RecyclerView
- private lateinit var discoverAdapter : DiscoverRecyclerViewAdapter
- private lateinit var hashtagsAdapter : HashtagsRecyclerViewAdapter
- private lateinit var accountsAdapter : AccountsRecyclerViewAdapter
+ private lateinit var trendingAdapter : TrendingRecyclerViewAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTrendingBinding.inflate(layoutInflater)
setContentView(binding.root)
- api = apiHolder.api ?: apiHolder.setToCurrentUser()
- recycler = binding.list
+ val recycler = binding.list
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val type = intent.getSerializableExtra(TRENDING_TAG) as TrendingType? ?: TrendingType.POSTS
- if(type == TrendingType.POSTS || type == TrendingType.DISCOVER) {
- // Set posts RecyclerView as a grid with 3 columns
- recycler.layoutManager = GridLayoutManager(this, 3)
- discoverAdapter = DiscoverRecyclerViewAdapter()
- recycler.adapter = discoverAdapter
- if(type == TrendingType.POSTS) {
- supportActionBar?.setTitle(R.string.trending_posts)
- } else {
- supportActionBar?.setTitle(R.string.discover)
+ when (type) {
+ TrendingType.POSTS, TrendingType.DISCOVER -> {
+ // Set posts RecyclerView as a grid with 3 columns
+ recycler.layoutManager = GridLayoutManager(this, 3)
+ supportActionBar?.setTitle(
+ if (type == TrendingType.POSTS) {
+ R.string.trending_posts
+ } else {
+ R.string.discover
+ }
+ )
+ this.trendingAdapter = DiscoverRecyclerViewAdapter()
+ }
+ TrendingType.HASHTAGS -> {
+ supportActionBar?.setTitle(R.string.trending_hashtags)
+ this.trendingAdapter = HashtagsRecyclerViewAdapter()
+ }
+ TrendingType.ACCOUNTS -> {
+ supportActionBar?.setTitle(R.string.popular_accounts)
+ this.trendingAdapter = AccountsRecyclerViewAdapter()
}
}
- if(type == TrendingType.HASHTAGS) {
- supportActionBar?.setTitle(R.string.trending_hashtags)
- hashtagsAdapter = HashtagsRecyclerViewAdapter()
- recycler.adapter = hashtagsAdapter
- }
- if(type == TrendingType.ACCOUNTS) {
- supportActionBar?.setTitle(R.string.popular_accounts)
- accountsAdapter = AccountsRecyclerViewAdapter()
- recycler.adapter = accountsAdapter
- }
+ recycler.adapter = this.trendingAdapter
getTrending(type)
binding.refreshLayout.setOnRefreshListener {
@@ -76,6 +74,7 @@ class TrendingActivity : BaseThemedWithBarActivity() {
binding.motionLayout.apply {
if(show){
transitionToEnd()
+ binding.errorLayout.errorText.setText(errorText)
} else {
transitionToStart()
}
@@ -87,25 +86,14 @@ class TrendingActivity : BaseThemedWithBarActivity() {
private fun getTrending(type: TrendingType) {
lifecycleScope.launchWhenCreated {
try {
- when(type) {
- TrendingType.POSTS -> {
- val trendingPosts = api.trendingPosts("daily")
- discoverAdapter.addPosts(trendingPosts)
- }
- TrendingType.HASHTAGS -> {
- val trendingTags = api.trendingHashtags()
- .map { it.copy(name = it.name.removePrefix("#")) }
- hashtagsAdapter.addHashtags(trendingTags)
- }
- TrendingType.ACCOUNTS -> {
- val trendingAccounts = api.popularAccounts()
- accountsAdapter.addAccounts(trendingAccounts)
- }
- TrendingType.DISCOVER -> {
- val posts = api.discover().posts
- discoverAdapter.addPosts(posts)
- }
+ val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser()
+ val content: List = when(type) {
+ TrendingType.POSTS -> api.trendingPosts(Range.daily)
+ TrendingType.HASHTAGS -> api.trendingHashtags().map { it.copy(name = it.name.removePrefix("#")) }
+ TrendingType.ACCOUNTS -> api.popularAccounts()
+ TrendingType.DISCOVER -> api.discover().posts
}
+ trendingAdapter.addPosts(content)
showError(show = false)
} catch (exception: IOException) {
showError()
@@ -116,25 +104,32 @@ class TrendingActivity : BaseThemedWithBarActivity() {
}
/**
- * [RecyclerView.Adapter] that can display a list of [Status]s' thumbnails for the discover view
+ * Abstract class for the different RecyclerViewAdapters used in this activity
*/
- class DiscoverRecyclerViewAdapter: RecyclerView.Adapter() {
- private val posts: ArrayList = ArrayList()
+ abstract class TrendingRecyclerViewAdapter: RecyclerView.Adapter(){
+ val data: ArrayList = ArrayList()
- fun addPosts(newPosts : List) {
- posts.clear()
- posts.addAll(newPosts)
+ @SuppressLint("NotifyDataSetChanged")
+ fun addPosts(newPosts: List){
+ data.clear()
+ data.addAll(newPosts)
notifyDataSetChanged()
}
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfilePostViewHolder {
- val view = LayoutInflater.from(parent.context)
- .inflate(R.layout.fragment_profile_posts, parent, false)
- return ProfilePostViewHolder(view)
- }
+ override fun getItemCount(): Int = data.size
+ }
- override fun onBindViewHolder(holder: ProfilePostViewHolder, position: Int) {
- val post = posts[position]
+ /**
+ * [RecyclerView.Adapter] that can display a list of [Status]s' thumbnails for the discover view
+ */
+ class DiscoverRecyclerViewAdapter: TrendingRecyclerViewAdapter() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfilePostViewHolder =
+ ProfilePostViewHolder.create(parent)
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ if (holder !is ProfilePostViewHolder) return
+
+ val post = data[position] as? Status
if((post?.media_attachments?.size ?: 0) > 1) {
holder.albumIcon.visibility = View.VISIBLE
} else {
@@ -144,15 +139,14 @@ class TrendingActivity : BaseThemedWithBarActivity() {
} else holder.videoIcon.visibility = View.GONE
}
- setSquareImageFromURL(holder.postView, post?.getPostPreviewURL(), holder.postPreview, post?.media_attachments?.firstOrNull()?.blurhash)
+ setSquareImageFromURL(holder.postView.root, post?.getPostPreviewURL(), holder.postPreview, post?.media_attachments?.firstOrNull()?.blurhash)
holder.postPreview.setOnClickListener {
- val intent = Intent(holder.postView.context, PostActivity::class.java)
+ val intent = Intent(holder.postView.root.context, PostActivity::class.java)
intent.putExtra(Status.POST_TAG, post)
- holder.postView.context.startActivity(intent)
+ holder.postView.root.context.startActivity(intent)
}
}
- override fun getItemCount(): Int = posts.size
}
companion object {
@@ -161,55 +155,38 @@ class TrendingActivity : BaseThemedWithBarActivity() {
enum class TrendingType {
POSTS, HASHTAGS, ACCOUNTS, DISCOVER
}
+
+ @Suppress("EnumEntryName", "unused")
+ enum class Range {
+ daily, monthly, yearly
+ }
}
/**
* [RecyclerView.Adapter] that can display a list of [Tag]s for the trending view
*/
- class HashtagsRecyclerViewAdapter: RecyclerView.Adapter() {
- private val tags: ArrayList = ArrayList()
+ class HashtagsRecyclerViewAdapter: TrendingRecyclerViewAdapter() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HashTagViewHolder =
+ HashTagViewHolder.create(parent)
- fun addHashtags(newTags : List) {
- tags.clear()
- tags.addAll(newTags)
- notifyDataSetChanged()
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ val tag = data[position] as Tag
+ (holder as HashTagViewHolder).bind(tag)
}
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HashTagViewHolder {
- return HashTagViewHolder.create(parent)
- }
-
- override fun onBindViewHolder(holder: HashTagViewHolder, position: Int) {
- val tag = tags[position]
- holder.bind(tag)
- }
-
- override fun getItemCount(): Int = tags.size
}
/**
* [RecyclerView.Adapter] that can display a list of [Account]s for the popular view
*/
- class AccountsRecyclerViewAdapter: RecyclerView.Adapter() {
- private val accounts: ArrayList = ArrayList()
+ class AccountsRecyclerViewAdapter: TrendingRecyclerViewAdapter() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder =
+ AccountViewHolder.create(parent)
- fun addAccounts(newAccounts : List) {
- accounts.clear()
- accounts.addAll(newAccounts)
- notifyDataSetChanged()
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ val account = data[position] as? Account
+ (holder as AccountViewHolder).bind(account)
}
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder {
- return AccountViewHolder.create(parent)
- }
-
- override fun onBindViewHolder(holder: AccountViewHolder, position: Int) {
- val account = accounts[position]
- holder.bind(account)
- }
-
- override fun getItemCount(): Int = accounts.size
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt
index 5313f130..4e8db5fa 100644
--- a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt
+++ b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt
@@ -7,6 +7,7 @@ import okhttp3.Interceptor
import org.pixeldroid.app.utils.api.objects.*
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
+import org.pixeldroid.app.searchDiscover.TrendingActivity
import org.pixeldroid.app.utils.api.objects.Collection
import org.pixeldroid.app.utils.api.objects.Tag
import org.pixeldroid.app.utils.db.AppDatabase
@@ -367,7 +368,7 @@ interface PixelfedAPI {
@GET("/api/v1.1/discover/posts/trending")
suspend fun trendingPosts(
- @Query("range") range: String
+ @Query("range") range: TrendingActivity.Companion.Range
) : List
@GET("/api/v1.1/discover/posts/hashtags")
diff --git a/app/src/main/res/drawable/switch_account.xml b/app/src/main/res/drawable/switch_account.xml
new file mode 100644
index 00000000..ace366b2
--- /dev/null
+++ b/app/src/main/res/drawable/switch_account.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_post_creation.xml b/app/src/main/res/layout/activity_post_creation.xml
index 57d5def9..98f621cc 100644
--- a/app/src/main/res/layout/activity_post_creation.xml
+++ b/app/src/main/res/layout/activity_post_creation.xml
@@ -6,96 +6,16 @@
android:layout_height="match_parent"
tools:context=".postCreation.PostCreationActivity">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:navGraph="@navigation/post_creation_graph" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_trending.xml b/app/src/main/res/layout/activity_trending.xml
index b38b6bd4..784b355e 100644
--- a/app/src/main/res/layout/activity_trending.xml
+++ b/app/src/main/res/layout/activity_trending.xml
@@ -38,7 +38,9 @@
android:layout_height="match_parent"
app:layoutDescription="@xml/error_layout_xml_error_scene">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_post_submission.xml b/app/src/main/res/layout/fragment_post_submission.xml
similarity index 89%
rename from app/src/main/res/layout/activity_post_submission.xml
rename to app/src/main/res/layout/fragment_post_submission.xml
index dec6867b..4c418050 100644
--- a/app/src/main/res/layout/activity_post_submission.xml
+++ b/app/src/main/res/layout/fragment_post_submission.xml
@@ -4,7 +4,18 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".postCreation.PostSubmissionActivity">
+ tools:context=".postCreation.PostSubmissionFragment"
+ android:id="@+id/postSubmissionFragment" >
+
+
+ app:layout_constraintTop_toBottomOf="@id/top_bar"/>
+ app:layout_constraintTop_toTopOf="parent" />
-
-
diff --git a/app/src/main/res/menu/post_submission_account_menu.xml b/app/src/main/res/menu/post_submission_account_menu.xml
index 4841e6bb..8b5c395d 100644
--- a/app/src/main/res/menu/post_submission_account_menu.xml
+++ b/app/src/main/res/menu/post_submission_account_menu.xml
@@ -6,6 +6,6 @@
android:id="@+id/action_switch_accounts"
android:orderInCategory="100"
android:title="@string/switch_accounts"
- android:icon="@drawable/material_drawer_ico_account"
+ android:icon="@drawable/switch_account"
app:showAsAction="ifRoom"/>
\ No newline at end of file
diff --git a/app/src/main/res/navigation/post_creation_graph.xml b/app/src/main/res/navigation/post_creation_graph.xml
new file mode 100644
index 00000000..0ae88742
--- /dev/null
+++ b/app/src/main/res/navigation/post_creation_graph.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 49723d7b..915342e4 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -6,7 +6,7 @@
Ungültige Domäne
Der Browser konnte nicht gestartet werden, haben Sie einen\?
Authentifizierung fehlgeschlagen
- Beim Erhalten des Token ist ein Fehler aufgetreten
+ Fehler beim Abruf des Tokens
Einstellungen
Erscheinungsbild
%1$s folgt dir
@@ -33,14 +33,14 @@
Mit Pixelfed verbinden
Sie müssen online sein, um das erste Konto hinzuzufügen und um PixelDroid zu verwenden :(
Kamera wechseln
- Konnte die App nicht mit diesem Server verbinden
+ App konnte sich nicht mit diesem Server verbinden
Konnte die Informationen der Instanz nicht abrufen
Fehler beim Hochladen!
Standard (Systemeinstellung)
Bild kann nicht gespeichert werden
Bild erfolgreich gespeichert
Upload-Fehler: falsches Bildformat.
- Das Hochladen des Beitrags ist fehlgeschlagen
+ Hochladen des Beitrags fehlgeschlagen
Konto hinzufügen
Weiteres Pixelfed-Konto hinzufügen
Hell
@@ -57,12 +57,12 @@
Beim Hochladen des Beitrags ist ein Fehler aufgetreten
Kommentarfehler!
Kommentar: %1$s geschrieben!
- Konnte Knopf zum Folgen nicht anzeigen
+ Schaltfläche \"Folgen\" konnte nicht angezeigt werden
Konnte nicht folgen
Kein Benutzername
-
-\nFolgend
- Diese Handlung ist nicht erlaubt
+\nFolgt
+ Diese Aktion ist nicht zulässig
Konnte nicht entfolgen
Dieser Zugangstoken ist ungültig
-
@@ -89,8 +89,8 @@
Mehr Optionen
Suchanfrage darf nicht leer sein
%1$s folgt
- %1$s Follower
- %1$s Beitrag
+ %1$s\'s Follower
+ %1$s\'s Beitrag
Über
PixelDroid ist freie und quelloffene Software lizenziert unter der GNU General Public License (Version 3 oder höher)
Projektwebseite: https://pixeldroid.org
@@ -105,18 +105,18 @@
Wechsle zur Rasteransicht
Das Bild zeigt einen roten Panda, das Maskottchen von Pixelfed, der ein Telefon benutzt
Wechsel zur Karussellansicht
- Melde Probleme oder hilf mit bei der Entwicklung der Anwendung:
- Helfe PixelDroid in deine Sprache zu übersetzen:
+ Melde Probleme oder helfe bei der Entwicklung der Anwendung:
+ Helfe dabei PixelDroid in deine Sprache zu übersetzen:
Sprache
Beitrag löschen\?
Löschen
Der Panda ist nicht glücklich. Ziehe um erneut zu laden.
Etwas ist schiefgelaufen…
- ENTDECKE
+ Entdecke
Öffne Menü
Profilbild
- Konnte nicht gemeldet werden
- Post wurde gemeldet
+ Beitrag konnte nicht gemeldet werden
+ Beitrag wurde gemeldet
Melde den Beitrag von @%1$s
Dieser Beitrag ist ein Album
Kommentar senden
@@ -133,7 +133,7 @@
- %d Kommentar
- %d Kommentare
- Ergänze hier eine Medienbeschreibung hier…
+ Füge hier eine Medienbeschreibung hinzu…
Bildbeschreibung speichern
- Die Beschreibung muss mindestens %d Zeichen enthalten.
@@ -150,13 +150,15 @@
Followeranfrage zurückziehen\?
Hier gibt es nichts zu sehen :(
- - Folgt %d
- - Folgt %d
+ - %d
+\nFolge ich
+ - %d
+\nFolge ich
- - %d
+
- %d
\nFollower
- - %d
+
- %d
\nFollower
@@ -168,7 +170,7 @@
- %d Gefällt-Mirs
Serverfehler: %1$d
- Die Größe von Bild %1$d im Album übersteigt mit %2$d kB die von deiner Instanz festgelegte Obergrenze von zulässige Obergrenze von %3$d kB je Bild.
+ Die Größe von Bild %1$d im Album übersteigt mit %2$d kB die von deiner Instanz festgelegte Obergrenze von %3$d kB je Bild.
Du hast mehr Bilder ausgewählt, als auf deinem Server zulässig sind (%1$s). Bilder jenseits des Limits wurden nicht berücksichtigt.
Die API ist auf deiner Instanz nicht aktiv. Bitte kontaktiere den Betreiber deiner Instanz, damit sie aktiviert werden kann.
Followeranfrage
@@ -176,7 +178,7 @@
Das Öffnen der Bearbeitungsseite ist gescheitert
#%1$s
Die Einwilligung die Kamera zu nutzen wurde nicht erteilt. Erlaube die Kameranutzung in den Einstellungen, wenn du die Kamera in PixelDroid verwenden willst
- Die Erlaubnis, auf die Dateien zuzugreifen wurde nicht erteilt. Erlaube den Zugriff auf Daten wenn du PixelDroid das Thumbnail anzeigen lassen willst
+ Die Erlaubnis auf Speichermedien zuzugreifen wurde nicht erteilt. Erlaube den Zugriff wenn du PixelDroid das Thumbnail anzeigen lassen willst
Datei %1$s wurde nicht gefunden
Benachrichtigung von %1$s
Neue Follower
@@ -197,16 +199,16 @@
%1$s, %2$s und %3$s
%1$s und %2$s
- Der von dir verwendete Server unterstützt keine Video-Uploads, daher wirst du die in diesem Beitrag enthaltende Videos möglicherweise nicht hochladen können
+ Der von dir verwendete Server unterstützt keine Video-Uploads, daher wirst du die in diesem Beitrag enthaltenen Videos möglicherweise nicht hochladen können
Dieser Beitrag ist ein Video
Video abspielen
Neuen Beitrag erstellen
Neuer Beitrag
%1$s hat einen Beitrag erstellt
- Fehler-Kodierung
+ Bei der Kodierung ist ein Fehler aufgetreten
Kodierung war erfolgreich!
- Kodierung %1$d%%
- Mindestens ein Video wird noch kodiert. Bitte warten bis alle fertig sind, bevor sie die Videos hochladen
+ Kodiere %1$d%%
+ Mindestens ein Video wird noch kodiert. Vor dem Hochladen bitte warten bis alle Vorgänge abgeschlossen sind
%1$s möchte dir folgen
Start
Suche
@@ -214,7 +216,7 @@
Öffentlich
Farbakzent
Farbakzent auswählen
- Auswählter Farbakzent
+ Ausgewählter Farbakzent
Erstellen
Diesen Farbakzent auswählen
@@ -224,6 +226,62 @@
Profil konnte nicht geladen werden
von %1$s
Fehler beim Hinzufügen der Bilder
- Vorschaubild dieser Benachrichtigung
- Vorschau eines Betrags
-
\ No newline at end of file
+ Vorschau des Bildes im Beitrag dieser Benachrichtigung
+ Vorschau eines Beitrags
+ Nächster Schritt
+ %1$s\'s Sammlung
+ Lesezeichen
+ Lesezeichen entfernen
+ Beliebte Konten
+ Bei Abbruch der Überarbeitung wird der ursprüngliche Beitrag nicht mehr vorhanden sein. Wirklich ohne Wiederveröffentlichung fortsetzen\?
+ Deine Änderungen wurden nicht gespeichert. Wirklich beenden\?
+ Wenn Dein Konto privat ist können andere nur mit Deiner Genehmigung Deine Fotos und Videos auf Pixelfed sehen. Bestehende Follower sind davon nicht betroffen.
+ Beitrag konnte nicht überarbeitet werden, Fehler %1$d
+ Überarbeiten
+ Die Überarbeitung dieses Beitrags ermöglicht Dir das Foto und seine Beschreibung zu ändern, aber alle aktuellen Kommentare und Likes werden gelöscht. Fortsetzen\?
+ Konnte Lesezeichen für den Beitrag nicht hinzufügen/entfernen, Fehler %1$d
+ Füge ein paar Details hinzu
+ Die Seite zum Erstellen der Sammlung konnte nicht geöffnet werden
+ Beitrag konnte nicht überarbeitet werden, besteht eine Internet-Verbindung\?
+ Konto wechseln
+ Dein Profil wird abgerufen...
+ Dein Name
+ Informationen über Dich
+ Privates Konto
+ Erweiterte Profil-Einstellungen
+ Verwende dynamische Farben von Deinem System
+ Speichern
+ Konnte Lesezeichen für den Beitrag nicht hinzufügen/entfernen, besteht eine Internet-Verbindung\?
+ %1$d%% für die Stabilisierung analysieren
+ Vorgefertigte Beschreibung für neue Beiträge
+ Beschreibungs-Vorlage
+ Entdecke beliebte Konten auf dieser Instanz
+ Entdecke beliebte Hashtags auf dieser Instanz
+ Beliebte Hashtags
+ Entdecke beliebte Beiträge des Tages
+ Beliebte Beiträge
+ Entdecke zufällige Beiträge des Tages
+ Rasteransicht
+ Feed-Ansicht
+ Lesezeichen
+ Sammlungen
+ Sammlung löschen
+ Beitrag erstellen
+ Beitrag löschen
+ Bist Du sicher dass Du diese Sammlung löschen willst\?
+ Wähle den hinzuzufügenden Beitrag aus
+ Beitrag zur Sammlung hinzugefügt
+ Beitrag konnte der Sammlung nicht hinzugefügt werden
+ Beitrag von der Sammlung entfernt
+ Änderungen gespeichert!
+ Etwas ist schief gelaufen. Tippe um es erneut zu versuchen
+ Ändere Dein Profilbild
+ Enthält NSFW-Medien
+ Dein Profil wird gespeichert
+ Wähle den zu löschenden Beitrag aus
+ Beitrag konnte nicht von der Sammlung entfernt werden
+
+ - %d Element erfolgreich geladen
+ - %d Elemente erfolgreich geladen
+
+
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 8d07bab9..fb32e226 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -197,7 +197,7 @@
%1$s udostępnił post
Miniatura zdjęcia w powiadomieniu o tym poście
- ODKRYJ
+ Odkrywanie
Możesz być zdezorientowany polem tekstowym pytającym o domenę twojej „instancji”.
\n
\nPixelfed to sfederowana platforma będąca częścią „Fediwersum”, co oznacza, że może ona komunikować się z innymi platformami tego „świata”, na przykład z Mastodonem (zobacz https://joinmastodon.org).
@@ -252,4 +252,35 @@
Z %1$s
Błąd podczas dodawania zdjęcia
Podgląd posta
-
+ Następny krok
+
+ - Dodanie elementu %d zakończone sukcesem
+ - Dodanie %d elementów zakończone sukcesem
+ - Ładowanie zakończone
+ - Ładowanie zakończone
+
+ Wzorzec opisu
+ Hasztagi zyskujące popularność
+ Wybierz co chcesz usunąć
+ Użyj dynamicznych kolorów z systemu
+ Dodaj szczegóły
+ Dodaj do zakładek
+ Usuń z zakładek
+ Szukaj popularnych kont na tej instancji
+ Popularne konta
+ Widok w siatce
+ Zakładki
+ Kolekcje
+ Usuń kolekcję
+ Dodaj wpis
+ Usuń wpis
+ Czy na pewno chcesz usunąć kolekcję\?
+ Wybierz co chcesz dodać
+ Dodany do kolekcji
+ Błąd dodawania do kolekcji
+ Błąd usuwania z kolekcji
+ Usunięto z kolekcji
+ Zapisz
+ Więcej ustawień profilu
+ Konto prywatne
+
\ No newline at end of file
diff --git a/fastlane/metadata/android/de/changelogs/17.txt b/fastlane/metadata/android/de/changelogs/17.txt
index f5b11dc6..806b2c3d 100644
--- a/fastlane/metadata/android/de/changelogs/17.txt
+++ b/fastlane/metadata/android/de/changelogs/17.txt
@@ -1,8 +1,8 @@
-* Verbesserungen bei den Kommentaren: Sie können jetzt einen Kommentar öffnen, um Antworten zu sehen, ihn zu liken, die Kommentare zeigen den Avatar des Verfassers, usw.
-* Aktualisierungen der Übersetzungen. Vielen Dank an die Übersetzer :). Hilf gerne mit, PixelDroid in deine Sprache zu übersetzen auf weblate.pixeldroid.org
-* Sicherheit: Abhängigkeitsüberprüfung stellt sicher, dass die in der App enthaltenen Abhängigkeiten nicht manipuliert wurden, PixelDroid verweigert nun jede Nicht-HTTPS-Verbindung
-* PixelDroid verwendet nun einen eigenen "PixelDroid"-Benutzer-Agenten anstelle des OkHttp-Bibliotheks-Agenten.
-* Alle hartkodierten Zeichenketten wurden aus der App entfernt, normalerweise ist jetzt alles übersetzbar.
-* Einige Verbesserungen am Code
-* Behebt das Speichern von Bildern, die aus der Bibliothek kommen oder für die App freigegeben sind.
-* Behebung eines weiteren Absturzes bei der Verwendung von Mastodon-Instanzen
+* Verbesserungen bei den Kommentaren: Öffnen um Antworten zu sehen, zu liken, außerdem zeigen sie den Avatar des Verfassers usw.
+* Aktualisierungen der Übersetzungen. Vielen Dank an die Übersetzer :) Hilf gerne mit PixelDroid in Deine Sprache zu übersetzen: weblate.pixeldroid.org
+* Sicherheit: Abhängigkeitsüberprüfung stellt sicher, dass die in der App enthaltenen Abhängigkeiten nicht manipuliert wurden, PixelDroid verweigert nun jede Nicht-HTTPS-Verbindung.
+* PixelDroid verwendet nun einen eigenen "PixelDroid"-Benutzer-Agenten anstelle des OkHttp-Bibliotheks-Agenten.
+* Alle hartkodierten Zeichenketten wurden aus der App entfernt, alles sollte nun übersetzbar sein.
+* Einige Verbesserungen am Code
+* Behebt das Speichern von Bildern, die aus der Bibliothek kommen oder für die App freigegeben sind.
+* Absturz bei der Verwendung von Mastodon-Instanzen behoben
diff --git a/fastlane/metadata/android/de/changelogs/18.txt b/fastlane/metadata/android/de/changelogs/18.txt
new file mode 100644
index 00000000..f80d1588
--- /dev/null
+++ b/fastlane/metadata/android/de/changelogs/18.txt
@@ -0,0 +1,11 @@
+*Benutzerdefinierte Fehlergrafik Roter Panda hinzugefügt
+
+* Freies Zuschneiden in der Bildbearbeitung
+
+* F-Droid-Metadaten verbessert
+
+* Übersetzungsaktualisierungen
+
+* Abhängigkeiten aktualisiert
+
+* Fehler behoben
diff --git a/fastlane/metadata/android/de/changelogs/19.txt b/fastlane/metadata/android/de/changelogs/19.txt
new file mode 100644
index 00000000..4e982cbf
--- /dev/null
+++ b/fastlane/metadata/android/de/changelogs/19.txt
@@ -0,0 +1,17 @@
+* Metadaten von Fotos vor dem Hochladen entfernen
+
+* Lesezeichen!
+
+* Profile als Feed oder Raster ansehen
+
+* Lege Vorlagen für Beschreibungen an
+
+* Das Benachrichtigungssymbol hat nun eine Markierung wenn Du neue Benachrichtigungen erhalten hast
+
+* Weitere Videobearbeitungsfunktionen: Zuschneiden, Geschwindigkeit ändern, Stabilisieren
+
+* Dynamische Farben: PixelDroid kann sich den Farben Deines Hintergrunds anpassen (Android 12 und höher)
+
+* Fehlerbehebungen
+
+* Übersetzungen aktualisiert
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
new file mode 100644
index 00000000..74d5606a
--- /dev/null
+++ b/gradle/verification-metadata.xml
@@ -0,0 +1,5114 @@
+
+
+
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+