Merge master
This commit is contained in:
commit
67e92a8dfa
@ -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"
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,310 @@
|
||||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Intent
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.*
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.FragmentPostCreationBinding
|
||||
import org.pixeldroid.app.postCreation.camera.CameraActivity
|
||||
import org.pixeldroid.app.postCreation.carousel.CarouselItem
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.fileExtension
|
||||
import org.pixeldroid.app.utils.getMimeType
|
||||
import org.pixeldroid.media_editor.photoEdit.PhotoEditActivity
|
||||
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
||||
class PostCreationFragment : BaseFragment() {
|
||||
|
||||
private var user: UserDatabaseEntity? = null
|
||||
private var instance: InstanceDatabaseEntity = InstanceDatabaseEntity("", "")
|
||||
|
||||
private lateinit var binding: FragmentPostCreationBinding
|
||||
private lateinit var model: PostCreationViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// Inflate the layout for this fragment
|
||||
binding = FragmentPostCreationBinding.inflate(layoutInflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
user = db.userDao().getActiveUser()
|
||||
|
||||
instance = user?.run {
|
||||
db.instanceDao().getAll().first { instanceDatabaseEntity ->
|
||||
instanceDatabaseEntity.uri.contains(instance_uri)
|
||||
}
|
||||
} ?: InstanceDatabaseEntity("", "")
|
||||
|
||||
val _model: PostCreationViewModel by activityViewModels {
|
||||
PostCreationViewModelFactory(
|
||||
requireActivity().application,
|
||||
requireActivity().intent.clipData!!,
|
||||
instance,
|
||||
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
|
||||
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false)
|
||||
)
|
||||
}
|
||||
model = _model
|
||||
|
||||
model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData ->
|
||||
// update UI
|
||||
binding.carousel.addData(
|
||||
newPhotoData.map {
|
||||
CarouselItem(
|
||||
it.imageUri, it.imageDescription, it.video,
|
||||
it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass,
|
||||
it.videoEncodeComplete, it.videoEncodeError,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.uiState.collect { uiState ->
|
||||
uiState.userMessage?.let {
|
||||
AlertDialog.Builder(binding.root.context).apply {
|
||||
setMessage(it)
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
|
||||
// Notify the ViewModel the message is displayed
|
||||
model.userMessageShown()
|
||||
}
|
||||
binding.addPhotoButton.isEnabled = uiState.addPhotoButtonEnabled
|
||||
binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled
|
||||
binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled
|
||||
binding.toolbarPostCreation.visibility =
|
||||
if (uiState.isCarousel) View.VISIBLE else View.INVISIBLE
|
||||
binding.carousel.layoutCarousel = uiState.isCarousel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.carousel.apply {
|
||||
layoutCarouselCallback = { model.becameCarousel(it)}
|
||||
maxEntries = instance.albumLimit
|
||||
addPhotoButtonCallback = {
|
||||
addPhoto()
|
||||
}
|
||||
updateDescriptionCallback = { position: Int, description: String ->
|
||||
model.updateDescription(position, description)
|
||||
}
|
||||
}
|
||||
// get the description and send the post
|
||||
binding.postCreationSendButton.setOnClickListener {
|
||||
if (validatePost() && model.isNotEmpty()) {
|
||||
findNavController().navigate(R.id.action_postCreationFragment_to_postSubmissionFragment)
|
||||
}
|
||||
}
|
||||
|
||||
binding.editPhotoButton.setOnClickListener {
|
||||
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
||||
edit(currentPosition)
|
||||
}
|
||||
}
|
||||
|
||||
binding.addPhotoButton.setOnClickListener {
|
||||
addPhoto()
|
||||
}
|
||||
|
||||
binding.savePhotoButton.setOnClickListener {
|
||||
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
||||
savePicture(it, currentPosition)
|
||||
}
|
||||
}
|
||||
|
||||
binding.removePhotoButton.setOnClickListener {
|
||||
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
||||
model.removeAt(currentPosition)
|
||||
model.cancelEncode(currentPosition)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up temporary files, if any
|
||||
val tempFiles = requireActivity().intent.getStringArrayExtra(PostCreationActivity.TEMP_FILES)
|
||||
tempFiles?.asList()?.forEach {
|
||||
val file = File(binding.root.context.cacheDir, it)
|
||||
model.trackTempFile(file)
|
||||
}
|
||||
|
||||
// Handle back pressed button
|
||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
val redraft = requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_REDRAFT, false)
|
||||
if (redraft) {
|
||||
val builder = AlertDialog.Builder(binding.root.context)
|
||||
builder.apply {
|
||||
setMessage(R.string.redraft_dialog_cancel)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
requireActivity().finish()
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
show()
|
||||
}
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK && result.data?.clipData != null) {
|
||||
result.data?.clipData?.let {
|
||||
model.setImages(model.addPossibleImages(it))
|
||||
}
|
||||
} else if (result.resultCode != Activity.RESULT_CANCELED) {
|
||||
Toast.makeText(requireActivity(), R.string.add_images_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addPhoto(){
|
||||
addPhotoResultContract.launch(
|
||||
Intent(requireActivity(), CameraActivity::class.java)
|
||||
)
|
||||
}
|
||||
|
||||
private fun savePicture(button: View, currentPosition: Int) {
|
||||
val originalUri = model.getPhotoData().value!![currentPosition].imageUri
|
||||
|
||||
val pair = getOutputFile(originalUri)
|
||||
val outputStream: OutputStream = pair.first
|
||||
val path: String = pair.second
|
||||
|
||||
requireActivity().contentResolver.openInputStream(originalUri)!!.use { input ->
|
||||
outputStream.use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
if(path.startsWith("file")) {
|
||||
MediaScannerConnection.scanFile(
|
||||
requireActivity(),
|
||||
arrayOf(path.toUri().toFile().absolutePath),
|
||||
null
|
||||
) { tried_path, uri ->
|
||||
if (uri == null) {
|
||||
Log.e(
|
||||
"NEW IMAGE SCAN FAILED",
|
||||
"Tried to scan $tried_path, but it failed"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Snackbar.make(
|
||||
button, getString(R.string.save_image_success),
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun getOutputFile(uri: Uri): Pair<OutputStream, String> {
|
||||
val extension = uri.fileExtension(requireActivity().contentResolver)
|
||||
|
||||
val name = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
|
||||
.format(System.currentTimeMillis()) + ".$extension"
|
||||
|
||||
val outputStream: OutputStream
|
||||
val path: String
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val resolver: ContentResolver = requireActivity().contentResolver
|
||||
val type = uri.getMimeType(requireActivity().contentResolver)
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
|
||||
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, type)
|
||||
contentValues.put(
|
||||
MediaStore.MediaColumns.RELATIVE_PATH,
|
||||
Environment.DIRECTORY_PICTURES
|
||||
)
|
||||
val store =
|
||||
if (type.startsWith("video")) MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||
else MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
val imageUri: Uri = resolver.insert(store, contentValues)!!
|
||||
path = imageUri.toString()
|
||||
outputStream = resolver.openOutputStream(imageUri)!!
|
||||
} else {
|
||||
val imagesDir = Environment.getExternalStoragePublicDirectory(getString(R.string.app_name))
|
||||
imagesDir.mkdir()
|
||||
val file = File(imagesDir, name)
|
||||
path = Uri.fromFile(file).toString()
|
||||
outputStream = file.outputStream()
|
||||
}
|
||||
return Pair(outputStream, path)
|
||||
}
|
||||
|
||||
|
||||
private fun validatePost(): Boolean {
|
||||
if (model.getPhotoData().value?.all { !it.video || it.videoEncodeComplete } == false) {
|
||||
AlertDialog.Builder(requireActivity()).apply {
|
||||
setMessage(R.string.still_encoding)
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private val editResultContract: ActivityResultLauncher<Intent> = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()){
|
||||
result: ActivityResult? ->
|
||||
if (result?.resultCode == Activity.RESULT_OK && result.data != null) {
|
||||
val position: Int = result.data!!.getIntExtra(PhotoEditActivity.PICTURE_POSITION, 0)
|
||||
model.modifyAt(position, result.data!!)
|
||||
?: Toast.makeText(requireActivity(), R.string.error_editing, Toast.LENGTH_SHORT).show()
|
||||
} else if(result?.resultCode != Activity.RESULT_CANCELED){
|
||||
Toast.makeText(requireActivity(), R.string.error_editing, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun edit(position: Int) {
|
||||
val intent = Intent(
|
||||
requireActivity(),
|
||||
if (model.getPhotoData().value!![position].video) VideoEditActivity::class.java else PhotoEditActivity::class.java
|
||||
)
|
||||
.putExtra(PhotoEditActivity.PICTURE_URI, model.getPhotoData().value!![position].imageUri)
|
||||
.putExtra(PhotoEditActivity.PICTURE_POSITION, position)
|
||||
|
||||
editResultContract.launch(intent)
|
||||
}
|
||||
}
|
||||
|
@ -2,14 +2,16 @@ package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.app.Application
|
||||
import android.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)
|
||||
}
|
||||
}
|
||||
|
@ -1,188 +0,0 @@
|
||||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityPostSubmissionBinding
|
||||
import org.pixeldroid.app.postCreation.PostCreationActivity.Companion.TEMP_FILES
|
||||
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.setSquareImageFromURL
|
||||
import java.io.File
|
||||
|
||||
|
||||
class PostSubmissionActivity : BaseThemedWithoutBarActivity() {
|
||||
|
||||
companion object {
|
||||
internal const val PICTURE_DESCRIPTION = "picture_description"
|
||||
internal const val PHOTO_DATA = "photo_data"
|
||||
}
|
||||
|
||||
private lateinit var accounts: List<UserDatabaseEntity>
|
||||
private var selectedAccount: Int = -1
|
||||
private lateinit var menu: Menu
|
||||
private var user: UserDatabaseEntity? = null
|
||||
private lateinit var instance: InstanceDatabaseEntity
|
||||
|
||||
private lateinit var binding: ActivityPostSubmissionBinding
|
||||
|
||||
private lateinit var model: PostSubmissionViewModel
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityPostSubmissionBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setTitle(R.string.add_details)
|
||||
|
||||
user = db.userDao().getActiveUser()
|
||||
accounts = db.userDao().getAll()
|
||||
|
||||
instance = user?.run {
|
||||
db.instanceDao().getAll().first { instanceDatabaseEntity ->
|
||||
instanceDatabaseEntity.uri.contains(instance_uri)
|
||||
}
|
||||
} ?: InstanceDatabaseEntity("", "")
|
||||
|
||||
val photoData = intent.getParcelableArrayListExtra<PhotoData>(PHOTO_DATA) as ArrayList<PhotoData>?
|
||||
|
||||
val _model: PostSubmissionViewModel by viewModels {
|
||||
PostSubmissionViewModelFactory(
|
||||
application,
|
||||
photoData!!
|
||||
)
|
||||
}
|
||||
model = _model
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.uiState.collect { uiState ->
|
||||
uiState.userMessage?.let {
|
||||
AlertDialog.Builder(binding.root.context).apply {
|
||||
setMessage(it)
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
|
||||
// Notify the ViewModel the message is displayed
|
||||
model.userMessageShown()
|
||||
}
|
||||
enableButton(uiState.postCreationSendButtonEnabled)
|
||||
binding.uploadProgressBar.visibility =
|
||||
if (uiState.uploadProgressBarVisible) VISIBLE else INVISIBLE
|
||||
binding.uploadProgressBar.progress = uiState.uploadProgress
|
||||
binding.uploadCompletedTextview.visibility =
|
||||
if (uiState.uploadCompletedTextviewVisible) VISIBLE else INVISIBLE
|
||||
binding.uploadError.visibility =
|
||||
if (uiState.uploadErrorVisible) VISIBLE else INVISIBLE
|
||||
binding.uploadErrorTextExplanation.visibility =
|
||||
if (uiState.uploadErrorExplanationVisible) VISIBLE else INVISIBLE
|
||||
|
||||
selectedAccount = accounts.indexOf(uiState.chosenAccount)
|
||||
|
||||
binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.newPostDescriptionInputField.doAfterTextChanged {
|
||||
model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text)
|
||||
}
|
||||
|
||||
binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
model.updateNSFW(isChecked)
|
||||
}
|
||||
|
||||
val existingDescription: String? = intent.getStringExtra(PICTURE_DESCRIPTION)
|
||||
|
||||
binding.newPostDescriptionInputField.setText(
|
||||
// Set description from redraft if any, otherwise from the template
|
||||
existingDescription ?: model.uiState.value.newPostDescriptionText
|
||||
)
|
||||
|
||||
|
||||
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
|
||||
|
||||
setSquareImageFromURL(View(applicationContext), photoData!![0].imageUri.toString(), binding.postPreview)
|
||||
// get the description and send the post
|
||||
binding.postCreationSendButton.setOnClickListener {
|
||||
if (validatePost()) model.upload()
|
||||
}
|
||||
|
||||
// Button to retry image upload when it fails
|
||||
binding.retryUploadButton.setOnClickListener {
|
||||
model.resetUploadStatus()
|
||||
model.upload()
|
||||
}
|
||||
|
||||
// Clean up temporary files, if any
|
||||
val tempFiles = intent.getStringArrayExtra(TEMP_FILES)
|
||||
tempFiles?.asList()?.forEach {
|
||||
val file = File(binding.root.context.cacheDir, it)
|
||||
model.trackTempFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(newMenu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.post_submission_account_menu, newMenu)
|
||||
menu = newMenu
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId){
|
||||
R.id.action_switch_accounts -> {
|
||||
AlertDialog.Builder(this).apply {
|
||||
setIcon(R.drawable.material_drawer_ico_account)
|
||||
setTitle(R.string.switch_accounts)
|
||||
setSingleChoiceItems(accounts.map { it.username + " (${it.fullHandle})" }.toTypedArray(), selectedAccount) { dialog, which ->
|
||||
if(selectedAccount != which){
|
||||
model.chooseAccount(accounts[which])
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
}.show()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun validatePost(): Boolean {
|
||||
binding.postTextInputLayout.run {
|
||||
val content = editText?.length() ?: 0
|
||||
if (content > counterMaxLength) {
|
||||
// error, too many characters
|
||||
error = resources.getQuantityString(R.plurals.description_max_characters, counterMaxLength, counterMaxLength)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun enableButton(enable: Boolean = true){
|
||||
binding.postCreationSendButton.isEnabled = enable
|
||||
if(enable){
|
||||
binding.postingProgressBar.visibility = GONE
|
||||
binding.postCreationSendButton.visibility = VISIBLE
|
||||
} else {
|
||||
binding.postingProgressBar.visibility = VISIBLE
|
||||
binding.postCreationSendButton.visibility = GONE
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.FragmentPostSubmissionBinding
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.setSquareImageFromURL
|
||||
|
||||
|
||||
class PostSubmissionFragment : BaseFragment() {
|
||||
|
||||
private lateinit var accounts: List<UserDatabaseEntity>
|
||||
private var selectedAccount: Int = -1
|
||||
|
||||
private var user: UserDatabaseEntity? = null
|
||||
private lateinit var instance: InstanceDatabaseEntity
|
||||
|
||||
private lateinit var binding: FragmentPostSubmissionBinding
|
||||
private lateinit var model: PostCreationViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// Inflate the layout for this fragment
|
||||
binding = FragmentPostSubmissionBinding.inflate(layoutInflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.topBar.setupWithNavController(findNavController())
|
||||
|
||||
user = db.userDao().getActiveUser()
|
||||
accounts = db.userDao().getAll()
|
||||
|
||||
instance = user?.run {
|
||||
db.instanceDao().getAll().first { instanceDatabaseEntity ->
|
||||
instanceDatabaseEntity.uri.contains(instance_uri)
|
||||
}
|
||||
} ?: InstanceDatabaseEntity("", "")
|
||||
|
||||
val _model: PostCreationViewModel by activityViewModels {
|
||||
PostCreationViewModelFactory(
|
||||
requireActivity().application,
|
||||
requireActivity().intent.clipData!!,
|
||||
instance,
|
||||
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
|
||||
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false)
|
||||
)
|
||||
}
|
||||
model = _model
|
||||
|
||||
// Display the values from the view model
|
||||
binding.nsfwSwitch.isChecked = model.uiState.value.nsfw
|
||||
binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText)
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.uiState.collect { uiState ->
|
||||
uiState.userMessage?.let {
|
||||
AlertDialog.Builder(binding.root.context).apply {
|
||||
setMessage(it)
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
|
||||
// Notify the ViewModel the message is displayed
|
||||
model.userMessageShown()
|
||||
}
|
||||
enableButton(uiState.postCreationSendButtonEnabled)
|
||||
binding.uploadProgressBar.visibility =
|
||||
if (uiState.uploadProgressBarVisible) View.VISIBLE else View.INVISIBLE
|
||||
binding.uploadProgressBar.progress = uiState.uploadProgress
|
||||
binding.uploadCompletedTextview.visibility =
|
||||
if (uiState.uploadCompletedTextviewVisible) View.VISIBLE else View.INVISIBLE
|
||||
binding.uploadError.visibility =
|
||||
if (uiState.uploadErrorVisible) View.VISIBLE else View.INVISIBLE
|
||||
binding.uploadErrorTextExplanation.visibility =
|
||||
if (uiState.uploadErrorExplanationVisible) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
selectedAccount = accounts.indexOf(uiState.chosenAccount)
|
||||
|
||||
binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.newPostDescriptionInputField.doAfterTextChanged {
|
||||
model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text)
|
||||
}
|
||||
|
||||
binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
model.updateNSFW(isChecked)
|
||||
}
|
||||
|
||||
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
|
||||
|
||||
setSquareImageFromURL(View(requireActivity()), model.getPhotoData().value?.get(0)?.imageUri.toString(), binding.postPreview)
|
||||
|
||||
// Get the description and send the post
|
||||
binding.postCreationSendButton.setOnClickListener {
|
||||
if (validatePost()) model.upload()
|
||||
}
|
||||
|
||||
// Button to retry image upload when it fails
|
||||
binding.retryUploadButton.setOnClickListener {
|
||||
model.resetUploadStatus()
|
||||
model.upload()
|
||||
}
|
||||
|
||||
// Handle back pressed button
|
||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
findNavController().navigate(R.id.action_postSubmissionFragment_to_postCreationFragment)
|
||||
}
|
||||
})
|
||||
|
||||
binding.topBar.addMenuProvider(object: MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
// Add menu items here
|
||||
menuInflater.inflate(R.menu.post_submission_account_menu, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
// Handle the menu selection
|
||||
return when (menuItem.itemId) {
|
||||
R.id.action_switch_accounts -> {
|
||||
AlertDialog.Builder(requireActivity()).apply {
|
||||
setIcon(R.drawable.switch_account)
|
||||
setTitle(R.string.switch_accounts)
|
||||
setSingleChoiceItems(accounts.map { it.username + " (${it.fullHandle})" }.toTypedArray(), selectedAccount) { dialog, which ->
|
||||
if (selectedAccount != which) {
|
||||
model.chooseAccount(accounts[which])
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
}.show()
|
||||
return true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
}
|
||||
|
||||
private fun validatePost(): Boolean {
|
||||
binding.postTextInputLayout.run {
|
||||
val content = editText?.length() ?: 0
|
||||
if (content > counterMaxLength) {
|
||||
// error, too many characters
|
||||
error = resources.getQuantityString(R.plurals.description_max_characters, counterMaxLength, counterMaxLength)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun enableButton(enable: Boolean = true){
|
||||
binding.postCreationSendButton.isEnabled = enable
|
||||
if(enable){
|
||||
binding.postingProgressBar.visibility = View.GONE
|
||||
binding.postCreationSendButton.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.postingProgressBar.visibility = View.VISIBLE
|
||||
binding.postCreationSendButton.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -54,7 +54,7 @@ data class PostSubmissionActivityUiState(
|
||||
val uploadErrorExplanationVisible: Boolean = false,
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
5
app/src/main/res/drawable/switch_account.xml
Normal file
5
app/src/main/res/drawable/switch_account.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?attr/colorOnBackground" android:pathData="M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM14,4c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM20,16L8,16v-1.5c0,-1.99 4,-3 6,-3s6,1.01 6,3L20,16z"/>
|
||||
</vector>
|
@ -6,96 +6,16 @@
|
||||
android:layout_height="match_parent"
|
||||
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>
|
@ -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"
|
||||
|
102
app/src/main/res/layout/fragment_post_creation.xml
Normal file
102
app/src/main/res/layout/fragment_post_creation.xml
Normal file
@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".postCreation.PostCreationFragment"
|
||||
android:id="@+id/postCreationFragment" >
|
||||
|
||||
<org.pixeldroid.app.postCreation.carousel.ImageCarousel
|
||||
android:id="@+id/carousel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:showCaption="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/buttonConstraints"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/buttonConstraints"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/post_creation_send_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:enabled="true"
|
||||
android:text="@string/upload_next_step"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/toolbarPostCreation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#40000000"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/savePhotoButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="30dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/save_to_gallery"
|
||||
android:tooltipText='@string/save_to_gallery'
|
||||
android:src="@drawable/download_file_30dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/removePhotoButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="30dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/delete"
|
||||
android:tooltipText='@string/delete'
|
||||
android:src="@drawable/delete_30dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/savePhotoButton"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/editPhotoButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="30dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/edit"
|
||||
android:tooltipText='@string/edit'
|
||||
android:src="@drawable/ic_baseline_edit_30"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/removePhotoButton"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/addPhotoButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="30dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/add_photo"
|
||||
android:tooltipText='@string/add_photo'
|
||||
android:src="@drawable/add_photo_button"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -4,7 +4,18 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
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>
|
||||
|
||||
|
@ -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>
|
25
app/src/main/res/navigation/post_creation_graph.xml
Normal file
25
app/src/main/res/navigation/post_creation_graph.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/post_creation_graph"
|
||||
app:startDestination="@id/postCreationFragment" >
|
||||
|
||||
<fragment
|
||||
android:id="@+id/postCreationFragment"
|
||||
android:name="org.pixeldroid.app.postCreation.PostCreationFragment"
|
||||
tools:layout="@layout/fragment_post_creation" >
|
||||
<action
|
||||
android:id="@+id/action_postCreationFragment_to_postSubmissionFragment"
|
||||
app:destination="@id/postSubmissionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/postSubmissionFragment"
|
||||
android:name="org.pixeldroid.app.postCreation.PostSubmissionFragment"
|
||||
android:label="@string/add_details"
|
||||
tools:layout="@layout/fragment_post_submission" >
|
||||
<action
|
||||
android:id="@+id/action_postSubmissionFragment_to_postCreationFragment"
|
||||
app:destination="@id/postCreationFragment" />
|
||||
</fragment>
|
||||
</navigation>
|
@ -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>
|
||||
|
@ -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>
|
@ -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
|
||||
|
11
fastlane/metadata/android/de/changelogs/18.txt
Normal file
11
fastlane/metadata/android/de/changelogs/18.txt
Normal 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
|
17
fastlane/metadata/android/de/changelogs/19.txt
Normal file
17
fastlane/metadata/android/de/changelogs/19.txt
Normal 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
|
5114
gradle/verification-metadata.xml
Normal file
5114
gradle/verification-metadata.xml
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user