Put editing in a module

This commit is contained in:
Matthieu 2022-10-28 20:49:25 +02:00
parent 650e7b248b
commit 6b42677f1e
53 changed files with 754 additions and 495 deletions

View File

@ -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"

View File

@ -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"

View File

@ -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)

View File

@ -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,15 +61,20 @@ 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) {
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
@ -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
}

View File

@ -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,
)

View File

@ -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
}
}
}
/**

View File

@ -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)

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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"/>

1
mediaEditor/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

62
mediaEditor/build.gradle Normal file
View File

@ -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"
}
}

21
mediaEditor/proguard-rules.pro vendored Normal file
View File

@ -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

View File

@ -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)
}
}

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>

View File

@ -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)
}
}

View File

@ -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))

View File

@ -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)

View File

@ -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

View File

@ -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 }
}
}

View File

@ -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)
}
}
}

View File

@ -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. */

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -2,3 +2,4 @@ rootProject.name='PixelDroid'
include ':app'
include ':scrambler'
project(':scrambler').projectDir = new File(rootDir, 'scrambler/scrambler/')
include ':mediaEditor'