Merge master

This commit is contained in:
Matthieu 2022-11-25 16:54:23 +01:00
commit 67e92a8dfa
25 changed files with 6331 additions and 773 deletions

View File

@ -68,6 +68,7 @@
<activity
android:name=".postCreation.PostCreationActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
@ -79,9 +80,6 @@
<data android:mimeType="video/*" />
</intent-filter>
</activity>
<activity
android:name=".postCreation.PostSubmissionActivity">
</activity>
<activity
android:name=".profile.FollowsActivity"
android:screenOrientation="sensorPortrait"

View File

@ -1,44 +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.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"
@ -46,8 +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
@ -55,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()
@ -71,231 +38,16 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
}
} ?: InstanceDatabaseEntity("", "")
val _model: PostCreationViewModel by viewModels {
PostCreationViewModelFactory(
application,
intent.clipData!!,
instance
)
}
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)
}
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<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)
}
}

View File

@ -0,0 +1,310 @@
package org.pixeldroid.app.postCreation
import android.app.Activity
import android.app.AlertDialog
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Intent
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.*
import android.provider.MediaStore
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.FragmentPostCreationBinding
import org.pixeldroid.app.postCreation.camera.CameraActivity
import org.pixeldroid.app.postCreation.carousel.CarouselItem
import org.pixeldroid.app.utils.BaseFragment
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.fileExtension
import org.pixeldroid.app.utils.getMimeType
import org.pixeldroid.media_editor.photoEdit.PhotoEditActivity
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
import java.io.File
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.*
class PostCreationFragment : BaseFragment() {
private var user: UserDatabaseEntity? = null
private var instance: InstanceDatabaseEntity = InstanceDatabaseEntity("", "")
private lateinit var binding: FragmentPostCreationBinding
private lateinit var model: PostCreationViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreateView(inflater, container, savedInstanceState)
// Inflate the layout for this fragment
binding = FragmentPostCreationBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
user = db.userDao().getActiveUser()
instance = user?.run {
db.instanceDao().getAll().first { instanceDatabaseEntity ->
instanceDatabaseEntity.uri.contains(instance_uri)
}
} ?: InstanceDatabaseEntity("", "")
val _model: PostCreationViewModel by activityViewModels {
PostCreationViewModelFactory(
requireActivity().application,
requireActivity().intent.clipData!!,
instance,
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false)
)
}
model = _model
model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData ->
// update UI
binding.carousel.addData(
newPhotoData.map {
CarouselItem(
it.imageUri, it.imageDescription, it.video,
it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass,
it.videoEncodeComplete, it.videoEncodeError,
)
}
)
}
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState ->
uiState.userMessage?.let {
AlertDialog.Builder(binding.root.context).apply {
setMessage(it)
setNegativeButton(android.R.string.ok) { _, _ -> }
}.show()
// Notify the ViewModel the message is displayed
model.userMessageShown()
}
binding.addPhotoButton.isEnabled = uiState.addPhotoButtonEnabled
binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled
binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled
binding.toolbarPostCreation.visibility =
if (uiState.isCarousel) View.VISIBLE else View.INVISIBLE
binding.carousel.layoutCarousel = uiState.isCarousel
}
}
}
binding.carousel.apply {
layoutCarouselCallback = { model.becameCarousel(it)}
maxEntries = instance.albumLimit
addPhotoButtonCallback = {
addPhoto()
}
updateDescriptionCallback = { position: Int, description: String ->
model.updateDescription(position, description)
}
}
// get the description and send the post
binding.postCreationSendButton.setOnClickListener {
if (validatePost() && model.isNotEmpty()) {
findNavController().navigate(R.id.action_postCreationFragment_to_postSubmissionFragment)
}
}
binding.editPhotoButton.setOnClickListener {
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
edit(currentPosition)
}
}
binding.addPhotoButton.setOnClickListener {
addPhoto()
}
binding.savePhotoButton.setOnClickListener {
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
savePicture(it, currentPosition)
}
}
binding.removePhotoButton.setOnClickListener {
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
model.removeAt(currentPosition)
model.cancelEncode(currentPosition)
}
}
// Clean up temporary files, if any
val tempFiles = requireActivity().intent.getStringArrayExtra(PostCreationActivity.TEMP_FILES)
tempFiles?.asList()?.forEach {
val file = File(binding.root.context.cacheDir, it)
model.trackTempFile(file)
}
// Handle back pressed button
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val redraft = requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_REDRAFT, false)
if (redraft) {
val builder = AlertDialog.Builder(binding.root.context)
builder.apply {
setMessage(R.string.redraft_dialog_cancel)
setPositiveButton(android.R.string.ok) { _, _ ->
requireActivity().finish()
}
setNegativeButton(android.R.string.cancel) { _, _ -> }
show()
}
} else {
requireActivity().finish()
}
}
})
}
private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK && result.data?.clipData != null) {
result.data?.clipData?.let {
model.setImages(model.addPossibleImages(it))
}
} else if (result.resultCode != Activity.RESULT_CANCELED) {
Toast.makeText(requireActivity(), R.string.add_images_error, Toast.LENGTH_SHORT).show()
}
}
private fun addPhoto(){
addPhotoResultContract.launch(
Intent(requireActivity(), CameraActivity::class.java)
)
}
private fun savePicture(button: View, currentPosition: Int) {
val originalUri = model.getPhotoData().value!![currentPosition].imageUri
val pair = getOutputFile(originalUri)
val outputStream: OutputStream = pair.first
val path: String = pair.second
requireActivity().contentResolver.openInputStream(originalUri)!!.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
if(path.startsWith("file")) {
MediaScannerConnection.scanFile(
requireActivity(),
arrayOf(path.toUri().toFile().absolutePath),
null
) { tried_path, uri ->
if (uri == null) {
Log.e(
"NEW IMAGE SCAN FAILED",
"Tried to scan $tried_path, but it failed"
)
}
}
}
Snackbar.make(
button, getString(R.string.save_image_success),
Snackbar.LENGTH_LONG
).show()
}
private fun getOutputFile(uri: Uri): Pair<OutputStream, String> {
val extension = uri.fileExtension(requireActivity().contentResolver)
val name = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
.format(System.currentTimeMillis()) + ".$extension"
val outputStream: OutputStream
val path: String
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver: ContentResolver = requireActivity().contentResolver
val type = uri.getMimeType(requireActivity().contentResolver)
val contentValues = ContentValues()
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, type)
contentValues.put(
MediaStore.MediaColumns.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES
)
val store =
if (type.startsWith("video")) MediaStore.Video.Media.EXTERNAL_CONTENT_URI
else MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val imageUri: Uri = resolver.insert(store, contentValues)!!
path = imageUri.toString()
outputStream = resolver.openOutputStream(imageUri)!!
} else {
val imagesDir = Environment.getExternalStoragePublicDirectory(getString(R.string.app_name))
imagesDir.mkdir()
val file = File(imagesDir, name)
path = Uri.fromFile(file).toString()
outputStream = file.outputStream()
}
return Pair(outputStream, path)
}
private fun validatePost(): Boolean {
if (model.getPhotoData().value?.all { !it.video || it.videoEncodeComplete } == false) {
AlertDialog.Builder(requireActivity()).apply {
setMessage(R.string.still_encoding)
setNegativeButton(android.R.string.ok) { _, _ -> }
}.show()
return false
}
return true
}
private val editResultContract: ActivityResultLauncher<Intent> = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()){
result: ActivityResult? ->
if (result?.resultCode == Activity.RESULT_OK && result.data != null) {
val position: Int = result.data!!.getIntExtra(PhotoEditActivity.PICTURE_POSITION, 0)
model.modifyAt(position, result.data!!)
?: Toast.makeText(requireActivity(), R.string.error_editing, Toast.LENGTH_SHORT).show()
} else if(result?.resultCode != Activity.RESULT_CANCELED){
Toast.makeText(requireActivity(), R.string.error_editing, Toast.LENGTH_SHORT).show()
}
}
private fun edit(position: Int) {
val intent = Intent(
requireActivity(),
if (model.getPhotoData().value!![position].video) VideoEditActivity::class.java else PhotoEditActivity::class.java
)
.putExtra(PhotoEditActivity.PICTURE_URI, model.getPhotoData().value!![position].imageUri)
.putExtra(PhotoEditActivity.PICTURE_POSITION, position)
editResultContract.launch(intent)
}
}

View File

@ -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<MutableList<PhotoData>> by lazy {
MutableLiveData<MutableList<PhotoData>>().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<PostCreationActivityUiState> = _uiState
@ -169,7 +206,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,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 <T : ViewModel> create(modelClass: Class<T>): 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<PixelDroidApplication>().contentResolver)
val strippedImage = File.createTempFile("temp_img", ".$extension", getApplication<PixelDroidApplication>().cacheDir)
val imageUri = data.imageUri
val (strippedOrNot, size) = try {
val orientation = ExifInterface(getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!).getAttributeInt(
ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
stripMetadata(imageUri, strippedImage, getApplication<PixelDroidApplication>().contentResolver)
// Restore EXIF orientation
val exifInterface = ExifInterface(strippedImage)
exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, orientation.toString())
exifInterface.saveAttributes()
Pair(strippedImage.inputStream(), strippedImage.length())
} catch (e: UnsupportedFileFormatException){
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
val imageInputStream = try {
getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!
} catch (e: FileNotFoundException){
_uiState.update { currentUiState ->
currentUiState.copy(
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
data.imageUri)
)
}
return
}
Pair(imageInputStream, data.size)
} catch (e: IOException){
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
_uiState.update { currentUiState ->
currentUiState.copy(
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
data.imageUri)
)
}
return
}
val type = data.imageUri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
val imagePart = ProgressRequestBody(strippedOrNot, size, type)
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", System.currentTimeMillis().toString(), imagePart)
.build()
val sub = imagePart.progressSubject
.subscribeOn(Schedulers.io())
.subscribe { percentage ->
data.progress = percentage.toInt()
_uiState.update { currentUiState ->
currentUiState.copy(
uploadProgress = getPhotoData().value!!.sumOf { it.progress ?: 0 } / getPhotoData().value!!.size
)
}
}
var postSub: Disposable? = null
val description = data.imageDescription?.let { MultipartBody.Part.createFormData("description", it) }
// Ugly temporary account switching, but it works well enough for now
val api = uiState.value.chosenAccount?.let {
apiHolder.setToCurrentUser(it)
} ?: apiHolder.api ?: apiHolder.setToCurrentUser()
val inter = api.mediaUpload(description, requestBody.parts[0])
apiHolder.api = null
postSub = inter
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ attachment: Attachment ->
data.progress = 0
data.uploadId = attachment.id!!
},
{ e: Throwable ->
_uiState.update { currentUiState ->
currentUiState.copy(
uploadErrorVisible = true,
uploadErrorExplanationText = if(e is HttpException){
getApplication<PixelDroidApplication>().getString(R.string.upload_error, e.code())
} else "",
uploadErrorExplanationVisible = e is HttpException,
)
}
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
e.printStackTrace()
postSub?.dispose()
sub.dispose()
},
{
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
data.progress = 100
if (getPhotoData().value!!.all { it.progress == 100 && it.uploadId != null }) {
_uiState.update { currentUiState ->
currentUiState.copy(
uploadProgressBarVisible = false,
uploadCompletedTextviewVisible = true
)
}
post()
}
postSub?.dispose()
sub.dispose()
}
)
}
}
private fun post() {
val description = uiState.value.newPostDescriptionText
// TODO: investigate why this works but booleans don't
val nsfw = if (uiState.value.nsfw) 1 else 0
_uiState.update { currentUiState ->
currentUiState.copy(
postCreationSendButtonEnabled = false
)
}
viewModelScope.launch {
try {
//Ugly temporary account switching, but it works well enough for now
val api = uiState.value.chosenAccount?.let {
apiHolder.setToCurrentUser(it)
} ?: apiHolder.api ?: apiHolder.setToCurrentUser()
api.postStatus(
statusText = description,
media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList(),
sensitive = nsfw
)
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_success),
Toast.LENGTH_SHORT).show()
val intent = Intent(getApplication(), MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
//TODO make the activity launch this instead (and surrounding toasts too)
getApplication<PixelDroidApplication>().startActivity(intent)
} catch (exception: IOException) {
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_error),
Toast.LENGTH_SHORT).show()
Log.e(TAG, exception.toString())
_uiState.update { currentUiState ->
currentUiState.copy(
postCreationSendButtonEnabled = true
)
}
} catch (exception: HttpException) {
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_failed),
Toast.LENGTH_SHORT).show()
Log.e(TAG, exception.response().toString() + exception.message().toString())
_uiState.update { currentUiState ->
currentUiState.copy(
postCreationSendButtonEnabled = true
)
}
} finally {
apiHolder.api = null
}
}
}
fun newPostDescriptionChanged(text: Editable?) {
_uiState.update { it.copy(newPostDescriptionText = text.toString()) }
}
fun updateNSFW(checked: Boolean) { _uiState.update { it.copy(nsfw = checked) } }
fun chooseAccount(which: UserDatabaseEntity) {
_uiState.update { it.copy(chosenAccount = which) }
}
}
class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity, val existingDescription: String?, val existingNSFW: Boolean) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java, String::class.java, Boolean::class.java).newInstance(application, clipdata, instance, existingDescription, existingNSFW)
}
}

View File

@ -1,188 +0,0 @@
package org.pixeldroid.app.postCreation
import android.app.AlertDialog
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import androidx.activity.viewModels
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityPostSubmissionBinding
import org.pixeldroid.app.postCreation.PostCreationActivity.Companion.TEMP_FILES
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.setSquareImageFromURL
import java.io.File
class PostSubmissionActivity : BaseThemedWithoutBarActivity() {
companion object {
internal const val PICTURE_DESCRIPTION = "picture_description"
internal const val PHOTO_DATA = "photo_data"
}
private lateinit var accounts: List<UserDatabaseEntity>
private var selectedAccount: Int = -1
private lateinit var menu: Menu
private var user: UserDatabaseEntity? = null
private lateinit var instance: InstanceDatabaseEntity
private lateinit var binding: ActivityPostSubmissionBinding
private lateinit var model: PostSubmissionViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPostSubmissionBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.add_details)
user = db.userDao().getActiveUser()
accounts = db.userDao().getAll()
instance = user?.run {
db.instanceDao().getAll().first { instanceDatabaseEntity ->
instanceDatabaseEntity.uri.contains(instance_uri)
}
} ?: InstanceDatabaseEntity("", "")
val photoData = intent.getParcelableArrayListExtra<PhotoData>(PHOTO_DATA) as ArrayList<PhotoData>?
val _model: PostSubmissionViewModel by viewModels {
PostSubmissionViewModelFactory(
application,
photoData!!
)
}
model = _model
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState ->
uiState.userMessage?.let {
AlertDialog.Builder(binding.root.context).apply {
setMessage(it)
setNegativeButton(android.R.string.ok) { _, _ -> }
}.show()
// Notify the ViewModel the message is displayed
model.userMessageShown()
}
enableButton(uiState.postCreationSendButtonEnabled)
binding.uploadProgressBar.visibility =
if (uiState.uploadProgressBarVisible) VISIBLE else INVISIBLE
binding.uploadProgressBar.progress = uiState.uploadProgress
binding.uploadCompletedTextview.visibility =
if (uiState.uploadCompletedTextviewVisible) VISIBLE else INVISIBLE
binding.uploadError.visibility =
if (uiState.uploadErrorVisible) VISIBLE else INVISIBLE
binding.uploadErrorTextExplanation.visibility =
if (uiState.uploadErrorExplanationVisible) VISIBLE else INVISIBLE
selectedAccount = accounts.indexOf(uiState.chosenAccount)
binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
}
}
}
binding.newPostDescriptionInputField.doAfterTextChanged {
model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text)
}
binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked ->
model.updateNSFW(isChecked)
}
val existingDescription: String? = intent.getStringExtra(PICTURE_DESCRIPTION)
binding.newPostDescriptionInputField.setText(
// Set description from redraft if any, otherwise from the template
existingDescription ?: model.uiState.value.newPostDescriptionText
)
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
setSquareImageFromURL(View(applicationContext), photoData!![0].imageUri.toString(), binding.postPreview)
// get the description and send the post
binding.postCreationSendButton.setOnClickListener {
if (validatePost()) model.upload()
}
// Button to retry image upload when it fails
binding.retryUploadButton.setOnClickListener {
model.resetUploadStatus()
model.upload()
}
// Clean up temporary files, if any
val tempFiles = intent.getStringArrayExtra(TEMP_FILES)
tempFiles?.asList()?.forEach {
val file = File(binding.root.context.cacheDir, it)
model.trackTempFile(file)
}
}
override fun onCreateOptionsMenu(newMenu: Menu): Boolean {
menuInflater.inflate(R.menu.post_submission_account_menu, newMenu)
menu = newMenu
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId){
R.id.action_switch_accounts -> {
AlertDialog.Builder(this).apply {
setIcon(R.drawable.material_drawer_ico_account)
setTitle(R.string.switch_accounts)
setSingleChoiceItems(accounts.map { it.username + " (${it.fullHandle})" }.toTypedArray(), selectedAccount) { dialog, which ->
if(selectedAccount != which){
model.chooseAccount(accounts[which])
}
dialog.dismiss()
}
setNegativeButton(android.R.string.cancel) { _, _ -> }
}.show()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun validatePost(): Boolean {
binding.postTextInputLayout.run {
val content = editText?.length() ?: 0
if (content > counterMaxLength) {
// error, too many characters
error = resources.getQuantityString(R.plurals.description_max_characters, counterMaxLength, counterMaxLength)
return false
}
}
return true
}
private fun enableButton(enable: Boolean = true){
binding.postCreationSendButton.isEnabled = enable
if(enable){
binding.postingProgressBar.visibility = GONE
binding.postCreationSendButton.visibility = VISIBLE
} else {
binding.postingProgressBar.visibility = VISIBLE
binding.postCreationSendButton.visibility = GONE
}
}
}

View File

@ -0,0 +1,192 @@
package org.pixeldroid.app.postCreation
import android.app.AlertDialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.core.view.MenuProvider
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.FragmentPostSubmissionBinding
import org.pixeldroid.app.utils.BaseFragment
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.setSquareImageFromURL
class PostSubmissionFragment : BaseFragment() {
private lateinit var accounts: List<UserDatabaseEntity>
private var selectedAccount: Int = -1
private var user: UserDatabaseEntity? = null
private lateinit var instance: InstanceDatabaseEntity
private lateinit var binding: FragmentPostSubmissionBinding
private lateinit var model: PostCreationViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreateView(inflater, container, savedInstanceState)
// Inflate the layout for this fragment
binding = FragmentPostSubmissionBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.topBar.setupWithNavController(findNavController())
user = db.userDao().getActiveUser()
accounts = db.userDao().getAll()
instance = user?.run {
db.instanceDao().getAll().first { instanceDatabaseEntity ->
instanceDatabaseEntity.uri.contains(instance_uri)
}
} ?: InstanceDatabaseEntity("", "")
val _model: PostCreationViewModel by activityViewModels {
PostCreationViewModelFactory(
requireActivity().application,
requireActivity().intent.clipData!!,
instance,
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false)
)
}
model = _model
// Display the values from the view model
binding.nsfwSwitch.isChecked = model.uiState.value.nsfw
binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText)
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState ->
uiState.userMessage?.let {
AlertDialog.Builder(binding.root.context).apply {
setMessage(it)
setNegativeButton(android.R.string.ok) { _, _ -> }
}.show()
// Notify the ViewModel the message is displayed
model.userMessageShown()
}
enableButton(uiState.postCreationSendButtonEnabled)
binding.uploadProgressBar.visibility =
if (uiState.uploadProgressBarVisible) View.VISIBLE else View.INVISIBLE
binding.uploadProgressBar.progress = uiState.uploadProgress
binding.uploadCompletedTextview.visibility =
if (uiState.uploadCompletedTextviewVisible) View.VISIBLE else View.INVISIBLE
binding.uploadError.visibility =
if (uiState.uploadErrorVisible) View.VISIBLE else View.INVISIBLE
binding.uploadErrorTextExplanation.visibility =
if (uiState.uploadErrorExplanationVisible) View.VISIBLE else View.INVISIBLE
selectedAccount = accounts.indexOf(uiState.chosenAccount)
binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
}
}
}
binding.newPostDescriptionInputField.doAfterTextChanged {
model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text)
}
binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked ->
model.updateNSFW(isChecked)
}
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
setSquareImageFromURL(View(requireActivity()), model.getPhotoData().value?.get(0)?.imageUri.toString(), binding.postPreview)
// Get the description and send the post
binding.postCreationSendButton.setOnClickListener {
if (validatePost()) model.upload()
}
// Button to retry image upload when it fails
binding.retryUploadButton.setOnClickListener {
model.resetUploadStatus()
model.upload()
}
// Handle back pressed button
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
findNavController().navigate(R.id.action_postSubmissionFragment_to_postCreationFragment)
}
})
binding.topBar.addMenuProvider(object: MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
// Add menu items here
menuInflater.inflate(R.menu.post_submission_account_menu, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
// Handle the menu selection
return when (menuItem.itemId) {
R.id.action_switch_accounts -> {
AlertDialog.Builder(requireActivity()).apply {
setIcon(R.drawable.switch_account)
setTitle(R.string.switch_accounts)
setSingleChoiceItems(accounts.map { it.username + " (${it.fullHandle})" }.toTypedArray(), selectedAccount) { dialog, which ->
if (selectedAccount != which) {
model.chooseAccount(accounts[which])
}
dialog.dismiss()
}
setNegativeButton(android.R.string.cancel) { _, _ -> }
}.show()
return true
}
else -> false
}
}
}, viewLifecycleOwner, Lifecycle.State.RESUMED)
}
private fun validatePost(): Boolean {
binding.postTextInputLayout.run {
val content = editText?.length() ?: 0
if (content > counterMaxLength) {
// error, too many characters
error = resources.getQuantityString(R.plurals.description_max_characters, counterMaxLength, counterMaxLength)
return false
}
}
return true
}
private fun enableButton(enable: Boolean = true){
binding.postCreationSendButton.isEnabled = enable
if(enable){
binding.postingProgressBar.visibility = View.GONE
binding.postCreationSendButton.visibility = View.VISIBLE
} else {
binding.postingProgressBar.visibility = View.VISIBLE
binding.postCreationSendButton.visibility = View.GONE
}
}
}

View File

@ -54,7 +54,7 @@ data class PostSubmissionActivityUiState(
val uploadErrorExplanationVisible: Boolean = false,
)
class PostSubmissionViewModel(application: Application, photodata: ArrayList<PhotoData>? = null) : AndroidViewModel(application) {
class PostSubmissionViewModel(application: Application, photodata: ArrayList<PhotoData>? = null, val existingDescription: String? = null) : AndroidViewModel(application) {
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
MutableLiveData<MutableList<PhotoData>>().also {
if (photodata != null) {
@ -74,7 +74,7 @@ class PostSubmissionViewModel(application: Application, photodata: ArrayList<Pho
PreferenceManager.getDefaultSharedPreferences(application)
val initialDescription = sharedPreferences.getString("prefill_description", "") ?: ""
_uiState = MutableStateFlow(PostSubmissionActivityUiState(newPostDescriptionText = initialDescription))
_uiState = MutableStateFlow(PostSubmissionActivityUiState(newPostDescriptionText = existingDescription ?: initialDescription))
}
val uiState: StateFlow<PostSubmissionActivityUiState> = _uiState
@ -235,7 +235,7 @@ class PostSubmissionViewModel(application: Application, photodata: ArrayList<Pho
val description = uiState.value.newPostDescriptionText
//TODO investigate why this works but booleans don't
val nsfw = if(uiState.value.nsfw) 1 else 0
val nsfw = if (uiState.value.nsfw) 1 else 0
_uiState.update { currentUiState ->
currentUiState.copy(
@ -307,8 +307,8 @@ class PostSubmissionViewModel(application: Application, photodata: ArrayList<Pho
}
class PostSubmissionViewModelFactory(val application: Application, val photoData: ArrayList<PhotoData>) : ViewModelProvider.Factory {
class PostSubmissionViewModelFactory(val application: Application, val photoData: ArrayList<PhotoData>, val existingDescription: String?) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
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)
}
}

View File

@ -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)

View File

@ -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<Uri> = mutableListOf()
val imageNames: MutableList<String> = mutableListOf()
val imageDescriptions: MutableList<String> =
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<Request> = 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<String>): Boolean {
for (name in listOfNames) {
val file = File(binding.root.context.cacheDir, name)
if (!file.exists()) {
return false
}
private fun allFilesExist(listOfNames: List<String>): Boolean {
return listOfNames.all {
File(binding.root.context.cacheDir, it).exists()
}
return true
}
companion object {

View File

@ -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)
}
}
}

View File

@ -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<FeedContent> = 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<ProfilePostViewHolder>() {
private val posts: ArrayList<Status?> = ArrayList()
abstract class TrendingRecyclerViewAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>(){
val data: ArrayList<FeedContent?> = ArrayList()
fun addPosts(newPosts : List<Status>) {
posts.clear()
posts.addAll(newPosts)
@SuppressLint("NotifyDataSetChanged")
fun addPosts(newPosts: List<FeedContent>){
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<HashTagViewHolder>() {
private val tags: ArrayList<Tag?> = ArrayList()
class HashtagsRecyclerViewAdapter: TrendingRecyclerViewAdapter() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HashTagViewHolder =
HashTagViewHolder.create(parent)
fun addHashtags(newTags : List<Tag>) {
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<AccountViewHolder>() {
private val accounts: ArrayList<Account?> = ArrayList()
class AccountsRecyclerViewAdapter: TrendingRecyclerViewAdapter() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder =
AccountViewHolder.create(parent)
fun addAccounts(newAccounts : List<Account>) {
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
}
}

View File

@ -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<Status>
@GET("/api/v1.1/discover/posts/hashtags")

View File

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?attr/colorOnBackground" android:pathData="M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM14,4c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM20,16L8,16v-1.5c0,-1.99 4,-3 6,-3s6,1.01 6,3L20,16z"/>
</vector>

View File

@ -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>

View File

@ -38,7 +38,9 @@
android:layout_height="match_parent"
app:layoutDescription="@xml/error_layout_xml_error_scene">
<include layout="@layout/error_layout"/>
<include
android:id="@+id/errorLayout"
layout="@layout/error_layout"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".postCreation.PostCreationFragment"
android:id="@+id/postCreationFragment" >
<org.pixeldroid.app.postCreation.carousel.ImageCarousel
android:id="@+id/carousel"
android:layout_width="match_parent"
android:layout_height="0dp"
app:showCaption="true"
app:layout_constraintBottom_toTopOf="@+id/buttonConstraints"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/buttonConstraints"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<Button
android:id="@+id/post_creation_send_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:enabled="true"
android:text="@string/upload_next_step"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/toolbarPostCreation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#40000000"
android:minHeight="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/savePhotoButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/save_to_gallery"
android:tooltipText='@string/save_to_gallery'
android:src="@drawable/download_file_30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/removePhotoButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/delete"
android:tooltipText='@string/delete'
android:src="@drawable/delete_30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/savePhotoButton"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/editPhotoButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/edit"
android:tooltipText='@string/edit'
android:src="@drawable/ic_baseline_edit_30"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/removePhotoButton"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/addPhotoButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="30dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/add_photo"
android:tooltipText='@string/add_photo'
android:src="@drawable/add_photo_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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" >
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/post_preview"
@ -13,7 +24,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 +35,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 +46,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 +76,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>

View File

@ -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>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/post_creation_graph"
app:startDestination="@id/postCreationFragment" >
<fragment
android:id="@+id/postCreationFragment"
android:name="org.pixeldroid.app.postCreation.PostCreationFragment"
tools:layout="@layout/fragment_post_creation" >
<action
android:id="@+id/action_postCreationFragment_to_postSubmissionFragment"
app:destination="@id/postSubmissionFragment" />
</fragment>
<fragment
android:id="@+id/postSubmissionFragment"
android:name="org.pixeldroid.app.postCreation.PostSubmissionFragment"
android:label="@string/add_details"
tools:layout="@layout/fragment_post_submission" >
<action
android:id="@+id/action_postSubmissionFragment_to_postCreationFragment"
app:destination="@id/postCreationFragment" />
</fragment>
</navigation>

View File

@ -6,7 +6,7 @@
<string name="invalid_domain">Ungültige Domäne</string>
<string name="browser_launch_failed">Der Browser konnte nicht gestartet werden, haben Sie einen\?</string>
<string name="auth_failed">Authentifizierung fehlgeschlagen</string>
<string name="token_error">Beim Erhalten des Token ist ein Fehler aufgetreten</string>
<string name="token_error">Fehler beim Abruf des Tokens</string>
<string name="title_activity_settings2">Einstellungen</string>
<string name="theme_header">Erscheinungsbild</string>
<string name="followed_notification">%1$s folgt dir</string>
@ -33,14 +33,14 @@
<string name="connect_to_pixelfed">Mit Pixelfed verbinden</string>
<string name="login_connection_required_once">Sie müssen online sein, um das erste Konto hinzuzufügen und um PixelDroid zu verwenden :(</string>
<string name="switch_camera_button_alt">Kamera wechseln</string>
<string name="registration_failed">Konnte die App nicht mit diesem Server verbinden</string>
<string name="registration_failed">App konnte sich nicht mit diesem Server verbinden</string>
<string name="instance_error">Konnte die Informationen der Instanz nicht abrufen</string>
<string name="upload_picture_failed">Fehler beim Hochladen!</string>
<string name="default_system">Standard (Systemeinstellung)</string>
<string name="save_image_failed">Bild kann nicht gespeichert werden</string>
<string name="save_image_success">Bild erfolgreich gespeichert</string>
<string name="picture_format_error">Upload-Fehler: falsches Bildformat.</string>
<string name="upload_post_failed">Das Hochladen des Beitrags ist fehlgeschlagen</string>
<string name="upload_post_failed">Hochladen des Beitrags fehlgeschlagen</string>
<string name="add_account_name">Konto hinzufügen</string>
<string name="add_account_description">Weiteres Pixelfed-Konto hinzufügen</string>
<string name="light_theme">Hell</string>
@ -57,12 +57,12 @@
<string name="upload_post_error">Beim Hochladen des Beitrags ist ein Fehler aufgetreten</string>
<string name="comment_error">Kommentarfehler!</string>
<string name="comment_posted">Kommentar: %1$s geschrieben!</string>
<string name="follow_button_failed">Konnte Knopf zum Folgen nicht anzeigen</string>
<string name="follow_button_failed">Schaltfläche \"Folgen\" konnte nicht angezeigt werden</string>
<string name="follow_error">Konnte nicht folgen</string>
<string name="no_username">Kein Benutzername</string>
<string name="default_nfollowing">-
\nFolgend</string>
<string name="action_not_allowed">Diese Handlung ist nicht erlaubt</string>
\nFolgt</string>
<string name="action_not_allowed">Diese Aktion ist nicht zulässig</string>
<string name="unfollow_error">Konnte nicht entfolgen</string>
<string name="access_token_invalid">Dieser Zugangstoken ist ungültig</string>
<string name="default_nposts">-
@ -89,8 +89,8 @@
<string name="status_more_options">Mehr Optionen</string>
<string name="search_empty_error">Suchanfrage darf nicht leer sein</string>
<string name="follows_title">%1$s folgt</string>
<string name="followers_title">%1$s Follower</string>
<string name="post_title">%1$s Beitrag</string>
<string name="followers_title">%1$s\'s Follower</string>
<string name="post_title">%1$s\'s Beitrag</string>
<string name="about">Über</string>
<string name="license_info">PixelDroid ist freie und quelloffene Software lizenziert unter der GNU General Public License (Version 3 oder höher)</string>
<string name="project_website">Projektwebseite: https://pixeldroid.org</string>
@ -105,18 +105,18 @@
<string name="switch_to_grid">Wechsle zur Rasteransicht</string>
<string name="mascot_description">Das Bild zeigt einen roten Panda, das Maskottchen von Pixelfed, der ein Telefon benutzt</string>
<string name="switch_to_carousel">Wechsel zur Karussellansicht</string>
<string name="issues_contribute">Melde Probleme oder hilf mit bei der Entwicklung der Anwendung:</string>
<string name="help_translate">Helfe PixelDroid in deine Sprache zu übersetzen:</string>
<string name="issues_contribute">Melde Probleme oder helfe bei der Entwicklung der Anwendung:</string>
<string name="help_translate">Helfe dabei PixelDroid in deine Sprache zu übersetzen:</string>
<string name="language">Sprache</string>
<string name="delete_dialog">Beitrag löschen\?</string>
<string name="delete">Löschen</string>
<string name="panda_pull_to_refresh_to_try_again">Der Panda ist nicht glücklich. Ziehe um erneut zu laden.</string>
<string name="something_went_wrong">Etwas ist schiefgelaufen…</string>
<string name="discover">ENTDECKE</string>
<string name="discover">Entdecke</string>
<string name="open_drawer_menu">Öffne Menü</string>
<string name="profile_picture">Profilbild</string>
<string name="report_error">Konnte nicht gemeldet werden</string>
<string name="reported">Post wurde gemeldet</string>
<string name="report_error">Beitrag konnte nicht gemeldet werden</string>
<string name="reported">Beitrag wurde gemeldet</string>
<string name="report_target">Melde den Beitrag von @%1$s</string>
<string name="post_is_album">Dieser Beitrag ist ein Album</string>
<string name="submit_comment">Kommentar senden</string>
@ -133,7 +133,7 @@
<item quantity="one">%d Kommentar</item>
<item quantity="other">%d Kommentare</item>
</plurals>
<string name="no_media_description">Ergänze hier eine Medienbeschreibung hier</string>
<string name="no_media_description">Füge hier eine Medienbeschreibung hinzu</string>
<string name="save_image_description">Bildbeschreibung speichern</string>
<plurals name="description_max_characters">
<item quantity="one">Die Beschreibung muss mindestens %d Zeichen enthalten.</item>
@ -150,13 +150,15 @@
<string name="dialog_message_cancel_follow_request">Followeranfrage zurückziehen\?</string>
<string name="empty_feed">Hier gibt es nichts zu sehen :(</string>
<plurals name="nb_following">
<item quantity="one">Folgt %d</item>
<item quantity="other">Folgt %d</item>
<item quantity="one">%d
\nFolge ich</item>
<item quantity="other">%d
\nFolge ich</item>
</plurals>
<plurals name="nb_followers">
<item quantity="one">%d
<item quantity="one">%d
\nFollower</item>
<item quantity="other">%d
<item quantity="other">%d
\nFollower</item>
</plurals>
<plurals name="shares">
@ -168,7 +170,7 @@
<item quantity="other">%d Gefällt-Mirs</item>
</plurals>
<string name="upload_error">Serverfehler: %1$d</string>
<string name="size_exceeds_instance_limit">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.</string>
<string name="size_exceeds_instance_limit">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.</string>
<string name="total_exceeds_album_limit">Du hast mehr Bilder ausgewählt, als auf deinem Server zulässig sind (%1$s). Bilder jenseits des Limits wurden nicht berücksichtigt.</string>
<string name="api_not_enabled_dialog">Die API ist auf deiner Instanz nicht aktiv. Bitte kontaktiere den Betreiber deiner Instanz, damit sie aktiviert werden kann.</string>
<string name="follow_requested">Followeranfrage</string>
@ -176,7 +178,7 @@
<string name="edit_link_failed">Das Öffnen der Bearbeitungsseite ist gescheitert</string>
<string name="hashtag_title">#%1$s</string>
<string name="no_camera_permission">Die Einwilligung die Kamera zu nutzen wurde nicht erteilt. Erlaube die Kameranutzung in den Einstellungen, wenn du die Kamera in PixelDroid verwenden willst</string>
<string name="no_storage_permission">Die Erlaubnis, auf die Dateien zuzugreifen wurde nicht erteilt. Erlaube den Zugriff auf Daten wenn du PixelDroid das Thumbnail anzeigen lassen willst</string>
<string name="no_storage_permission">Die Erlaubnis auf Speichermedien zuzugreifen wurde nicht erteilt. Erlaube den Zugriff wenn du PixelDroid das Thumbnail anzeigen lassen willst</string>
<string name="file_not_found">Datei %1$s wurde nicht gefunden</string>
<string name="other_notification">Benachrichtigung von %1$s</string>
<string name="followed_notification_channel">Neue Follower</string>
@ -197,16 +199,16 @@
</plurals>
<string name="notification_summary_medium">%1$s, %2$s und %3$s</string>
<string name="notification_summary_small">%1$s und %2$s</string>
<string name="video_not_supported">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</string>
<string name="video_not_supported">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</string>
<string name="post_is_video">Dieser Beitrag ist ein Video</string>
<string name="play_video">Video abspielen</string>
<string name="new_post_shortcut_long">Neuen Beitrag erstellen</string>
<string name="new_post_shortcut_short">Neuer Beitrag</string>
<string name="status_notification">%1$s hat einen Beitrag erstellt</string>
<string name="encode_error">Fehler-Kodierung</string>
<string name="encode_error">Bei der Kodierung ist ein Fehler aufgetreten</string>
<string name="encode_success">Kodierung war erfolgreich!</string>
<string name="encode_progress">Kodierung %1$d%%</string>
<string name="still_encoding">Mindestens ein Video wird noch kodiert. Bitte warten bis alle fertig sind, bevor sie die Videos hochladen</string>
<string name="encode_progress">Kodiere %1$d%%</string>
<string name="still_encoding">Mindestens ein Video wird noch kodiert. Vor dem Hochladen bitte warten bis alle Vorgänge abgeschlossen sind</string>
<string name="follow_request">%1$s möchte dir folgen</string>
<string name="home_feed">Start</string>
<string name="search_discover_feed">Suche</string>
@ -214,7 +216,7 @@
<string name="public_feed">Öffentlich</string>
<string name="accentColorTitle">Farbakzent</string>
<string name="accentColorSummary">Farbakzent auswählen</string>
<string name="color_chosen">Auswählter Farbakzent</string>
<string name="color_chosen">Ausgewählter Farbakzent</string>
<string name="create_feed">Erstellen</string>
<string name="color_choice_button">Diesen Farbakzent auswählen</string>
<plurals name="replies_count">
@ -224,6 +226,62 @@
<string name="profile_error">Profil konnte nicht geladen werden</string>
<string name="from_other_domain">von %1$s</string>
<string name="add_images_error">Fehler beim Hinzufügen der Bilder</string>
<string name="notification_thumbnail">Vorschaubild dieser Benachrichtigung</string>
<string name="post_preview">Vorschau eines Betrags</string>
</resources>
<string name="notification_thumbnail">Vorschau des Bildes im Beitrag dieser Benachrichtigung</string>
<string name="post_preview">Vorschau eines Beitrags</string>
<string name="upload_next_step">Nächster Schritt</string>
<string name="collection_title">%1$s\'s Sammlung</string>
<string name="bookmark">Lesezeichen</string>
<string name="unbookmark">Lesezeichen entfernen</string>
<string name="popular_accounts">Beliebte Konten</string>
<string name="redraft_dialog_cancel">Bei Abbruch der Überarbeitung wird der ursprüngliche Beitrag nicht mehr vorhanden sein. Wirklich ohne Wiederveröffentlichung fortsetzen\?</string>
<string name="profile_save_changes">Deine Änderungen wurden nicht gespeichert. Wirklich beenden\?</string>
<string name="private_account_explanation">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.</string>
<string name="redraft_post_failed_error">Beitrag konnte nicht überarbeitet werden, Fehler %1$d</string>
<string name="redraft">Überarbeiten</string>
<string name="redraft_dialog_launch">Die Überarbeitung dieses Beitrags ermöglicht Dir das Foto und seine Beschreibung zu ändern, aber alle aktuellen Kommentare und Likes werden gelöscht. Fortsetzen\?</string>
<string name="bookmark_post_failed_error">Konnte Lesezeichen für den Beitrag nicht hinzufügen/entfernen, Fehler %1$d</string>
<string name="add_details">Füge ein paar Details hinzu</string>
<string name="new_collection_link_failed">Die Seite zum Erstellen der Sammlung konnte nicht geöffnet werden</string>
<string name="redraft_post_failed_io_except">Beitrag konnte nicht überarbeitet werden, besteht eine Internet-Verbindung\?</string>
<string name="switch_accounts">Konto wechseln</string>
<string name="fetching_profile">Dein Profil wird abgerufen...</string>
<string name="your_name">Dein Name</string>
<string name="your_bio">Informationen über Dich</string>
<string name="private_account">Privates Konto</string>
<string name="more_profile_settings">Erweiterte Profil-Einstellungen</string>
<string name="use_dynamic_color">Verwende dynamische Farben von Deinem System</string>
<string name="save">Speichern</string>
<string name="bookmark_post_failed_io_except">Konnte Lesezeichen für den Beitrag nicht hinzufügen/entfernen, besteht eine Internet-Verbindung\?</string>
<string name="analyzing_stabilization">%1$d%% für die Stabilisierung analysieren</string>
<string name="description_template_summary">Vorgefertigte Beschreibung für neue Beiträge</string>
<string name="description_template">Beschreibungs-Vorlage</string>
<string name="explore_accounts">Entdecke beliebte Konten auf dieser Instanz</string>
<string name="explore_hashtags">Entdecke beliebte Hashtags auf dieser Instanz</string>
<string name="trending_hashtags">Beliebte Hashtags</string>
<string name="daily_trending">Entdecke beliebte Beiträge des Tages</string>
<string name="trending_posts">Beliebte Beiträge</string>
<string name="explore_posts">Entdecke zufällige Beiträge des Tages</string>
<string name="grid_view">Rasteransicht</string>
<string name="feed_view">Feed-Ansicht</string>
<string name="bookmarks">Lesezeichen</string>
<string name="collections">Sammlungen</string>
<string name="delete_collection">Sammlung löschen</string>
<string name="collection_add_post">Beitrag erstellen</string>
<string name="collection_remove_post">Beitrag löschen</string>
<string name="delete_collection_warning">Bist Du sicher dass Du diese Sammlung löschen willst\?</string>
<string name="add_to_collection">Wähle den hinzuzufügenden Beitrag aus</string>
<string name="added_post_to_collection">Beitrag zur Sammlung hinzugefügt</string>
<string name="error_add_post_to_collection">Beitrag konnte der Sammlung nicht hinzugefügt werden</string>
<string name="removed_post_from_collection">Beitrag von der Sammlung entfernt</string>
<string name="profile_saved">Änderungen gespeichert!</string>
<string name="error_profile">Etwas ist schief gelaufen. Tippe um es erneut zu versuchen</string>
<string name="change_profile_picture">Ändere Dein Profilbild</string>
<string name="contains_nsfw">Enthält NSFW-Medien</string>
<string name="saving_profile">Dein Profil wird gespeichert</string>
<string name="delete_from_collection">Wähle den zu löschenden Beitrag aus</string>
<string name="error_remove_post_from_collection">Beitrag konnte nicht von der Sammlung entfernt werden</string>
<plurals name="items_load_success">
<item quantity="one">%d Element erfolgreich geladen</item>
<item quantity="other">%d Elemente erfolgreich geladen</item>
</plurals>
</resources>

View File

@ -197,7 +197,7 @@
</plurals>
<string name="status_notification">%1$s udostępnił post</string>
<string name="notification_thumbnail">Miniatura zdjęcia w powiadomieniu o tym poście</string>
<string name="discover">ODKRYJ</string>
<string name="discover">Odkrywanie</string>
<string name="whats_an_instance_explanation">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 @@
<string name="from_other_domain">Z %1$s</string>
<string name="add_images_error">Błąd podczas dodawania zdjęcia</string>
<string name="post_preview">Podgląd posta</string>
</resources>
<string name="upload_next_step">Następny krok</string>
<plurals name="items_load_success">
<item quantity="one">Dodanie elementu %d zakończone sukcesem</item>
<item quantity="few">Dodanie %d elementów zakończone sukcesem</item>
<item quantity="many">Ładowanie zakończone</item>
<item quantity="other">Ładowanie zakończone</item>
</plurals>
<string name="description_template">Wzorzec opisu</string>
<string name="trending_hashtags">Hasztagi zyskujące popularność</string>
<string name="delete_from_collection">Wybierz co chcesz usunąć</string>
<string name="use_dynamic_color">Użyj dynamicznych kolorów z systemu</string>
<string name="add_details">Dodaj szczegóły</string>
<string name="bookmark">Dodaj do zakładek</string>
<string name="unbookmark">Usuń z zakładek</string>
<string name="explore_accounts">Szukaj popularnych kont na tej instancji</string>
<string name="popular_accounts">Popularne konta</string>
<string name="grid_view">Widok w siatce</string>
<string name="bookmarks">Zakładki</string>
<string name="collections">Kolekcje</string>
<string name="delete_collection">Usuń kolekcję</string>
<string name="collection_add_post">Dodaj wpis</string>
<string name="collection_remove_post">Usuń wpis</string>
<string name="delete_collection_warning">Czy na pewno chcesz usunąć kolekcję\?</string>
<string name="add_to_collection">Wybierz co chcesz dodać</string>
<string name="added_post_to_collection">Dodany do kolekcji</string>
<string name="error_add_post_to_collection">Błąd dodawania do kolekcji</string>
<string name="error_remove_post_from_collection">Błąd usuwania z kolekcji</string>
<string name="removed_post_from_collection">Usunięto z kolekcji</string>
<string name="save">Zapisz</string>
<string name="more_profile_settings">Więcej ustawień profilu</string>
<string name="private_account">Konto prywatne</string>
</resources>

View File

@ -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

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff