Put editing in a module
This commit is contained in:
parent
650e7b248b
commit
6b42677f1e
|
@ -177,8 +177,6 @@ dependencies {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||||
implementation 'com.arthenica:ffmpeg-kit-min-gpl:5.1.LTS'
|
|
||||||
|
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.7.0'
|
implementation 'com.google.android.material:material:1.7.0'
|
||||||
|
|
||||||
|
@ -201,6 +199,7 @@ dependencies {
|
||||||
implementation 'info.androidhive:imagefilters:1.0.7'
|
implementation 'info.androidhive:imagefilters:1.0.7'
|
||||||
implementation 'com.github.yalantis:ucrop:2.2.8-native'
|
implementation 'com.github.yalantis:ucrop:2.2.8-native'
|
||||||
implementation project(path: ':scrambler')
|
implementation project(path: ':scrambler')
|
||||||
|
implementation project(path: ':mediaEditor')
|
||||||
|
|
||||||
implementation('com.github.bumptech.glide:glide:4.14.2') {
|
implementation('com.github.bumptech.glide:glide:4.14.2') {
|
||||||
exclude group: "com.android.support"
|
exclude group: "com.android.support"
|
||||||
|
|
|
@ -31,9 +31,6 @@
|
||||||
android:name=".posts.AlbumActivity"
|
android:name=".posts.AlbumActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/AppTheme.ActionBar.Transparent"/>
|
android:theme="@style/AppTheme.ActionBar.Transparent"/>
|
||||||
<activity
|
|
||||||
android:name=".postCreation.photoEdit.VideoEditActivity"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".posts.MediaViewerActivity"
|
android:name=".posts.MediaViewerActivity"
|
||||||
|
@ -55,7 +52,10 @@
|
||||||
android:name=".posts.ReportActivity"
|
android:name=".posts.ReportActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
tools:ignore="LockedOrientationActivity" />
|
tools:ignore="LockedOrientationActivity" />
|
||||||
<activity android:name=".postCreation.photoEdit.PhotoEditActivity" />
|
<activity
|
||||||
|
android:name="org.pixeldroid.media_editor.photoEdit.VideoEditActivity"
|
||||||
|
android:exported="false"/>
|
||||||
|
<activity android:name="org.pixeldroid.media_editor.photoEdit.PhotoEditActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".postCreation.PostCreationActivity"
|
android:name=".postCreation.PostCreationActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|
|
@ -27,41 +27,24 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.arthenica.ffmpegkit.*
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.pixeldroid.app.R
|
import org.pixeldroid.app.R
|
||||||
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
|
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
|
||||||
import org.pixeldroid.app.postCreation.camera.CameraActivity
|
import org.pixeldroid.app.postCreation.camera.CameraActivity
|
||||||
import org.pixeldroid.app.postCreation.carousel.CarouselItem
|
import org.pixeldroid.app.postCreation.carousel.CarouselItem
|
||||||
import org.pixeldroid.app.postCreation.photoEdit.PhotoEditActivity
|
|
||||||
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity
|
|
||||||
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
|
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
|
||||||
import org.pixeldroid.app.utils.convert
|
|
||||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||||
import org.pixeldroid.app.utils.ffmpegCompliantUri
|
|
||||||
import org.pixeldroid.app.utils.fileExtension
|
import org.pixeldroid.app.utils.fileExtension
|
||||||
import org.pixeldroid.app.utils.getMimeType
|
import org.pixeldroid.app.utils.getMimeType
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
const val TAG = "Post Creation Activity"
|
const val TAG = "Post Creation Activity"
|
||||||
|
|
||||||
data class PhotoData(
|
|
||||||
var imageUri: Uri,
|
|
||||||
var size: Long,
|
|
||||||
var uploadId: String? = null,
|
|
||||||
var progress: Int? = null,
|
|
||||||
var imageDescription: String? = null,
|
|
||||||
var video: Boolean,
|
|
||||||
var videoEncodeProgress: Int? = null,
|
|
||||||
var videoEncodeStabilizationFirstPass: Boolean? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
||||||
|
|
||||||
private var user: UserDatabaseEntity? = null
|
private var user: UserDatabaseEntity? = null
|
||||||
|
@ -69,8 +52,6 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
||||||
|
|
||||||
private lateinit var binding: ActivityPostCreationBinding
|
private lateinit var binding: ActivityPostCreationBinding
|
||||||
|
|
||||||
private val resultHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper())
|
|
||||||
|
|
||||||
private lateinit var model: PostCreationViewModel
|
private lateinit var model: PostCreationViewModel
|
||||||
|
|
||||||
|
|
||||||
|
@ -94,7 +75,11 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
||||||
// update UI
|
// update UI
|
||||||
binding.carousel.addData(
|
binding.carousel.addData(
|
||||||
newPhotoData.map {
|
newPhotoData.map {
|
||||||
CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass)
|
CarouselItem(
|
||||||
|
it.imageUri, it.imageDescription, it.video,
|
||||||
|
it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass,
|
||||||
|
it.videoEncodeComplete, it.videoEncodeError,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -129,26 +114,6 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
||||||
|
|
||||||
|
|
||||||
binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
|
binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
|
||||||
|
|
||||||
uiState.newEncodingJobPosition?.let { position ->
|
|
||||||
uiState.newEncodingJobMuted?.let { muted ->
|
|
||||||
uiState.newEncodingJobVideoStart.let { videoStart ->
|
|
||||||
uiState.newEncodingJobVideoEnd.let { videoEnd ->
|
|
||||||
uiState.newEncodingJobSpeedIndex?.let { speedIndex ->
|
|
||||||
uiState.newEncodingJobVideoCrop?.let { crop ->
|
|
||||||
uiState.newEncodingJobStabilize?.let { stabilize ->
|
|
||||||
startEncoding(position, muted,
|
|
||||||
videoStart, videoEnd,
|
|
||||||
speedIndex, crop, stabilize,
|
|
||||||
)
|
|
||||||
model.encodingStarted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -322,7 +287,7 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
||||||
private val editResultContract: ActivityResultLauncher<Intent> = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){
|
private val editResultContract: ActivityResultLauncher<Intent> = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){
|
||||||
result: ActivityResult? ->
|
result: ActivityResult? ->
|
||||||
if (result?.resultCode == Activity.RESULT_OK && result.data != null) {
|
if (result?.resultCode == Activity.RESULT_OK && result.data != null) {
|
||||||
val position: Int = result.data!!.getIntExtra(PhotoEditActivity.PICTURE_POSITION, 0)
|
val position: Int = result.data!!.getIntExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_POSITION, 0)
|
||||||
model.modifyAt(position, result.data!!)
|
model.modifyAt(position, result.data!!)
|
||||||
?: Toast.makeText(applicationContext, R.string.error_editing, Toast.LENGTH_SHORT).show()
|
?: Toast.makeText(applicationContext, R.string.error_editing, Toast.LENGTH_SHORT).show()
|
||||||
} else if(result?.resultCode != Activity.RESULT_CANCELED){
|
} else if(result?.resultCode != Activity.RESULT_CANCELED){
|
||||||
|
@ -330,207 +295,13 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @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?,
|
|
||||||
speedIndex: Int,
|
|
||||||
crop: VideoEditActivity.RelativeCropPosition,
|
|
||||||
stabilize: Float
|
|
||||||
) {
|
|
||||||
val originalUri = model.getPhotoData().value!![position].imageUri
|
|
||||||
|
|
||||||
// Having a meaningful suffix is necessary so that ffmpeg knows what to put in output
|
|
||||||
val suffix = originalUri.fileExtension(contentResolver)
|
|
||||||
val file = File.createTempFile("temp_video", ".$suffix", cacheDir)
|
|
||||||
//val file = File.createTempFile("temp_video", ".webm", cacheDir)
|
|
||||||
model.trackTempFile(file)
|
|
||||||
val fileUri = file.toUri()
|
|
||||||
val outputVideoPath = ffmpegCompliantUri(fileUri)
|
|
||||||
|
|
||||||
val inputUri = model.getPhotoData().value!![position].imageUri
|
|
||||||
|
|
||||||
val ffmpegCompliantUri: String = ffmpegCompliantUri(inputUri)
|
|
||||||
|
|
||||||
val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(ffmpegCompliantUri(inputUri)).mediaInformation
|
|
||||||
val totalVideoDuration = mediaInformation?.duration?.toFloatOrNull()
|
|
||||||
|
|
||||||
fun secondPass(stabilizeString: String = ""){
|
|
||||||
val speed = VideoEditActivity.speedChoices[speedIndex]
|
|
||||||
|
|
||||||
val mutedString = if(muted || speedIndex != 1) "-an" else null
|
|
||||||
val startString: List<String?> = if(videoStart != null) listOf("-ss", "${videoStart/speed.toFloat()}") else listOf(null, null)
|
|
||||||
|
|
||||||
val endString: List<String?> = if(videoEnd != null) listOf("-to", "${videoEnd/speed.toFloat() - (videoStart ?: 0f)/speed.toFloat()}") else listOf(null, null)
|
|
||||||
|
|
||||||
// iw and ih are variables for the original width and height values, FFmpeg will know them
|
|
||||||
val cropString = if(crop.notCropped()) "" else "crop=${crop.relativeWidth}*iw:${crop.relativeHeight}*ih:${crop.relativeX}*iw:${crop.relativeY}*ih"
|
|
||||||
val separator = if(speedIndex != 1 && !crop.notCropped()) "," else ""
|
|
||||||
val speedString = if(speedIndex != 1) "setpts=PTS/${speed}" else ""
|
|
||||||
|
|
||||||
val separatorStabilize = if(stabilizeString == "" || (speedString == "" && cropString == "")) "" else ","
|
|
||||||
|
|
||||||
val speedAndCropString: List<String?> = if(speedIndex!= 1 || !crop.notCropped() || stabilizeString.isNotEmpty())
|
|
||||||
listOf("-filter:v", stabilizeString + separatorStabilize + speedString + separator + cropString)
|
|
||||||
// Stream copy is not compatible with filter, but when not filtering we can copy the stream without re-encoding
|
|
||||||
else listOf("-c", "copy")
|
|
||||||
|
|
||||||
// This should be set when re-encoding is required (otherwise it defaults to mpeg which then doesn't play)
|
|
||||||
val encodePreset: List<String?> = if(speedIndex != 1 && !crop.notCropped()) listOf("-c:v", "libx264", "-preset", "ultrafast") else listOf(null, null, null, null)
|
|
||||||
|
|
||||||
val session: FFmpegSession =
|
|
||||||
FFmpegKit.executeWithArgumentsAsync(listOfNotNull(
|
|
||||||
startString[0], startString[1],
|
|
||||||
"-i", ffmpegCompliantUri,
|
|
||||||
speedAndCropString[0], speedAndCropString[1],
|
|
||||||
endString[0], endString[1],
|
|
||||||
mutedString, "-y",
|
|
||||||
encodePreset[0], encodePreset[1], encodePreset[2], encodePreset[3],
|
|
||||||
outputVideoPath,
|
|
||||||
).toTypedArray(),
|
|
||||||
//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, _) = outputVideoPath.toUri().let {
|
|
||||||
model.setUriAtPosition(it, position)
|
|
||||||
model.getSizeAndVideoValidate(it, position)
|
|
||||||
}
|
|
||||||
model.setVideoEncodeAtPosition(position, null)
|
|
||||||
model.setSizeAtPosition(imageSize, position)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
model.setVideoEncodeAtPosition(position, 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 speedupDurationModifier = VideoEditActivity.speedChoices[speedIndex].toFloat()
|
|
||||||
|
|
||||||
val newTotalDuration = (it - (videoStart ?: 0f) - (it - (videoEnd ?: it)))/speedupDurationModifier
|
|
||||||
timeInMilliseconds / (10*newTotalDuration)
|
|
||||||
}
|
|
||||||
resultHandler.post {
|
|
||||||
completePercentage?.let {
|
|
||||||
val rounded: Int = it.roundToInt()
|
|
||||||
model.setVideoEncodeAtPosition(position, rounded)
|
|
||||||
binding.carousel.updateProgress(rounded, position, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.d(TAG, "Encoding video: %$completePercentage.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
model.registerNewFFmpegSession(position, session.sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stabilizationFirstPass(){
|
|
||||||
|
|
||||||
val shakeResultsFile = File.createTempFile("temp_shake_results", ".trf", cacheDir)
|
|
||||||
model.trackTempFile(shakeResultsFile)
|
|
||||||
val shakeResultsFileUri = shakeResultsFile.toUri()
|
|
||||||
val shakeResultsFileSafeUri = ffmpegCompliantUri(shakeResultsFileUri).removePrefix("file://")
|
|
||||||
|
|
||||||
val inputSafeUri: String = ffmpegCompliantUri(inputUri)
|
|
||||||
|
|
||||||
// Map chosen "stabilization force" to shakiness, from 3 to 10
|
|
||||||
val shakiness = (0f..100f).convert(stabilize, 3f..10f).roundToInt()
|
|
||||||
|
|
||||||
val analyzeVideoCommandList = listOf(
|
|
||||||
"-y", "-i", inputSafeUri,
|
|
||||||
"-vf", "vidstabdetect=shakiness=$shakiness:accuracy=15:result=$shakeResultsFileSafeUri",
|
|
||||||
"-f", "null", "-"
|
|
||||||
).toTypedArray()
|
|
||||||
|
|
||||||
FFmpegKit.executeWithArgumentsAsync(analyzeVideoCommandList,
|
|
||||||
{ firstPass ->
|
|
||||||
if (ReturnCode.isSuccess(firstPass.returnCode)) {
|
|
||||||
// Map chosen "stabilization force" to shakiness, from 8 to 40
|
|
||||||
val smoothing = (0f..100f).convert(stabilize, 8f..40f).roundToInt()
|
|
||||||
|
|
||||||
val stabilizeVideoCommand =
|
|
||||||
"vidstabtransform=smoothing=$smoothing:input=${ffmpegCompliantUri(shakeResultsFileUri).removePrefix("file://")}"
|
|
||||||
secondPass(stabilizeVideoCommand)
|
|
||||||
} else {
|
|
||||||
Log.e(
|
|
||||||
"PostCreationActivityEncoding",
|
|
||||||
"Video stabilization first pass failed!"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ log -> Log.d("PostCreationActivityEncoding", log.message) },
|
|
||||||
{ statistics: Statistics? ->
|
|
||||||
|
|
||||||
val timeInMilliseconds: Int? = statistics?.time
|
|
||||||
timeInMilliseconds?.let {
|
|
||||||
if (timeInMilliseconds > 0) {
|
|
||||||
val completePercentage = totalVideoDuration?.let {
|
|
||||||
val speedupDurationModifier =
|
|
||||||
VideoEditActivity.speedChoices[speedIndex].toFloat()
|
|
||||||
|
|
||||||
val newTotalDuration = (it - (videoStart ?: 0f) - (it - (videoEnd
|
|
||||||
?: it))) / speedupDurationModifier
|
|
||||||
timeInMilliseconds / (10 * newTotalDuration)
|
|
||||||
}
|
|
||||||
resultHandler.post {
|
|
||||||
completePercentage?.let {
|
|
||||||
val rounded: Int = it.roundToInt()
|
|
||||||
model.setVideoEncodeAtPosition(position, rounded, true)
|
|
||||||
binding.carousel.updateProgress(rounded, position, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.d(TAG, "Stabilization pass: %$completePercentage.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(stabilize > 0.01f) {
|
|
||||||
// Stabilization was requested: we need an additional first pass to get stabilization data
|
|
||||||
stabilizationFirstPass()
|
|
||||||
} else {
|
|
||||||
// Immediately call the second pass, no stabilization needed
|
|
||||||
secondPass()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun edit(position: Int) {
|
private fun edit(position: Int) {
|
||||||
val intent = Intent(
|
val intent = Intent(
|
||||||
this,
|
this,
|
||||||
if(model.getPhotoData().value!![position].video) VideoEditActivity::class.java else PhotoEditActivity::class.java
|
if(model.getPhotoData().value!![position].video) org.pixeldroid.media_editor.photoEdit.VideoEditActivity::class.java else org.pixeldroid.media_editor.photoEdit.PhotoEditActivity::class.java
|
||||||
)
|
)
|
||||||
.putExtra(PhotoEditActivity.PICTURE_URI, model.getPhotoData().value!![position].imageUri)
|
.putExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_URI, model.getPhotoData().value!![position].imageUri)
|
||||||
.putExtra(PhotoEditActivity.PICTURE_POSITION, position)
|
.putExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_POSITION, position)
|
||||||
|
|
||||||
editResultContract.launch(intent)
|
editResultContract.launch(intent)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package org.pixeldroid.app.postCreation
|
package org.pixeldroid.app.postCreation
|
||||||
|
|
||||||
import android.R.attr.orientation
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -14,7 +13,6 @@ import androidx.core.net.toUri
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.arthenica.ffmpegkit.FFmpegKit
|
|
||||||
import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException
|
import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException
|
||||||
import com.jarsilio.android.scrambler.stripMetadata
|
import com.jarsilio.android.scrambler.stripMetadata
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
@ -27,9 +25,8 @@ import kotlinx.coroutines.launch
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import org.pixeldroid.app.MainActivity
|
import org.pixeldroid.app.MainActivity
|
||||||
import org.pixeldroid.app.R
|
import org.pixeldroid.app.R
|
||||||
import org.pixeldroid.app.postCreation.photoEdit.PhotoEditActivity
|
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
|
||||||
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity
|
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity.RelativeCropPosition
|
||||||
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity.RelativeCropPosition
|
|
||||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
import org.pixeldroid.app.utils.api.objects.Attachment
|
||||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||||
|
@ -64,14 +61,19 @@ data class PostCreationActivityUiState(
|
||||||
val uploadErrorVisible: Boolean = false,
|
val uploadErrorVisible: Boolean = false,
|
||||||
val uploadErrorExplanationText: String = "",
|
val uploadErrorExplanationText: String = "",
|
||||||
val uploadErrorExplanationVisible: Boolean = false,
|
val uploadErrorExplanationVisible: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
val newEncodingJobPosition: Int? = null,
|
data class PhotoData(
|
||||||
val newEncodingJobMuted: Boolean? = null,
|
var imageUri: Uri,
|
||||||
val newEncodingJobSpeedIndex: Int? = null,
|
var size: Long,
|
||||||
val newEncodingJobVideoStart: Float? = null,
|
var uploadId: String? = null,
|
||||||
val newEncodingJobVideoEnd: Float? = null,
|
var progress: Int? = null,
|
||||||
val newEncodingJobVideoCrop: RelativeCropPosition? = null,
|
var imageDescription: String? = null,
|
||||||
val newEncodingJobStabilize: Float? = null,
|
var video: Boolean,
|
||||||
|
var videoEncodeProgress: Int? = null,
|
||||||
|
var videoEncodeStabilizationFirstPass: Boolean? = null,
|
||||||
|
var videoEncodeComplete: Boolean = false,
|
||||||
|
var videoEncodeError: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
class PostCreationViewModel(application: Application, clipdata: ClipData? = null, val instance: InstanceDatabaseEntity? = null) : AndroidViewModel(application) {
|
class PostCreationViewModel(application: Application, clipdata: ClipData? = null, val instance: InstanceDatabaseEntity? = null) : AndroidViewModel(application) {
|
||||||
|
@ -97,9 +99,8 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
||||||
|
|
||||||
val uiState: StateFlow<PostCreationActivityUiState> = _uiState
|
val uiState: StateFlow<PostCreationActivityUiState> = _uiState
|
||||||
|
|
||||||
|
|
||||||
// Map photoData indexes to FFmpeg Session IDs
|
// Map photoData indexes to FFmpeg Session IDs
|
||||||
private val sessionMap: MutableMap<Int, Long> = mutableMapOf()
|
private val sessionMap: MutableMap<Uri, Long> = mutableMapOf()
|
||||||
// Keep track of temporary files to delete them (avoids filling cache super fast with videos)
|
// Keep track of temporary files to delete them (avoids filling cache super fast with videos)
|
||||||
private val tempFiles: java.util.ArrayList<File> = java.util.ArrayList()
|
private val tempFiles: java.util.ArrayList<File> = java.util.ArrayList()
|
||||||
|
|
||||||
|
@ -109,18 +110,6 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encodingStarted() {
|
|
||||||
_uiState.update { currentUiState ->
|
|
||||||
currentUiState.copy(
|
|
||||||
newEncodingJobPosition = null,
|
|
||||||
newEncodingJobMuted = null,
|
|
||||||
newEncodingJobSpeedIndex = null,
|
|
||||||
newEncodingJobVideoStart = null,
|
|
||||||
newEncodingJobVideoEnd = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPhotoData(): LiveData<MutableList<PhotoData>> = photoData
|
fun getPhotoData(): LiveData<MutableList<PhotoData>> = photoData
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -210,10 +199,18 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
||||||
photoData.value = photoData.value?.map { it.copy(uploadId = null, progress = null) }?.toMutableList()
|
photoData.value = photoData.value?.map { it.copy(uploadId = null, progress = null) }?.toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setVideoEncodeAtPosition(position: Int, progress: Int?, stabilizationFirstPass: Boolean = false) {
|
fun setVideoEncodeAtPosition(uri: Uri, progress: Int?, stabilizationFirstPass: Boolean = false, error: Boolean = false) {
|
||||||
photoData.value?.set(position, photoData.value!![position].copy(videoEncodeProgress = progress, videoEncodeStabilizationFirstPass = stabilizationFirstPass))
|
photoData.value?.indexOfFirst { it.imageUri == uri }?.let { position ->
|
||||||
|
photoData.value?.set(position,
|
||||||
|
photoData.value!![position].copy(
|
||||||
|
videoEncodeProgress = progress,
|
||||||
|
videoEncodeStabilizationFirstPass = stabilizationFirstPass,
|
||||||
|
videoEncodeError = error,
|
||||||
|
)
|
||||||
|
)
|
||||||
photoData.value = photoData.value
|
photoData.value = photoData.value
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setUriAtPosition(uri: Uri, position: Int) {
|
fun setUriAtPosition(uri: Uri, position: Int) {
|
||||||
photoData.value?.set(position, photoData.value!![position].copy(imageUri = uri))
|
photoData.value?.set(position, photoData.value!![position].copy(imageUri = uri))
|
||||||
|
@ -416,37 +413,24 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
||||||
if (video) {
|
if (video) {
|
||||||
val modified: Boolean = data.getBooleanExtra(VideoEditActivity.MODIFIED, false)
|
val modified: Boolean = data.getBooleanExtra(VideoEditActivity.MODIFIED, false)
|
||||||
if(modified){
|
if(modified){
|
||||||
val muted: Boolean = data.getBooleanExtra(VideoEditActivity.MUTED, false)
|
val videoEncodingArguments: VideoEditActivity.VideoEditArguments? = data.getSerializableExtra(VideoEditActivity.VIDEO_ARGUMENTS_TAG) as? VideoEditActivity.VideoEditArguments
|
||||||
val speedIndex: Int = data.getIntExtra(VideoEditActivity.SPEED, 1)
|
|
||||||
val videoStart: Float? = data.getFloatExtra(VideoEditActivity.VIDEO_START, -1f).let {
|
|
||||||
if(it == -1f) null else it
|
|
||||||
}
|
|
||||||
val videoEnd: Float? = data.getFloatExtra(VideoEditActivity.VIDEO_END, -1f).let {
|
|
||||||
if(it == -1f) null else it
|
|
||||||
}
|
|
||||||
|
|
||||||
val videoCrop: RelativeCropPosition = data.getSerializableExtra(VideoEditActivity.VIDEO_CROP) as RelativeCropPosition
|
sessionMap[imageUri]?.let { VideoEditActivity.cancelEncoding(it) }
|
||||||
|
|
||||||
val videoStabilize: Float = data.getFloatExtra(VideoEditActivity.VIDEO_STABILIZE, 0f)
|
videoEncodingArguments?.let {
|
||||||
|
videoEncodeStabilizationFirstPass = videoEncodingArguments.videoStabilize > 0.01f
|
||||||
videoEncodeStabilizationFirstPass = videoStabilize > 0.01f
|
|
||||||
videoEncodeProgress = 0
|
videoEncodeProgress = 0
|
||||||
|
|
||||||
sessionMap[position]?.let { FFmpegKit.cancel(it) }
|
VideoEditActivity.startEncoding(imageUri, it,
|
||||||
_uiState.update { currentUiState ->
|
context = getApplication<PixelDroidApplication>(),
|
||||||
currentUiState.copy(
|
registerNewFFmpegSession = ::registerNewFFmpegSession,
|
||||||
newEncodingJobPosition = position,
|
trackTempFile = ::trackTempFile,
|
||||||
newEncodingJobMuted = muted,
|
videoEncodeProgress = ::videoEncodeProgress
|
||||||
newEncodingJobSpeedIndex = speedIndex,
|
|
||||||
newEncodingJobVideoStart = videoStart,
|
|
||||||
newEncodingJobVideoEnd = videoEnd,
|
|
||||||
newEncodingJobVideoCrop = videoCrop,
|
|
||||||
newEncodingJobStabilize = videoStabilize
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
imageUri = data.getStringExtra(PhotoEditActivity.PICTURE_URI)!!.toUri()
|
imageUri = data.getStringExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_URI)!!.toUri()
|
||||||
val (imageSize, imageVideo) = getSizeAndVideoValidate(imageUri, position)
|
val (imageSize, imageVideo) = getSizeAndVideoValidate(imageUri, position)
|
||||||
size = imageSize
|
size = imageSize
|
||||||
video = imageVideo
|
video = imageVideo
|
||||||
|
@ -466,24 +450,59 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
||||||
_uiState.update { it.copy(newPostDescriptionText = text.toString()) }
|
_uiState.update { it.copy(newPostDescriptionText = text.toString()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 is not null, it means the video is done and we can change Uris
|
||||||
|
val (size, _) = getSizeAndVideoValidate(outputVideoPath, position)
|
||||||
|
|
||||||
|
photoData.value?.set(position,
|
||||||
|
photoData.value!![position].copy(
|
||||||
|
imageUri = outputVideoPath,
|
||||||
|
videoEncodeProgress = progress,
|
||||||
|
videoEncodeStabilizationFirstPass = firstPass,
|
||||||
|
videoEncodeComplete = true,
|
||||||
|
videoEncodeError = error,
|
||||||
|
size = size,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
photoData.value?.set(position,
|
||||||
|
photoData.value!![position].copy(
|
||||||
|
videoEncodeProgress = progress,
|
||||||
|
videoEncodeStabilizationFirstPass = firstPass,
|
||||||
|
videoEncodeComplete = false,
|
||||||
|
videoEncodeError = error,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run assignment in main thread
|
||||||
|
viewModelScope.launch {
|
||||||
|
photoData.value = photoData.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun trackTempFile(file: File) {
|
fun trackTempFile(file: File) {
|
||||||
tempFiles.add(file)
|
tempFiles.add(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelEncode(currentPosition: Int) {
|
fun cancelEncode(currentPosition: Int) {
|
||||||
sessionMap[currentPosition]?.let { FFmpegKit.cancel(it) }
|
sessionMap[photoData.value?.getOrNull(currentPosition)?.imageUri]?.let { VideoEditActivity.cancelEncoding(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
FFmpegKit.cancel()
|
VideoEditActivity.cancelEncoding()
|
||||||
tempFiles.forEach {
|
tempFiles.forEach {
|
||||||
it.delete()
|
it.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun registerNewFFmpegSession(position: Int, sessionId: Long) {
|
fun registerNewFFmpegSession(position: Uri, sessionId: Long) {
|
||||||
sessionMap[position] = sessionId
|
sessionMap[position] = sessionId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,5 +7,7 @@ data class CarouselItem constructor(
|
||||||
val caption: String? = null,
|
val caption: String? = null,
|
||||||
val video: Boolean,
|
val video: Boolean,
|
||||||
var encodeProgress: Int?,
|
var encodeProgress: Int?,
|
||||||
var stabilizationFirstPass: Boolean?
|
var stabilizationFirstPass: Boolean?,
|
||||||
|
var encodeComplete: Boolean = false,
|
||||||
|
var encodeError: Boolean = false,
|
||||||
)
|
)
|
|
@ -18,13 +18,10 @@ import androidx.recyclerview.widget.*
|
||||||
import org.pixeldroid.app.R
|
import org.pixeldroid.app.R
|
||||||
import org.pixeldroid.app.databinding.ImageCarouselBinding
|
import org.pixeldroid.app.databinding.ImageCarouselBinding
|
||||||
import me.relex.circleindicator.CircleIndicator2
|
import me.relex.circleindicator.CircleIndicator2
|
||||||
import org.jetbrains.annotations.NotNull
|
|
||||||
import org.jetbrains.annotations.Nullable
|
|
||||||
|
|
||||||
|
|
||||||
class ImageCarousel(
|
class ImageCarousel(
|
||||||
@NotNull context: Context,
|
context: Context,
|
||||||
@Nullable private var attributeSet: AttributeSet?
|
private var attributeSet: AttributeSet?
|
||||||
) : ConstraintLayout(context, attributeSet), OnItemClickListener {
|
) : ConstraintLayout(context, attributeSet), OnItemClickListener {
|
||||||
|
|
||||||
private var adapter: CarouselAdapter? = null
|
private var adapter: CarouselAdapter? = null
|
||||||
|
@ -91,17 +88,7 @@ class ImageCarousel(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (position != RecyclerView.NO_POSITION && field != position) {
|
if (position != RecyclerView.NO_POSITION && field != position) {
|
||||||
val thisProgress = data?.getOrNull(position)?.encodeProgress
|
updateProgress()
|
||||||
if (thisProgress != null) {
|
|
||||||
binding.encodeInfoCard.visibility = VISIBLE
|
|
||||||
binding.encodeProgress.visibility = VISIBLE
|
|
||||||
binding.encodeInfoText.text = (if(data?.getOrNull(position)?.stabilizationFirstPass == true){
|
|
||||||
context.getString(R.string.analyzing_stabilization)
|
|
||||||
} else context.getString(R.string.encode_progress)).format(thisProgress)
|
|
||||||
binding.encodeProgress.progress = thisProgress
|
|
||||||
} else {
|
|
||||||
binding.encodeInfoCard.visibility = GONE
|
|
||||||
}
|
|
||||||
} else if(position == RecyclerView.NO_POSITION) binding.encodeInfoCard.visibility = GONE
|
} else if(position == RecyclerView.NO_POSITION) binding.encodeInfoCard.visibility = GONE
|
||||||
|
|
||||||
if (position != RecyclerView.NO_POSITION && recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {
|
if (position != RecyclerView.NO_POSITION && recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||||
|
@ -558,36 +545,42 @@ class ImageCarousel(
|
||||||
|
|
||||||
this@ImageCarousel.data = data.toMutableList()
|
this@ImageCarousel.data = data.toMutableList()
|
||||||
|
|
||||||
|
updateProgress()
|
||||||
initOnScrollStateChange()
|
initOnScrollStateChange()
|
||||||
}
|
}
|
||||||
showNavigationButtons = data.size != 1
|
showNavigationButtons = data.size != 1
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateProgress(progress: Int?, position: Int, error: Boolean){
|
private fun updateProgress(){
|
||||||
data?.getOrNull(position)?.encodeProgress = progress
|
|
||||||
if(currentPosition == position) {
|
val currentItem = data?.getOrNull(currentPosition)
|
||||||
if (progress == null) {
|
|
||||||
|
currentItem?.let {
|
||||||
|
if(it.encodeError){
|
||||||
|
binding.encodeInfoCard.visibility = VISIBLE
|
||||||
binding.encodeProgress.visibility = GONE
|
binding.encodeProgress.visibility = GONE
|
||||||
if(error){
|
|
||||||
binding.encodeInfoText.setText(R.string.encode_error)
|
binding.encodeInfoText.setText(R.string.encode_error)
|
||||||
binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.error),
|
binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.error),
|
||||||
null, null, null)
|
null, null, null)
|
||||||
|
} else if(it.encodeComplete){
|
||||||
} else {
|
binding.encodeInfoCard.visibility = VISIBLE
|
||||||
|
binding.encodeProgress.visibility = GONE
|
||||||
binding.encodeInfoText.setText(R.string.encode_success)
|
binding.encodeInfoText.setText(R.string.encode_success)
|
||||||
binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.check_circle_24),
|
binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.check_circle_24),
|
||||||
null, null, null)
|
null, null, null)
|
||||||
}
|
} else if(it.encodeProgress != null){
|
||||||
} else {
|
|
||||||
binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
|
binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
|
||||||
binding.encodeProgress.visibility = VISIBLE
|
binding.encodeProgress.visibility = VISIBLE
|
||||||
binding.encodeInfoCard.visibility = VISIBLE
|
binding.encodeInfoCard.visibility = VISIBLE
|
||||||
binding.encodeProgress.progress = progress
|
binding.encodeProgress.progress = it.encodeProgress ?: 0
|
||||||
binding.encodeInfoText.text = (if(data?.getOrNull(position)?.stabilizationFirstPass == true){
|
binding.encodeInfoText.text = (if(it.stabilizationFirstPass == true){
|
||||||
context.getString(R.string.analyzing_stabilization)
|
context.getString(R.string.analyzing_stabilization)
|
||||||
} else context.getString(R.string.encode_progress)).format(progress)
|
} else context.getString(R.string.encode_progress)).format(it.encodeProgress ?: 0)
|
||||||
|
} else {
|
||||||
|
binding.encodeInfoCard.visibility = GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -102,47 +102,6 @@ fun Context.ffmpegCompliantUri(inputUri: Uri?): String =
|
||||||
else inputUri.toString()
|
else inputUri.toString()
|
||||||
|
|
||||||
|
|
||||||
fun bitmapFromUri(contentResolver: ContentResolver, uri: Uri?): Bitmap =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
ImageDecoder
|
|
||||||
.decodeBitmap(
|
|
||||||
ImageDecoder.createSource(contentResolver, uri!!)
|
|
||||||
)
|
|
||||||
{ decoder, _, _ -> decoder.isMutableRequired = true }
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)
|
|
||||||
modifyOrientation(bitmap!!, contentResolver, uri!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun modifyOrientation(
|
|
||||||
bitmap: Bitmap,
|
|
||||||
contentResolver: ContentResolver,
|
|
||||||
uri: Uri
|
|
||||||
): Bitmap {
|
|
||||||
val inputStream = contentResolver.openInputStream(uri)!!
|
|
||||||
val ei = ExifInterface(inputStream)
|
|
||||||
return when (ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)) {
|
|
||||||
ExifInterface.ORIENTATION_ROTATE_90 -> bitmap.rotate(90f)
|
|
||||||
ExifInterface.ORIENTATION_ROTATE_180 -> bitmap.rotate(180f)
|
|
||||||
ExifInterface.ORIENTATION_ROTATE_270 -> bitmap.rotate(270f)
|
|
||||||
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> bitmap.flip(horizontal = true, vertical = false)
|
|
||||||
ExifInterface.ORIENTATION_FLIP_VERTICAL -> bitmap.flip(horizontal = false, vertical = true)
|
|
||||||
else -> bitmap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Bitmap.rotate(degrees: Float): Bitmap {
|
|
||||||
val matrix = Matrix()
|
|
||||||
matrix.postRotate(degrees)
|
|
||||||
return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Bitmap.flip(horizontal: Boolean, vertical: Boolean): Bitmap {
|
|
||||||
val matrix = Matrix()
|
|
||||||
matrix.preScale(if (horizontal) -1f else 1f, if (vertical) -1f else 1f)
|
|
||||||
return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun BaseActivity.openUrl(url: String): Boolean {
|
fun BaseActivity.openUrl(url: String): Boolean {
|
||||||
|
|
||||||
|
@ -234,12 +193,6 @@ fun Context.themeActionBar(): Int? {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Maps a Float from this range to target range */
|
|
||||||
fun ClosedRange<Float>.convert(number: Float, target: ClosedRange<Float>): Float {
|
|
||||||
val ratio = number / (endInclusive - start)
|
|
||||||
return (ratio * (target.endInclusive - target.start))
|
|
||||||
}
|
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
fun Context.getColorFromAttr(@AttrRes attrColor: Int): Int = MaterialColors.getColor(this, attrColor, Color.BLACK)
|
fun Context.getColorFromAttr(@AttrRes attrColor: Int): Int = MaterialColors.getColor(this, attrColor, Color.BLACK)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<vector android:height="24dp"
|
<vector android:height="24dp"
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M3,10h11v2H3V10zM3,8h11V6H3V8zM3,16h7v-2H3V16zM18.01,12.87l0.71,-0.71c0.39,-0.39 1.02,-0.39 1.41,0l0.71,0.71c0.39,0.39 0.39,1.02 0,1.41l-0.71,0.71L18.01,12.87zM17.3,13.58l-5.3,5.3V21h2.12l5.3,-5.3L17.3,13.58z"/>
|
<path android:fillColor="?attr/colorOnBackground" android:pathData="M3,10h11v2H3V10zM3,8h11V6H3V8zM3,16h7v-2H3V16zM18.01,12.87l0.71,-0.71c0.39,-0.39 1.02,-0.39 1.41,0l0.71,0.71c0.39,0.39 0.39,1.02 0,1.41l-0.71,0.71L18.01,12.87zM17.3,13.58l-5.3,5.3V21h2.12l5.3,-5.3L17.3,13.58z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -91,9 +91,6 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
|
||||||
<string name="add_account_name">Add Account</string>
|
<string name="add_account_name">Add Account</string>
|
||||||
<string name="add_account_description">Add another Pixelfed Account</string>
|
<string name="add_account_description">Add another Pixelfed Account</string>
|
||||||
<!-- Post creation -->
|
<!-- Post creation -->
|
||||||
<string name="permission_denied">Permission denied</string>
|
|
||||||
<string name="save_image_failed">Unable to save image</string>
|
|
||||||
<string name="save_image_success">Image successfully saved</string>
|
|
||||||
<plurals name="description_max_characters">
|
<plurals name="description_max_characters">
|
||||||
<item quantity="one">"Description must contain %d character at most."</item>
|
<item quantity="one">"Description must contain %d character at most."</item>
|
||||||
<item quantity="other">"Description must contain %d characters at most."</item>
|
<item quantity="other">"Description must contain %d characters at most."</item>
|
||||||
|
@ -117,24 +114,6 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
|
||||||
<string name="video_not_supported">"The server you are using doesn't support video uploads, you might not be able to upload videos included in this post"</string>
|
<string name="video_not_supported">"The server you are using doesn't support video uploads, you might not be able to upload videos included in this post"</string>
|
||||||
<string name="upload_error">Error code returned by server: %1$d</string>
|
<string name="upload_error">Error code returned by server: %1$d</string>
|
||||||
|
|
||||||
|
|
||||||
<!-- Post editing -->
|
|
||||||
<string name="lbl_brightness">Brightness</string>
|
|
||||||
<string name="lbl_contrast">Contrast</string>
|
|
||||||
<string name="lbl_saturation">Saturation</string>
|
|
||||||
<string name="tab_filters">Filters</string>
|
|
||||||
<string name="edit">Edit</string>
|
|
||||||
<string name="filter_thumbnail">Thumbnail of filter</string>
|
|
||||||
<string name="normal_filter">Normal</string>
|
|
||||||
<string name="busy_dialog_text">Still processing image, wait for that to finish first!</string>
|
|
||||||
<string name="busy_dialog_ok_button">OK, wait for that.</string>
|
|
||||||
<string name="crop_result_error">"Couldn't retrieve image after crop"</string>
|
|
||||||
<string name="image_preview">Preview of the image being edited</string>
|
|
||||||
<string name="crop_button">Button to crop or rotate the image</string>
|
|
||||||
<string name="save_before_returning">Save your edits?</string>
|
|
||||||
<string name="no_cancel_edit">No, cancel edit</string>
|
|
||||||
<string name="error_editing">Error while editing</string>
|
|
||||||
|
|
||||||
<!-- Camera -->
|
<!-- Camera -->
|
||||||
<string name="capture_button_alt">Capture</string>
|
<string name="capture_button_alt">Capture</string>
|
||||||
<string name="switch_camera_button_alt">Switch camera</string>
|
<string name="switch_camera_button_alt">Switch camera</string>
|
||||||
|
@ -239,7 +218,6 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
|
||||||
<string name="report_target">Report @%1$s\'s post</string>
|
<string name="report_target">Report @%1$s\'s post</string>
|
||||||
<string name="reported">Post reported</string>
|
<string name="reported">Post reported</string>
|
||||||
<string name="report_error">Could not send report</string>
|
<string name="report_error">Could not send report</string>
|
||||||
<string name="toolbar_title_edit">Edit</string>
|
|
||||||
<string name="profile_picture">Profile picture</string>
|
<string name="profile_picture">Profile picture</string>
|
||||||
<string name="open_drawer_menu">Open drawer menu</string>
|
<string name="open_drawer_menu">Open drawer menu</string>
|
||||||
<string name="discover">DISCOVER</string>
|
<string name="discover">DISCOVER</string>
|
||||||
|
@ -265,23 +243,10 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
|
||||||
<string name="no_camera_permission">Camera permission not granted, grant the permission in settings if you want to let PixelDroid use the camera</string>
|
<string name="no_camera_permission">Camera permission not granted, grant the permission in settings if you want to let PixelDroid use the camera</string>
|
||||||
<string name="no_storage_permission">Storage permission not granted, grant the permission in settings if you want to let PixelDroid show the thumbnail</string>
|
<string name="no_storage_permission">Storage permission not granted, grant the permission in settings if you want to let PixelDroid show the thumbnail</string>
|
||||||
<string name="play_video">Play video</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_error">Error encoding</string>
|
||||||
<string name="encode_success">Encode success!</string>
|
<string name="encode_success">Encode success!</string>
|
||||||
<string name="encode_progress">Encode %1$d%%</string>
|
<string name="encode_progress">Encode %1$d%%</string>
|
||||||
<string name="stabilize_video">Stabilize video</string>
|
|
||||||
<string name="stabilize_video_intensity">Change intensity of stabilization</string>
|
|
||||||
<string name="stabilization_saved">Stabilization saved</string>
|
|
||||||
<string name="analyzing_stabilization">Analysis for stabilization %1$d%%</string>
|
<string name="analyzing_stabilization">Analysis for stabilization %1$d%%</string>
|
||||||
<string name="select_video_range">Select what to keep of the video</string>
|
|
||||||
<string name="mute_video">Mute video</string>
|
|
||||||
<string name="video_speed">Change video speed</string>
|
|
||||||
<string name="video_crop">Crop video</string>
|
|
||||||
<string name="save_crop">Save crop</string>
|
|
||||||
<string name="crop_saved">Crop saved</string>
|
|
||||||
<string name="still_encoding">One or more videos are still encoding. Wait for them to finish before uploading</string>
|
<string name="still_encoding">One or more videos are still encoding. Wait for them to finish before uploading</string>
|
||||||
<string name="new_post_shortcut_long">Create new post</string>
|
<string name="new_post_shortcut_long">Create new post</string>
|
||||||
<string name="new_post_shortcut_short">New post</string>
|
<string name="new_post_shortcut_short">New post</string>
|
||||||
|
|
|
@ -7,7 +7,7 @@ buildscript {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
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"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
|
|
@ -83,6 +83,14 @@
|
||||||
<sha256 value="2e9372ba7780ef44952adbf86b66e1f08682c1e5277c926185f6564a13799efe" origin="Generated by Gradle"/>
|
<sha256 value="2e9372ba7780ef44952adbf86b66e1f08682c1e5277c926185f6564a13799efe" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="androidx.annotation" name="annotation" version="1.2.0">
|
||||||
|
<artifact name="annotation-1.2.0.jar">
|
||||||
|
<sha256 value="9029262bddce116e6d02be499e4afdba21f24c239087b76b3b57d7e98b490a36" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
<artifact name="annotation-1.2.0.module">
|
||||||
|
<sha256 value="2efcab81ef91b211bacd206eaacd995a51f633a2e96b57a8fc00144c5f9c56b3" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="androidx.annotation" name="annotation" version="1.3.0">
|
<component group="androidx.annotation" name="annotation" version="1.3.0">
|
||||||
<artifact name="annotation-1.3.0.jar">
|
<artifact name="annotation-1.3.0.jar">
|
||||||
<sha256 value="97dc45afefe3a1e421da42b8b6e9f90491477c45fc6178203e3a5e8a05ee8553" origin="Generated by Gradle"/>
|
<sha256 value="97dc45afefe3a1e421da42b8b6e9f90491477c45fc6178203e3a5e8a05ee8553" origin="Generated by Gradle"/>
|
||||||
|
@ -156,6 +164,14 @@
|
||||||
<sha256 value="f3940daf193120c6413176d5f09a9847432eab2af68b1a09663f43a2621a04a8" origin="Generated by Gradle"/>
|
<sha256 value="f3940daf193120c6413176d5f09a9847432eab2af68b1a09663f43a2621a04a8" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="androidx.appcompat" name="appcompat" version="1.5.0">
|
||||||
|
<artifact name="appcompat-1.5.0.aar">
|
||||||
|
<sha256 value="ee3c914528409787069d9ee903243dac0204a09f9119c4f0aa1a2aa92188acac" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
<artifact name="appcompat-1.5.0.module">
|
||||||
|
<sha256 value="34daf88a8e4a65367dfa56957e3e1fdcaec5688b442bbb542e43ebe226c4589b" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="androidx.appcompat" name="appcompat" version="1.5.1">
|
<component group="androidx.appcompat" name="appcompat" version="1.5.1">
|
||||||
<artifact name="appcompat-1.5.1.aar">
|
<artifact name="appcompat-1.5.1.aar">
|
||||||
<sha256 value="50f2c7756e28a7da648bd4551ee3b31e0b71863a6bf591f0ca978428219c5eab" origin="Generated by Gradle"/>
|
<sha256 value="50f2c7756e28a7da648bd4551ee3b31e0b71863a6bf591f0ca978428219c5eab" origin="Generated by Gradle"/>
|
||||||
|
@ -175,6 +191,14 @@
|
||||||
<sha256 value="0b8d946fae07779737ed36a428adaa5df8f93f1471821a748a079b21f1f70310" origin="Generated by Gradle"/>
|
<sha256 value="0b8d946fae07779737ed36a428adaa5df8f93f1471821a748a079b21f1f70310" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="androidx.appcompat" name="appcompat-resources" version="1.5.0">
|
||||||
|
<artifact name="appcompat-resources-1.5.0.aar">
|
||||||
|
<sha256 value="34511f11765eb4dfb61e7b3285019b6488b10f6a9093b028aa108ca0d33fc8c5" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
<artifact name="appcompat-resources-1.5.0.module">
|
||||||
|
<sha256 value="2a8c4441ca15e236e81f43ef2e8b917737d553170452c101d4455cd0aef64348" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="androidx.appcompat" name="appcompat-resources" version="1.5.1">
|
<component group="androidx.appcompat" name="appcompat-resources" version="1.5.1">
|
||||||
<artifact name="appcompat-resources-1.5.1.aar">
|
<artifact name="appcompat-resources-1.5.1.aar">
|
||||||
<sha256 value="6cfa9caae1869ffe03da9ec3ebf47ad84d70a7185ee86ec63aa8289cbb457e67" origin="Generated by Gradle"/>
|
<sha256 value="6cfa9caae1869ffe03da9ec3ebf47ad84d70a7185ee86ec63aa8289cbb457e67" origin="Generated by Gradle"/>
|
||||||
|
@ -1007,6 +1031,14 @@
|
||||||
<sha256 value="9fdb7dfd08028f5994df9c8baa90871cf2b5849daad335311f761c6df8ad9674" origin="Generated by Gradle"/>
|
<sha256 value="9fdb7dfd08028f5994df9c8baa90871cf2b5849daad335311f761c6df8ad9674" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="androidx.exifinterface" name="exifinterface" version="1.1.0-beta01">
|
||||||
|
<artifact name="exifinterface-1.1.0-beta01.aar">
|
||||||
|
<sha256 value="fc9829cf381becc7f88d2e48e1680b05680c2564836e266bcebee08c02cd999c" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
<artifact name="exifinterface-1.1.0-beta01.pom">
|
||||||
|
<sha256 value="50e953aeea999d9274631a0dca3cff0229d7e4b1451dc6ffdf0731c8e029afc3" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="androidx.exifinterface" name="exifinterface" version="1.2.0">
|
<component group="androidx.exifinterface" name="exifinterface" version="1.2.0">
|
||||||
<artifact name="exifinterface-1.2.0.aar">
|
<artifact name="exifinterface-1.2.0.aar">
|
||||||
<sha256 value="aae68e513095e475a7670556eacba772ec2bb592d17187091578d3fef947aea7" origin="Generated by Gradle"/>
|
<sha256 value="aae68e513095e475a7670556eacba772ec2bb592d17187091578d3fef947aea7" origin="Generated by Gradle"/>
|
||||||
|
@ -1042,6 +1074,11 @@
|
||||||
<sha256 value="8aecb68c916c539e233a05548a8f0e5c2c6d463b0dea65048768707f38adbb01" origin="Generated by Gradle"/>
|
<sha256 value="8aecb68c916c539e233a05548a8f0e5c2c6d463b0dea65048768707f38adbb01" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="androidx.fragment" name="fragment" version="1.2.5">
|
||||||
|
<artifact name="fragment-1.2.5.pom">
|
||||||
|
<sha256 value="df0aca46b62bb47cc662cbcee63372db6d2a2859478ee38b594fba3433fe30a5" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="androidx.fragment" name="fragment" version="1.3.6">
|
<component group="androidx.fragment" name="fragment" version="1.3.6">
|
||||||
<artifact name="fragment-1.3.6.aar">
|
<artifact name="fragment-1.3.6.aar">
|
||||||
<sha256 value="12f0831b4f08092d5dda272c1923c11a022ff20ceffed3e801751e21bb8d1c1e" origin="Generated by Gradle"/>
|
<sha256 value="12f0831b4f08092d5dda272c1923c11a022ff20ceffed3e801751e21bb8d1c1e" origin="Generated by Gradle"/>
|
||||||
|
@ -6425,6 +6462,14 @@
|
||||||
<sha256 value="648c5163c8a03d215ae4f4b41ad926ec3a7355f6d5dc93a13df531034bffae2f" origin="Generated by Gradle"/>
|
<sha256 value="648c5163c8a03d215ae4f4b41ad926ec3a7355f6d5dc93a13df531034bffae2f" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="com.squareup.okhttp3" name="okhttp" version="3.12.13">
|
||||||
|
<artifact name="okhttp-3.12.13.jar">
|
||||||
|
<sha256 value="508234e024ef7e270ab1a6d5b356f5b98e786511239ca986d684fd1e2cf7bc82" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
<artifact name="okhttp-3.12.13.pom">
|
||||||
|
<sha256 value="ae2cae0aaefda1c8a4d835113c7c2db569aebff52b4590df23bfb8d426a76ea6" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="com.squareup.okhttp3" name="okhttp" version="4.9.2">
|
<component group="com.squareup.okhttp3" name="okhttp" version="4.9.2">
|
||||||
<artifact name="okhttp-4.9.2.jar">
|
<artifact name="okhttp-4.9.2.jar">
|
||||||
<sha256 value="3b2ee1b768c1df28c30d2fe6b38dda4d2c519210e30ca3d27950618c563c92de" origin="Generated by Gradle"/>
|
<sha256 value="3b2ee1b768c1df28c30d2fe6b38dda4d2c519210e30ca3d27950618c563c92de" origin="Generated by Gradle"/>
|
||||||
|
@ -6444,6 +6489,19 @@
|
||||||
<sha256 value="64df3b9baddd00eaa4bc727641fd157c65f39465e68eaecaa5a1d0417fc2f269" origin="Generated by Gradle"/>
|
<sha256 value="64df3b9baddd00eaa4bc727641fd157c65f39465e68eaecaa5a1d0417fc2f269" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="com.squareup.okhttp3" name="parent" version="3.12.13">
|
||||||
|
<artifact name="parent-3.12.13.pom">
|
||||||
|
<sha256 value="c48b857f4fb5d6b393fc87244d4646e951f0a36ecd1957c4c12fe65bdb9e2a02" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
|
<component group="com.squareup.okio" name="okio" version="1.15.0">
|
||||||
|
<artifact name="okio-1.15.0.jar">
|
||||||
|
<sha256 value="693fa319a7e8843300602b204023b7674f106ebcb577f2dd5807212b66118bd2" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
<artifact name="okio-1.15.0.pom">
|
||||||
|
<sha256 value="f1c10b1480d1ab75fb051a07b273e37cda2525e97e1607ee83e6833b70e1bfce" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="com.squareup.okio" name="okio" version="1.16.0">
|
<component group="com.squareup.okio" name="okio" version="1.16.0">
|
||||||
<artifact name="okio-1.16.0.jar">
|
<artifact name="okio-1.16.0.jar">
|
||||||
<sha256 value="ec0484ff1903640e3845c2b10abb99eff2d32308ffe3459e5f92309a451b9c7e" origin="Generated by Gradle"/>
|
<sha256 value="ec0484ff1903640e3845c2b10abb99eff2d32308ffe3459e5f92309a451b9c7e" origin="Generated by Gradle"/>
|
||||||
|
@ -6495,6 +6553,11 @@
|
||||||
<sha256 value="a4eef641be5e5463d2789c3c73601f9a99f36e1377ca007e30f0fee53a054cdc" origin="Generated by Gradle"/>
|
<sha256 value="a4eef641be5e5463d2789c3c73601f9a99f36e1377ca007e30f0fee53a054cdc" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="com.squareup.okio" name="okio-parent" version="1.15.0">
|
||||||
|
<artifact name="okio-parent-1.15.0.pom">
|
||||||
|
<sha256 value="34e09a3ea2aacd721df399470d05a47aecdad801269a212195ffbf9d21056db2" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="com.squareup.okio" name="okio-parent" version="1.16.0">
|
<component group="com.squareup.okio" name="okio-parent" version="1.16.0">
|
||||||
<artifact name="okio-parent-1.16.0.pom">
|
<artifact name="okio-parent-1.16.0.pom">
|
||||||
<sha256 value="0b7424c3faab3bb5333096e39957f88f8d50ce0c98bfba71a3fcfaa0aaf0552c" origin="Generated by Gradle"/>
|
<sha256 value="0b7424c3faab3bb5333096e39957f88f8d50ce0c98bfba71a3fcfaa0aaf0552c" origin="Generated by Gradle"/>
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
|
@ -0,0 +1,62 @@
|
||||||
|
plugins {
|
||||||
|
id 'com.android.library'
|
||||||
|
id 'org.jetbrains.kotlin.android'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace 'org.pixeldroid.media_editor'
|
||||||
|
compileSdk 33
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk 23
|
||||||
|
targetSdk 33
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
implementation 'androidx.core:core-ktx:1.9.0'
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||||
|
implementation 'com.google.android.material:material:1.7.0'
|
||||||
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||||
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
|
|
||||||
|
implementation 'info.androidhive:imagefilters:1.0.7'
|
||||||
|
implementation 'com.github.yalantis:ucrop:2.2.8-native'
|
||||||
|
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1'
|
||||||
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
|
||||||
|
implementation 'androidx.media2:media2-widget:1.2.1'
|
||||||
|
implementation 'androidx.media2:media2-player:1.2.1'
|
||||||
|
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||||
|
implementation 'com.arthenica:ffmpeg-kit-min-gpl:5.1.LTS'
|
||||||
|
implementation('com.github.bumptech.glide:glide:4.14.2') {
|
||||||
|
exclude group: "com.android.support"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,24 @@
|
||||||
|
package org.pixeldroid.media_editor
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("org.pixeldroid.media_editor", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest>
|
||||||
|
</manifest>
|
|
@ -1,4 +1,4 @@
|
||||||
package org.pixeldroid.app.postCreation.photoEdit
|
package org.pixeldroid.media_editor.photoEdit
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
@ -6,8 +6,7 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
import org.pixeldroid.app.R
|
import org.pixeldroid.media_editor.databinding.FragmentEditImageBinding
|
||||||
import org.pixeldroid.app.databinding.FragmentEditImageBinding
|
|
||||||
|
|
||||||
class EditImageFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
class EditImageFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
||||||
|
|
||||||
|
@ -52,13 +51,13 @@ class EditImageFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
||||||
var prog = progress
|
var prog = progress
|
||||||
|
|
||||||
listener?.let {
|
listener?.let {
|
||||||
when(seekBar!!.id) {
|
when(seekBar) {
|
||||||
R.id.seekbar_brightness -> it.onBrightnessChange(progress - 100)
|
binding.seekbarBrightness -> it.onBrightnessChange(progress - 100)
|
||||||
R.id.seekbar_saturation -> {
|
binding.seekbarSaturation -> {
|
||||||
prog += 10
|
prog += 10
|
||||||
it.onSaturationChange(.10f * prog)
|
it.onSaturationChange(.10f * prog)
|
||||||
}
|
}
|
||||||
R.id.seekbar_contrast -> {
|
binding.seekbarContrast -> {
|
||||||
it.onContrastChange(.10f * prog)
|
it.onContrastChange(.10f * prog)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package org.pixeldroid.app.postCreation.photoEdit
|
package org.pixeldroid.media_editor.photoEdit
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -15,9 +15,8 @@ import com.zomato.photofilters.imageprocessors.Filter
|
||||||
import com.zomato.photofilters.utils.ThumbnailItem
|
import com.zomato.photofilters.utils.ThumbnailItem
|
||||||
import com.zomato.photofilters.utils.ThumbnailsManager
|
import com.zomato.photofilters.utils.ThumbnailsManager
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.pixeldroid.app.R
|
import org.pixeldroid.media_editor.R
|
||||||
import org.pixeldroid.app.databinding.FragmentFilterListBinding
|
import org.pixeldroid.media_editor.databinding.FragmentFilterListBinding
|
||||||
import org.pixeldroid.app.utils.bitmapFromUri
|
|
||||||
|
|
||||||
class FilterListFragment : Fragment() {
|
class FilterListFragment : Fragment() {
|
||||||
|
|
||||||
|
@ -52,7 +51,9 @@ class FilterListFragment : Fragment() {
|
||||||
private fun displayImage() {
|
private fun displayImage() {
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
val tbImage: Bitmap = bitmapFromUri(requireActivity().contentResolver, PhotoEditActivity.imageUri)
|
val tbImage: Bitmap = bitmapFromUri(requireActivity().contentResolver,
|
||||||
|
PhotoEditActivity.imageUri
|
||||||
|
)
|
||||||
setupFilter(tbImage)
|
setupFilter(tbImage)
|
||||||
|
|
||||||
tbItemList.addAll(ThumbnailsManager.processThumbs(context))
|
tbItemList.addAll(ThumbnailsManager.processThumbs(context))
|
|
@ -1,4 +1,4 @@
|
||||||
package org.pixeldroid.app.postCreation.photoEdit
|
package org.pixeldroid.media_editor.photoEdit
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
|
@ -14,6 +14,7 @@ import android.view.MenuItem
|
||||||
import android.view.View.GONE
|
import android.view.View.GONE
|
||||||
import android.view.View.VISIBLE
|
import android.view.View.VISIBLE
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
@ -27,12 +28,8 @@ import com.zomato.photofilters.imageprocessors.Filter
|
||||||
import com.zomato.photofilters.imageprocessors.subfilters.BrightnessSubFilter
|
import com.zomato.photofilters.imageprocessors.subfilters.BrightnessSubFilter
|
||||||
import com.zomato.photofilters.imageprocessors.subfilters.ContrastSubFilter
|
import com.zomato.photofilters.imageprocessors.subfilters.ContrastSubFilter
|
||||||
import com.zomato.photofilters.imageprocessors.subfilters.SaturationSubfilter
|
import com.zomato.photofilters.imageprocessors.subfilters.SaturationSubfilter
|
||||||
import org.pixeldroid.app.R
|
import org.pixeldroid.media_editor.databinding.ActivityPhotoEditBinding
|
||||||
import org.pixeldroid.app.databinding.ActivityPhotoEditBinding
|
import org.pixeldroid.media_editor.R
|
||||||
import org.pixeldroid.app.postCreation.PostCreationActivity
|
|
||||||
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
|
|
||||||
import org.pixeldroid.app.utils.bitmapFromUri
|
|
||||||
import org.pixeldroid.app.utils.getColorFromAttr
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
@ -49,7 +46,7 @@ private val REQUIRED_PERMISSIONS = arrayOf(
|
||||||
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
)
|
)
|
||||||
|
|
||||||
class PhotoEditActivity : BaseThemedWithBarActivity() {
|
class PhotoEditActivity : AppCompatActivity() {
|
||||||
|
|
||||||
var saving: Boolean = false
|
var saving: Boolean = false
|
||||||
private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888
|
private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888
|
||||||
|
@ -78,8 +75,8 @@ class PhotoEditActivity : BaseThemedWithBarActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object{
|
companion object{
|
||||||
internal const val PICTURE_URI = "picture_uri"
|
const val PICTURE_URI = "picture_uri"
|
||||||
internal const val PICTURE_POSITION = "picture_position"
|
const val PICTURE_POSITION = "picture_position"
|
||||||
|
|
||||||
private var executor: ExecutorService = newSingleThreadExecutor()
|
private var executor: ExecutorService = newSingleThreadExecutor()
|
||||||
private var future: Future<*>? = null
|
private var future: Future<*>? = null
|
||||||
|
@ -179,6 +176,7 @@ class PhotoEditActivity : BaseThemedWithBarActivity() {
|
||||||
saving = false
|
saving = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (noEdits()) super.onBackPressed()
|
if (noEdits()) super.onBackPressed()
|
||||||
else {
|
else {
|
||||||
|
@ -369,7 +367,7 @@ class PhotoEditActivity : BaseThemedWithBarActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendBackImage(file: String) {
|
private fun sendBackImage(file: String) {
|
||||||
val intent = Intent(this, PostCreationActivity::class.java)
|
val intent = Intent()
|
||||||
.apply {
|
.apply {
|
||||||
putExtra(PICTURE_URI, file)
|
putExtra(PICTURE_URI, file)
|
||||||
putExtra(PICTURE_POSITION, picturePosition)
|
putExtra(PICTURE_POSITION, picturePosition)
|
|
@ -1,4 +1,4 @@
|
||||||
package org.pixeldroid.app.postCreation.photoEdit
|
package org.pixeldroid.media_editor.photoEdit
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -7,13 +7,13 @@ import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.zomato.photofilters.utils.ThumbnailItem
|
import com.zomato.photofilters.utils.ThumbnailItem
|
||||||
import org.pixeldroid.app.R
|
import org.pixeldroid.media_editor.R
|
||||||
import org.pixeldroid.app.databinding.ThumbnailListItemBinding
|
import org.pixeldroid.media_editor.databinding.ThumbnailListItemBinding
|
||||||
import org.pixeldroid.app.utils.getColorFromAttr
|
|
||||||
|
|
||||||
class ThumbnailAdapter (private val context: Context,
|
class ThumbnailAdapter (private val context: Context,
|
||||||
private val tbItemList: List<ThumbnailItem>,
|
private val tbItemList: List<ThumbnailItem>,
|
||||||
private val listener: FilterListFragment): RecyclerView.Adapter<ThumbnailAdapter.MyViewHolder>() {
|
private val listener: FilterListFragment
|
||||||
|
): RecyclerView.Adapter<ThumbnailAdapter.MyViewHolder>() {
|
||||||
|
|
||||||
private var selectedIndex = 0
|
private var selectedIndex = 0
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
package org.pixeldroid.media_editor.photoEdit
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.exifinterface.media.ExifInterface
|
||||||
|
import com.arthenica.ffmpegkit.FFmpegKitConfig
|
||||||
|
import com.google.android.material.color.MaterialColors
|
||||||
|
|
||||||
|
|
||||||
|
fun bitmapFromUri(contentResolver: ContentResolver, uri: Uri?): Bitmap =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
ImageDecoder
|
||||||
|
.decodeBitmap(
|
||||||
|
ImageDecoder.createSource(contentResolver, uri!!)
|
||||||
|
)
|
||||||
|
{ decoder, _, _ -> decoder.isMutableRequired = true }
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)
|
||||||
|
modifyOrientation(bitmap!!, contentResolver, uri!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun modifyOrientation(
|
||||||
|
bitmap: Bitmap,
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
uri: Uri
|
||||||
|
): Bitmap {
|
||||||
|
val inputStream = contentResolver.openInputStream(uri)!!
|
||||||
|
val ei = ExifInterface(inputStream)
|
||||||
|
return when (ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)) {
|
||||||
|
ExifInterface.ORIENTATION_ROTATE_90 -> bitmap.rotate(90f)
|
||||||
|
ExifInterface.ORIENTATION_ROTATE_180 -> bitmap.rotate(180f)
|
||||||
|
ExifInterface.ORIENTATION_ROTATE_270 -> bitmap.rotate(270f)
|
||||||
|
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> bitmap.flip(horizontal = true, vertical = false)
|
||||||
|
ExifInterface.ORIENTATION_FLIP_VERTICAL -> bitmap.flip(horizontal = false, vertical = true)
|
||||||
|
else -> bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bitmap.rotate(degrees: Float): Bitmap {
|
||||||
|
val matrix = Matrix()
|
||||||
|
matrix.postRotate(degrees)
|
||||||
|
return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bitmap.flip(horizontal: Boolean, vertical: Boolean): Bitmap {
|
||||||
|
val matrix = Matrix()
|
||||||
|
matrix.preScale(if (horizontal) -1f else 1f, if (vertical) -1f else 1f)
|
||||||
|
return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
fun Context.getColorFromAttr(@AttrRes attrColor: Int): Int = MaterialColors.getColor(this, attrColor, Color.BLACK)
|
||||||
|
|
||||||
|
fun Context.ffmpegCompliantUri(inputUri: Uri?): String =
|
||||||
|
if (inputUri?.scheme == "content")
|
||||||
|
FFmpegKitConfig.getSafParameterForRead(this, inputUri)
|
||||||
|
else inputUri.toString()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method converts dp unit to equivalent pixels, depending on device density.
|
||||||
|
*/
|
||||||
|
fun Int.dpToPx(context: Context): Int {
|
||||||
|
return TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
this.toFloat(),
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Maps a Float from this range to target range */
|
||||||
|
fun ClosedRange<Float>.convert(number: Float, target: ClosedRange<Float>): Float {
|
||||||
|
val ratio = number / (endInclusive - start)
|
||||||
|
return (ratio * (target.endInclusive - target.start))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Uri.fileExtension(contentResolver: ContentResolver): String? {
|
||||||
|
return if (scheme == "content") {
|
||||||
|
contentResolver.getType(this)?.takeLastWhile { it != '/' }
|
||||||
|
} else {
|
||||||
|
MimeTypeMap.getFileExtensionFromUrl(toString()).ifEmpty { null }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
package org.pixeldroid.app.postCreation.photoEdit
|
package org.pixeldroid.media_editor.photoEdit
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
|
@ -18,6 +20,7 @@ import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.os.HandlerCompat
|
import androidx.core.os.HandlerCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
@ -27,23 +30,24 @@ import androidx.media2.common.UriMediaItem
|
||||||
import androidx.media2.player.MediaPlayer
|
import androidx.media2.player.MediaPlayer
|
||||||
import com.arthenica.ffmpegkit.FFmpegKit
|
import com.arthenica.ffmpegkit.FFmpegKit
|
||||||
import com.arthenica.ffmpegkit.FFmpegKitConfig
|
import com.arthenica.ffmpegkit.FFmpegKitConfig
|
||||||
|
import com.arthenica.ffmpegkit.FFmpegSession
|
||||||
import com.arthenica.ffmpegkit.FFprobeKit
|
import com.arthenica.ffmpegkit.FFprobeKit
|
||||||
import com.arthenica.ffmpegkit.MediaInformation
|
import com.arthenica.ffmpegkit.MediaInformation
|
||||||
import com.arthenica.ffmpegkit.ReturnCode
|
import com.arthenica.ffmpegkit.ReturnCode
|
||||||
|
import com.arthenica.ffmpegkit.Statistics
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.google.android.material.slider.RangeSlider
|
import com.google.android.material.slider.RangeSlider
|
||||||
import com.google.android.material.slider.Slider
|
import com.google.android.material.slider.Slider
|
||||||
import org.pixeldroid.app.R
|
import org.pixeldroid.media_editor.R
|
||||||
import org.pixeldroid.app.databinding.ActivityVideoEditBinding
|
import org.pixeldroid.media_editor.databinding.ActivityVideoEditBinding
|
||||||
import org.pixeldroid.app.postCreation.PostCreationActivity
|
|
||||||
import org.pixeldroid.app.postCreation.carousel.dpToPx
|
|
||||||
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
|
|
||||||
import org.pixeldroid.app.utils.ffmpegCompliantUri
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class VideoEditActivity : BaseThemedWithBarActivity() {
|
const val TAG = "VideoEditActivity"
|
||||||
|
|
||||||
|
class VideoEditActivity : AppCompatActivity() {
|
||||||
|
|
||||||
data class RelativeCropPosition(
|
data class RelativeCropPosition(
|
||||||
// Width of the selected part of the video, relative to the width of the video
|
// Width of the selected part of the video, relative to the width of the video
|
||||||
|
@ -63,6 +67,16 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class VideoEditArguments(
|
||||||
|
val muted: Boolean,
|
||||||
|
val videoStart: Float?,
|
||||||
|
val videoEnd: Float? ,
|
||||||
|
val speedIndex: Int,
|
||||||
|
val videoCrop: RelativeCropPosition,
|
||||||
|
val videoStabilize: Float
|
||||||
|
): Serializable
|
||||||
|
|
||||||
|
private lateinit var videoUri: Uri
|
||||||
private lateinit var mediaPlayer: MediaPlayer
|
private lateinit var mediaPlayer: MediaPlayer
|
||||||
private var videoPosition: Int = -1
|
private var videoPosition: Int = -1
|
||||||
|
|
||||||
|
@ -117,11 +131,11 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
||||||
|
|
||||||
val resultHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper())
|
val resultHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper())
|
||||||
|
|
||||||
val uri = intent.getParcelableExtra<Uri>(PhotoEditActivity.PICTURE_URI)!!
|
videoUri = intent.getParcelableExtra(PhotoEditActivity.PICTURE_URI)!!
|
||||||
|
|
||||||
videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1)
|
videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1)
|
||||||
|
|
||||||
val inputVideoPath = ffmpegCompliantUri(uri)
|
val inputVideoPath = ffmpegCompliantUri(videoUri)
|
||||||
val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(inputVideoPath).mediaInformation
|
val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(inputVideoPath).mediaInformation
|
||||||
|
|
||||||
//Duration in seconds, or null
|
//Duration in seconds, or null
|
||||||
|
@ -132,7 +146,7 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
||||||
binding.videoRangeSeekBar.values = listOf(0f,(duration?: 100f) / 2, duration ?: 100f)
|
binding.videoRangeSeekBar.values = listOf(0f,(duration?: 100f) / 2, duration ?: 100f)
|
||||||
|
|
||||||
|
|
||||||
val mediaItem: UriMediaItem = UriMediaItem.Builder(uri).build()
|
val mediaItem: UriMediaItem = UriMediaItem.Builder(videoUri).build()
|
||||||
mediaItem.metadata = MediaMetadata.Builder()
|
mediaItem.metadata = MediaMetadata.Builder()
|
||||||
.putString(MediaMetadata.METADATA_KEY_TITLE, "")
|
.putString(MediaMetadata.METADATA_KEY_TITLE, "")
|
||||||
.build()
|
.build()
|
||||||
|
@ -165,7 +179,7 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.cropper.setOnClickListener {
|
binding.cropper.setOnClickListener {
|
||||||
showCropInterface(show = true, uri = uri)
|
showCropInterface(show = true, uri = videoUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.saveCropButton.setOnClickListener {
|
binding.saveCropButton.setOnClickListener {
|
||||||
|
@ -270,13 +284,13 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
||||||
val thumbInterval: Float? = duration?.div(7)
|
val thumbInterval: Float? = duration?.div(7)
|
||||||
|
|
||||||
thumbInterval?.let {
|
thumbInterval?.let {
|
||||||
thumbnail(uri, resultHandler, binding.thumbnail1, it)
|
thumbnail(videoUri, resultHandler, binding.thumbnail1, it)
|
||||||
thumbnail(uri, resultHandler, binding.thumbnail2, it.times(2))
|
thumbnail(videoUri, resultHandler, binding.thumbnail2, it.times(2))
|
||||||
thumbnail(uri, resultHandler, binding.thumbnail3, it.times(3))
|
thumbnail(videoUri, resultHandler, binding.thumbnail3, it.times(3))
|
||||||
thumbnail(uri, resultHandler, binding.thumbnail4, it.times(4))
|
thumbnail(videoUri, resultHandler, binding.thumbnail4, it.times(4))
|
||||||
thumbnail(uri, resultHandler, binding.thumbnail5, it.times(5))
|
thumbnail(videoUri, resultHandler, binding.thumbnail5, it.times(5))
|
||||||
thumbnail(uri, resultHandler, binding.thumbnail6, it.times(6))
|
thumbnail(videoUri, resultHandler, binding.thumbnail6, it.times(6))
|
||||||
thumbnail(uri, resultHandler, binding.thumbnail7, it.times(7))
|
thumbnail(videoUri, resultHandler, binding.thumbnail7, it.times(7))
|
||||||
}
|
}
|
||||||
|
|
||||||
resetControls()
|
resetControls()
|
||||||
|
@ -369,19 +383,20 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
||||||
|
|
||||||
private fun returnWithValues() {
|
private fun returnWithValues() {
|
||||||
//TODO Check if some of these should be null to indicate no changes in that category? Ex start/end
|
//TODO Check if some of these should be null to indicate no changes in that category? Ex start/end
|
||||||
val intent = Intent(this, PostCreationActivity::class.java)
|
val intent = Intent()
|
||||||
.apply {
|
.apply {
|
||||||
putExtra(PhotoEditActivity.PICTURE_POSITION, videoPosition)
|
putExtra(PhotoEditActivity.PICTURE_POSITION, videoPosition)
|
||||||
putExtra(MUTED, binding.muter.isSelected)
|
putExtra(VIDEO_ARGUMENTS_TAG, VideoEditArguments(
|
||||||
putExtra(SPEED, speed)
|
binding.muter.isSelected, binding.videoRangeSeekBar.values.first(),
|
||||||
|
binding.videoRangeSeekBar.values[2],
|
||||||
|
speed,
|
||||||
|
cropRelativeDimensions,
|
||||||
|
stabilization
|
||||||
|
)
|
||||||
|
)
|
||||||
putExtra(MODIFIED, !noEdits())
|
putExtra(MODIFIED, !noEdits())
|
||||||
putExtra(VIDEO_START, binding.videoRangeSeekBar.values.first())
|
|
||||||
putExtra(VIDEO_END, binding.videoRangeSeekBar.values[2])
|
|
||||||
putExtra(VIDEO_CROP, cropRelativeDimensions)
|
|
||||||
putExtra(VIDEO_STABILIZE, stabilization)
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||||
}
|
}
|
||||||
|
|
||||||
setResult(Activity.RESULT_OK, intent)
|
setResult(Activity.RESULT_OK, intent)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
@ -451,15 +466,182 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val VIDEO_TAG = "VideoEditTag"
|
const val VIDEO_ARGUMENTS_TAG = "org.pixeldroid.media_editor.VideoEditTag"
|
||||||
const val MUTED = "VideoEditMutedTag"
|
|
||||||
const val SPEED = "VideoEditSpeedTag"
|
|
||||||
// List of choices of speeds
|
// List of choices of speeds
|
||||||
val speedChoices: List<Number> = listOf(0.5, 1, 2, 4, 8)
|
val speedChoices: List<Number> = listOf(0.5, 1, 2, 4, 8)
|
||||||
const val VIDEO_START = "VideoEditVideoStartTag"
|
|
||||||
const val VIDEO_END = "VideoEditVideoEndTag"
|
|
||||||
const val VIDEO_CROP = "VideoEditVideoCropTag"
|
|
||||||
const val VIDEO_STABILIZE = "VideoEditVideoStabilizeTag"
|
|
||||||
const val MODIFIED = "VideoEditModifiedTag"
|
const val MODIFIED = "VideoEditModifiedTag"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
fun startEncoding(
|
||||||
|
originalUri: Uri,
|
||||||
|
arguments: VideoEditArguments,
|
||||||
|
context: Context,
|
||||||
|
//TODO make interfaces for these callbacks, or something more explicit
|
||||||
|
registerNewFFmpegSession: (Uri, Long) -> Unit,
|
||||||
|
trackTempFile: (File) -> Unit,
|
||||||
|
videoEncodeProgress: (Uri, Int, Boolean, Uri?, Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Having a meaningful suffix is necessary so that ffmpeg knows what to put in output
|
||||||
|
val suffix = originalUri.fileExtension(context.contentResolver)
|
||||||
|
val file = File.createTempFile("temp_video", ".$suffix", context.cacheDir)
|
||||||
|
//val file = File.createTempFile("temp_video", ".webm", cacheDir)
|
||||||
|
trackTempFile(file)
|
||||||
|
val fileUri = file.toUri()
|
||||||
|
val outputVideoPath = context.ffmpegCompliantUri(fileUri)
|
||||||
|
|
||||||
|
val ffmpegCompliantUri: String = context.ffmpegCompliantUri(originalUri)
|
||||||
|
|
||||||
|
val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(context.ffmpegCompliantUri(originalUri)).mediaInformation
|
||||||
|
val totalVideoDuration = mediaInformation?.duration?.toFloatOrNull()
|
||||||
|
|
||||||
|
fun secondPass(stabilizeString: String = ""){
|
||||||
|
val speed = speedChoices[arguments.speedIndex]
|
||||||
|
|
||||||
|
val mutedString = if(arguments.muted || arguments.speedIndex != 1) "-an" else null
|
||||||
|
val startString: List<String?> = if(arguments.videoStart != null) listOf("-ss", "${arguments.videoStart/speed.toFloat()}") else listOf(null, null)
|
||||||
|
|
||||||
|
val endString: List<String?> = if(arguments.videoEnd != null) listOf("-to", "${arguments.videoEnd/speed.toFloat() - (arguments.videoStart ?: 0f)/speed.toFloat()}") else listOf(null, null)
|
||||||
|
|
||||||
|
// iw and ih are variables for the original width and height values, FFmpeg will know them
|
||||||
|
val cropString = if(arguments.videoCrop.notCropped()) "" else "crop=${arguments.videoCrop.relativeWidth}*iw:${arguments.videoCrop.relativeHeight}*ih:${arguments.videoCrop.relativeX}*iw:${arguments.videoCrop.relativeY}*ih"
|
||||||
|
val separator = if(arguments.speedIndex != 1 && !arguments.videoCrop.notCropped()) "," else ""
|
||||||
|
val speedString = if(arguments.speedIndex != 1) "setpts=PTS/${speed}" else ""
|
||||||
|
|
||||||
|
val separatorStabilize = if(stabilizeString == "" || (speedString == "" && cropString == "")) "" else ","
|
||||||
|
|
||||||
|
val speedAndCropString: List<String?> = if(arguments.speedIndex!= 1 || !arguments.videoCrop.notCropped() || stabilizeString.isNotEmpty())
|
||||||
|
listOf("-filter:v", stabilizeString + separatorStabilize + speedString + separator + cropString)
|
||||||
|
// Stream copy is not compatible with filter, but when not filtering we can copy the stream without re-encoding
|
||||||
|
else listOf("-c", "copy")
|
||||||
|
|
||||||
|
// This should be set when re-encoding is required (otherwise it defaults to mpeg which then doesn't play)
|
||||||
|
val encodePreset: List<String?> = if(arguments.speedIndex != 1 && !arguments.videoCrop.notCropped()) listOf("-c:v", "libx264", "-preset", "ultrafast") else listOf(null, null, null, null)
|
||||||
|
|
||||||
|
val session: FFmpegSession =
|
||||||
|
FFmpegKit.executeWithArgumentsAsync(listOfNotNull(
|
||||||
|
startString[0], startString[1],
|
||||||
|
"-i", ffmpegCompliantUri,
|
||||||
|
speedAndCropString[0], speedAndCropString[1],
|
||||||
|
endString[0], endString[1],
|
||||||
|
mutedString, "-y",
|
||||||
|
encodePreset[0], encodePreset[1], encodePreset[2], encodePreset[3],
|
||||||
|
outputVideoPath,
|
||||||
|
).toTypedArray(),
|
||||||
|
//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)) {
|
||||||
|
|
||||||
|
videoEncodeProgress(originalUri, 100, false, outputVideoPath.toUri(), false)
|
||||||
|
|
||||||
|
Log.d(TAG, "Encode completed successfully in ${session.duration} milliseconds")
|
||||||
|
} else {
|
||||||
|
videoEncodeProgress(originalUri, 0, false, outputVideoPath.toUri(), true)
|
||||||
|
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 speedupDurationModifier = speedChoices[arguments.speedIndex].toFloat()
|
||||||
|
|
||||||
|
val newTotalDuration = (it - (arguments.videoStart ?: 0f) - (it - (arguments.videoEnd ?: it)))/speedupDurationModifier
|
||||||
|
timeInMilliseconds / (10*newTotalDuration)
|
||||||
|
}
|
||||||
|
completePercentage?.let {
|
||||||
|
val rounded: Int = it.roundToInt()
|
||||||
|
videoEncodeProgress(originalUri, rounded, false, null, false)
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Encoding video: %$completePercentage.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerNewFFmpegSession(originalUri, session.sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stabilizationFirstPass(){
|
||||||
|
|
||||||
|
val shakeResultsFile = File.createTempFile("temp_shake_results", ".trf", context.cacheDir)
|
||||||
|
trackTempFile(shakeResultsFile)
|
||||||
|
val shakeResultsFileUri = shakeResultsFile.toUri()
|
||||||
|
val shakeResultsFileSafeUri = context.ffmpegCompliantUri(shakeResultsFileUri).removePrefix("file://")
|
||||||
|
|
||||||
|
val inputSafeUri: String = context.ffmpegCompliantUri(originalUri)
|
||||||
|
|
||||||
|
// Map chosen "stabilization force" to shakiness, from 3 to 10
|
||||||
|
val shakiness = (0f..100f).convert(arguments.videoStabilize, 3f..10f).roundToInt()
|
||||||
|
|
||||||
|
val analyzeVideoCommandList = listOf(
|
||||||
|
"-y", "-i", inputSafeUri,
|
||||||
|
"-vf", "vidstabdetect=shakiness=$shakiness:accuracy=15:result=$shakeResultsFileSafeUri",
|
||||||
|
"-f", "null", "-"
|
||||||
|
).toTypedArray()
|
||||||
|
|
||||||
|
val session: FFmpegSession =
|
||||||
|
FFmpegKit.executeWithArgumentsAsync(analyzeVideoCommandList,
|
||||||
|
{ firstPass ->
|
||||||
|
if (ReturnCode.isSuccess(firstPass.returnCode)) {
|
||||||
|
// Map chosen "stabilization force" to shakiness, from 8 to 40
|
||||||
|
val smoothing = (0f..100f).convert(arguments.videoStabilize, 8f..40f).roundToInt()
|
||||||
|
|
||||||
|
val stabilizeVideoCommand =
|
||||||
|
"vidstabtransform=smoothing=$smoothing:input=${context.ffmpegCompliantUri(shakeResultsFileUri).removePrefix("file://")}"
|
||||||
|
secondPass(stabilizeVideoCommand)
|
||||||
|
} else {
|
||||||
|
Log.e(
|
||||||
|
"PostCreationActivityEncoding",
|
||||||
|
"Video stabilization first pass failed!"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ log -> Log.d("PostCreationActivityEncoding", log.message) },
|
||||||
|
{ statistics: Statistics? ->
|
||||||
|
|
||||||
|
val timeInMilliseconds: Int? = statistics?.time
|
||||||
|
timeInMilliseconds?.let {
|
||||||
|
if (timeInMilliseconds > 0) {
|
||||||
|
val completePercentage = totalVideoDuration?.let {
|
||||||
|
// At this stage, we didn't change speed or start/end of the video
|
||||||
|
timeInMilliseconds / (10 * it)
|
||||||
|
}
|
||||||
|
completePercentage?.let {
|
||||||
|
val rounded: Int = it.roundToInt()
|
||||||
|
videoEncodeProgress(originalUri, rounded, true, null, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Stabilization pass: %$completePercentage.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
registerNewFFmpegSession(originalUri, session.sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(arguments.videoStabilize > 0.01f) {
|
||||||
|
// Stabilization was requested: we need an additional first pass to get stabilization data
|
||||||
|
stabilizationFirstPass()
|
||||||
|
} else {
|
||||||
|
// Immediately call the second pass, no stabilization needed
|
||||||
|
secondPass()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelEncoding(){
|
||||||
|
FFmpegKit.cancel()
|
||||||
|
}
|
||||||
|
fun cancelEncoding(sessionId: Long){
|
||||||
|
FFmpegKit.cancel(sessionId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package org.pixeldroid.app.postCreation.photoEdit.cropper
|
package org.pixeldroid.media_editor.photoEdit.cropper
|
||||||
|
|
||||||
// Simplified version of https://github.com/ArthurHub/Android-Image-Cropper , which is
|
// Simplified version of https://github.com/ArthurHub/Android-Image-Cropper , which is
|
||||||
// licensed under the Apache License, Version 2.0. The modifications made to it for PixelDroid
|
// licensed under the Apache License, Version 2.0. The modifications made to it for PixelDroid
|
||||||
|
@ -18,8 +18,8 @@ import com.bumptech.glide.load.DataSource
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
import com.bumptech.glide.request.RequestListener
|
import com.bumptech.glide.request.RequestListener
|
||||||
import com.bumptech.glide.request.target.Target
|
import com.bumptech.glide.request.target.Target
|
||||||
import org.pixeldroid.app.databinding.CropImageViewBinding
|
import org.pixeldroid.media_editor.databinding.CropImageViewBinding
|
||||||
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity
|
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
|
||||||
|
|
||||||
|
|
||||||
/** Custom view that provides cropping capabilities to an image. */
|
/** Custom view that provides cropping capabilities to an image. */
|
|
@ -1,4 +1,4 @@
|
||||||
package org.pixeldroid.app.postCreation.photoEdit.cropper
|
package org.pixeldroid.media_editor.photoEdit.cropper
|
||||||
|
|
||||||
// Simplified version of https://github.com/ArthurHub/Android-Image-Cropper , which is
|
// Simplified version of https://github.com/ArthurHub/Android-Image-Cropper , which is
|
||||||
// licensed under the Apache License, Version 2.0. The modifications made to it for PixelDroid
|
// licensed under the Apache License, Version 2.0. The modifications made to it for PixelDroid
|
||||||
|
@ -16,7 +16,7 @@ import android.util.AttributeSet
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity.RelativeCropPosition
|
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity.RelativeCropPosition
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package org.pixeldroid.app.postCreation.photoEdit.cropper
|
package org.pixeldroid.media_editor.photoEdit.cropper
|
||||||
|
|
||||||
// Simplified version of https://github.com/ArthurHub/Android-Image-Cropper , which is
|
// Simplified version of https://github.com/ArthurHub/Android-Image-Cropper , which is
|
||||||
// licensed under the Apache License, Version 2.0. The modifications made to it for PixelDroid
|
// licensed under the Apache License, Version 2.0. The modifications made to it for PixelDroid
|
|
@ -1,4 +1,4 @@
|
||||||
package org.pixeldroid.app.postCreation.photoEdit.cropper
|
package org.pixeldroid.media_editor.photoEdit.cropper
|
||||||
|
|
||||||
// Simplified version of https://github.com/ArthurHub/Android-Image-Cropper , which is
|
// Simplified version of https://github.com/ArthurHub/Android-Image-Cropper , which is
|
||||||
// licensed under the Apache License, Version 2.0. The modifications made to it for PixelDroid
|
// licensed under the Apache License, Version 2.0. The modifications made to it for PixelDroid
|
|
@ -0,0 +1,12 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="36dp"
|
||||||
|
android:height="36dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
|
||||||
|
<path android:fillColor="?attr/colorOnPrimaryContainer" android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorPrimaryContainer"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
|
||||||
|
</vector>
|
|
@ -5,7 +5,7 @@
|
||||||
android:id="@+id/coordinator_edit"
|
android:id="@+id/coordinator_edit"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".postCreation.photoEdit.PhotoEditActivity">
|
tools:context="org.pixeldroid.media_editor.photoEdit.PhotoEditActivity">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
|
@ -19,7 +19,7 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<org.pixeldroid.app.postCreation.photoEdit.cropper.CropImageView
|
<org.pixeldroid.media_editor.photoEdit.cropper.CropImageView
|
||||||
android:id="@+id/cropImageView"
|
android:id="@+id/cropImageView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<org.pixeldroid.app.postCreation.photoEdit.cropper.CropImageView
|
<org.pixeldroid.media_editor.photoEdit.cropper.CropImageView
|
||||||
android:id="@+id/cropImageView"
|
android:id="@+id/cropImageView"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
|
@ -6,7 +6,7 @@
|
||||||
android:layout_width="match_parent">
|
android:layout_width="match_parent">
|
||||||
|
|
||||||
|
|
||||||
<org.pixeldroid.app.postCreation.photoEdit.cropper.CropOverlayView
|
<org.pixeldroid.media_editor.photoEdit.cropper.CropOverlayView
|
||||||
android:id="@+id/CropOverlayView"
|
android:id="@+id/CropOverlayView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
|
@ -4,7 +4,7 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".postCreation.photoEdit.EditImageFragment">
|
tools:context="org.pixeldroid.media_editor.photoEdit.EditImageFragment">
|
||||||
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
|
@ -3,7 +3,7 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".postCreation.photoEdit.FilterListFragment">
|
tools:context="org.pixeldroid.media_editor.photoEdit.FilterListFragment">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler_view"
|
android:id="@+id/recycler_view"
|
|
@ -0,0 +1,16 @@
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.PixelDroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
|
<!-- Primary brand color. -->
|
||||||
|
<item name="colorPrimary">@color/purple_200</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||||
|
<item name="colorOnPrimary">@color/black</item>
|
||||||
|
<!-- Secondary brand color. -->
|
||||||
|
<item name="colorSecondary">@color/teal_200</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||||
|
<item name="colorOnSecondary">@color/black</item>
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
|
@ -0,0 +1,36 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Media Editor</string>
|
||||||
|
<!-- Post editing -->
|
||||||
|
<string name="lbl_brightness">Brightness</string>
|
||||||
|
<string name="lbl_contrast">Contrast</string>
|
||||||
|
<string name="lbl_saturation">Saturation</string>
|
||||||
|
<string name="tab_filters">Filters</string>
|
||||||
|
<string name="edit">Edit</string>
|
||||||
|
<string name="filter_thumbnail">Thumbnail of filter</string>
|
||||||
|
<string name="normal_filter">Normal</string>
|
||||||
|
<string name="busy_dialog_text">Still processing image, wait for that to finish first!</string>
|
||||||
|
<string name="busy_dialog_ok_button">OK, wait for that.</string>
|
||||||
|
<string name="crop_result_error">"Couldn't retrieve image after crop"</string>
|
||||||
|
<string name="image_preview">Preview of the image being edited</string>
|
||||||
|
<string name="crop_button">Button to crop or rotate the image</string>
|
||||||
|
<string name="save_before_returning">Save your edits?</string>
|
||||||
|
<string name="no_cancel_edit">No, cancel edit</string>
|
||||||
|
<string name="error_editing">Error while editing</string>
|
||||||
|
<string name="toolbar_title_edit">Edit</string>
|
||||||
|
<string name="stabilize_video">Stabilize video</string>
|
||||||
|
<string name="stabilize_video_intensity">Change intensity of stabilization</string>
|
||||||
|
<string name="save_image_failed">Unable to save image</string>
|
||||||
|
<string name="save_image_success">Image successfully saved</string>
|
||||||
|
<string name="mute_video">Mute video</string>
|
||||||
|
<string name="save_crop">Save crop</string>
|
||||||
|
<string name="video_crop">Crop video</string>
|
||||||
|
<string name="select_video_range">Select what to keep of the video</string>
|
||||||
|
<string name="video_speed">Change video speed</string>
|
||||||
|
<string name="crop_saved">Crop saved</string>
|
||||||
|
<string name="stabilization_saved">Stabilization saved</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="permission_denied">Permission denied</string>
|
||||||
|
|
||||||
|
</resources>
|
|
@ -0,0 +1,16 @@
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.PixelDroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
|
<!-- Primary brand color. -->
|
||||||
|
<item name="colorPrimary">@color/purple_500</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||||
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
|
<!-- Secondary brand color. -->
|
||||||
|
<item name="colorSecondary">@color/teal_200</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||||
|
<item name="colorOnSecondary">@color/black</item>
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
|
@ -0,0 +1,17 @@
|
||||||
|
package org.pixeldroid.media_editor
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1 @@
|
||||||
Subproject commit 961934d4c4ff8127e89a16ec1169b5bff7136820
|
Subproject commit 6b5fbf81c5a52a97468d906c561887ab560422ff
|
|
@ -2,3 +2,4 @@ rootProject.name='PixelDroid'
|
||||||
include ':app'
|
include ':app'
|
||||||
include ':scrambler'
|
include ':scrambler'
|
||||||
project(':scrambler').projectDir = new File(rootDir, 'scrambler/scrambler/')
|
project(':scrambler').projectDir = new File(rootDir, 'scrambler/scrambler/')
|
||||||
|
include ':mediaEditor'
|
Loading…
Reference in New Issue