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.arthenica:ffmpeg-kit-min-gpl:5.1.LTS'
|
||||
|
||||
|
||||
implementation 'com.google.android.material:material:1.7.0'
|
||||
|
||||
|
@ -201,6 +199,7 @@ dependencies {
|
|||
implementation 'info.androidhive:imagefilters:1.0.7'
|
||||
implementation 'com.github.yalantis:ucrop:2.2.8-native'
|
||||
implementation project(path: ':scrambler')
|
||||
implementation project(path: ':mediaEditor')
|
||||
|
||||
implementation('com.github.bumptech.glide:glide:4.14.2') {
|
||||
exclude group: "com.android.support"
|
||||
|
|
|
@ -31,9 +31,6 @@
|
|||
android:name=".posts.AlbumActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/AppTheme.ActionBar.Transparent"/>
|
||||
<activity
|
||||
android:name=".postCreation.photoEdit.VideoEditActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".posts.MediaViewerActivity"
|
||||
|
@ -55,7 +52,10 @@
|
|||
android:name=".posts.ReportActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
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
|
||||
android:name=".postCreation.PostCreationActivity"
|
||||
android:exported="true"
|
||||
|
|
|
@ -27,41 +27,24 @@ import androidx.lifecycle.Lifecycle
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.arthenica.ffmpegkit.*
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
|
||||
import org.pixeldroid.app.postCreation.camera.CameraActivity
|
||||
import org.pixeldroid.app.postCreation.carousel.CarouselItem
|
||||
import org.pixeldroid.app.postCreation.photoEdit.PhotoEditActivity
|
||||
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity
|
||||
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.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.ffmpegCompliantUri
|
||||
import org.pixeldroid.app.utils.fileExtension
|
||||
import org.pixeldroid.app.utils.getMimeType
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
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() {
|
||||
|
||||
private var user: UserDatabaseEntity? = null
|
||||
|
@ -69,8 +52,6 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
|||
|
||||
private lateinit var binding: ActivityPostCreationBinding
|
||||
|
||||
private val resultHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper())
|
||||
|
||||
private lateinit var model: PostCreationViewModel
|
||||
|
||||
|
||||
|
@ -94,7 +75,11 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
|||
// update UI
|
||||
binding.carousel.addData(
|
||||
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
|
||||
|
||||
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()){
|
||||
result: ActivityResult? ->
|
||||
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!!)
|
||||
?: Toast.makeText(applicationContext, R.string.error_editing, Toast.LENGTH_SHORT).show()
|
||||
} 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) {
|
||||
val intent = Intent(
|
||||
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(PhotoEditActivity.PICTURE_POSITION, position)
|
||||
.putExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_URI, model.getPhotoData().value!![position].imageUri)
|
||||
.putExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_POSITION, position)
|
||||
|
||||
editResultContract.launch(intent)
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.R.attr.orientation
|
||||
import android.app.Application
|
||||
import android.content.ClipData
|
||||
import android.content.Intent
|
||||
|
@ -14,7 +13,6 @@ import androidx.core.net.toUri
|
|||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.lifecycle.*
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.arthenica.ffmpegkit.FFmpegKit
|
||||
import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException
|
||||
import com.jarsilio.android.scrambler.stripMetadata
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
|
@ -27,9 +25,8 @@ import kotlinx.coroutines.launch
|
|||
import okhttp3.MultipartBody
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.postCreation.photoEdit.PhotoEditActivity
|
||||
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity
|
||||
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity.RelativeCropPosition
|
||||
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
|
||||
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity.RelativeCropPosition
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
|
@ -64,14 +61,19 @@ data class PostCreationActivityUiState(
|
|||
val uploadErrorVisible: Boolean = false,
|
||||
val uploadErrorExplanationText: String = "",
|
||||
val uploadErrorExplanationVisible: Boolean = false,
|
||||
)
|
||||
|
||||
val newEncodingJobPosition: Int? = null,
|
||||
val newEncodingJobMuted: Boolean? = null,
|
||||
val newEncodingJobSpeedIndex: Int? = null,
|
||||
val newEncodingJobVideoStart: Float? = null,
|
||||
val newEncodingJobVideoEnd: Float? = null,
|
||||
val newEncodingJobVideoCrop: RelativeCropPosition? = null,
|
||||
val newEncodingJobStabilize: Float? = null,
|
||||
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,
|
||||
var videoEncodeComplete: Boolean = false,
|
||||
var videoEncodeError: Boolean = false,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
|
||||
// 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)
|
||||
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
|
||||
|
||||
/**
|
||||
|
@ -210,10 +199,18 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
|||
photoData.value = photoData.value?.map { it.copy(uploadId = null, progress = null) }?.toMutableList()
|
||||
}
|
||||
|
||||
fun setVideoEncodeAtPosition(position: Int, progress: Int?, stabilizationFirstPass: Boolean = false) {
|
||||
photoData.value?.set(position, photoData.value!![position].copy(videoEncodeProgress = progress, videoEncodeStabilizationFirstPass = stabilizationFirstPass))
|
||||
fun setVideoEncodeAtPosition(uri: Uri, progress: Int?, stabilizationFirstPass: Boolean = false, error: Boolean = false) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
fun setUriAtPosition(uri: Uri, position: Int) {
|
||||
photoData.value?.set(position, photoData.value!![position].copy(imageUri = uri))
|
||||
|
@ -416,37 +413,24 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
|||
if (video) {
|
||||
val modified: Boolean = data.getBooleanExtra(VideoEditActivity.MODIFIED, false)
|
||||
if(modified){
|
||||
val muted: Boolean = data.getBooleanExtra(VideoEditActivity.MUTED, false)
|
||||
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 videoEncodingArguments: VideoEditActivity.VideoEditArguments? = data.getSerializableExtra(VideoEditActivity.VIDEO_ARGUMENTS_TAG) as? VideoEditActivity.VideoEditArguments
|
||||
|
||||
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)
|
||||
|
||||
videoEncodeStabilizationFirstPass = videoStabilize > 0.01f
|
||||
videoEncodingArguments?.let {
|
||||
videoEncodeStabilizationFirstPass = videoEncodingArguments.videoStabilize > 0.01f
|
||||
videoEncodeProgress = 0
|
||||
|
||||
sessionMap[position]?.let { FFmpegKit.cancel(it) }
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
newEncodingJobPosition = position,
|
||||
newEncodingJobMuted = muted,
|
||||
newEncodingJobSpeedIndex = speedIndex,
|
||||
newEncodingJobVideoStart = videoStart,
|
||||
newEncodingJobVideoEnd = videoEnd,
|
||||
newEncodingJobVideoCrop = videoCrop,
|
||||
newEncodingJobStabilize = videoStabilize
|
||||
VideoEditActivity.startEncoding(imageUri, it,
|
||||
context = getApplication<PixelDroidApplication>(),
|
||||
registerNewFFmpegSession = ::registerNewFFmpegSession,
|
||||
trackTempFile = ::trackTempFile,
|
||||
videoEncodeProgress = ::videoEncodeProgress
|
||||
)
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
size = imageSize
|
||||
video = imageVideo
|
||||
|
@ -466,24 +450,59 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
|
|||
_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) {
|
||||
tempFiles.add(file)
|
||||
}
|
||||
|
||||
fun cancelEncode(currentPosition: Int) {
|
||||
sessionMap[currentPosition]?.let { FFmpegKit.cancel(it) }
|
||||
sessionMap[photoData.value?.getOrNull(currentPosition)?.imageUri]?.let { VideoEditActivity.cancelEncoding(it) }
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
FFmpegKit.cancel()
|
||||
VideoEditActivity.cancelEncoding()
|
||||
tempFiles.forEach {
|
||||
it.delete()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun registerNewFFmpegSession(position: Int, sessionId: Long) {
|
||||
fun registerNewFFmpegSession(position: Uri, sessionId: Long) {
|
||||
sessionMap[position] = sessionId
|
||||
}
|
||||
|
||||
|
|
|
@ -7,5 +7,7 @@ data class CarouselItem constructor(
|
|||
val caption: String? = null,
|
||||
val video: Boolean,
|
||||
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.databinding.ImageCarouselBinding
|
||||
import me.relex.circleindicator.CircleIndicator2
|
||||
import org.jetbrains.annotations.NotNull
|
||||
import org.jetbrains.annotations.Nullable
|
||||
|
||||
|
||||
class ImageCarousel(
|
||||
@NotNull context: Context,
|
||||
@Nullable private var attributeSet: AttributeSet?
|
||||
context: Context,
|
||||
private var attributeSet: AttributeSet?
|
||||
) : ConstraintLayout(context, attributeSet), OnItemClickListener {
|
||||
|
||||
private var adapter: CarouselAdapter? = null
|
||||
|
@ -91,17 +88,7 @@ class ImageCarousel(
|
|||
}
|
||||
|
||||
if (position != RecyclerView.NO_POSITION && field != position) {
|
||||
val thisProgress = data?.getOrNull(position)?.encodeProgress
|
||||
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
|
||||
}
|
||||
updateProgress()
|
||||
} else if(position == RecyclerView.NO_POSITION) binding.encodeInfoCard.visibility = GONE
|
||||
|
||||
if (position != RecyclerView.NO_POSITION && recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
|
@ -558,36 +545,42 @@ class ImageCarousel(
|
|||
|
||||
this@ImageCarousel.data = data.toMutableList()
|
||||
|
||||
updateProgress()
|
||||
initOnScrollStateChange()
|
||||
}
|
||||
showNavigationButtons = data.size != 1
|
||||
}
|
||||
|
||||
fun updateProgress(progress: Int?, position: Int, error: Boolean){
|
||||
data?.getOrNull(position)?.encodeProgress = progress
|
||||
if(currentPosition == position) {
|
||||
if (progress == null) {
|
||||
private fun updateProgress(){
|
||||
|
||||
val currentItem = data?.getOrNull(currentPosition)
|
||||
|
||||
currentItem?.let {
|
||||
if(it.encodeError){
|
||||
binding.encodeInfoCard.visibility = VISIBLE
|
||||
binding.encodeProgress.visibility = GONE
|
||||
if(error){
|
||||
binding.encodeInfoText.setText(R.string.encode_error)
|
||||
binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.error),
|
||||
null, null, null)
|
||||
|
||||
} else {
|
||||
} else if(it.encodeComplete){
|
||||
binding.encodeInfoCard.visibility = VISIBLE
|
||||
binding.encodeProgress.visibility = GONE
|
||||
binding.encodeInfoText.setText(R.string.encode_success)
|
||||
binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.check_circle_24),
|
||||
null, null, null)
|
||||
}
|
||||
} else {
|
||||
} else if(it.encodeProgress != null){
|
||||
binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
|
||||
binding.encodeProgress.visibility = VISIBLE
|
||||
binding.encodeInfoCard.visibility = VISIBLE
|
||||
binding.encodeProgress.progress = progress
|
||||
binding.encodeInfoText.text = (if(data?.getOrNull(position)?.stabilizationFirstPass == true){
|
||||
binding.encodeProgress.progress = it.encodeProgress ?: 0
|
||||
binding.encodeInfoText.text = (if(it.stabilizationFirstPass == true){
|
||||
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()
|
||||
|
||||
|
||||
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 {
|
||||
|
||||
|
@ -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
|
||||
fun Context.getColorFromAttr(@AttrRes attrColor: Int): Int = MaterialColors.getColor(this, attrColor, Color.BLACK)
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<vector android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@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>
|
||||
|
|
|
@ -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_description">Add another Pixelfed Account</string>
|
||||
<!-- 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">
|
||||
<item quantity="one">"Description must contain %d character 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="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 -->
|
||||
<string name="capture_button_alt">Capture</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="reported">Post reported</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="open_drawer_menu">Open drawer menu</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_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="video_edit_not_yet_supported">Video editing is not yet supported</string>
|
||||
<string name="thumbnail_reel_video_edit">Reel showing thumbnails of the video you are editing</string>
|
||||
<string name="reset_edit_menu">RESET</string>
|
||||
<string name="save_edit_menu">SAVE</string>
|
||||
<string name="encode_error">Error encoding</string>
|
||||
<string name="encode_success">Encode success!</string>
|
||||
<string name="encode_progress">Encode %1$d%%</string>
|
||||
<string name="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="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="new_post_shortcut_long">Create new post</string>
|
||||
<string name="new_post_shortcut_short">New post</string>
|
||||
|
|
|
@ -7,7 +7,7 @@ buildscript {
|
|||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.0.0-alpha05'
|
||||
classpath 'com.android.tools.build:gradle:8.0.0-alpha06'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
|
|
@ -83,6 +83,14 @@
|
|||
<sha256 value="2e9372ba7780ef44952adbf86b66e1f08682c1e5277c926185f6564a13799efe" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</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">
|
||||
<artifact name="annotation-1.3.0.jar">
|
||||
<sha256 value="97dc45afefe3a1e421da42b8b6e9f90491477c45fc6178203e3a5e8a05ee8553" origin="Generated by Gradle"/>
|
||||
|
@ -156,6 +164,14 @@
|
|||
<sha256 value="f3940daf193120c6413176d5f09a9847432eab2af68b1a09663f43a2621a04a8" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</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">
|
||||
<artifact name="appcompat-1.5.1.aar">
|
||||
<sha256 value="50f2c7756e28a7da648bd4551ee3b31e0b71863a6bf591f0ca978428219c5eab" origin="Generated by Gradle"/>
|
||||
|
@ -175,6 +191,14 @@
|
|||
<sha256 value="0b8d946fae07779737ed36a428adaa5df8f93f1471821a748a079b21f1f70310" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</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">
|
||||
<artifact name="appcompat-resources-1.5.1.aar">
|
||||
<sha256 value="6cfa9caae1869ffe03da9ec3ebf47ad84d70a7185ee86ec63aa8289cbb457e67" origin="Generated by Gradle"/>
|
||||
|
@ -1007,6 +1031,14 @@
|
|||
<sha256 value="9fdb7dfd08028f5994df9c8baa90871cf2b5849daad335311f761c6df8ad9674" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</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">
|
||||
<artifact name="exifinterface-1.2.0.aar">
|
||||
<sha256 value="aae68e513095e475a7670556eacba772ec2bb592d17187091578d3fef947aea7" origin="Generated by Gradle"/>
|
||||
|
@ -1042,6 +1074,11 @@
|
|||
<sha256 value="8aecb68c916c539e233a05548a8f0e5c2c6d463b0dea65048768707f38adbb01" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</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">
|
||||
<artifact name="fragment-1.3.6.aar">
|
||||
<sha256 value="12f0831b4f08092d5dda272c1923c11a022ff20ceffed3e801751e21bb8d1c1e" origin="Generated by Gradle"/>
|
||||
|
@ -6425,6 +6462,14 @@
|
|||
<sha256 value="648c5163c8a03d215ae4f4b41ad926ec3a7355f6d5dc93a13df531034bffae2f" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</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">
|
||||
<artifact name="okhttp-4.9.2.jar">
|
||||
<sha256 value="3b2ee1b768c1df28c30d2fe6b38dda4d2c519210e30ca3d27950618c563c92de" origin="Generated by Gradle"/>
|
||||
|
@ -6444,6 +6489,19 @@
|
|||
<sha256 value="64df3b9baddd00eaa4bc727641fd157c65f39465e68eaecaa5a1d0417fc2f269" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</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">
|
||||
<artifact name="okio-1.16.0.jar">
|
||||
<sha256 value="ec0484ff1903640e3845c2b10abb99eff2d32308ffe3459e5f92309a451b9c7e" origin="Generated by Gradle"/>
|
||||
|
@ -6495,6 +6553,11 @@
|
|||
<sha256 value="a4eef641be5e5463d2789c3c73601f9a99f36e1377ca007e30f0fee53a054cdc" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</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">
|
||||
<artifact name="okio-parent-1.16.0.pom">
|
||||
<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 androidx.fragment.app.Fragment
|
||||
|
@ -6,8 +6,7 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.SeekBar
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.FragmentEditImageBinding
|
||||
import org.pixeldroid.media_editor.databinding.FragmentEditImageBinding
|
||||
|
||||
class EditImageFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
||||
|
||||
|
@ -52,13 +51,13 @@ class EditImageFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
|||
var prog = progress
|
||||
|
||||
listener?.let {
|
||||
when(seekBar!!.id) {
|
||||
R.id.seekbar_brightness -> it.onBrightnessChange(progress - 100)
|
||||
R.id.seekbar_saturation -> {
|
||||
when(seekBar) {
|
||||
binding.seekbarBrightness -> it.onBrightnessChange(progress - 100)
|
||||
binding.seekbarSaturation -> {
|
||||
prog += 10
|
||||
it.onSaturationChange(.10f * prog)
|
||||
}
|
||||
R.id.seekbar_contrast -> {
|
||||
binding.seekbarContrast -> {
|
||||
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.os.Bundle
|
||||
|
@ -15,9 +15,8 @@ import com.zomato.photofilters.imageprocessors.Filter
|
|||
import com.zomato.photofilters.utils.ThumbnailItem
|
||||
import com.zomato.photofilters.utils.ThumbnailsManager
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.FragmentFilterListBinding
|
||||
import org.pixeldroid.app.utils.bitmapFromUri
|
||||
import org.pixeldroid.media_editor.R
|
||||
import org.pixeldroid.media_editor.databinding.FragmentFilterListBinding
|
||||
|
||||
class FilterListFragment : Fragment() {
|
||||
|
||||
|
@ -52,7 +51,9 @@ class FilterListFragment : Fragment() {
|
|||
private fun displayImage() {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
val tbImage: Bitmap = bitmapFromUri(requireActivity().contentResolver, PhotoEditActivity.imageUri)
|
||||
val tbImage: Bitmap = bitmapFromUri(requireActivity().contentResolver,
|
||||
PhotoEditActivity.imageUri
|
||||
)
|
||||
setupFilter(tbImage)
|
||||
|
||||
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.AlertDialog
|
||||
|
@ -14,6 +14,7 @@ import android.view.MenuItem
|
|||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
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.ContrastSubFilter
|
||||
import com.zomato.photofilters.imageprocessors.subfilters.SaturationSubfilter
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityPhotoEditBinding
|
||||
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 org.pixeldroid.media_editor.databinding.ActivityPhotoEditBinding
|
||||
import org.pixeldroid.media_editor.R
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
@ -49,7 +46,7 @@ private val REQUIRED_PERMISSIONS = arrayOf(
|
|||
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
)
|
||||
|
||||
class PhotoEditActivity : BaseThemedWithBarActivity() {
|
||||
class PhotoEditActivity : AppCompatActivity() {
|
||||
|
||||
var saving: Boolean = false
|
||||
private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888
|
||||
|
@ -78,8 +75,8 @@ class PhotoEditActivity : BaseThemedWithBarActivity() {
|
|||
}
|
||||
|
||||
companion object{
|
||||
internal const val PICTURE_URI = "picture_uri"
|
||||
internal const val PICTURE_POSITION = "picture_position"
|
||||
const val PICTURE_URI = "picture_uri"
|
||||
const val PICTURE_POSITION = "picture_position"
|
||||
|
||||
private var executor: ExecutorService = newSingleThreadExecutor()
|
||||
private var future: Future<*>? = null
|
||||
|
@ -179,6 +176,7 @@ class PhotoEditActivity : BaseThemedWithBarActivity() {
|
|||
saving = false
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onBackPressed() {
|
||||
if (noEdits()) super.onBackPressed()
|
||||
else {
|
||||
|
@ -369,7 +367,7 @@ class PhotoEditActivity : BaseThemedWithBarActivity() {
|
|||
}
|
||||
|
||||
private fun sendBackImage(file: String) {
|
||||
val intent = Intent(this, PostCreationActivity::class.java)
|
||||
val intent = Intent()
|
||||
.apply {
|
||||
putExtra(PICTURE_URI, file)
|
||||
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.view.LayoutInflater
|
||||
|
@ -7,13 +7,13 @@ import android.widget.ImageView
|
|||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.zomato.photofilters.utils.ThumbnailItem
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ThumbnailListItemBinding
|
||||
import org.pixeldroid.app.utils.getColorFromAttr
|
||||
import org.pixeldroid.media_editor.R
|
||||
import org.pixeldroid.media_editor.databinding.ThumbnailListItemBinding
|
||||
|
||||
class ThumbnailAdapter (private val context: Context,
|
||||
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
|
||||
|
|
@ -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.AlertDialog
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
|
@ -18,6 +20,7 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.os.HandlerCompat
|
||||
import androidx.core.view.isVisible
|
||||
|
@ -27,23 +30,24 @@ import androidx.media2.common.UriMediaItem
|
|||
import androidx.media2.player.MediaPlayer
|
||||
import com.arthenica.ffmpegkit.FFmpegKit
|
||||
import com.arthenica.ffmpegkit.FFmpegKitConfig
|
||||
import com.arthenica.ffmpegkit.FFmpegSession
|
||||
import com.arthenica.ffmpegkit.FFprobeKit
|
||||
import com.arthenica.ffmpegkit.MediaInformation
|
||||
import com.arthenica.ffmpegkit.ReturnCode
|
||||
import com.arthenica.ffmpegkit.Statistics
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.slider.RangeSlider
|
||||
import com.google.android.material.slider.Slider
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityVideoEditBinding
|
||||
import org.pixeldroid.app.postCreation.PostCreationActivity
|
||||
import org.pixeldroid.app.postCreation.carousel.dpToPx
|
||||
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
|
||||
import org.pixeldroid.app.utils.ffmpegCompliantUri
|
||||
import org.pixeldroid.media_editor.R
|
||||
import org.pixeldroid.media_editor.databinding.ActivityVideoEditBinding
|
||||
import java.io.File
|
||||
import java.io.Serializable
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class VideoEditActivity : BaseThemedWithBarActivity() {
|
||||
const val TAG = "VideoEditActivity"
|
||||
|
||||
class VideoEditActivity : AppCompatActivity() {
|
||||
|
||||
data class RelativeCropPosition(
|
||||
// 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 var videoPosition: Int = -1
|
||||
|
||||
|
@ -117,11 +131,11 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
|||
|
||||
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)
|
||||
|
||||
val inputVideoPath = ffmpegCompliantUri(uri)
|
||||
val inputVideoPath = ffmpegCompliantUri(videoUri)
|
||||
val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(inputVideoPath).mediaInformation
|
||||
|
||||
//Duration in seconds, or null
|
||||
|
@ -132,7 +146,7 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
|||
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()
|
||||
.putString(MediaMetadata.METADATA_KEY_TITLE, "")
|
||||
.build()
|
||||
|
@ -165,7 +179,7 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
|||
}
|
||||
|
||||
binding.cropper.setOnClickListener {
|
||||
showCropInterface(show = true, uri = uri)
|
||||
showCropInterface(show = true, uri = videoUri)
|
||||
}
|
||||
|
||||
binding.saveCropButton.setOnClickListener {
|
||||
|
@ -270,13 +284,13 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
|||
val thumbInterval: Float? = duration?.div(7)
|
||||
|
||||
thumbInterval?.let {
|
||||
thumbnail(uri, resultHandler, binding.thumbnail1, it)
|
||||
thumbnail(uri, resultHandler, binding.thumbnail2, it.times(2))
|
||||
thumbnail(uri, resultHandler, binding.thumbnail3, it.times(3))
|
||||
thumbnail(uri, resultHandler, binding.thumbnail4, it.times(4))
|
||||
thumbnail(uri, resultHandler, binding.thumbnail5, it.times(5))
|
||||
thumbnail(uri, resultHandler, binding.thumbnail6, it.times(6))
|
||||
thumbnail(uri, resultHandler, binding.thumbnail7, it.times(7))
|
||||
thumbnail(videoUri, resultHandler, binding.thumbnail1, it)
|
||||
thumbnail(videoUri, resultHandler, binding.thumbnail2, it.times(2))
|
||||
thumbnail(videoUri, resultHandler, binding.thumbnail3, it.times(3))
|
||||
thumbnail(videoUri, resultHandler, binding.thumbnail4, it.times(4))
|
||||
thumbnail(videoUri, resultHandler, binding.thumbnail5, it.times(5))
|
||||
thumbnail(videoUri, resultHandler, binding.thumbnail6, it.times(6))
|
||||
thumbnail(videoUri, resultHandler, binding.thumbnail7, it.times(7))
|
||||
}
|
||||
|
||||
resetControls()
|
||||
|
@ -369,19 +383,20 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
|||
|
||||
private fun returnWithValues() {
|
||||
//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 {
|
||||
putExtra(PhotoEditActivity.PICTURE_POSITION, videoPosition)
|
||||
putExtra(MUTED, binding.muter.isSelected)
|
||||
putExtra(SPEED, speed)
|
||||
putExtra(VIDEO_ARGUMENTS_TAG, VideoEditArguments(
|
||||
binding.muter.isSelected, binding.videoRangeSeekBar.values.first(),
|
||||
binding.videoRangeSeekBar.values[2],
|
||||
speed,
|
||||
cropRelativeDimensions,
|
||||
stabilization
|
||||
)
|
||||
)
|
||||
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)
|
||||
}
|
||||
|
||||
setResult(Activity.RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
|
@ -451,15 +466,182 @@ class VideoEditActivity : BaseThemedWithBarActivity() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val VIDEO_TAG = "VideoEditTag"
|
||||
const val MUTED = "VideoEditMutedTag"
|
||||
const val SPEED = "VideoEditSpeedTag"
|
||||
const val VIDEO_ARGUMENTS_TAG = "org.pixeldroid.media_editor.VideoEditTag"
|
||||
// List of choices of speeds
|
||||
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"
|
||||
|
||||
/**
|
||||
* @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
|
||||
// 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.request.RequestListener
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import org.pixeldroid.app.databinding.CropImageViewBinding
|
||||
import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity
|
||||
import org.pixeldroid.media_editor.databinding.CropImageViewBinding
|
||||
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
|
||||
|
||||
|
||||
/** 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
|
||||
// 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.view.MotionEvent
|
||||
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.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
|
||||
// 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
|
||||
// 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:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".postCreation.photoEdit.PhotoEditActivity">
|
||||
tools:context="org.pixeldroid.media_editor.photoEdit.PhotoEditActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
|
@ -19,7 +19,7 @@
|
|||
app:layout_constraintStart_toStartOf="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:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
|
@ -1,5 +1,5 @@
|
|||
<?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"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
|
@ -6,7 +6,7 @@
|
|||
android:layout_width="match_parent">
|
||||
|
||||
|
||||
<org.pixeldroid.app.postCreation.photoEdit.cropper.CropOverlayView
|
||||
<org.pixeldroid.media_editor.photoEdit.cropper.CropOverlayView
|
||||
android:id="@+id/CropOverlayView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
|
@ -4,7 +4,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".postCreation.photoEdit.EditImageFragment">
|
||||
tools:context="org.pixeldroid.media_editor.photoEdit.EditImageFragment">
|
||||
|
||||
|
||||
<TextView
|
|
@ -3,7 +3,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".postCreation.photoEdit.FilterListFragment">
|
||||
tools:context="org.pixeldroid.media_editor.photoEdit.FilterListFragment">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
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 ':scrambler'
|
||||
project(':scrambler').projectDir = new File(rootDir, 'scrambler/scrambler/')
|
||||
include ':mediaEditor'
|
Loading…
Reference in New Issue