From fb6a9f5128c72b29d52b621b0d95d965377ad877 Mon Sep 17 00:00:00 2001 From: Mihai-Cristian Condrea Date: Sat, 29 Jun 2024 22:51:56 +0300 Subject: [PATCH] Added image preview and a better analyze screen for a better UX --- app/build.gradle.kts | 2 + .../ui/dialogs/VersionInfoDialogComposable.kt | 23 +- .../d4rk/cleaner/ui/home/HomeComposable.kt | 387 +++++++++--------- .../com/d4rk/cleaner/utils/ImageUtils.kt | 61 +++ gradle/libs.versions.toml | 2 + 5 files changed, 262 insertions(+), 213 deletions(-) create mode 100644 app/src/main/kotlin/com/d4rk/cleaner/utils/ImageUtils.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0f593d4..c4c541e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -141,6 +141,7 @@ dependencies { // Image Compression implementation(libs.compressor) implementation(libs.glide) + implementation(libs.coil.compose) // Test testImplementation(libs.junit) @@ -151,6 +152,7 @@ dependencies { debugImplementation(libs.androidx.ui.test.manifest) + // TODO: Clean-up implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.gridlayout:gridlayout:1.0.0") diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/dialogs/VersionInfoDialogComposable.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/dialogs/VersionInfoDialogComposable.kt index 020cb0d..de1b567 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/dialogs/VersionInfoDialogComposable.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/dialogs/VersionInfoDialogComposable.kt @@ -1,11 +1,5 @@ package com.d4rk.cleaner.ui.dialogs -import android.content.res.Resources -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.drawable.AdaptiveIconDrawable -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -25,6 +19,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.d4rk.cleaner.BuildConfig import com.d4rk.cleaner.R +import com.d4rk.cleaner.utils.toBitmapDrawable @Composable fun VersionInfoDialog(onDismiss: () -> Unit) { @@ -73,20 +68,4 @@ fun VersionInfoContent() { ) } } -} - -fun Drawable.toBitmapDrawable(): BitmapDrawable { - return when (this) { - is BitmapDrawable -> this - is AdaptiveIconDrawable -> { - val bitmap = - Bitmap.createBitmap(intrinsicWidth, intrinsicHeight, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bitmap) - setBounds(0, 0, canvas.width, canvas.height) - draw(canvas) - BitmapDrawable(Resources.getSystem(), bitmap) - } - - else -> throw IllegalArgumentException("Unsupported drawable type: ${this::class.java.name}") - } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/home/HomeComposable.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/home/HomeComposable.kt index e90c1cc..61ed606 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/home/HomeComposable.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/home/HomeComposable.kt @@ -1,7 +1,6 @@ package com.d4rk.cleaner.ui.home import android.app.Activity -import android.content.Context import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState @@ -13,12 +12,13 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkHorizontally import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource 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.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -26,12 +26,14 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilterChip @@ -44,26 +46,32 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.asFlow import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.AsyncImage +import coil.request.ImageRequest import com.d4rk.cleaner.R import com.d4rk.cleaner.utils.CircularDeterminateIndicator import com.d4rk.cleaner.utils.bounceClick +import com.d4rk.cleaner.utils.getFileIcon +import com.d4rk.cleaner.utils.getVideoThumbnail import com.google.common.io.Files.getFileExtension import java.io.File @Composable fun HomeComposable() { val context = LocalContext.current - val viewModel: HomeViewModel = viewModel() + val viewModel : HomeViewModel = viewModel() val progress by viewModel.progress.observeAsState(0.3f) val storageUsed by viewModel.storageUsed.observeAsState("0") val storageTotal by viewModel.storageTotal.observeAsState("0") @@ -75,117 +83,113 @@ fun HomeComposable() { ) { Box( modifier = Modifier - .weight(4f) - .fillMaxWidth() + .weight(4f) + .fillMaxWidth() ) { - if (!showCleaningComposable) { + if (! showCleaningComposable) { CircularDeterminateIndicator( - progress = progress, - storageUsed = storageUsed, - storageTotal = storageTotal, + progress = progress , + storageUsed = storageUsed , + storageTotal = storageTotal , modifier = Modifier - .align(Alignment.TopCenter) - .offset(y = 98.dp) + .align(Alignment.TopCenter) + .offset(y = 98.dp) ) Image( - painter = painterResource(R.drawable.ic_clean), - contentDescription = null, + painter = painterResource(R.drawable.ic_clean) , + contentDescription = null , modifier = Modifier - .align(Alignment.BottomCenter) - .padding(24.dp) - .size(128.dp, 66.dp) + .align(Alignment.BottomCenter) + .padding(24.dp) + .size(128.dp , 66.dp) ) - } else { + } + else { AnalyzeComposable() } } Row( modifier = Modifier - .fillMaxWidth() - .height(102.dp) - .padding(bottom = 16.dp), + .fillMaxWidth() + .height(102.dp) + .padding(bottom = 16.dp) , horizontalArrangement = Arrangement.SpaceEvenly ) { AnimatedVisibility( - visible = showCleaningComposable, - enter = fadeIn(animationSpec = tween(durationMillis = 400)) + - expandHorizontally( - animationSpec = tween(durationMillis = 400), - expandFrom = Alignment.Start - ), - exit = fadeOut(animationSpec = tween(durationMillis = 400)) + - shrinkHorizontally( - animationSpec = tween(durationMillis = 400), - shrinkTowards = Alignment.Start - ), + visible = showCleaningComposable , + enter = fadeIn(animationSpec = tween(durationMillis = 400)) + expandHorizontally( + animationSpec = tween(durationMillis = 400) , expandFrom = Alignment.Start + ) , + exit = fadeOut(animationSpec = tween(durationMillis = 400)) + shrinkHorizontally( + animationSpec = tween(durationMillis = 400) , shrinkTowards = Alignment.Start + ) , modifier = Modifier.weight(1f) ) { - val enabled = !isAnalyzing && selectedFileCount > 0 + val enabled = ! isAnalyzing && selectedFileCount > 0 val animateStateButtonColor = animateColorAsState( - targetValue = if (enabled) MaterialTheme.colorScheme.secondaryContainer else Color.LightGray, - animationSpec = tween(400, 0, LinearEasing), label = "" + targetValue = if (enabled) MaterialTheme.colorScheme.secondaryContainer else Color.LightGray , + animationSpec = tween(400 , 0 , LinearEasing) , + label = "" ) FilledTonalButton( modifier = Modifier - .weight(1f) - .fillMaxHeight() - .animateContentSize() - .padding(start = 16.dp, end = 8.dp) - .bounceClick(), + .weight(1f) + .fillMaxHeight() + .animateContentSize() + .padding(start = 16.dp , end = 8.dp) + .bounceClick() , onClick = { viewModel.clean(activity = context as Activity) - }, - shape = MaterialTheme.shapes.medium, - enabled = enabled, + } , + shape = MaterialTheme.shapes.medium , + enabled = enabled , colors = ButtonDefaults.filledTonalButtonColors( - containerColor = animateStateButtonColor.value, - ), + containerColor = animateStateButtonColor.value , + ) , ) { Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally , + verticalArrangement = Arrangement.Center , modifier = Modifier - .fillMaxSize() - .padding(ButtonDefaults.ContentPadding) + .fillMaxSize() + .padding(ButtonDefaults.ContentPadding) ) { Icon( - painterResource(R.drawable.ic_broom), - contentDescription = null, + painterResource(R.drawable.ic_broom) , + contentDescription = null , modifier = Modifier.size(ButtonDefaults.IconSize) ) - Text(text = "Clean", style = MaterialTheme.typography.bodyMedium) + Text(text = "Clean" , style = MaterialTheme.typography.bodyMedium) } } } FilledTonalButton( modifier = Modifier - .weight(1f) - .fillMaxHeight() - .animateContentSize() - .padding(start = if (showCleaningComposable) 8.dp else 16.dp, end = 16.dp) - .bounceClick(), - onClick = { - if (!showCleaningComposable) { + .weight(1f) + .fillMaxHeight() + .animateContentSize() + .padding(start = if (showCleaningComposable) 8.dp else 16.dp , end = 16.dp) + .bounceClick() , onClick = { + if (! showCleaningComposable) { viewModel.analyze(activity = context as Activity) } - }, - shape = MaterialTheme.shapes.medium + } , shape = MaterialTheme.shapes.medium ) { Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally , + verticalArrangement = Arrangement.Center , modifier = Modifier - .fillMaxSize() - .padding(ButtonDefaults.ContentPadding) + .fillMaxSize() + .padding(ButtonDefaults.ContentPadding) ) { Icon( - painterResource(R.drawable.ic_search), - contentDescription = null, + painterResource(R.drawable.ic_search) , + contentDescription = null , modifier = Modifier.size(ButtonDefaults.IconSize) ) - Text(text = "Analyze", style = MaterialTheme.typography.bodyMedium) + Text(text = "Analyze" , style = MaterialTheme.typography.bodyMedium) } } } @@ -202,7 +206,7 @@ fun HomeComposable() { */ @Composable fun AnalyzeComposable() { - val viewModel: HomeViewModel = viewModel() + val viewModel : HomeViewModel = viewModel() val files by viewModel.scannedFiles.asFlow().collectAsState(initial = listOf()) val allFilesSelected by viewModel.allFilesSelected val selectedFileCount by viewModel.selectedFileCount.collectAsState() @@ -213,60 +217,135 @@ fun AnalyzeComposable() { Column( modifier = Modifier - .animateContentSize() - .fillMaxWidth() - .padding(16.dp), + .animateContentSize() + .fillMaxWidth() + .padding(16.dp) , horizontalAlignment = Alignment.End ) { OutlinedCard( modifier = Modifier - .weight(1f) - .fillMaxWidth(), + .weight(1f) + .fillMaxWidth() , ) { - Column { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - items(files) { file -> - FileItemComposable( - file = file, - item = file.name, - context = LocalContext.current, - viewModel = viewModel, - ) - } + LazyVerticalGrid( + columns = GridCells.Fixed(3) , + verticalArrangement = Arrangement.spacedBy(8.dp) , + horizontalArrangement = Arrangement.spacedBy(8.dp) , + modifier = Modifier.padding(8.dp) + ) { + items(files) { file -> + FileCard(file = file , viewModel = viewModel) } } } Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() , + verticalAlignment = Alignment.CenterVertically , + horizontalArrangement = Arrangement.SpaceBetween , ) { val statusText = if (selectedFileCount > 0) { "Status: Selected $selectedFileCount files" - } else { + } + else { "Status: No files selected" } val statusColor by animateColorAsState( targetValue = if (selectedFileCount > 0) { MaterialTheme.colorScheme.primary - } else { + } + else { MaterialTheme.colorScheme.secondary - }, - animationSpec = tween(), label = "" + } , animationSpec = tween() , label = "" ) Text( - text = statusText, - color = statusColor, - modifier = Modifier.animateContentSize() + text = statusText , color = statusColor , modifier = Modifier.animateContentSize() ) SelectAllComposable( - checked = allFilesSelected, - onCheckedChange = { viewModel.selectAllFiles(it) }, + checked = allFilesSelected , + onCheckedChange = { viewModel.selectAllFiles(it) } , + ) + } + } +} + +@Composable +fun FileCard(file : File , viewModel : HomeViewModel) { + val context = LocalContext.current + val fileExtension = getFileExtension(file.name) + Card( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .bounceClick() , + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Box(modifier = Modifier.fillMaxSize()) { + when (fileExtension) { + in context.resources.getStringArray(R.array.image_extensions).toList() -> { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current).data(file) + .crossfade(true).build() , + contentDescription = file.name , + contentScale = ContentScale.Crop , + modifier = Modifier.fillMaxSize() + ) + } + + in context.resources.getStringArray(R.array.video_extensions).toList() -> { + val thumbnailBitmap = getVideoThumbnail(file.absolutePath) + if (thumbnailBitmap != null) { + Image( + bitmap = thumbnailBitmap.asImageBitmap(), + contentDescription = file.name, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + } else { + Icon( + painter = painterResource(R.drawable.ic_video_file), + contentDescription = null, + modifier = Modifier + .size(24.dp) + .align(Alignment.Center) + ) + } + } + + else -> { + Icon( + painter = painterResource(getFileIcon(fileExtension , context)) , + contentDescription = null , + modifier = Modifier + .size(24.dp) + .align(Alignment.Center) + ) + } + } + + Checkbox( + checked = viewModel.fileSelectionStates[file] ?: false , + onCheckedChange = { isChecked -> + viewModel.fileSelectionStates[file] = isChecked + viewModel._selectedFileCount.value = + viewModel.fileSelectionStates.values.count { it } + viewModel.allFilesSelected.value = + viewModel.fileSelectionStates.values.all { it } + } , + modifier = Modifier.align(Alignment.TopEnd) + ) + + Text( + text = file.name , + maxLines = 1 , + overflow = TextOverflow.Ellipsis , + modifier = Modifier + .fillMaxWidth() + .background( + color = Color.Black.copy(alpha = 0.5f) + ) + .padding(8.dp) + .align(Alignment.BottomCenter) ) } } @@ -283,108 +362,34 @@ fun AnalyzeComposable() { */ @Composable fun SelectAllComposable( - checked: Boolean, onCheckedChange: (Boolean) -> Unit + checked : Boolean , onCheckedChange : (Boolean) -> Unit ) { Row( modifier = Modifier - .fillMaxWidth() - .animateContentSize(), - verticalAlignment = Alignment.CenterVertically, + .fillMaxWidth() + .animateContentSize() , + verticalAlignment = Alignment.CenterVertically , horizontalArrangement = Arrangement.End ) { val interactionSource = remember { MutableInteractionSource() } FilterChip( - modifier = Modifier.bounceClick(), - selected = checked, + modifier = Modifier.bounceClick() , + selected = checked , onClick = { - onCheckedChange(!checked) - }, - label = { Text("Select All") }, + onCheckedChange(! checked) + } , + label = { Text("Select All") } , leadingIcon = { - AnimatedContent(targetState = checked, label = "") { targetChecked -> + AnimatedContent(targetState = checked , label = "") { targetChecked -> if (targetChecked) { Icon( - imageVector = Icons.Filled.Check, - contentDescription = null, + imageVector = Icons.Filled.Check , + contentDescription = null , ) } } - }, - interactionSource = interactionSource, + } , + interactionSource = interactionSource , ) } } - -/** - * Composable function representing an item in a cleaning list with an icon, text label, and checkbox. - * - * This composable displays a row containing an icon, text label, and checkbox for a given cleaning list item. - * The user can tap the checkbox to select or deselect the item. - * - * @param item The text label to display for the cleaning item. - * @param isChecked The state of the checkbox indicating whether the item is selected or not. - * @param onCheckedChange A callback that is triggered when the checkbox state changes. - * The new state of the checkbox (`isChecked`) is provided as a parameter to this callback. - * @param context The Android `Context` used to access resources like file extensions and corresponding icons. - */ - -val fileIconMap = mutableMapOf() - -@Composable -fun FileItemComposable( - file: File, item: String = "", viewModel: HomeViewModel, context: Context -) { - context.resources.getStringArray(R.array.apk_extensions).forEach { - fileIconMap[it] = R.drawable.ic_apk_document - } - - context.resources.getStringArray(R.array.archive_extensions).forEach { - fileIconMap[it] = R.drawable.ic_archive_filter - } - - context.resources.getStringArray(R.array.audio_extensions).forEach { - fileIconMap[it] = R.drawable.ic_audio_file - } - - context.resources.getStringArray(R.array.video_extensions).forEach { - fileIconMap[it] = R.drawable.ic_video_file - } - - context.resources.getStringArray(R.array.image_extensions).forEach { - fileIconMap[it] = R.drawable.ic_image - } - - fileIconMap["nomedia"] = R.drawable.ic_draft - fileIconMap["vcf"] = R.drawable.ic_contact_page - - val fileExtension = getFileExtension(item) - val iconResource = fileIconMap[fileExtension] ?: R.drawable.ic_file_present - val fileSelectionStates = remember { mutableStateMapOf() } - Row( - modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center - ) { - Icon( - painter = painterResource(iconResource), - contentDescription = null, - modifier = Modifier.align(Alignment.CenterVertically) - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = item, modifier = Modifier - .weight(1f) - .align(Alignment.CenterVertically) - ) - Spacer(modifier = Modifier.width(4.dp)) - Checkbox( - checked = viewModel.fileSelectionStates[file] ?: false, - onCheckedChange = { isChecked -> - fileSelectionStates[file] = isChecked - viewModel.fileSelectionStates[file] = isChecked - viewModel._selectedFileCount.value = - viewModel.fileSelectionStates.values.count { it } - viewModel.allFilesSelected.value = viewModel.fileSelectionStates.values.all { it } - }, - modifier = Modifier.align(Alignment.CenterVertically) - ) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/utils/ImageUtils.kt b/app/src/main/kotlin/com/d4rk/cleaner/utils/ImageUtils.kt new file mode 100644 index 0000000..407f309 --- /dev/null +++ b/app/src/main/kotlin/com/d4rk/cleaner/utils/ImageUtils.kt @@ -0,0 +1,61 @@ +package com.d4rk.cleaner.utils + +import android.content.Context +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.media.MediaMetadataRetriever +import com.d4rk.cleaner.R + +fun getVideoThumbnail(videoPath: String): Bitmap? { + val mediaMetadataRetriever = MediaMetadataRetriever() + try { + mediaMetadataRetriever.setDataSource(videoPath) + return mediaMetadataRetriever.getFrameAtTime(1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) // Extract frame at 1 second + } catch (_ : Exception) { + + } finally { + mediaMetadataRetriever.release() + } + return null +} + +fun getFileIcon(extension : String , context : Context) : Int { + return when (extension.lowercase()) { + in context.resources.getStringArray(R.array.apk_extensions) + .toList() -> R.drawable.ic_apk_document + + in context.resources.getStringArray(R.array.image_extensions) + .toList() -> R.drawable.ic_image + + in context.resources.getStringArray(R.array.video_extensions) + .toList() -> R.drawable.ic_video_file + + in context.resources.getStringArray(R.array.audio_extensions) + .toList() -> R.drawable.ic_audio_file + + in context.resources.getStringArray(R.array.archive_extensions) + .toList() -> R.drawable.ic_archive_filter + + else -> R.drawable.ic_file_present + } +} + +fun Drawable.toBitmapDrawable() : BitmapDrawable { + return when (this) { + is BitmapDrawable -> this + is AdaptiveIconDrawable -> { + val bitmap = + Bitmap.createBitmap(intrinsicWidth , intrinsicHeight , Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + setBounds(0 , 0 , canvas.width , canvas.height) + draw(canvas) + BitmapDrawable(Resources.getSystem() , bitmap) + } + + else -> throw IllegalArgumentException("Unsupported drawable type: ${this::class.java.name}") + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 29f9018..177e80e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ agp = "8.5.0" appcompat = "1.7.0" appUpdateKtx = "2.1.0" billing = "7.0.0" +coilCompose = "2.6.0" composeBom = "2024.06.00" compressor = "3.0.1" constraintlayoutCompose = "1.0.1" @@ -56,6 +57,7 @@ androidx-runtime-rxjava2 = { module = "androidx.compose.runtime:runtime-rxjava2" androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" } app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "appUpdateKtx" } billing = { module = "com.android.billingclient:billing", version.ref = "billing" } +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } compressor = { module = "id.zelory:compressor", version.ref = "compressor" } firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx" } firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }