diff --git a/common/build.gradle b/common/build.gradle index 4d1b8835..bc4966b8 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -106,6 +106,7 @@ dependencies { // Accompanist implementation libs.composeAccompanist.navigation + implementation libs.composeAccompanist.permissions // Kotlin reflect implementation libs.kotlin.reflect diff --git a/design/build.gradle b/design/build.gradle index 38319e3a..b0435c9f 100644 --- a/design/build.gradle +++ b/design/build.gradle @@ -69,9 +69,11 @@ dependencies { implementation libs.composeDependencies.composePreview implementation libs.composeDependencies.composeMaterial3 implementation libs.composeDependencies.composeMaterial2 + implementation libs.composeDependencies.composeMaterialIconsCore implementation libs.composeDependencies.composeNavigation implementation libs.composeDependencies.composeConstraintLayout implementation libs.composeDependencies.lifecycleRuntime + implementation libs.composeDependencies.runtime debugImplementation libs.composeDependencies.composeUiTooling debugImplementation libs.composeDependencies.composeUiTestManifest @@ -91,7 +93,12 @@ dependencies { // Local modules implementation project(":cdn:models:presentation") + implementation project(":common") + implementation project(":tempattachment") // Bottom sheet implementation libs.bottomSheet + + implementation libs.composeAccompanist.permissions + } \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/ImagePicker.kt b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/ImagePicker.kt new file mode 100644 index 00000000..0cd75d3d --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/ImagePicker.kt @@ -0,0 +1,209 @@ +package com.urlaunched.android.design.ui.imagepicker + +import android.Manifest +import android.net.Uri +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.urlaunched.android.common.files.FileHelper +import com.urlaunched.android.common.files.TakeCameraPictureContract +import com.urlaunched.android.design.resources.dimens.Dimens +import com.urlaunched.android.design.ui.image.UrlImage +import com.urlaunched.android.design.ui.imagepicker.model.ImagePickerDialogStyle +import com.urlaunched.android.design.ui.imagepicker.model.ImagePickerTextStyles +import com.urlaunched.android.design.ui.imagepicker.model.ImageSource +import com.urlaunched.android.design.ui.imagepicker.model.ImagesCount +import java.io.File + +private const val FILE_PROVIDER = ".provider" + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun ImagePicker( + state: ImagePickerState, + gallerySource: ImageSource.Gallery, + cameraSource: ImageSource.Camera, + headlineText: String, + dismissButtonText: String, + style: ImagePickerDialogStyle = ImagePickerDialogStyle(), + textStyles: ImagePickerTextStyles = ImagePickerTextStyles() +) { + if (!LocalInspectionMode.current) { + val context = LocalContext.current + val cameraPickerContract = remember { + TakeCameraPictureContract(fileProviderAuthority = context.packageName + FILE_PROVIDER) + } + + val cameraLauncher = rememberLauncherForActivityResult( + contract = cameraPickerContract, + onResult = { file -> + if (file != null) { + state.processPickedFiles(files = listOf(file), context = context) + } + } + ) + + val pickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickVisualMedia(), + onResult = { uri -> + if (uri != null) { + state.processPickedUris(uris = listOf(uri), context = context) + } + } + ) + + val multiplePhotoPickerLauncher = if (state.maxImagesCount > ImagesCount.SINGLE_IMAGE) { + rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickMultipleVisualMedia(maxItems = state.maxImagesCount.count), + onResult = { uris -> + state.processPickedUris(uris = uris, context = context) + } + ) + } else { + null + } + + val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) + + val cameraRequestPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { granted -> + if (granted) { + launchCameraPicker(cameraLauncher) + } + } + ) + + if (state.deleteTempFilesOnDispose) { + DisposableEffect(Unit) { + onDispose { + FileHelper.deleteTempFilesFromCache(context) + } + } + } + + if (state.isSourceSelectorDialogShown) { + ImageSourceSelectorDialog( + onGalleryClick = { + if (state.maxImagesCount > ImagesCount.SINGLE_IMAGE && multiplePhotoPickerLauncher != null) { + launchMultipleImagePicker(multiplePhotoPickerLauncher) + } else { + launchImagePicker(pickerLauncher) + } + }, + onCameraClick = { + if (!cameraPermissionState.status.isGranted) { + cameraRequestPermissionLauncher.launch(Manifest.permission.CAMERA) + } else { + launchCameraPicker(cameraLauncher) + } + }, + onDismiss = state::hideSourceSelectorDialog, + cameraSource = cameraSource, + gallerySource = gallerySource, + headlineText = headlineText, + dismissButtonText = dismissButtonText, + style = style, + textStyles = textStyles + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ImagePickerPreview() { + var pickedImage by remember { mutableStateOf>(emptyList()) } + val imagePickerState = rememberImagePickerState( + onError = {}, + onImagesPicked = { images -> + pickedImage += images + } + ) + + ImagePicker( + state = imagePickerState, + cameraSource = ImageSource.Camera(title = "Camera"), + gallerySource = ImageSource.Gallery(title = "Gallery"), + headlineText = "Choose source", + dismissButtonText = "Cancel", + ) + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(space = Dimens.spacingBig, alignment = Alignment.CenterVertically) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space = Dimens.spacingNormal, alignment = Alignment.CenterHorizontally) + ) { + pickedImage.forEach { image -> + UrlImage( + model = image, + modifier = Modifier + .size(100.dp) + .clip(RoundedCornerShape(Dimens.cornerRadiusNormal)) + ) + } + } + + Button( + enabled = pickedImage.size < 3, + onClick = { + imagePickerState.showSourceSelectorDialog(ImagesCount(count = 3 - pickedImage.size)) + } + ) { + Text("Pick images") + } + } +} + +private fun launchImagePicker(pickerLauncher: ManagedActivityResultLauncher) { + pickerLauncher.launch( + PickVisualMediaRequest( + mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly + ) + ) +} + +private fun launchCameraPicker(pickerLauncher: ManagedActivityResultLauncher) { + pickerLauncher.launch(Unit) +} + +private fun launchMultipleImagePicker( + pickerLauncher: ManagedActivityResultLauncher> +) { + pickerLauncher.launch( + PickVisualMediaRequest( + mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly + ) + ) +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/ImagePickerState.kt b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/ImagePickerState.kt new file mode 100644 index 00000000..2e5e0c64 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/ImagePickerState.kt @@ -0,0 +1,139 @@ +package com.urlaunched.android.design.ui.imagepicker + +import android.content.Context +import android.net.Uri +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import com.urlaunched.android.common.compression.CompressImageUtil +import com.urlaunched.android.common.files.FilePickerHelper +import com.urlaunched.android.design.ui.imagepicker.model.CompressionConfig +import com.urlaunched.android.design.ui.imagepicker.model.ImagePickerError +import com.urlaunched.android.design.ui.imagepicker.model.ImagesCount +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File + +class ImagePickerState( + val compressionConfig: CompressionConfig = CompressionConfig(), + val coroutineScope: CoroutineScope, + val deleteTempFilesOnDispose: Boolean, + val onError: suspend (error: ImagePickerError) -> Unit, + val onImagesPicked: (images: List) -> Unit +) { + var maxImagesCount by mutableStateOf(ImagesCount.SINGLE_IMAGE) + private set + + var isSourceSelectorDialogShown by mutableStateOf(false) + private set + + var isFileCopyingProceed by mutableStateOf(false) + private set + + var isImageCompressionProceed by mutableStateOf(false) + private set + + fun processPickedUris(uris: List, context: Context) { + coroutineScope.launch(Dispatchers.IO) { + isFileCopyingProceed = true + + val files = uris.mapNotNull { uri -> + FilePickerHelper.createFileFromUri(context, uri) + } + + isFileCopyingProceed = false + + processPickedFiles(files = files, context = context) + } + } + + fun processPickedFiles(files: List, context: Context) { + coroutineScope.launch(Dispatchers.IO) { + isImageCompressionProceed = true + val validFiles = mutableListOf() + + files.forEach { file -> + if (file.length() > compressionConfig.maxSizeBytes) { + onError(ImagePickerError.PHOTO_TOO_LARGE) + } else { + try { + compressImage(file, context)?.let { compressedFile -> + validFiles.add(compressedFile) + } ?: onError(ImagePickerError.COMPRESSION_FAILED) + } catch (e: Exception) { + onError(ImagePickerError.COMPRESSION_FAILED) + isImageCompressionProceed = false + return@launch + } + } + } + + onImagesPicked(validFiles) + isImageCompressionProceed = false + } + } + + private fun compressImage(image: File, context: Context): File? { + return CompressImageUtil.compressImage( + input = image, + context = context, + targetSize = compressionConfig.targetSizeBytes, + minWidth = compressionConfig.minWidth, + minHeight = compressionConfig.minHeight + ) + } + + fun showSourceSelectorDialog( + maxImagesCount: ImagesCount = ImagesCount.SINGLE_IMAGE + ) { + this.maxImagesCount = maxImagesCount + isSourceSelectorDialogShown = true + } + + fun hideSourceSelectorDialog() { + isSourceSelectorDialogShown = false + } +} + +@Composable +fun rememberImagePickerState( + photoTooLargeErrorMessage: String, + compressionErrorMessage: String, + compressionConfig: CompressionConfig = CompressionConfig(), + coroutineScope: CoroutineScope = rememberCoroutineScope(), + deleteTempFilesOnDispose: Boolean = true, + showErrorSnackbar: suspend (message: String) -> Unit, + onImagesPicked: (images: List) -> Unit +) = rememberImagePickerState( + compressionConfig = compressionConfig, + coroutineScope = coroutineScope, + deleteTempFilesOnDispose = deleteTempFilesOnDispose, + onImagesPicked = onImagesPicked, + onError = { error -> + when (error) { + ImagePickerError.PHOTO_TOO_LARGE -> showErrorSnackbar(photoTooLargeErrorMessage) + ImagePickerError.COMPRESSION_FAILED -> showErrorSnackbar(compressionErrorMessage) + } + } +) + +@Composable +internal fun rememberImagePickerState( + compressionConfig: CompressionConfig = CompressionConfig(), + coroutineScope: CoroutineScope = rememberCoroutineScope(), + deleteTempFilesOnDispose: Boolean = true, + onError: suspend (error: ImagePickerError) -> Unit, + onImagesPicked: (images: List) -> Unit +) = remember { + ImagePickerState( + compressionConfig = compressionConfig, + coroutineScope = coroutineScope, + onError = onError, + deleteTempFilesOnDispose = deleteTempFilesOnDispose, + onImagesPicked = onImagesPicked + ) +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/ImageSourceSelectorDialog.kt b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/ImageSourceSelectorDialog.kt new file mode 100644 index 00000000..8a2bb299 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/ImageSourceSelectorDialog.kt @@ -0,0 +1,162 @@ +package com.urlaunched.android.design.ui.imagepicker + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.window.Dialog +import com.urlaunched.android.design.ui.clickable.debouncedClickable +import com.urlaunched.android.design.ui.imagepicker.dimens.ImagePickerDimens +import com.urlaunched.android.design.ui.imagepicker.model.ImagePickerDialogStyle +import com.urlaunched.android.design.ui.imagepicker.model.ImagePickerTextStyles +import com.urlaunched.android.design.ui.imagepicker.model.ImageSource + +@Composable +internal fun ImageSourceSelectorDialog( + onDismiss: () -> Unit, + gallerySource: ImageSource.Gallery, + cameraSource: ImageSource.Camera, + headlineText: String, + dismissButtonText: String, + style: ImagePickerDialogStyle = ImagePickerDialogStyle(), + textStyles: ImagePickerTextStyles = ImagePickerTextStyles(), + onGalleryClick: () -> Unit, + onCameraClick: () -> Unit +) { + Dialog( + onDismissRequest = onDismiss + ) { + Column( + modifier = Modifier + .background(style.containerColor, style.shape) + .padding(style.contentPadding), + verticalArrangement = Arrangement.Center + ) { + Text( + text = headlineText, + style = textStyles.headlineStyle + ) + + Spacer(modifier = Modifier.height(style.contentSpacing)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceAround, + verticalAlignment = Alignment.CenterVertically + ) { + ImageSourceOption( + modifier = Modifier.weight(1f), + textStyle = textStyles.optionTextStyle, + imageSource = cameraSource, + onClick = { + onCameraClick() + onDismiss() + } + ) + + ImageSourceOption( + modifier = Modifier.weight(1f), + textStyle = textStyles.optionTextStyle, + imageSource = gallerySource, + onClick = { + onGalleryClick() + onDismiss() + } + ) + } + + Spacer(modifier = Modifier.height(style.contentSpacing)) + + DismissButton( + text = dismissButtonText, + textStyle = textStyles.dismissButtonStyle, + modifier = Modifier.align(Alignment.End), + onClick = onDismiss + ) + } + } +} + +@Composable +private fun DismissButton( + modifier: Modifier = Modifier, + text: String, + textStyle: TextStyle = TextStyle(), + onClick: () -> Unit +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(ImagePickerDimens.dismissButtonCornerRadius)) + .debouncedClickable(onClick = onClick) + .padding(ImagePickerDimens.dismissButtonInnerPadding) + ) { + Text( + text = text, + style = textStyle + ) + } +} + +@Composable +private fun ImageSourceOption( + modifier: Modifier = Modifier, + imageSource: ImageSource, + textStyle: TextStyle = TextStyle(), + onClick: () -> Unit +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(ImagePickerDimens.imageSourceCornerRadius)) + .debouncedClickable(onClick = onClick), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(imageSource.iconResId), + contentDescription = imageSource.contentDescription, + modifier = Modifier.size(ImagePickerDimens.imageSourceOptionsIconSize), + tint = imageSource.iconTint + ) + } + + Spacer(modifier = Modifier.height(ImagePickerDimens.imageSourceTitleSpacing)) + + Text( + text = imageSource.title, + style = textStyle + ) + } +} + +@Preview +@Composable +private fun ImageSourceSelectorDialogPreview() { + ImageSourceSelectorDialog( + onDismiss = {}, + cameraSource = ImageSource.Camera(title = "Camera"), + gallerySource = ImageSource.Gallery(title = "Gallery"), + headlineText = "Choose source", + dismissButtonText = "Cancel", + onGalleryClick = {}, + onCameraClick = {} + ) +} diff --git a/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/dimens/CompressionConstants.kt b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/dimens/CompressionConstants.kt new file mode 100644 index 00000000..153dd356 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/dimens/CompressionConstants.kt @@ -0,0 +1,8 @@ +package com.urlaunched.android.design.ui.imagepicker.dimens + +internal object CompressionConstants { + const val DEFAULT_MAX_SIZE_BYTES: Long = 15 * 1024 * 1024 + const val DEFAULT_TARGET_SIZE_BYTES: Long = 5 * 1000 * 1000 + const val DEFAULT_MIN_HEIGHT = 1080 + const val DEFAULT_MIN_WIDTH = 1920 +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/dimens/ImagePickerDimens.kt b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/dimens/ImagePickerDimens.kt new file mode 100644 index 00000000..d3f371f8 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/dimens/ImagePickerDimens.kt @@ -0,0 +1,17 @@ +package com.urlaunched.android.design.ui.imagepicker.dimens + +import androidx.compose.ui.unit.dp +import com.urlaunched.android.design.resources.dimens.Dimens + +internal object ImagePickerDimens { + val imageSourceOptionsIconSize = 63.dp + val imageSourceCornerRadius = Dimens.cornerRadiusNormal + val imageSourceTitleSpacing = Dimens.spacingNormalSpecial + + val dismissButtonCornerRadius = Dimens.cornerRadiusNormal + val dismissButtonInnerPadding = Dimens.spacingNormalSpecial + + val imagePickerDialogCornerRadius = Dimens.cornerRadiusNormal + val imagePickerDialogContentPadding = Dimens.spacingBig + val imagePickerDialogContentSpacing = Dimens.spacingBig +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/CompressionConfig.kt b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/CompressionConfig.kt new file mode 100644 index 00000000..c692bb8b --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/CompressionConfig.kt @@ -0,0 +1,11 @@ +package com.urlaunched.android.design.ui.imagepicker.model + +import com.urlaunched.android.design.ui.imagepicker.dimens.CompressionConstants + +data class CompressionConfig( + val minWidth: Int = CompressionConstants.DEFAULT_MIN_WIDTH, + val minHeight: Int = CompressionConstants.DEFAULT_MIN_HEIGHT, + val targetSizeBytes: Long = CompressionConstants.DEFAULT_TARGET_SIZE_BYTES, + val maxSizeBytes: Long = CompressionConstants.DEFAULT_MAX_SIZE_BYTES +) + diff --git a/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/ImagePickerDialogStyle.kt b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/ImagePickerDialogStyle.kt new file mode 100644 index 00000000..9af7dcc5 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/ImagePickerDialogStyle.kt @@ -0,0 +1,15 @@ +package com.urlaunched.android.design.ui.imagepicker.model + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import com.urlaunched.android.design.ui.imagepicker.dimens.ImagePickerDimens + +data class ImagePickerDialogStyle( + val shape: Shape = RoundedCornerShape(ImagePickerDimens.imagePickerDialogCornerRadius), + val containerColor: Color = Color.White, + val contentPadding: PaddingValues = PaddingValues(ImagePickerDimens.imagePickerDialogContentPadding), + val contentSpacing: Dp = ImagePickerDimens.imagePickerDialogContentSpacing +) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/ImagePickerError.kt b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/ImagePickerError.kt new file mode 100644 index 00000000..439ce50c --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/ImagePickerError.kt @@ -0,0 +1,6 @@ +package com.urlaunched.android.design.ui.imagepicker.model + +enum class ImagePickerError { + PHOTO_TOO_LARGE, + COMPRESSION_FAILED +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/ImagePickerTextStyles.kt b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/ImagePickerTextStyles.kt new file mode 100644 index 00000000..e99f41e7 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/ImagePickerTextStyles.kt @@ -0,0 +1,9 @@ +package com.urlaunched.android.design.ui.imagepicker.model + +import androidx.compose.ui.text.TextStyle + +data class ImagePickerTextStyles( + val headlineStyle: TextStyle = TextStyle(), + val optionTextStyle: TextStyle = TextStyle(), + val dismissButtonStyle: TextStyle = TextStyle() +) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/ImageSource.kt b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/ImageSource.kt new file mode 100644 index 00000000..91867e5f --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/ImageSource.kt @@ -0,0 +1,29 @@ +package com.urlaunched.android.design.ui.imagepicker.model + +import androidx.annotation.DrawableRes +import androidx.compose.ui.graphics.Color +import com.urlaunched.android.design.R + +sealed class ImageSource { + @get:DrawableRes + abstract val iconResId: Int + abstract val title: String + abstract val contentDescription: String? + abstract val iconTint: Color + + data class Camera( + override val title: String, + @DrawableRes + override val iconResId: Int = R.drawable.ic_camera, + override val contentDescription: String? = title, + override val iconTint: Color = Color.Unspecified + ) : ImageSource() + + data class Gallery( + override val title: String, + @DrawableRes + override val iconResId: Int = R.drawable.ic_gallery, + override val contentDescription: String? = title, + override val iconTint: Color = Color.Unspecified + ) : ImageSource() +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/ImagesCount.kt b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/ImagesCount.kt new file mode 100644 index 00000000..4647fc75 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/imagePicker/model/ImagesCount.kt @@ -0,0 +1,20 @@ +package com.urlaunched.android.design.ui.imagepicker.model + +import androidx.annotation.IntRange + +@JvmInline +value class ImagesCount( + @IntRange(from = 1) + val count: Int +) : Comparable { + + override fun compareTo(other: ImagesCount): Int { + return this.count.compareTo(other.count) + } + + companion object { + val SINGLE_IMAGE = ImagesCount(1) + val MULTIPLES_IMAGES_DEFAULT = ImagesCount(6) + val UNLIMITED = ImagesCount(Int.MAX_VALUE) + } +} \ No newline at end of file diff --git a/design/src/main/res/drawable/ic_camera.xml b/design/src/main/res/drawable/ic_camera.xml new file mode 100644 index 00000000..12d78aa6 --- /dev/null +++ b/design/src/main/res/drawable/ic_camera.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/design/src/main/res/drawable/ic_gallery.xml b/design/src/main/res/drawable/ic_gallery.xml new file mode 100644 index 00000000..80db52e5 --- /dev/null +++ b/design/src/main/res/drawable/ic_gallery.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f5bcf8ab..f235bef2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -87,8 +87,8 @@ bottomSheetVersion = "1.33.0" # plugins # dependencies -androidApplicationPluginVersion = "8.4.2" -androidLibraryPluginVersion = "8.4.2" +androidApplicationPluginVersion = "8.6.0" +androidLibraryPluginVersion = "8.6.0" kotlinAndroidPluginVersion = "2.1.20" kotlinJvmPluginVersion = "2.1.20" kotlinSerializationPluginVersion = "2.1.20" @@ -121,6 +121,7 @@ composeDependencies-composeUi = { module = "androidx.compose.ui:ui", version.ref composeDependencies-composePreview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "composeVersion" } composeDependencies-composeMaterial3 = { module = "androidx.compose.material3:material3", version.ref = "composeMaterial3Version" } composeDependencies-composeMaterial2 = { module = "androidx.compose.material:material", version.ref = "composeMaterial2Version" } +composeDependencies-composeMaterialIconsCore = { module = "androidx.compose.material:material-icons-core", version.ref = "composeVersion" } composeDependencies-composeNavigation = { module = "androidx.navigation:navigation-compose", version.ref = "composeNavigationVersion" } composeDependencies-composeConstraintLayout = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "composeConstraintLayout" } composeDependencies-composeUiTooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "composeVersion" } @@ -184,6 +185,7 @@ firebaseDependencies-remoteconfig = { module = "com.google.firebase:firebase-con firebaseDependencies-performance = { module = "com.google.firebase:firebase-perf" } composeAccompanist-navigation = { module = "com.google.accompanist:accompanist-navigation-material", version.ref = "accompanistVersion" } +composeAccompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistVersion" } paparazi-core = { module = "app.cash.paparazzi:paparazzi", version.ref = "paparazziPluginVersion"} junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a..a4413138 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME