diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt index cfb792d8..42c56d5a 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt @@ -64,6 +64,12 @@ data class PhotoData( 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" + } + private var user: UserDatabaseEntity? = null private lateinit var instance: InstanceDatabaseEntity @@ -87,7 +93,13 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() { } } ?: InstanceDatabaseEntity("", "") - val _model: PostCreationViewModel by viewModels { PostCreationViewModelFactory(application, intent.clipData!!, instance) } + val _model: PostCreationViewModel by viewModels { + PostCreationViewModelFactory( + application, + intent.clipData!!, + instance + ) + } model = _model model.getPhotoData().observe(this) { newPhotoData -> @@ -99,9 +111,6 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() { ) } - //Get initial text value from model (for template) - binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText) - lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { model.uiState.collect { uiState -> @@ -116,15 +125,20 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() { } binding.addPhotoButton.isEnabled = uiState.addPhotoButtonEnabled enableButton(uiState.postCreationSendButtonEnabled) - binding.uploadProgressBar.visibility = if(uiState.uploadProgressBarVisible) VISIBLE else INVISIBLE + binding.uploadProgressBar.visibility = + if (uiState.uploadProgressBarVisible) VISIBLE else INVISIBLE binding.uploadProgressBar.progress = uiState.uploadProgress - binding.uploadCompletedTextview.visibility = if(uiState.uploadCompletedTextviewVisible) VISIBLE else INVISIBLE + binding.uploadCompletedTextview.visibility = + if (uiState.uploadCompletedTextviewVisible) VISIBLE else INVISIBLE binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled - binding.uploadError.visibility = if(uiState.uploadErrorVisible) VISIBLE else INVISIBLE - binding.uploadErrorTextExplanation.visibility = if(uiState.uploadErrorExplanationVisible) VISIBLE else INVISIBLE + binding.uploadError.visibility = + if (uiState.uploadErrorVisible) VISIBLE else INVISIBLE + binding.uploadErrorTextExplanation.visibility = + if (uiState.uploadErrorExplanationVisible) VISIBLE else INVISIBLE - binding.toolbarPostCreation.visibility = if(uiState.isCarousel) VISIBLE else INVISIBLE + binding.toolbarPostCreation.visibility = + if (uiState.isCarousel) VISIBLE else INVISIBLE binding.carousel.layoutCarousel = uiState.isCarousel @@ -155,6 +169,15 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() { binding.newPostDescriptionInputField.doAfterTextChanged { model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text) } + + 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 binding.carousel.apply { @@ -201,6 +224,30 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() { model.cancelEncode(currentPosition) } } + + // Clean up temporary files, if any + val tempFiles = intent.getStringArrayExtra(TEMP_FILES) + tempFiles?.asList()?.forEach { + val file = File(binding.root.context.cacheDir, it) + model.trackTempFile(file) + } + } + + override fun onBackPressed() { + 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() + } } private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt index 8ec21575..6b2713c3 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt @@ -1,6 +1,5 @@ package org.pixeldroid.app.postCreation -import android.R.attr.orientation import android.app.Application import android.content.ClipData import android.content.Intent @@ -144,10 +143,10 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null } } for (i in 0 until count) { - clipData.getItemAt(i).uri.let { + clipData.getItemAt(i).let { val sizeAndVideoPair: Pair = - getSizeAndVideoValidate(it, (previousList?.size ?: 0) + dataToAdd.size + 1) - dataToAdd.add(PhotoData(imageUri = it, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second)) + getSizeAndVideoValidate(it.uri, (previousList?.size ?: 0) + dataToAdd.size + 1) + dataToAdd.add(PhotoData(imageUri = it.uri, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second, imageDescription = it.text?.toString())) } } return previousList?.plus(dataToAdd)?.toMutableList() ?: mutableListOf() diff --git a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt index 2cd900a8..ebc80ff0 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt @@ -3,11 +3,14 @@ package org.pixeldroid.app.posts import android.Manifest import android.app.Activity import android.app.AlertDialog +import android.content.ClipData 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 import android.util.Log import android.view.LayoutInflater @@ -37,10 +40,17 @@ import com.karumi.dexter.listener.PermissionDeniedResponse import com.karumi.dexter.listener.PermissionGrantedResponse import com.karumi.dexter.listener.single.BasePermissionListener import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import okhttp3.* +import okio.BufferedSink +import okio.buffer +import okio.sink import org.pixeldroid.app.R import org.pixeldroid.app.databinding.AlbumImageViewBinding import org.pixeldroid.app.databinding.OpenedAlbumBinding import org.pixeldroid.app.databinding.PostFragmentBinding +import org.pixeldroid.app.postCreation.PostCreationActivity +import org.pixeldroid.app.postCreation.photoEdit.PhotoEditActivity import org.pixeldroid.app.posts.MediaViewerActivity.Companion.openActivity import org.pixeldroid.app.utils.BlurHashDecoder import org.pixeldroid.app.utils.api.PixelfedAPI @@ -55,6 +65,8 @@ import org.pixeldroid.app.utils.setProfileImageFromURL import retrofit2.HttpException import java.io.File import java.io.IOException +import java.net.URI +import java.util.concurrent.atomic.AtomicInteger import kotlin.math.roundToInt @@ -322,7 +334,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold //Call the api function status?.id?.let { id -> try { - if(bookmarked) { + if (bookmarked) { api.bookmarkStatus(id) } else { api.undoBookmarkStatus(id) @@ -337,7 +349,10 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold } catch (exception: HttpException) { Toast.makeText( binding.root.context, - binding.root.context.getString(R.string.bookmark_post_failed_error, exception.code()), + binding.root.context.getString( + R.string.bookmark_post_failed_error, + exception.code() + ), Toast.LENGTH_SHORT ).show() } catch (exception: IOException) { @@ -442,28 +457,195 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold setPositiveButton(android.R.string.ok) { _, _ -> lifecycleScope.launch { - val user = db.userDao().getActiveUser()!! - status?.id?.let { id -> - db.homePostDao().delete(id, user.user_id, user.instance_uri) - db.publicPostDao().delete(id, user.user_id, user.instance_uri) - try { - val api = apiHolder.api ?: apiHolder.setToCurrentUser() - api.deleteStatus(id) - binding.root.visibility = View.GONE - } catch (exception: HttpException) { + deletePost(apiHolder.api ?: apiHolder.setToCurrentUser(), db) + } + } + setNegativeButton(android.R.string.cancel) { _, _ -> } + show() + } + + true + } + R.id.post_more_menu_redraft -> { + val builder = AlertDialog.Builder(binding.root.context) + builder.apply { + setMessage(R.string.redraft_dialog_launch) + setPositiveButton(android.R.string.ok) { _, _ -> + + lifecycleScope.launch { + try { + // Create new post creation activity + val intent = + Intent(context, PostCreationActivity::class.java) + + // Get descriptions and images from original post + val postDescription = status?.content ?: "" + val postAttachments = + status?.media_attachments!! // Catch possible exception from !! (?) + val imageUris: MutableList = mutableListOf() + val imageNames: MutableList = mutableListOf() + val imageDescriptions: MutableList = + mutableListOf() + + 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 counter = AtomicInteger(0) + + // Define callback function for after downloading the images + fun continuation() { + // Wait for all outstanding downloads to finish + if (counter.incrementAndGet() == imageUris.size) { + if (allFilesExist(imageNames)) { + val counterInt = counter.get() + Toast.makeText( + binding.root.context, + binding.root.context.resources.getQuantityString( + R.plurals.items_load_success, + counterInt, + counterInt + ), + Toast.LENGTH_SHORT + ).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 + ) + if (clipData == null) { + clipData = ClipData( + "", + emptyArray(), + imageItem + ) + } else { + clipData!!.addItem(imageItem) + } + } + + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + // Pass post description of existing post to new post creation activity + intent.putExtra( + PostCreationActivity.PICTURE_DESCRIPTION, + fromHtml(postDescription).toString() + ) + if (imageNames.isNotEmpty()) { + intent.putExtra( + PostCreationActivity.TEMP_FILES, + imageNames.toTypedArray() + ) + } + intent.putExtra( + PostCreationActivity.POST_REDRAFT, + true + ) + + // Launch post creation activity + binding.root.context.startActivity(intent) + } + } + } + + if (!allFilesExist(imageNames)) { + // Track download progress Toast.makeText( - binding.root.context, - binding.root.context.getString(R.string.delete_post_failed_error, exception.code()), - Toast.LENGTH_SHORT - ).show() - } catch (exception: IOException) { - Toast.makeText( - binding.root.context, - binding.root.context.getString(R.string.delete_post_failed_io_except), - Toast.LENGTH_SHORT + binding.root.context, + binding.root.context.getString(R.string.image_download_downloading), + Toast.LENGTH_SHORT ).show() } + + // 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() + + // Check whether image is in cache directory already (maybe rather do so using Glide in the future?) + if (!downloadedFile.exists()) { + OkHttpClient().newCall(downloadRequest) + .enqueue(object : Callback { + override fun onFailure( + call: Call, + e: IOException + ) { + Looper.prepare() + downloadedFile.delete() + Toast.makeText( + binding.root.context, + binding.root.context.getString(R.string.redraft_post_failed_io_except), + Toast.LENGTH_SHORT + ).show() + } + + @Throws(IOException::class) + override fun onResponse( + call: Call, + response: Response + ) { + val sink: BufferedSink = + downloadedFile.sink().buffer() + sink.writeAll(response.body!!.source()) + sink.close() + Looper.prepare() + continuation() + } + }) + } else { + continuation() + } + } + + } catch (exception: HttpException) { + Toast.makeText( + binding.root.context, + binding.root.context.getString( + R.string.redraft_post_failed_error, + exception.code() + ), + Toast.LENGTH_SHORT + ).show() + } catch (exception: IOException) { + Toast.makeText( + binding.root.context, + binding.root.context.getString(R.string.redraft_post_failed_io_except), + Toast.LENGTH_SHORT + ).show() } + + // Delete original post + deletePost( + apiHolder.api ?: apiHolder.setToCurrentUser(), + db + ) + } } setNegativeButton(android.R.string.cancel) { _, _ -> } @@ -546,6 +728,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold status?.media_attachments?.let { binding.postPagerHost.images = ArrayList(it) } } + private fun ImageView.animateView() { visibility = View.VISIBLE when (val drawable = drawable) { @@ -627,8 +810,39 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold } } + private suspend fun deletePost(api: PixelfedAPI, db: AppDatabase) { + val user = db.userDao().getActiveUser()!! + status?.id?.let { id -> + db.homePostDao().delete(id, user.user_id, user.instance_uri) + db.publicPostDao().delete(id, user.user_id, user.instance_uri) + try { + api.deleteStatus(id) + binding.root.visibility = View.GONE + } catch (exception: HttpException) { + Toast.makeText( + binding.root.context, + binding.root.context.getString(R.string.delete_post_failed_error, exception.code()), + Toast.LENGTH_SHORT + ).show() + } catch (exception: IOException) { + Toast.makeText( + binding.root.context, + binding.root.context.getString(R.string.delete_post_failed_io_except), + Toast.LENGTH_SHORT + ).show() + } + } + } - + private fun allFilesExist(listOfNames: MutableList): Boolean { + for (name in listOfNames) { + val file = File(binding.root.context.cacheDir, name) + if (!file.exists()) { + return false + } + } + return true + } companion object { fun create(parent: ViewGroup): StatusViewHolder { diff --git a/app/src/main/res/menu/post_more_menu.xml b/app/src/main/res/menu/post_more_menu.xml index b46bf28c..effe6c4c 100644 --- a/app/src/main/res/menu/post_more_menu.xml +++ b/app/src/main/res/menu/post_more_menu.xml @@ -32,6 +32,10 @@ android:id="@+id/post_more_menu_group_delete" android:visible="false"> + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c6c03d17..8d1c4d3e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -148,6 +148,10 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Download has failed, please try again Downloading… Image downloaded successfully + + %d item loaded successfully + %d items loaded successfully + No description @@ -246,12 +250,17 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Discover doesn\'t load infinitely. Pull to refresh for other images. Something went wrong… This panda is not happy. Pull to refresh to try again. + Redraft + Redrafting this post will allow you to edit the photo and its description, but it will delete all current comments and likes. Continue? + If you cancel this redraft, the original post will no longer be on your account. Continue without reposting? Delete Delete this post? Language Help translate PixelDroid to your language: Report issues or contribute to the application: Image showing a red panda, Pixelfed\'s mascot, using a phone + Could not redraft the post, error %1$d + Could not redraft the post, check your connection? Could not delete the post, error %1$d Could not delete the post, check your connection? Could not (un)bookmark the post, error %1$d diff --git a/build.gradle b/build.gradle index 78010cad..d94fccf2 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.0.0-alpha05' + classpath 'com.android.tools.build:gradle:8.0.0-alpha06' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 8b3703e8..19db0ece 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -3463,6 +3463,9 @@ + + + @@ -3495,6 +3498,9 @@ + + +