diff --git a/README.md b/README.md index 4a665e1..b3d923b 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Lassi is simplest way to pick media (either image, video, audio or doc) * Enable/disable camera from LassiOption * You can open System Default view for file selection by using MediaType.FILE_TYPE_WITH_SYSTEM_VIEW * Photo Picker feature integration +* Group picked album images with camera capture for editing # Usage @@ -65,7 +66,7 @@ Lassi is simplest way to pick media (either image, video, audio or doc) ```kotlin val intent = Lassi(this) - .with(LassiOption.CAMERA_AND_GALLERY) // choose Option CAMERA, GALLERY or CAMERA_AND_GALLERY + .with(LassiOption.CAMERA_AND_GALLERY) // choose Option CAMERA, GALLERY, CAMERA_AND_GALLERY or PICKER .setMaxCount(5) .setGridSize(3) .setMediaType(MediaType.VIDEO) // MediaType : VIDEO IMAGE, AUDIO OR DOC diff --git a/app/src/main/java/com/lassi/app/MainActivity.kt b/app/src/main/java/com/lassi/app/MainActivity.kt index d659fde..e226dac 100644 --- a/app/src/main/java/com/lassi/app/MainActivity.kt +++ b/app/src/main/java/com/lassi/app/MainActivity.kt @@ -240,7 +240,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { } R.id.btnPhotoVideoPicker -> { - val intent = lassi.with(LassiOption.CAMERA_AND_GALLERY).setMaxCount(4) + val intent = lassi.with(LassiOption.PICKER).setMaxCount(4) .setMediaType(MediaType.PHOTO_VIDEO_PICKER) .setStatusBarColor(R.color.colorPrimaryDark) .setToolbarColor(R.color.colorPrimary) @@ -253,7 +253,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { } R.id.btnPhotoPicker -> { - val intent = lassi.with(LassiOption.CAMERA_AND_GALLERY).setMaxCount(4) + val intent = lassi.with(LassiOption.PICKER).setMaxCount(4) .setAscSort(SortingOption.ASCENDING).setGridSize(2) .setMediaType(MediaType.PHOTO_PICKER) .setPlaceHolder(R.drawable.ic_image_placeholder) @@ -273,7 +273,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { } R.id.btnVideoMediaPicker -> { - val intent = lassi.with(LassiOption.CAMERA_AND_GALLERY).setMaxCount(4) + val intent = lassi.with(LassiOption.PICKER).setMaxCount(4) .setMediaType(MediaType.VIDEO_PICKER) .setStatusBarColor(R.color.colorPrimaryDark) .setToolbarColor(R.color.colorPrimary) diff --git a/lassi/src/main/java/com/lassi/domain/media/LassiConfig.kt b/lassi/src/main/java/com/lassi/domain/media/LassiConfig.kt index 3d1eeef..1e6f2f4 100644 --- a/lassi/src/main/java/com/lassi/domain/media/LassiConfig.kt +++ b/lassi/src/main/java/com/lassi/domain/media/LassiConfig.kt @@ -31,7 +31,7 @@ data class LassiConfig( var maxCount: Int = KeyUtils.DEFAULT_MEDIA_COUNT, var ascFlag: Int = KeyUtils.DEFAULT_ORDER, var gridSize: Int = KeyUtils.DEFAULT_GRID_SIZE, - var lassiOption: LassiOption = LassiOption.CAMERA_AND_GALLERY, + var lassiOption: LassiOption = LassiOption.PICKER, var minTime: Long = KeyUtils.DEFAULT_DURATION, var maxTime: Long = KeyUtils.DEFAULT_DURATION, var cropType: CropImageView.CropShape = CropImageView.CropShape.RECTANGLE, diff --git a/lassi/src/main/java/com/lassi/domain/media/LassiOption.kt b/lassi/src/main/java/com/lassi/domain/media/LassiOption.kt index 8e13392..16e3cdb 100644 --- a/lassi/src/main/java/com/lassi/domain/media/LassiOption.kt +++ b/lassi/src/main/java/com/lassi/domain/media/LassiOption.kt @@ -3,5 +3,6 @@ package com.lassi.domain.media enum class LassiOption { CAMERA, GALLERY, - CAMERA_AND_GALLERY + CAMERA_AND_GALLERY, + PICKER } \ No newline at end of file diff --git a/lassi/src/main/java/com/lassi/presentation/builder/Lassi.kt b/lassi/src/main/java/com/lassi/presentation/builder/Lassi.kt index 23821c4..2b04031 100644 --- a/lassi/src/main/java/com/lassi/presentation/builder/Lassi.kt +++ b/lassi/src/main/java/com/lassi/presentation/builder/Lassi.kt @@ -197,6 +197,9 @@ class Lassi(private val context: Context) { * Allow Media picket to capture/record from camera while multiple media selection */ fun with(lassiOption: LassiOption): Lassi { + // Reset config to defaults for every new build chain to avoid leaking + // previous session state (e.g., isCrop from disableCrop()). + lassiConfig = LassiConfig() lassiConfig.lassiOption = lassiOption return this } diff --git a/lassi/src/main/java/com/lassi/presentation/camera/CameraFragment.kt b/lassi/src/main/java/com/lassi/presentation/camera/CameraFragment.kt index 26ee293..9a81ee3 100644 --- a/lassi/src/main/java/com/lassi/presentation/camera/CameraFragment.kt +++ b/lassi/src/main/java/com/lassi/presentation/camera/CameraFragment.kt @@ -8,6 +8,7 @@ import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.provider.Settings +import android.util.Log import android.view.LayoutInflater import android.view.View import androidx.activity.result.contract.ActivityResultContracts @@ -55,35 +56,105 @@ import java.io.File class CameraFragment : LassiBaseViewModelFragment(), View.OnClickListener { + private val TAG: String = "CameraFragment" private lateinit var cameraMode: Mode - + private val config = LassiConfig.getConfig() private val cameraViewModel by lazy { ViewModelProvider( this, SelectedMediaViewModelFactory(requireContext()) )[SelectedMediaViewModel::class.java] } - private val folderViewModel by lazy { - ViewModelProvider( - this, FolderViewModelFactory(requireContext()) - )[FolderViewModel::class.java] - } + private var currentCropIndex = 0 + private var croppedMediaList: ArrayList = ArrayList() + private var mediaList: ArrayList = ArrayList() + private var selected = + LassiConfig.getConfig().selectedMedias // this gives the gallery selected images. + private var isFromCropNext = false - private val startVideoContract = registerForActivityResult(StartVideoContract()) { miMedia -> + private val cropImage = registerForActivityResult(CropImageContract()) { miMedia -> if (LassiConfig.isSingleMediaSelection()) { + isFromCropNext = true miMedia?.let { setResultOk(arrayListOf(it)) } + return@registerForActivityResult + } + + if (miMedia != null) { + // Replace cropped into selected + selected[currentCropIndex] = miMedia + + // Compress the cropped image + val compressed = compressSingleMedia(miMedia) + selected[currentCropIndex] = compressed + + croppedMediaList.add(compressed) + } + cropNext() + } + + private fun getSelectedAndCapturedImages() { + selected = config.selectedMedias + selected.addAll(mediaList) + } + + private fun startCroppingSequence() { + if (selected.isNotEmpty()) { + currentCropIndex = 0 + croppedMediaList.clear() + val firstPath = selected[0].path + if (!firstPath.isNullOrEmpty()) { + val uri = Uri.fromFile(File(firstPath)) + croppingOptions(uri) + } else { + // skip invalid first path + cropImage.launch(null) + } } else { - LassiConfig.getConfig().selectedMedias.add(miMedia!!) - cameraViewModel.addSelectedMedia(miMedia) - folderViewModel.checkInsert() - if (LassiConfig.getConfig().lassiOption == LassiOption.CAMERA_AND_GALLERY || LassiConfig.getConfig().lassiOption == LassiOption.GALLERY) { - setResultOk(arrayListOf(miMedia)) + Log.d(TAG, "startCroppingSequence: selected is empty") + } + } + + private fun cropNext() { + currentCropIndex++ + if (currentCropIndex < selected.size) { + val path = selected[currentCropIndex].path + if (!path.isNullOrEmpty()) { + val uri = Uri.fromFile(File(path)) + croppingOptions(uri) + } else { + cropNext() } + } else { + isFromCropNext = true + setResultOk(selected) } } - private val cropImage = registerForActivityResult(CropImageContract()) { miMedia -> + private fun compressSingleMedia(miMedia: MiMedia): MiMedia { + miMedia.path?.let { path -> + val uri = Uri.fromFile(File(path)) + val compressFormat = getCompressFormatForUri(uri, requireContext()) + val newUri = writeBitmapToUri( + requireContext(), + decodeUriToBitmap(requireContext(), uri), + compressQuality = LassiConfig.getConfig().compressionRatio, + customOutputUri = null, + compressFormat = compressFormat + ) + return miMedia.copy(path = newUri.path) + } + return miMedia // return original if path is null + } + + private val folderViewModel by lazy { + ViewModelProvider( + this, FolderViewModelFactory(requireContext()) + )[FolderViewModel::class.java] + } + + private val startVideoContract = registerForActivityResult(StartVideoContract()) { miMedia -> if (LassiConfig.isSingleMediaSelection()) { + isFromCropNext = true miMedia?.let { setResultOk(arrayListOf(it)) } } else { LassiConfig.getConfig().selectedMedias.add(miMedia!!) @@ -91,7 +162,6 @@ class CameraFragment : LassiBaseViewModelFragment val config = LassiConfig.getConfig() - if (config.isCrop && config.maxCount <= 1) { - croppingOptions(uri) - } else { - val mediaList = arrayListOf(createMiMedia(uri.path)) - if (config.compressionRatio > 0) { - compressMedia(mediaList) - } else { - setResultOk(mediaList) - } + + mediaList = arrayListOf(createMiMedia(uri.path)) + croppedMediaList.addAll(config.selectedMedias + mediaList) + if (config.compressionRatio > 0 && !config.isCrop) { + compressMedia(croppedMediaList) + } else { // user has selected the crop option. + setResultOk(croppedMediaList) } }) } @@ -206,36 +274,27 @@ class CameraFragment : LassiBaseViewModelFragment - CropImageOptions( - imageSourceIncludeCamera = it, - imageSourceIncludeGallery = includeGallery, - cropShape = CropImageView.CropShape.RECTANGLE, - showCropOverlay = true, - guidelines = CropImageView.Guidelines.ON, - multiTouchEnabled = false, - outputCompressQuality = LassiConfig.getConfig().compressionRatio - ) - } - }?.let { - CropImageContractOptions( - uri = uri, - cropImageOptions = it, - ) - } + private fun croppingOptions(uri: Uri) { + val config = LassiConfig.getConfig() + val aspectX: Int = config.cropAspectRatio?.x ?: return + val aspectY: Int = config.cropAspectRatio?.y ?: return + + val cropOptions = CropImageOptions( + imageSourceIncludeCamera = false, + imageSourceIncludeGallery = false, + cropShape = config.cropType, + showCropOverlay = true, + guidelines = CropImageView.Guidelines.ON, + multiTouchEnabled = false, + aspectRatioX = aspectX, + aspectRatioY = aspectY, + fixAspectRatio = config.enableActualCircleCrop, + outputCompressQuality = config.compressionRatio ) - } + val contractOptions = CropImageContractOptions(uri, cropOptions) + cropImage.launch(contractOptions) + } private fun toggleCamera() { binding.apply { @@ -260,6 +319,7 @@ class CameraFragment : LassiBaseViewModelFragment toggleCamera() R.id.ivFlash -> { //Check whether the flashlight is available or not? @@ -408,14 +468,12 @@ class CameraFragment : LassiBaseViewModelFragment?) { - val intent = Intent().apply { - putExtra(KeyUtils.SELECTED_MEDIA, selectedMedia) + if (isFromCropNext || !config.isCrop) { // the !config.isCrop condition is given as when the crop is been disabled from the main activity, we don't go to the else part. + /** + * this will be when calling from the cropNext() + */ + val intent = Intent().apply { + putExtra(KeyUtils.SELECTED_MEDIA, selectedMedia) + } + activity?.setResult(Activity.RESULT_OK, intent) + activity?.finish() + isFromCropNext = false + } else { + /** + * everytime else + */ + getSelectedAndCapturedImages() + startCroppingSequence() } - activity?.setResult(Activity.RESULT_OK, intent) - activity?.finish() } private fun requestForPermissions() { diff --git a/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderFragment.kt b/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderFragment.kt index d0818fd..ab56da7 100644 --- a/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderFragment.kt +++ b/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderFragment.kt @@ -548,7 +548,7 @@ class FolderFragment : LassiBaseViewModelFragment