diff --git a/app/build.gradle b/app/build.gradle index 2a42b91e..9e5d3e18 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fa34b884..aa21b0cf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,9 +31,6 @@ android:name=".posts.AlbumActivity" android:exported="false" android:theme="@style/AppTheme.ActionBar.Transparent"/> - - + + - 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() - } - } - } - } - } - } - } } } } @@ -369,7 +334,7 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() { private val editResultContract: ActivityResultLauncher = 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){ @@ -377,207 +342,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 = if(videoStart != null) listOf("-ss", "${videoStart/speed.toFloat()}") else listOf(null, null) - - val endString: List = 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 = 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 = 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) diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt index 6b2713c3..ebd120ec 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt @@ -13,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 @@ -26,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 @@ -63,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> by lazy { @@ -96,9 +99,8 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null val uiState: StateFlow = _uiState - // Map photoData indexes to FFmpeg Session IDs - private val sessionMap: MutableMap = mutableMapOf() + private val sessionMap: MutableMap = mutableMapOf() // Keep track of temporary files to delete them (avoids filling cache super fast with videos) private val tempFiles: java.util.ArrayList = java.util.ArrayList() @@ -108,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> = photoData /** @@ -209,9 +199,17 @@ 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)) - photoData.value = photoData.value + 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) { @@ -415,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) + videoEncodingArguments?.let { + videoEncodeStabilizationFirstPass = videoEncodingArguments.videoStabilize > 0.01f + videoEncodeProgress = 0 - videoEncodeStabilizationFirstPass = 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(), + 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 @@ -465,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 } diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/CarouselItem.kt b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/CarouselItem.kt index 6adb2b83..1b48ff95 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/CarouselItem.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/CarouselItem.kt @@ -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, ) \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt index 97b9f89c..7bedd698 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt @@ -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) { - 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) + private fun updateProgress(){ - } else { - binding.encodeInfoText.setText(R.string.encode_success) - binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.check_circle_24), + val currentItem = data?.getOrNull(currentPosition) + + currentItem?.let { + if(it.encodeError){ + binding.encodeInfoCard.visibility = VISIBLE + binding.encodeProgress.visibility = GONE + 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 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 } } + } /** diff --git a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt index 190361d3..621ae44e 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt @@ -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.convert(number: Float, target: ClosedRange): 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) diff --git a/app/src/main/res/drawable/note.xml b/app/src/main/res/drawable/note.xml index ea87af28..ee2a4649 100644 --- a/app/src/main/res/drawable/note.xml +++ b/app/src/main/res/drawable/note.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8d1c4d3e..8e612a9a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -91,9 +91,6 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Add Account Add another Pixelfed Account - Permission denied - Unable to save image - Image successfully saved "Description must contain %d character at most." "Description must contain %d characters at most." @@ -117,24 +114,6 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" "The server you are using doesn't support video uploads, you might not be able to upload videos included in this post" Error code returned by server: %1$d - - - Brightness - Contrast - Saturation - Filters - Edit - Thumbnail of filter - Normal - Still processing image, wait for that to finish first! - OK, wait for that. - "Couldn't retrieve image after crop" - Preview of the image being edited - Button to crop or rotate the image - Save your edits? - No, cancel edit - Error while editing - Capture Switch camera @@ -243,7 +222,6 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Report @%1$s\'s post Post reported Could not send report - Edit Profile picture Open drawer menu DISCOVER @@ -274,23 +252,10 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Camera permission not granted, grant the permission in settings if you want to let PixelDroid use the camera Storage permission not granted, grant the permission in settings if you want to let PixelDroid show the thumbnail Play video - Video editing is not yet supported - Reel showing thumbnails of the video you are editing - RESET - SAVE Error encoding Encode success! Encode %1$d%% - Stabilize video - Change intensity of stabilization - Stabilization saved Analysis for stabilization %1$d%% - Select what to keep of the video - Mute video - Change video speed - Crop video - Save crop - Crop saved One or more videos are still encoding. Wait for them to finish before uploading Create new post New post diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 19db0ece..21197758 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -83,6 +83,14 @@ + + + + + + + + @@ -156,6 +164,14 @@ + + + + + + + + @@ -175,6 +191,14 @@ + + + + + + + + @@ -1007,6 +1031,14 @@ + + + + + + + + @@ -1042,6 +1074,11 @@ + + + + + @@ -6431,6 +6468,14 @@ + + + + + + + + @@ -6450,6 +6495,19 @@ + + + + + + + + + + + + + @@ -6501,6 +6559,11 @@ + + + + + diff --git a/mediaEditor/.gitignore b/mediaEditor/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/mediaEditor/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/mediaEditor/build.gradle b/mediaEditor/build.gradle new file mode 100644 index 00000000..e415469e --- /dev/null +++ b/mediaEditor/build.gradle @@ -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" + } + +} \ No newline at end of file diff --git a/mediaEditor/proguard-rules.pro b/mediaEditor/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/mediaEditor/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/mediaEditor/src/androidTest/java/org/pixeldroid/media_editor/ExampleInstrumentedTest.kt b/mediaEditor/src/androidTest/java/org/pixeldroid/media_editor/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..89737fff --- /dev/null +++ b/mediaEditor/src/androidTest/java/org/pixeldroid/media_editor/ExampleInstrumentedTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/mediaEditor/src/main/AndroidManifest.xml b/mediaEditor/src/main/AndroidManifest.xml new file mode 100644 index 00000000..69fc4129 --- /dev/null +++ b/mediaEditor/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/EditImageFragment.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/EditImageFragment.kt similarity index 88% rename from app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/EditImageFragment.kt rename to mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/EditImageFragment.kt index e09bf2bb..7777c928 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/EditImageFragment.kt +++ b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/EditImageFragment.kt @@ -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) } } diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/FilterListFragment.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/FilterListFragment.kt similarity index 92% rename from app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/FilterListFragment.kt rename to mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/FilterListFragment.kt index 9e139ba5..dcb3a589 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/FilterListFragment.kt +++ b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/FilterListFragment.kt @@ -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)) diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/PhotoEditActivity.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/PhotoEditActivity.kt similarity index 96% rename from app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/PhotoEditActivity.kt rename to mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/PhotoEditActivity.kt index 66dbebaf..37e30302 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/PhotoEditActivity.kt +++ b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/PhotoEditActivity.kt @@ -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) diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/ThumbnailAdapter.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/ThumbnailAdapter.kt similarity index 90% rename from app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/ThumbnailAdapter.kt rename to mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/ThumbnailAdapter.kt index bad3c242..95a3e989 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/ThumbnailAdapter.kt +++ b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/ThumbnailAdapter.kt @@ -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, - private val listener: FilterListFragment): RecyclerView.Adapter() { + private val listener: FilterListFragment +): RecyclerView.Adapter() { private var selectedIndex = 0 diff --git a/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/Utils.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/Utils.kt new file mode 100644 index 00000000..151b5e33 --- /dev/null +++ b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/Utils.kt @@ -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.convert(number: Float, target: ClosedRange): 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 } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/VideoEditActivity.kt similarity index 57% rename from app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt rename to mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/VideoEditActivity.kt index 43060fed..d93fe9de 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt +++ b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/VideoEditActivity.kt @@ -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(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 = 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 = if(arguments.videoStart != null) listOf("-ss", "${arguments.videoStart/speed.toFloat()}") else listOf(null, null) + + val endString: List = 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 = 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 = 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) + } } } \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropImageView.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropImageView.kt similarity index 95% rename from app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropImageView.kt rename to mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropImageView.kt index b16239b0..46567226 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropImageView.kt +++ b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropImageView.kt @@ -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. */ diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropOverlayView.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropOverlayView.kt similarity index 99% rename from app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropOverlayView.kt rename to mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropOverlayView.kt index 72955ab7..152aa7c0 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropOverlayView.kt +++ b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropOverlayView.kt @@ -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 diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropWindowHandler.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropWindowHandler.kt similarity index 99% rename from app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropWindowHandler.kt rename to mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropWindowHandler.kt index fa4d00c1..fb77634e 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropWindowHandler.kt +++ b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropWindowHandler.kt @@ -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 diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropWindowMoveHandler.kt b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropWindowMoveHandler.kt similarity index 99% rename from app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropWindowMoveHandler.kt rename to mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropWindowMoveHandler.kt index f27eada6..920d7774 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/cropper/CropWindowMoveHandler.kt +++ b/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/cropper/CropWindowMoveHandler.kt @@ -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 diff --git a/mediaEditor/src/main/res/drawable/check_circle_24.xml b/mediaEditor/src/main/res/drawable/check_circle_24.xml new file mode 100644 index 00000000..86bf1fbe --- /dev/null +++ b/mediaEditor/src/main/res/drawable/check_circle_24.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/drawable/double_circle.xml b/mediaEditor/src/main/res/drawable/double_circle.xml similarity index 100% rename from app/src/main/res/drawable/double_circle.xml rename to mediaEditor/src/main/res/drawable/double_circle.xml diff --git a/app/src/main/res/drawable/ic_crop_black_24dp.xml b/mediaEditor/src/main/res/drawable/ic_crop_black_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_crop_black_24dp.xml rename to mediaEditor/src/main/res/drawable/ic_crop_black_24dp.xml diff --git a/app/src/main/res/drawable/ic_save_24dp.xml b/mediaEditor/src/main/res/drawable/ic_save_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_save_24dp.xml rename to mediaEditor/src/main/res/drawable/ic_save_24dp.xml diff --git a/app/src/main/res/drawable/restore_24dp.xml b/mediaEditor/src/main/res/drawable/restore_24dp.xml similarity index 100% rename from app/src/main/res/drawable/restore_24dp.xml rename to mediaEditor/src/main/res/drawable/restore_24dp.xml diff --git a/app/src/main/res/drawable/selector_mute.xml b/mediaEditor/src/main/res/drawable/selector_mute.xml similarity index 100% rename from app/src/main/res/drawable/selector_mute.xml rename to mediaEditor/src/main/res/drawable/selector_mute.xml diff --git a/app/src/main/res/drawable/speed.xml b/mediaEditor/src/main/res/drawable/speed.xml similarity index 100% rename from app/src/main/res/drawable/speed.xml rename to mediaEditor/src/main/res/drawable/speed.xml diff --git a/app/src/main/res/drawable/thumb_left.xml b/mediaEditor/src/main/res/drawable/thumb_left.xml similarity index 100% rename from app/src/main/res/drawable/thumb_left.xml rename to mediaEditor/src/main/res/drawable/thumb_left.xml diff --git a/app/src/main/res/drawable/thumb_right.xml b/mediaEditor/src/main/res/drawable/thumb_right.xml similarity index 100% rename from app/src/main/res/drawable/thumb_right.xml rename to mediaEditor/src/main/res/drawable/thumb_right.xml diff --git a/app/src/main/res/drawable/video_stable.xml b/mediaEditor/src/main/res/drawable/video_stable.xml similarity index 100% rename from app/src/main/res/drawable/video_stable.xml rename to mediaEditor/src/main/res/drawable/video_stable.xml diff --git a/app/src/main/res/drawable/volume_off.xml b/mediaEditor/src/main/res/drawable/volume_off.xml similarity index 100% rename from app/src/main/res/drawable/volume_off.xml rename to mediaEditor/src/main/res/drawable/volume_off.xml diff --git a/app/src/main/res/drawable/volume_up.xml b/mediaEditor/src/main/res/drawable/volume_up.xml similarity index 100% rename from app/src/main/res/drawable/volume_up.xml rename to mediaEditor/src/main/res/drawable/volume_up.xml diff --git a/app/src/main/res/layout/activity_photo_edit.xml b/mediaEditor/src/main/res/layout/activity_photo_edit.xml similarity index 96% rename from app/src/main/res/layout/activity_photo_edit.xml rename to mediaEditor/src/main/res/layout/activity_photo_edit.xml index 0544616a..a2e75050 100644 --- a/app/src/main/res/layout/activity_photo_edit.xml +++ b/mediaEditor/src/main/res/layout/activity_photo_edit.xml @@ -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"> - - - + tools:context="org.pixeldroid.media_editor.photoEdit.EditImageFragment"> + tools:context="org.pixeldroid.media_editor.photoEdit.FilterListFragment"> + + + \ No newline at end of file diff --git a/mediaEditor/src/main/res/values/colors.xml b/mediaEditor/src/main/res/values/colors.xml new file mode 100644 index 00000000..f8c6127d --- /dev/null +++ b/mediaEditor/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/mediaEditor/src/main/res/values/strings.xml b/mediaEditor/src/main/res/values/strings.xml new file mode 100644 index 00000000..9a2d6d5a --- /dev/null +++ b/mediaEditor/src/main/res/values/strings.xml @@ -0,0 +1,36 @@ + + Media Editor + + Brightness + Contrast + Saturation + Filters + Edit + Thumbnail of filter + Normal + Still processing image, wait for that to finish first! + OK, wait for that. + "Couldn't retrieve image after crop" + Preview of the image being edited + Button to crop or rotate the image + Save your edits? + No, cancel edit + Error while editing + Edit + Stabilize video + Change intensity of stabilization + Unable to save image + Image successfully saved + Mute video + Save crop + Crop video + Select what to keep of the video + Change video speed + Crop saved + Stabilization saved + Reel showing thumbnails of the video you are editing + RESET + SAVE + Permission denied + + \ No newline at end of file diff --git a/mediaEditor/src/main/res/values/themes.xml b/mediaEditor/src/main/res/values/themes.xml new file mode 100644 index 00000000..f394411b --- /dev/null +++ b/mediaEditor/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/mediaEditor/src/test/java/org/pixeldroid/media_editor/ExampleUnitTest.kt b/mediaEditor/src/test/java/org/pixeldroid/media_editor/ExampleUnitTest.kt new file mode 100644 index 00000000..5842564e --- /dev/null +++ b/mediaEditor/src/test/java/org/pixeldroid/media_editor/ExampleUnitTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/scrambler b/scrambler index 961934d4..6b5fbf81 160000 --- a/scrambler +++ b/scrambler @@ -1 +1 @@ -Subproject commit 961934d4c4ff8127e89a16ec1169b5bff7136820 +Subproject commit 6b5fbf81c5a52a97468d906c561887ab560422ff diff --git a/settings.gradle b/settings.gradle index 754d9b25..b5f573c9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,3 +2,4 @@ rootProject.name='PixelDroid' include ':app' include ':scrambler' project(':scrambler').projectDir = new File(rootDir, 'scrambler/scrambler/') +include ':mediaEditor' \ No newline at end of file