Video edit
This commit is contained in:
parent
1f03f96d7a
commit
8cecfa3de6
@ -28,8 +28,7 @@
|
||||
android:theme="@style/AppTheme.ActionBar.Transparent"/>
|
||||
<activity
|
||||
android:name=".postCreation.photoEdit.VideoEditActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/AppTheme.ActionBar.Transparent"/>
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".posts.MediaViewerActivity"
|
||||
|
@ -5,9 +5,7 @@ import android.app.AlertDialog
|
||||
import android.content.*
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.*
|
||||
import android.provider.MediaStore
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
@ -18,13 +16,19 @@ 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.core.os.HandlerCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.arthenica.ffmpegkit.*
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import okhttp3.MultipartBody
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
|
||||
@ -32,23 +36,26 @@ import org.pixeldroid.app.postCreation.camera.CameraActivity
|
||||
import org.pixeldroid.app.postCreation.carousel.CarouselItem
|
||||
import org.pixeldroid.app.postCreation.carousel.ImageCarousel
|
||||
import org.pixeldroid.app.postCreation.photoEdit.PhotoEditActivity
|
||||
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
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 okhttp3.MultipartBody
|
||||
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity
|
||||
import org.pixeldroid.app.posts.PostActivity
|
||||
import org.pixeldroid.app.utils.api.objects.Status
|
||||
import org.pixeldroid.app.utils.ffmpegSafeUri
|
||||
import retrofit2.HttpException
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
import kotlin.math.ceil
|
||||
import com.arthenica.ffmpegkit.FFprobeKit
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.math.ceil
|
||||
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
private const val TAG = "Post Creation Activity"
|
||||
|
||||
@ -59,6 +66,7 @@ data class PhotoData(
|
||||
var progress: Int? = null,
|
||||
var imageDescription: String? = null,
|
||||
var video: Boolean,
|
||||
var videoEncodeProgress: Int? = null,
|
||||
)
|
||||
|
||||
class PostCreationActivity : BaseActivity() {
|
||||
@ -70,11 +78,22 @@ class PostCreationActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityPostCreationBinding
|
||||
|
||||
private val resultHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper())
|
||||
|
||||
// Map photoData indexes to FFmpeg Session IDs
|
||||
private val sessionMap: MutableMap<Int, Long> = mutableMapOf()
|
||||
// Keep track of temporary files to delete them (avoids filling cache super fast with videos)
|
||||
private val tempFiles: ArrayList<File> = ArrayList()
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityPostCreationBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
|
||||
|
||||
user = db.userDao().getActiveUser()
|
||||
|
||||
instance = user?.run {
|
||||
@ -89,7 +108,7 @@ class PostCreationActivity : BaseActivity() {
|
||||
intent.clipData?.let { addPossibleImages(it) }
|
||||
|
||||
val carousel: ImageCarousel = binding.carousel
|
||||
carousel.addData(photoData.map { CarouselItem(it.imageUri, video = it.video) })
|
||||
carousel.addData(photoData.map { CarouselItem(it.imageUri, video = it.video, encodeProgress = null) })
|
||||
carousel.layoutCarouselCallback = {
|
||||
if(it){
|
||||
// Became a carousel
|
||||
@ -109,7 +128,7 @@ class PostCreationActivity : BaseActivity() {
|
||||
|
||||
// get the description and send the post
|
||||
binding.postCreationSendButton.setOnClickListener {
|
||||
if (validateDescription() && photoData.isNotEmpty()) upload()
|
||||
if (validatePost() && photoData.isNotEmpty()) upload()
|
||||
}
|
||||
|
||||
// Button to retry image upload when it fails
|
||||
@ -123,7 +142,7 @@ class PostCreationActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
binding.editPhotoButton.setOnClickListener {
|
||||
carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition ->
|
||||
carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
||||
edit(currentPosition)
|
||||
}
|
||||
}
|
||||
@ -133,27 +152,36 @@ class PostCreationActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
binding.savePhotoButton.setOnClickListener {
|
||||
carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition ->
|
||||
carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
||||
savePicture(it, currentPosition)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
binding.removePhotoButton.setOnClickListener {
|
||||
carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition ->
|
||||
carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
|
||||
photoData.removeAt(currentPosition)
|
||||
carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video) })
|
||||
sessionMap[currentPosition]?.let { FFmpegKit.cancel(it) }
|
||||
carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) })
|
||||
binding.addPhotoButton.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
FFmpegKit.cancel()
|
||||
tempFiles.forEach {
|
||||
it.delete()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will add as many images as possible to [photoData], from the [clipData], and if
|
||||
* ([photoData].size + [clipData].itemCount) > [albumLimit] then it will only add as many images
|
||||
* ([photoData].size + [clipData].itemCount) > [InstanceDatabaseEntity.albumLimit] then it will only add as many images
|
||||
* as are legal (if any) and a dialog will be shown to the user alerting them of this fact.
|
||||
*/
|
||||
private fun addPossibleImages(clipData: ClipData){
|
||||
private fun addPossibleImages(clipData: ClipData) {
|
||||
var count = clipData.itemCount
|
||||
if(count + photoData.size > instance.albumLimit){
|
||||
AlertDialog.Builder(this).apply {
|
||||
@ -168,7 +196,7 @@ class PostCreationActivity : BaseActivity() {
|
||||
}
|
||||
for (i in 0 until count) {
|
||||
clipData.getItemAt(i).uri.let {
|
||||
val sizeAndVideoPair: Pair<Long, Boolean> = it.getSizeAndVideoValidate()
|
||||
val sizeAndVideoPair: Pair<Long, Boolean> = it.getSizeAndVideoValidate(photoData.size + 1)
|
||||
photoData.add(PhotoData(imageUri = it, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second))
|
||||
}
|
||||
}
|
||||
@ -178,7 +206,7 @@ class PostCreationActivity : BaseActivity() {
|
||||
* Returns the size of the file of the Uri, and whether it is a video,
|
||||
* and opens a dialog in case it is too big or in case the file is unsupported.
|
||||
*/
|
||||
private fun Uri.getSizeAndVideoValidate(): Pair<Long, Boolean> {
|
||||
private fun Uri.getSizeAndVideoValidate(editPosition: Int): Pair<Long, Boolean> {
|
||||
val size: Long =
|
||||
if (toString().startsWith("content")) {
|
||||
contentResolver.query(this, null, null, null, null)
|
||||
@ -209,7 +237,7 @@ class PostCreationActivity : BaseActivity() {
|
||||
if (sizeInkBytes > instance.maxPhotoSize || sizeInkBytes > instance.maxVideoSize) {
|
||||
val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize
|
||||
AlertDialog.Builder(this@PostCreationActivity).apply {
|
||||
setMessage(getString(R.string.size_exceeds_instance_limit, photoData.size + 1, sizeInkBytes, maxSize))
|
||||
setMessage(getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize))
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
}
|
||||
@ -221,7 +249,7 @@ class PostCreationActivity : BaseActivity() {
|
||||
result.data?.clipData?.let {
|
||||
addPossibleImages(it)
|
||||
}
|
||||
binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video) })
|
||||
binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) })
|
||||
} else if (result.resultCode != Activity.RESULT_CANCELED) {
|
||||
Toast.makeText(applicationContext, "Error while adding images", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@ -294,7 +322,7 @@ class PostCreationActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
|
||||
private fun validateDescription(): Boolean {
|
||||
private fun validatePost(): Boolean {
|
||||
binding.postTextInputLayout.run {
|
||||
val content = editText?.length() ?: 0
|
||||
if (content > counterMaxLength) {
|
||||
@ -303,6 +331,13 @@ class PostCreationActivity : BaseActivity() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if(!photoData.all { it.videoEncodeProgress == null }){
|
||||
AlertDialog.Builder(this).apply {
|
||||
setMessage(R.string.still_encoding)
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -435,20 +470,132 @@ class PostCreationActivity : BaseActivity() {
|
||||
if (result?.resultCode == Activity.RESULT_OK && result.data != null) {
|
||||
val position: Int = result.data!!.getIntExtra(PhotoEditActivity.PICTURE_POSITION, 0)
|
||||
photoData.getOrNull(position)?.apply {
|
||||
imageUri = result.data!!.getStringExtra(PhotoEditActivity.PICTURE_URI)!!.toUri()
|
||||
val (imageSize, imageVideo) = imageUri.getSizeAndVideoValidate()
|
||||
size = imageSize
|
||||
video = imageVideo
|
||||
if (video) {
|
||||
val muted: Boolean = result.data!!.getBooleanExtra(VideoEditActivity.MUTED, false)
|
||||
val videoStart: Float? = result.data!!.getFloatExtra(VideoEditActivity.VIDEO_START, -1f).let {
|
||||
if(it == -1f) null else it
|
||||
}
|
||||
val modified: Boolean = result.data!!.getBooleanExtra(VideoEditActivity.MODIFIED, false)
|
||||
val videoEnd: Float? = result.data!!.getFloatExtra(VideoEditActivity.VIDEO_END, -1f).let {
|
||||
if(it == -1f) null else it
|
||||
}
|
||||
if(modified){
|
||||
videoEncodeProgress = 0
|
||||
sessionMap[position]?.let { FFmpegKit.cancel(it) }
|
||||
startEncoding(position, muted, videoStart, videoEnd)
|
||||
}
|
||||
} else {
|
||||
imageUri = result.data!!.getStringExtra(PhotoEditActivity.PICTURE_URI)!!.toUri()
|
||||
val (imageSize, imageVideo) = imageUri.getSizeAndVideoValidate(position)
|
||||
size = imageSize
|
||||
video = imageVideo
|
||||
}
|
||||
progress = null
|
||||
uploadId = null
|
||||
} ?: Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show()
|
||||
|
||||
binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video) })
|
||||
binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) })
|
||||
} else if(result?.resultCode != Activity.RESULT_CANCELED){
|
||||
Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param muted should audio tracks be removed in the output
|
||||
* @param videoStart when we want to start the video, in seconds, or null if we
|
||||
* don't want to remove the start
|
||||
* @param videoEnd when we want to end the video, in seconds, or null if we
|
||||
* don't want to remove the end
|
||||
*/
|
||||
private fun startEncoding(position: Int, muted: Boolean, videoStart: Float?, videoEnd: Float?) {
|
||||
val originalUri = photoData[position].imageUri
|
||||
|
||||
// Having a meaningful suffix is necessary so that ffmpeg knows what to put in output
|
||||
val suffix = if(originalUri.scheme == "content") {
|
||||
contentResolver.getType(photoData[position].imageUri)?.takeLastWhile { it != '/' }
|
||||
} else {
|
||||
originalUri.toString().takeLastWhile { it != '/' }
|
||||
}
|
||||
val file = File.createTempFile("temp_video", ".$suffix")
|
||||
//val file = File.createTempFile("temp_video", ".webm")
|
||||
tempFiles.add(file)
|
||||
val fileUri = file.toUri()
|
||||
val outputVideoPath = ffmpegSafeUri(fileUri)
|
||||
|
||||
val inputUri = photoData[position].imageUri
|
||||
|
||||
val inputSafePath = ffmpegSafeUri(inputUri)
|
||||
|
||||
val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(ffmpegSafeUri(inputUri)).mediaInformation
|
||||
val totalVideoDuration = mediaInformation?.duration?.toFloatOrNull()
|
||||
|
||||
val mutedString = if(muted) "-an" else ""
|
||||
val startString = if(videoStart != null) "-ss $videoStart" else ""
|
||||
|
||||
val endString = if(videoEnd != null) "-to ${videoEnd - (videoStart ?: 0f)}" else ""
|
||||
|
||||
val session: FFmpegSession = FFmpegKit.executeAsync("$startString -i $inputSafePath $endString -c copy $mutedString -y $outputVideoPath",
|
||||
//val session: FFmpegSession = FFmpegKit.executeAsync("$startString -i $inputSafePath $endString -c:v libvpx-vp9 -c:a copy -an -y $outputVideoPath",
|
||||
{ session ->
|
||||
val returnCode = session.returnCode
|
||||
if (ReturnCode.isSuccess(returnCode)) {
|
||||
fun successResult() {
|
||||
// Hide progress indicator in carousel
|
||||
binding.carousel.updateProgress(null, position, false)
|
||||
val (imageSize, imageVideo) = outputVideoPath.toUri().let {
|
||||
photoData[position].imageUri = it
|
||||
it.getSizeAndVideoValidate(position)
|
||||
}
|
||||
photoData[position].videoEncodeProgress = null
|
||||
photoData[position].size = imageSize
|
||||
binding.carousel.addData(photoData.map {
|
||||
CarouselItem(it.imageUri,
|
||||
it.imageDescription,
|
||||
it.video,
|
||||
it.videoEncodeProgress)
|
||||
})
|
||||
}
|
||||
|
||||
val post = resultHandler.post {
|
||||
successResult()
|
||||
}
|
||||
if(!post) {
|
||||
Log.e(TAG, "Failed to post changes, trying to recover in 100ms")
|
||||
resultHandler.postDelayed({successResult()}, 100)
|
||||
}
|
||||
Log.d(TAG, "Encode completed successfully in ${session.duration} milliseconds")
|
||||
} else {
|
||||
resultHandler.post {
|
||||
binding.carousel.updateProgress(null, position, error = true)
|
||||
photoData[position].videoEncodeProgress = null
|
||||
}
|
||||
Log.e(TAG, "Encode failed with state ${session.state} and rc $returnCode.${session.failStackTrace}")
|
||||
}
|
||||
},
|
||||
{ log -> Log.d("PostCreationActivityEncoding", log.message) }
|
||||
) { statistics: Statistics? ->
|
||||
|
||||
val timeInMilliseconds: Int? = statistics?.time
|
||||
timeInMilliseconds?.let {
|
||||
if (timeInMilliseconds > 0) {
|
||||
val completePercentage = totalVideoDuration?.let {
|
||||
val newTotalDuration = it - (videoStart ?: 0f) - (it - (videoEnd ?: it))
|
||||
timeInMilliseconds / (10*newTotalDuration)
|
||||
}
|
||||
resultHandler.post {
|
||||
completePercentage?.let {
|
||||
val rounded = it.roundToInt()
|
||||
photoData[position].videoEncodeProgress = rounded
|
||||
binding.carousel.updateProgress(rounded, position, false)
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "Encoding video: %$completePercentage.")
|
||||
}
|
||||
}
|
||||
}
|
||||
sessionMap[position] = session.sessionId
|
||||
}
|
||||
|
||||
private fun edit(position: Int) {
|
||||
val intent = Intent(
|
||||
this,
|
||||
|
@ -0,0 +1,30 @@
|
||||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.content.ClipData
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
|
||||
class PostCreationViewModel : ViewModel() {
|
||||
private val photoData: MutableLiveData<List<PhotoData>> by lazy {
|
||||
MutableLiveData<List<PhotoData>>().also {
|
||||
loadUsers()
|
||||
}
|
||||
}
|
||||
|
||||
fun getUsers(): LiveData<List<PhotoData>> {
|
||||
return photoData
|
||||
}
|
||||
|
||||
private fun loadUsers() {
|
||||
// Do an asynchronous operation to fetch users.
|
||||
}
|
||||
}
|
||||
class PostCreationViewModelFactory(val bundle: ClipData? = null) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.getConstructor(ClipData::class.java).newInstance(bundle)
|
||||
}
|
||||
|
||||
}
|
@ -5,5 +5,6 @@ import android.net.Uri
|
||||
data class CarouselItem constructor(
|
||||
val imageUrl: Uri,
|
||||
val caption: String? = null,
|
||||
val video: Boolean
|
||||
val video: Boolean,
|
||||
var encodeProgress: Int?
|
||||
)
|
@ -69,7 +69,7 @@ class ImageCarousel(
|
||||
|
||||
|
||||
private var isBuiltInIndicator = false
|
||||
private var data: List<CarouselItem>? = null
|
||||
private var data: MutableList<CarouselItem>? = null
|
||||
|
||||
var onItemClickListener: OnItemClickListener? = this
|
||||
set(value) {
|
||||
@ -88,28 +88,34 @@ class ImageCarousel(
|
||||
/**
|
||||
* Get or set current item position
|
||||
*/
|
||||
var currentPosition = -1
|
||||
var currentPosition = RecyclerView.NO_POSITION
|
||||
get() {
|
||||
return snapHelper.getSnapPosition(recyclerView.layoutManager)
|
||||
}
|
||||
set(value) {
|
||||
val position = when {
|
||||
value >= data?.size ?: 0 -> {
|
||||
-1
|
||||
}
|
||||
value < 0 -> {
|
||||
-1
|
||||
}
|
||||
else -> {
|
||||
value
|
||||
}
|
||||
val position = when (value) {
|
||||
!in 0..((data?.size?.minus(1)) ?: 0) -> RecyclerView.NO_POSITION
|
||||
else -> value
|
||||
}
|
||||
|
||||
field = position
|
||||
if (position != RecyclerView.NO_POSITION && field != position) {
|
||||
val thisProgress = data?.get(position)?.encodeProgress
|
||||
if (thisProgress != null) {
|
||||
binding.encodeProgress.visibility = VISIBLE
|
||||
binding.encodeInfoText.visibility = VISIBLE
|
||||
binding.encodeInfoText.text =
|
||||
context.getString(R.string.encode_progress).format(thisProgress)
|
||||
binding.encodeProgress.progress = thisProgress
|
||||
} else {
|
||||
binding.encodeProgress.visibility = INVISIBLE
|
||||
binding.encodeInfoText.visibility = INVISIBLE
|
||||
}
|
||||
} else binding.encodeProgress.visibility = INVISIBLE
|
||||
|
||||
if (position != -1) {
|
||||
if (position != RecyclerView.NO_POSITION && recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
recyclerView.smoothScrollToPosition(position)
|
||||
}
|
||||
field = position
|
||||
}
|
||||
|
||||
/**
|
||||
@ -450,10 +456,9 @@ class ImageCarousel(
|
||||
private fun initListeners() {
|
||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
val position = currentPosition
|
||||
|
||||
if (showCaption) {
|
||||
val position = snapHelper.getSnapPosition(recyclerView.layoutManager)
|
||||
|
||||
if (position >= 0) {
|
||||
val dataItem = adapter?.getItem(position)
|
||||
|
||||
@ -469,6 +474,8 @@ class ImageCarousel(
|
||||
}
|
||||
}
|
||||
|
||||
if(dx !=0 || dy != 0) currentPosition = position
|
||||
|
||||
onScrollListener?.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
}
|
||||
@ -561,12 +568,37 @@ class ImageCarousel(
|
||||
adapter?.apply {
|
||||
addAll(data)
|
||||
|
||||
this@ImageCarousel.data = data
|
||||
this@ImageCarousel.data = data.toMutableList()
|
||||
|
||||
initOnScrollStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateProgress(progress: Int?, position: Int, error: Boolean){
|
||||
data?.get(position)?.encodeProgress = progress
|
||||
if(currentPosition == position) {
|
||||
if (progress == null) {
|
||||
binding.encodeProgress.visibility = INVISIBLE
|
||||
binding.encodeInfoText.visibility = VISIBLE
|
||||
if(error){
|
||||
binding.encodeInfoText.setText(R.string.encode_error)
|
||||
binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.error),
|
||||
null, null, null)
|
||||
|
||||
} else {
|
||||
binding.encodeInfoText.setText(R.string.encode_success)
|
||||
binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.check_circle_24),
|
||||
null, null, null)
|
||||
}
|
||||
} else {
|
||||
binding.encodeProgress.visibility = VISIBLE
|
||||
binding.encodeProgress.progress = progress
|
||||
binding.encodeInfoText.visibility = VISIBLE
|
||||
binding.encodeInfoText.text = context.getString(R.string.encode_progress).format(progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Goto previous item.
|
||||
*/
|
||||
|
@ -148,7 +148,7 @@ class PhotoEditActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.edit_photo_menu, menu)
|
||||
menuInflater.inflate(R.menu.edit_menu, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -191,8 +191,8 @@ class PhotoEditActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
fun onFilterSelected(filter: Filter) {
|
||||
filteredImage = compressedOriginalImage!!.copy(BITMAP_CONFIG, true)
|
||||
|
@ -1,57 +1,279 @@
|
||||
package org.pixeldroid.app.postCreation.photoEdit
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.format.DateUtils
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.SeekBar
|
||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.os.HandlerCompat
|
||||
import androidx.media.AudioAttributesCompat
|
||||
import androidx.media2.common.MediaMetadata
|
||||
import androidx.media2.common.UriMediaItem
|
||||
import androidx.media2.player.MediaPlayer
|
||||
import com.arthenica.ffmpegkit.*
|
||||
import com.arthenica.ffmpegkit.MediaInformation.KEY_DURATION
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.slider.RangeSlider
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityVideoEditBinding
|
||||
import org.pixeldroid.app.postCreation.PostCreationActivity
|
||||
import org.pixeldroid.app.postCreation.carousel.dpToPx
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.ffmpegSafeUri
|
||||
import java.io.File
|
||||
import java.text.NumberFormat
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
class VideoEditActivity : BaseActivity() {
|
||||
|
||||
private lateinit var mediaPlayer: MediaPlayer
|
||||
private var videoPosition: Int = -1
|
||||
private lateinit var binding: ActivityVideoEditBinding
|
||||
// Map photoData indexes to FFmpeg Session IDs
|
||||
private val sessionList: ArrayList<Long> = arrayListOf()
|
||||
private val tempFiles: ArrayList<File> = ArrayList()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityVideoEditBinding.inflate(layoutInflater)
|
||||
|
||||
binding = ActivityVideoEditBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val uri = intent.getParcelableExtra(PhotoEditActivity.PICTURE_URI) as Uri?
|
||||
val videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1)
|
||||
|
||||
val inputVideoPath =if(uri.toString().startsWith("content://")) FFmpegKitConfig.getSafParameterForRead(this, uri) else uri.toString()
|
||||
val inputVideoPath2 =if(uri.toString().startsWith("content://")) FFmpegKitConfig.getSafParameterForRead(this, uri) else uri.toString()
|
||||
supportActionBar?.setTitle(R.string.toolbar_title_edit)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setHomeButtonEnabled(true)
|
||||
|
||||
|
||||
binding.videoRangeSeekBar.setCustomThumbDrawablesForValues(R.drawable.thumb_left,R.drawable.double_circle,R.drawable.thumb_right)
|
||||
binding.videoRangeSeekBar.thumbRadius = 20.dpToPx(this)
|
||||
|
||||
|
||||
val resultHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper())
|
||||
|
||||
val uri = intent.getParcelableExtra<Uri>(PhotoEditActivity.PICTURE_URI)!!
|
||||
videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1)
|
||||
|
||||
val inputVideoPath = ffmpegSafeUri(uri)
|
||||
val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(inputVideoPath).mediaInformation
|
||||
|
||||
val duration: Long? = mediaInformation?.getNumberProperty(KEY_DURATION)
|
||||
|
||||
val file = File.createTempFile("temp_img", ".png").toUri()
|
||||
|
||||
val outputImagePath =if(file.toString().startsWith("content://")) FFmpegKitConfig.getSafParameterForWrite(this, file) else file.toString()
|
||||
|
||||
val session = FFmpegKit.execute(
|
||||
"-i $inputVideoPath2 -filter_complex \"select='not(mod(n,1000))',scale=240:-1,tile=layout=4x1\" -vframes 1 -q:v 2 -y $outputImagePath"
|
||||
)
|
||||
if (ReturnCode.isSuccess(session.returnCode)) {
|
||||
Glide.with(this).load(file).into(binding.thumbnails)
|
||||
// SUCCESS
|
||||
} else if (ReturnCode.isCancel(session.returnCode)) {
|
||||
|
||||
// CANCEL
|
||||
} else {
|
||||
|
||||
// FAILURE
|
||||
Log.d("VideoEditActivity",
|
||||
String.format("Command failed with state %s and rc %s.%s",
|
||||
session.state,
|
||||
session.returnCode,
|
||||
session.failStackTrace))
|
||||
binding.muter.setOnClickListener {
|
||||
binding.muter.isSelected = !binding.muter.isSelected
|
||||
}
|
||||
|
||||
//Duration in seconds, or null
|
||||
val duration: Float? = mediaInformation?.duration?.toFloatOrNull()
|
||||
|
||||
binding.videoRangeSeekBar.valueFrom = 0f
|
||||
binding.videoRangeSeekBar.valueTo = duration ?: 100f
|
||||
binding.videoRangeSeekBar.values = listOf(0f,(duration?: 100f) / 2, duration ?: 100f)
|
||||
|
||||
|
||||
val mediaItem: UriMediaItem = UriMediaItem.Builder(uri).build()
|
||||
mediaItem.metadata = MediaMetadata.Builder()
|
||||
.putString(MediaMetadata.METADATA_KEY_TITLE, "")
|
||||
.build()
|
||||
|
||||
mediaPlayer = MediaPlayer(this)
|
||||
mediaPlayer.setMediaItem(mediaItem)
|
||||
|
||||
//binding.videoView.mediaControlView?.setMediaController()
|
||||
|
||||
// Configure audio
|
||||
mediaPlayer.setAudioAttributes(AudioAttributesCompat.Builder()
|
||||
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
|
||||
.setUsage(AudioAttributesCompat.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributesCompat.CONTENT_TYPE_MOVIE)
|
||||
.build()
|
||||
)
|
||||
|
||||
findViewById<FrameLayout?>(R.id.progress_bar)?.visibility = View.GONE
|
||||
|
||||
mediaPlayer.prepare()
|
||||
|
||||
binding.muter.setOnClickListener {
|
||||
if(!binding.muter.isSelected) mediaPlayer.playerVolume = 0f
|
||||
else mediaPlayer.playerVolume = 1f
|
||||
binding.muter.isSelected = !binding.muter.isSelected
|
||||
}
|
||||
|
||||
binding.videoView.setPlayer(mediaPlayer)
|
||||
|
||||
mediaPlayer.seekTo((binding.videoRangeSeekBar.values[1]*1000).toLong())
|
||||
|
||||
object : Runnable {
|
||||
override fun run() {
|
||||
val getCurrent = mediaPlayer.currentPosition / 1000f
|
||||
if(getCurrent >= binding.videoRangeSeekBar.values[0] && getCurrent <= binding.videoRangeSeekBar.values[2] ) {
|
||||
binding.videoRangeSeekBar.values = listOf(binding.videoRangeSeekBar.values[0],getCurrent, binding.videoRangeSeekBar.values[2])
|
||||
}
|
||||
Handler(Looper.getMainLooper()).postDelayed(this, 1000)
|
||||
}
|
||||
}.run()
|
||||
|
||||
binding.videoRangeSeekBar.addOnChangeListener { rangeSlider: RangeSlider, value, fromUser ->
|
||||
// Responds to when the middle slider's value is changed
|
||||
if(fromUser && value != rangeSlider.values[0] && value != rangeSlider.values[2]) {
|
||||
mediaPlayer.seekTo((rangeSlider.values[1]*1000).toLong())
|
||||
}
|
||||
}
|
||||
|
||||
binding.videoRangeSeekBar.setLabelFormatter { value: Float ->
|
||||
DateUtils.formatElapsedTime(value.toLong())
|
||||
}
|
||||
|
||||
|
||||
val thumbInterval: Float? = duration?.div(7)
|
||||
|
||||
thumbInterval?.let {
|
||||
thumbnail(uri, resultHandler, binding.thumbnail1, it)
|
||||
thumbnail(uri, resultHandler, binding.thumbnail2, it.times(2))
|
||||
thumbnail(uri, resultHandler, binding.thumbnail3, it.times(3))
|
||||
thumbnail(uri, resultHandler, binding.thumbnail4, it.times(4))
|
||||
thumbnail(uri, resultHandler, binding.thumbnail5, it.times(5))
|
||||
thumbnail(uri, resultHandler, binding.thumbnail6, it.times(6))
|
||||
thumbnail(uri, resultHandler, binding.thumbnail7, it.times(7))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.edit_menu, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
||||
when(item.itemId) {
|
||||
android.R.id.home -> onBackPressed()
|
||||
R.id.action_save -> {
|
||||
returnWithValues()
|
||||
}
|
||||
R.id.action_reset -> {
|
||||
resetControls()
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (noEdits()) super.onBackPressed()
|
||||
else {
|
||||
val builder = AlertDialog.Builder(this)
|
||||
builder.apply {
|
||||
setMessage(R.string.save_before_returning)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
returnWithValues()
|
||||
}
|
||||
setNegativeButton(R.string.no_cancel_edit) { _, _ ->
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
// Create the AlertDialog
|
||||
builder.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun noEdits(): Boolean {
|
||||
val videoPositions = binding.videoRangeSeekBar.values.let {
|
||||
it[0] == 0f && it[2] == binding.videoRangeSeekBar.valueTo
|
||||
}
|
||||
val muted = binding.muter.isSelected
|
||||
return !muted && videoPositions
|
||||
}
|
||||
|
||||
|
||||
private fun returnWithValues() {
|
||||
val intent = Intent(this, PostCreationActivity::class.java)
|
||||
.apply {
|
||||
putExtra(PhotoEditActivity.PICTURE_POSITION, videoPosition)
|
||||
putExtra(MUTED, binding.muter.isSelected)
|
||||
putExtra(MODIFIED, !noEdits())
|
||||
putExtra(VIDEO_START, binding.videoRangeSeekBar.values.first())
|
||||
putExtra(VIDEO_END, binding.videoRangeSeekBar.values[2])
|
||||
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
}
|
||||
|
||||
setResult(Activity.RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun resetControls() {
|
||||
binding.videoRangeSeekBar.values = listOf(0f, binding.videoRangeSeekBar.valueTo/2, binding.videoRangeSeekBar.valueTo)
|
||||
binding.muter.isSelected = false
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
sessionList.forEach {
|
||||
FFmpegKit.cancel(it)
|
||||
}
|
||||
tempFiles.forEach{
|
||||
it.delete()
|
||||
}
|
||||
mediaPlayer.close()
|
||||
}
|
||||
|
||||
private fun thumbnail(
|
||||
inputUri: Uri?,
|
||||
resultHandler: Handler,
|
||||
thumbnail: ImageView,
|
||||
thumbTime: Float,
|
||||
) {
|
||||
val file = File.createTempFile("temp_img", ".bmp")
|
||||
tempFiles.add(file)
|
||||
val fileUri = file.toUri()
|
||||
val inputSafePath = ffmpegSafeUri(inputUri)
|
||||
|
||||
val outputImagePath =
|
||||
if(fileUri.toString().startsWith("content://"))
|
||||
FFmpegKitConfig.getSafParameterForWrite(this, fileUri)
|
||||
else fileUri.toString()
|
||||
val session = FFmpegKit.executeAsync(
|
||||
"-noaccurate_seek -ss $thumbTime -i $inputSafePath -vf scale=${thumbnail.width}:${thumbnail.height} -frames:v 1 -f image2 -y $outputImagePath",
|
||||
{ session ->
|
||||
val state = session.state
|
||||
val returnCode = session.returnCode
|
||||
|
||||
if (ReturnCode.isSuccess(returnCode)) {
|
||||
// SUCCESS
|
||||
resultHandler.post {
|
||||
if(!this.isFinishing)
|
||||
Glide.with(this).load(outputImagePath).centerCrop().into(thumbnail)
|
||||
}
|
||||
}
|
||||
// CALLED WHEN SESSION IS EXECUTED
|
||||
Log.d("VideoEditActivity", "FFmpeg process exited with state $state and rc $returnCode.${session.failStackTrace}")
|
||||
},
|
||||
{/* CALLED WHEN SESSION PRINTS LOGS */ }) { /*CALLED WHEN SESSION GENERATES STATISTICS*/ }
|
||||
sessionList.add(session.sessionId)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
mediaPlayer.pause()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIDEO_TAG = "VideoEditTag"
|
||||
const val MUTED = "VideoEditMutedTag"
|
||||
const val VIDEO_START = "VideoEditVideoStartTag"
|
||||
const val VIDEO_END = "VideoEditVideoEndTag"
|
||||
const val MODIFIED = "VideoEditModifiedTag"
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.arthenica.ffmpegkit.FFmpegKitConfig
|
||||
import okhttp3.HttpUrl
|
||||
import org.pixeldroid.app.R
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
@ -65,6 +66,12 @@ fun normalizeDomain(domain: String): String {
|
||||
.trim(Char::isWhitespace)
|
||||
}
|
||||
|
||||
fun Context.ffmpegSafeUri(inputUri: Uri?): String =
|
||||
if (inputUri?.scheme == "content")
|
||||
FFmpegKitConfig.getSafParameterForRead(this, inputUri)
|
||||
else inputUri.toString()
|
||||
|
||||
|
||||
fun bitmapFromUri(contentResolver: ContentResolver, uri: Uri?): Bitmap =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
ImageDecoder
|
||||
@ -107,7 +114,7 @@ fun Bitmap.flip(horizontal: Boolean, vertical: Boolean): Bitmap {
|
||||
return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
|
||||
}
|
||||
|
||||
fun BaseActivity.openUrl(url: String): Boolean{
|
||||
fun BaseActivity.openUrl(url: String): Boolean {
|
||||
|
||||
val intent = CustomTabsIntent.Builder().build()
|
||||
|
||||
|
11
app/src/main/res/drawable/double_circle.xml
Normal file
11
app/src/main/res/drawable/double_circle.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<vector android:height="10dp"
|
||||
android:viewportHeight="36" android:viewportWidth="36"
|
||||
android:width="10dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/black" android:pathData=
|
||||
"M 18 18
|
||||
m -9, 0
|
||||
a 9,9 0 1,0 18,0
|
||||
a 9,9 0 1,0 -18,0"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M18,8C12.48,8,8,12.48,8,18s4.48,10,10,10s10,-4.48,10,-10S23.52,8,18,8zM18,26c-4.42,0,-8,-3.58,-8,-8s3.58,-8,8,-8s8,3.58,8,8s-3.58,8,-8,8z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M18,18m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/error.xml
Normal file
5
app/src/main/res/drawable/error.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/selector_mute.xml
Normal file
9
app/src/main/res/drawable/selector_mute.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:drawable="@drawable/volume_up"
|
||||
android:state_selected="false" />
|
||||
<item
|
||||
android:drawable="@drawable/volume_off"
|
||||
android:state_selected="true"/>
|
||||
</selector>
|
25
app/src/main/res/drawable/thumb_left.xml
Normal file
25
app/src/main/res/drawable/thumb_left.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="17.586dp"
|
||||
android:height="20.915dp"
|
||||
android:viewportWidth="17.586"
|
||||
android:viewportHeight="20.915">
|
||||
<path
|
||||
android:pathData="m5.29,0h8.006v20.915h-8.006a1,1 45,0 1,-1 -1v-18.915a1,1 135,0 1,1 -1z"
|
||||
android:strokeWidth="0.264583"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="m10.259,6.794 l-3.664,3.664 3.664,3.664z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="m8.434,12.251 l-1.79,-1.791 1.801,-1.8 1.801,-1.8l0,3.591c0,1.975 -0.005,3.591 -0.011,3.591 -0.006,0 -0.816,-0.806 -1.801,-1.791z"
|
||||
android:strokeWidth="0.042"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="m8.434,12.251 l-1.79,-1.791 1.801,-1.8 1.801,-1.8l0,3.591c0,1.975 -0.005,3.591 -0.011,3.591 -0.006,0 -0.816,-0.806 -1.801,-1.791z"
|
||||
android:strokeWidth="0.042"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="m8.434,12.251 l-1.79,-1.791 1.801,-1.8 1.801,-1.8l0,3.591c0,1.975 -0.005,3.591 -0.011,3.591 -0.006,0 -0.816,-0.806 -1.801,-1.791z"
|
||||
android:strokeWidth="0.042"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
29
app/src/main/res/drawable/thumb_right.xml
Normal file
29
app/src/main/res/drawable/thumb_right.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="17.586dp"
|
||||
android:height="20.915dp"
|
||||
android:viewportWidth="17.586"
|
||||
android:viewportHeight="20.915">
|
||||
<group
|
||||
android:scaleX="-1"
|
||||
android:translateX="17.586">
|
||||
<path
|
||||
android:pathData="m5.29,0h8.006v20.915h-8.006a1,1 45,0 1,-1 -1v-18.915a1,1 135,0 1,1 -1z"
|
||||
android:strokeWidth="0.264583"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="m10.259,6.794 l-3.664,3.664 3.664,3.664z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="m8.434,12.251 l-1.79,-1.791 1.801,-1.8 1.801,-1.8l0,3.591c0,1.975 -0.005,3.591 -0.011,3.591 -0.006,0 -0.816,-0.806 -1.801,-1.791z"
|
||||
android:strokeWidth="0.042"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="m8.434,12.251 l-1.79,-1.791 1.801,-1.8 1.801,-1.8l0,3.591c0,1.975 -0.005,3.591 -0.011,3.591 -0.006,0 -0.816,-0.806 -1.801,-1.791z"
|
||||
android:strokeWidth="0.042"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="m8.434,12.251 l-1.79,-1.791 1.801,-1.8 1.801,-1.8l0,3.591c0,1.975 -0.005,3.591 -0.011,3.591 -0.006,0 -0.816,-0.806 -1.801,-1.791z"
|
||||
android:strokeWidth="0.042"
|
||||
android:fillColor="#000000"/>
|
||||
</group>
|
||||
</vector>
|
5
app/src/main/res/drawable/volume_off.xml
Normal file
5
app/src/main/res/drawable/volume_off.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v2.21l2.45,2.45c0.03,-0.2 0.05,-0.41 0.05,-0.63zM19,12c0,0.94 -0.2,1.82 -0.54,2.64l1.51,1.51C20.63,14.91 21,13.5 21,12c0,-4.28 -2.99,-7.86 -7,-8.77v2.06c2.89,0.86 5,3.54 5,6.71zM4.27,3L3,4.27 7.73,9L3,9v6h4l5,5v-6.73l4.25,4.25c-0.67,0.52 -1.42,0.93 -2.25,1.18v2.06c1.38,-0.31 2.63,-0.95 3.69,-1.81L19.73,21 21,19.73l-9,-9L4.27,3zM12,4L9.91,6.09 12,8.18L12,4z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/volume_up.xml
Normal file
5
app/src/main/res/drawable/volume_up.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"/>
|
||||
</vector>
|
@ -1,15 +1,112 @@
|
||||
<?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"
|
||||
android:background="@android:color/black">
|
||||
android:background="@android:color/black"
|
||||
android:scrollbarThumbHorizontal="@drawable/thumb_left">
|
||||
|
||||
|
||||
|
||||
<androidx.media2.widget.VideoView
|
||||
android:id="@+id/videoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:background="#000000"
|
||||
app:layout_constraintBottom_toTopOf="@+id/thumbnail4"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnails"
|
||||
android:id="@+id/muter"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginBottom="48dp"
|
||||
android:contentDescription="@string/add_comment"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="4dp"
|
||||
android:src="@drawable/selector_mute"
|
||||
app:layout_constraintBottom_toTopOf="@+id/thumbnail1"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<com.google.android.material.slider.RangeSlider
|
||||
android:id="@+id/videoRangeSeekBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/select_video_range"
|
||||
android:elevation="5dp"
|
||||
android:layout_marginStart="-15dp"
|
||||
android:layout_marginEnd="-15dp"
|
||||
android:valueFrom="0.0"
|
||||
android:valueTo="100.0"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/thumbnail1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/thumbnail_reel_video_edit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@+id/thumbnail2" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/thumbnail_reel_video_edit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/thumbnail1"
|
||||
app:layout_constraintRight_toLeftOf="@+id/thumbnail3" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/thumbnail_reel_video_edit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/thumbnail2"
|
||||
app:layout_constraintRight_toLeftOf="@+id/thumbnail4" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail4"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/thumbnail_reel_video_edit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/thumbnail3"
|
||||
app:layout_constraintRight_toLeftOf="@+id/thumbnail5" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail5"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/thumbnail_reel_video_edit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/thumbnail4"
|
||||
app:layout_constraintRight_toLeftOf="@+id/thumbnail6" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/thumbnail_reel_video_edit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/thumbnail5"
|
||||
app:layout_constraintRight_toLeftOf="@+id/thumbnail7" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail7"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@string/thumbnail_reel_video_edit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@+id/thumbnail6"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -162,4 +162,30 @@
|
||||
app:layout_constraintTop_toTopOf="@+id/indicator"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:visibility="invisible"
|
||||
tools:visibility="visible"
|
||||
tools:progress="30"
|
||||
android:id="@+id/encodeProgress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="64dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:indeterminate="false"
|
||||
android:max="100"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/encodeInfoText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:drawableStartCompat="@drawable/ic_heart"
|
||||
android:drawablePadding="6dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/encodeProgress"
|
||||
tools:text="Encoding..." />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -7,13 +7,13 @@
|
||||
<item
|
||||
android:id="@+id/action_reset"
|
||||
android:orderInCategory="100"
|
||||
android:title="RESET"
|
||||
android:title="@string/reset_edit_menu"
|
||||
android:icon="@drawable/restore_24dp"
|
||||
app:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/action_save"
|
||||
android:orderInCategory="101"
|
||||
android:title="SAVE"
|
||||
android:title="@string/save_edit_menu"
|
||||
android:icon="@drawable/ic_save_24dp"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
@ -263,4 +263,11 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
|
||||
<string name="play_video">Play video</string>
|
||||
<string name="video_edit_not_yet_supported">Video editing is not yet supported</string>
|
||||
<string name="thumbnail_reel_video_edit">Reel showing thumbnails of the video you are editing</string>
|
||||
<string name="reset_edit_menu">RESET</string>
|
||||
<string name="save_edit_menu">SAVE</string>
|
||||
<string name="encode_error">Error encoding</string>
|
||||
<string name="encode_success">Encode success!</string>
|
||||
<string name="encode_progress">Encode %1$d%%</string>
|
||||
<string name="select_video_range">Select what to keep of the video</string>
|
||||
<string name="still_encoding">One or more videos are still encoding. Wait for them to finish before uploading</string>
|
||||
</resources>
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -4,4 +4,4 @@ distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
distributionSha256Sum=f581709a9c35e9cb92e16f585d2c4bc99b2b1a5f85d2badbd3dc6bff59e1e6dd
|
||||
distributionSha256Sum=b586e04868a22fd817c8971330fec37e298f3242eb85c374181b12d637f80302
|
||||
|
Loading…
x
Reference in New Issue
Block a user