From e5300806c1d43f71f1a717b1e765527c672b739a Mon Sep 17 00:00:00 2001 From: Mihai-Cristian Condrea Date: Sun, 13 Oct 2024 19:57:43 +0300 Subject: [PATCH] Refactored the code base --- .../data/model/ui/screens/UiHomeModel.kt | 36 +- .../cleaner/ui/screens/home/AnalyzeScreen.kt | 578 +++++++++++++++++ .../cleaner/ui/screens/home/HomeScreen.kt | 610 +----------------- .../cleaner/ui/screens/home/HomeViewModel.kt | 186 ++++-- .../screens/home/repository/HomeRepository.kt | 10 + .../HomeRepositoryImplementation.kt | 25 +- 6 files changed, 788 insertions(+), 657 deletions(-) create mode 100644 app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/AnalyzeScreen.kt diff --git a/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/screens/UiHomeModel.kt b/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/screens/UiHomeModel.kt index b6f5d37..4a92438 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/screens/UiHomeModel.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/screens/UiHomeModel.kt @@ -3,15 +3,29 @@ package com.d4rk.cleaner.data.model.ui.screens import java.io.File data class UiHomeModel( - val progress: Float = 0f, - val storageUsed: String = "", - val storageTotal: String = "", - val showCleaningComposable: Boolean = false, - val scannedFiles: List = emptyList(), - val emptyFolders: List = emptyList(), - val allFilesSelected: Boolean = false, - val fileSelectionStates: Map = emptyMap(), - val selectedFileCount: Int = 0, - val showRescanDialog: Boolean = false, - val noFilesFound: Boolean = false, + val storageUsageProgress : Float = 0f , + val usedStorageFormatted : String = "" , + val totalStorageFormatted : String = "" , + var analyzeState : UiAnalyzeModel = UiAnalyzeModel() , + val isRescanDialogVisible : Boolean = false , +) + +data class UiAnalyzeModel( + val isAnalyzeScreenVisible : Boolean = false , + val scannedFileList : List = emptyList() , + val emptyFolderList : List = emptyList() , + val areAllFilesSelected : Boolean = false , + val fileSelectionMap : Map = emptyMap() , + val selectedFilesCount : Int = 0 , + val isFileScanEmpty : Boolean = false , + var fileTypesData : FileTypesData = FileTypesData() , +) + +data class FileTypesData( + val apkExtensions : List = emptyList() , + val imageExtensions : List = emptyList() , + val videoExtensions : List = emptyList() , + val audioExtensions : List = emptyList() , + val archiveExtensions : List = emptyList() , + val fileTypesTitles : List = emptyList() , ) \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/AnalyzeScreen.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/AnalyzeScreen.kt new file mode 100644 index 0000000..68ca2ec --- /dev/null +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/AnalyzeScreen.kt @@ -0,0 +1,578 @@ +package com.d4rk.cleaner.ui.screens.home + +import android.content.Context +import android.content.Intent +import android.view.SoundEffectConstants +import android.view.View +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.fillMaxSize +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.lazy.LazyColumn +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.DeleteForever +import androidx.compose.material.icons.outlined.Folder +import androidx.compose.material.icons.outlined.FolderOff +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.core.content.FileProvider +import coil.ImageLoader +import coil.compose.AsyncImage +import coil.decode.VideoFrameDecoder +import coil.request.ImageRequest +import coil.request.videoFramePercent +import com.d4rk.cleaner.R +import com.d4rk.cleaner.data.model.ui.screens.UiHomeModel +import com.d4rk.cleaner.ui.components.NonLazyGrid +import com.d4rk.cleaner.ui.components.TwoRowButtons +import com.d4rk.cleaner.ui.components.animations.bounceClick +import com.d4rk.cleaner.ui.components.animations.hapticPagerSwipe +import com.d4rk.cleaner.utils.TimeHelper +import com.d4rk.cleaner.utils.cleaning.getFileIcon +import com.google.common.io.Files.getFileExtension +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@Composable +fun AnalyzeScreen( + imageLoader : ImageLoader , + view : View , + viewModel : HomeViewModel , + data : UiHomeModel +) { + val coroutineScope : CoroutineScope = rememberCoroutineScope() + val enabled = data.analyzeState.selectedFilesCount > 0 + + val filesTypesTitles = data.analyzeState.fileTypesData.fileTypesTitles + val apkExtensions = data.analyzeState.fileTypesData.apkExtensions + val imageExtensions = data.analyzeState.fileTypesData.imageExtensions + val videoExtensions = data.analyzeState.fileTypesData.videoExtensions + val audioExtensions = data.analyzeState.fileTypesData.audioExtensions + val archiveExtensions = data.analyzeState.fileTypesData.archiveExtensions + + val emptyFoldersString = stringResource(R.string.empty_folders) + + val groupedFiles = remember( + data.analyzeState, + ) { + val filesMap = data.analyzeState.scannedFileList.groupBy { file -> + when (file.extension.lowercase()) { + in imageExtensions -> { + return@groupBy filesTypesTitles[0] + } + in audioExtensions -> { + return@groupBy filesTypesTitles[1] + } + in videoExtensions -> { + return@groupBy filesTypesTitles[2] + } + in apkExtensions -> { + return@groupBy filesTypesTitles[3] + } + in archiveExtensions -> { + return@groupBy filesTypesTitles[4] + } + else -> { + return@groupBy filesTypesTitles[6] + } + } + }.filter { it.value.isNotEmpty() } + + val finalMap = filesMap.toMutableMap() + + if (data.analyzeState.emptyFolderList.isNotEmpty()) { + finalMap[emptyFoldersString] = data.analyzeState.emptyFolderList + } + + return@remember finalMap + } + + if (groupedFiles.isEmpty()) { + Box(modifier = Modifier.fillMaxSize()) { + CircularProgressIndicator() + } + } + + Column( + modifier = Modifier + .animateContentSize() + .fillMaxWidth() + .padding(16.dp) , + horizontalAlignment = Alignment.End + ) { + OutlinedCard( + modifier = Modifier + .weight(1f) + .fillMaxWidth() , + ) { + when { + data.analyzeState.scannedFileList.isEmpty() -> { + Box(modifier = Modifier.fillMaxSize() , contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + + groupedFiles.isEmpty() || data.analyzeState.isFileScanEmpty -> { + Box(modifier = Modifier.fillMaxSize() , contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + imageVector = Icons.Outlined.FolderOff , + contentDescription = null , + modifier = Modifier.size(64.dp) , + tint = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(id = R.string.no_files_found) , + style = MaterialTheme.typography.bodyLarge , + color = MaterialTheme.colorScheme.onSurface + ) + + OutlinedButton(modifier = Modifier.bounceClick() , onClick = { + viewModel.rescanFiles() + }) { + Icon( + modifier = Modifier.size(ButtonDefaults.IconSize) , + imageVector = Icons.Outlined.Refresh , + contentDescription = "Close" + ) + Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) + Text(text = "Try again") + } + } + } + } + + else -> { + val tabs = groupedFiles.keys.toList() + val pagerState : PagerState = rememberPagerState(pageCount = { tabs.size }) + + Row( + modifier = Modifier.fillMaxWidth() , + verticalAlignment = Alignment.CenterVertically + ) { + ScrollableTabRow( + selectedTabIndex = pagerState.currentPage , + modifier = Modifier.weight(1f) , + edgePadding = 0.dp , + indicator = { tabPositions -> + TabRowDefaults.PrimaryIndicator( + modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]) , + shape = RoundedCornerShape( + topStart = 3.dp , + topEnd = 3.dp , + bottomEnd = 0.dp , + bottomStart = 0.dp , + ) , + ) + } , + ) { + tabs.forEachIndexed { index , title -> + Tab(modifier = Modifier.bounceClick() , + selected = pagerState.currentPage == index , + onClick = { + view.playSoundEffect(SoundEffectConstants.CLICK) + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + } , + text = { Text(text = title) }) + } + } + + IconButton(modifier = Modifier.bounceClick() , onClick = { + view.playSoundEffect(SoundEffectConstants.CLICK) + viewModel.onCloseAnalyzeComposable() + }) { + Icon(imageVector = Icons.Outlined.Close , contentDescription = "Close") + } + } + + HorizontalPager( + modifier = Modifier.hapticPagerSwipe(pagerState) , + state = pagerState , + ) { page -> + val filesForCurrentPage = groupedFiles[tabs[page]] ?: emptyList() + + val filesByDate = filesForCurrentPage.groupBy { file -> + SimpleDateFormat( + "yyyy-MM-dd" , Locale.getDefault() + ).format(Date(file.lastModified())) + } + + FilesByDateSection( + modifier = Modifier , + filesByDate = filesByDate , + fileSelectionStates = data.analyzeState.fileSelectionMap , + imageLoader = imageLoader , + onFileSelectionChange = viewModel::onFileSelectionChange , + view = view , + ) + } + } + } + } + if (data.analyzeState.scannedFileList.isNotEmpty()) { + Row( + modifier = Modifier.fillMaxWidth() , + verticalAlignment = Alignment.CenterVertically , + horizontalArrangement = Arrangement.SpaceBetween , + ) { + val statusText : String = if (data.analyzeState.selectedFilesCount > 0) { + pluralStringResource( + id = R.plurals.status_selected_files , + count = data.analyzeState.selectedFilesCount , + data.analyzeState.selectedFilesCount + ) + } + else { + stringResource(id = R.string.status_no_files_selected) + } + val statusColor : Color by animateColorAsState( + targetValue = if (data.analyzeState.selectedFilesCount > 0) { + MaterialTheme.colorScheme.primary + } + else { + MaterialTheme.colorScheme.secondary + } , animationSpec = tween() , label = "Selected Files Status Color Animation" + ) + + Text( + text = statusText , + color = statusColor , + modifier = Modifier.animateContentSize() + ) + SelectAllComposable(viewModel = viewModel , view = view) + } + + TwoRowButtons( + modifier = Modifier , + enabled = enabled , + onStartButtonClick = { + viewModel.moveToTrash() + } , + onStartButtonIcon = Icons.Outlined.Delete , + onStartButtonText = R.string.move_to_trash , + + onEndButtonClick = { + viewModel.clean() + } , + onEndButtonIcon = Icons.Outlined.DeleteForever , + onEndButtonText = R.string.delete_forever , + view = view , + ) + } + } +} + +@Composable +fun FilesByDateSection( + modifier : Modifier , + filesByDate : Map> , + fileSelectionStates : Map , + imageLoader : ImageLoader , + onFileSelectionChange : (File , Boolean) -> Unit , + view : View , +) { + LazyColumn( + modifier = modifier.fillMaxSize() + ) { + val sortedDates = filesByDate.keys.sortedByDescending { dateString -> + SimpleDateFormat("yyyy-MM-dd" , Locale.getDefault()).parse(dateString) + } + sortedDates.forEach { date -> + val files = filesByDate[date] ?: emptyList() + item(key = date) { + DateHeader( + files = files , + fileSelectionStates = fileSelectionStates , + onFileSelectionChange = onFileSelectionChange , + view = view + ) + } + + item(key = "$date-grid") { + FilesGrid( + files = files , + imageLoader = imageLoader , + fileSelectionStates = fileSelectionStates , + onFileSelectionChange = onFileSelectionChange , + view = view + ) + } + } + } +} + +@Composable +fun DateHeader( + files : List , + fileSelectionStates : Map , + onFileSelectionChange : (File , Boolean) -> Unit , + view : View , +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp , vertical = 4.dp) , + verticalAlignment = Alignment.CenterVertically , + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier.padding(start = 8.dp) , + text = TimeHelper.formatDate(Date(files[0].lastModified())) + ) + val allFilesForDateSelected = files.all { fileSelectionStates[it] == true } + Checkbox(modifier = Modifier.bounceClick() , + checked = allFilesForDateSelected , + onCheckedChange = { isChecked -> + view.playSoundEffect(SoundEffectConstants.CLICK) + files.forEach { file -> + onFileSelectionChange(file , isChecked) + } + }) + } +} + +@Composable +fun FilesGrid( + files : List , + imageLoader : ImageLoader , + fileSelectionStates : Map , + onFileSelectionChange : (File , Boolean) -> Unit , + view : View , +) { + Box( + modifier = Modifier.fillMaxSize() + ) { + NonLazyGrid( + columns = 3 , itemCount = files.size , modifier = Modifier.padding(horizontal = 8.dp) + ) { index -> + val file = files[index] + FileCard( + file = file , + imageLoader = imageLoader , + isChecked = fileSelectionStates[file] == true , + onCheckedChange = { isChecked -> onFileSelectionChange(file , isChecked) } , + view = view + ) + } + } +} + +@Composable +fun FileCard( + file : File , imageLoader : ImageLoader , onCheckedChange : (Boolean) -> Unit , + isChecked : Boolean , + view : View , +) { + val isFolder = file.isDirectory + val context : Context = LocalContext.current + val fileExtension : String = remember(file.name) { getFileExtension(file.name) } + + val imageExtensions = + remember { context.resources.getStringArray(R.array.image_extensions).toList() } + val videoExtensions = + remember { context.resources.getStringArray(R.array.video_extensions).toList() } + + Card( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(ratio = 1f) + .bounceClick() + .clickable { + if (! file.isDirectory) { + val intent = Intent(Intent.ACTION_VIEW) + val uri = FileProvider.getUriForFile( + context , "${context.packageName}.fileprovider" , file + ) + intent.setDataAndType(uri , context.contentResolver.getType(uri)) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + context.startActivity(intent) + } + } , + ) { + Box(modifier = Modifier.fillMaxSize()) { + if (isFolder) { + Icon( + imageVector = Icons.Outlined.Folder , + contentDescription = "Folder icon" , + modifier = Modifier + .size(24.dp) + .align(Alignment.Center) + ) + } + else { + when (fileExtension) { + in imageExtensions -> { + AsyncImage( + model = remember(file) { + ImageRequest.Builder(context).data(file).size(64) + .crossfade(enable = true).build() + } , + imageLoader = imageLoader , + contentDescription = file.name , + contentScale = ContentScale.Crop , + modifier = Modifier.fillMaxSize() , + ) + } + + in videoExtensions -> { + AsyncImage( + model = remember(file) { + ImageRequest.Builder(context).data(file) + .decoderFactory { result , options , _ -> + VideoFrameDecoder(result.source , options) + }.videoFramePercent(framePercent = 0.5) + .crossfade(enable = true).build() + } , + imageLoader = imageLoader , + contentDescription = file.name , + contentScale = ContentScale.Crop , + modifier = Modifier.fillMaxSize() + ) + } + + else -> { + val fileIcon = remember(fileExtension) { + getFileIcon( + fileExtension , context + ) + } + Icon( + painter = painterResource(fileIcon) , + contentDescription = null , + modifier = Modifier + .size(24.dp) + .align(Alignment.Center) + ) + } + } + } + + Checkbox( + checked = isChecked , onCheckedChange = { checked -> + view.playSoundEffect(SoundEffectConstants.CLICK) + onCheckedChange(checked) + } , modifier = Modifier.align(Alignment.TopEnd) + ) + + Text( + text = file.name , + maxLines = 1 , + overflow = TextOverflow.Ellipsis , + modifier = Modifier + .fillMaxWidth() + .background( + color = Color.Black.copy(alpha = 0.4f) + ) + .padding(8.dp) + .align(Alignment.BottomCenter) + ) + } + } +} + +/** + * Composable function for selecting or deselecting all items. + * + * This composable displays a filter chip labeled "Select All". When tapped, it toggles the + * selection state and invokes the `onCheckedChange` callback. + * + * @param checked A boolean value indicating whether all items are currently selected. + * @param onCheckedChange A callback function that is invoked when the user taps the chip to change the selection state. + */ +@Composable +fun SelectAllComposable( + viewModel : HomeViewModel , + view : View , +) { + val uiState : UiHomeModel by viewModel.uiState.collectAsState() + + Row( + modifier = Modifier + .fillMaxWidth() + .animateContentSize() , + verticalAlignment = Alignment.CenterVertically , + horizontalArrangement = Arrangement.End + ) { + val interactionSource : MutableInteractionSource = remember { MutableInteractionSource() } + FilterChip( + modifier = Modifier.bounceClick() , + selected = uiState.analyzeState.areAllFilesSelected , + onClick = { + view.playSoundEffect(SoundEffectConstants.CLICK) + viewModel.toggleSelectAllFiles() + } , + label = { Text(text = stringResource(id = R.string.select_all)) } , + leadingIcon = { + AnimatedContent( + targetState = uiState.analyzeState.areAllFilesSelected , + label = "Checkmark Animation" + ) { targetChecked -> + if (targetChecked) { + Icon( + imageVector = Icons.Filled.Check , + contentDescription = null , + ) + } + } + } , + interactionSource = interactionSource , + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeScreen.kt index 483462e..92a151a 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeScreen.kt @@ -2,642 +2,100 @@ package com.d4rk.cleaner.ui.screens.home import android.app.Activity import android.content.Context -import android.content.Intent -import android.view.SoundEffectConstants import android.view.View -import androidx.compose.animation.AnimatedContent import androidx.compose.animation.Crossfade -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -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.lazy.LazyColumn -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.PagerState -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.outlined.Close -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material.icons.outlined.DeleteForever -import androidx.compose.material.icons.outlined.Folder -import androidx.compose.material.icons.outlined.FolderOff -import androidx.compose.material.icons.outlined.Refresh -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.FilterChip -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.ScrollableTabRow -import androidx.compose.material3.Tab -import androidx.compose.material3.TabRowDefaults -import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.res.stringArrayResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.core.content.FileProvider import androidx.lifecycle.viewmodel.compose.viewModel import coil.ImageLoader -import coil.compose.AsyncImage -import coil.decode.VideoFrameDecoder import coil.disk.DiskCache import coil.memory.MemoryCache -import coil.request.ImageRequest -import coil.request.videoFramePercent -import com.d4rk.cleaner.R import com.d4rk.cleaner.data.model.ui.error.UiErrorModel import com.d4rk.cleaner.data.model.ui.screens.UiHomeModel import com.d4rk.cleaner.ui.components.CircularDeterminateIndicator -import com.d4rk.cleaner.ui.components.NonLazyGrid -import com.d4rk.cleaner.ui.components.TwoRowButtons -import com.d4rk.cleaner.ui.components.animations.bounceClick -import com.d4rk.cleaner.ui.components.animations.hapticPagerSwipe import com.d4rk.cleaner.ui.components.dialogs.ErrorAlertDialog import com.d4rk.cleaner.utils.PermissionsUtils -import com.d4rk.cleaner.utils.TimeHelper -import com.d4rk.cleaner.utils.cleaning.getFileIcon -import com.google.common.io.Files.getFileExtension -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import java.io.File -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale @Composable fun HomeScreen() { - val context: Context = LocalContext.current - val view: View = LocalView.current - val viewModel: HomeViewModel = viewModel() - val uiState: UiHomeModel by viewModel.uiState.collectAsState() - val uiErrorModel: UiErrorModel by viewModel.uiErrorModel.collectAsState() - val imageLoader: ImageLoader = remember { + val context : Context = LocalContext.current + val view : View = LocalView.current + val viewModel : HomeViewModel = viewModel() + + + val uiState : UiHomeModel by viewModel.uiState.collectAsState() + val uiErrorModel : UiErrorModel by viewModel.uiErrorModel.collectAsState() + val imageLoader : ImageLoader = remember { ImageLoader.Builder(context).memoryCache { MemoryCache.Builder(context).maxSizePercent(percent = 0.24).build() }.diskCache { DiskCache.Builder().directory(context.cacheDir.resolve(relative = "image_cache")) - .maxSizePercent(percent = 0.02).build() + .maxSizePercent(percent = 0.02).build() }.build() } LaunchedEffect(Unit) { - if (!PermissionsUtils.hasStoragePermissions(context)) { + // viewModel.populateFileTypesData() + + // Permissions + if (! PermissionsUtils.hasStoragePermissions(context)) { PermissionsUtils.requestStoragePermissions(context as Activity) } } if (uiErrorModel.showErrorDialog) { - ErrorAlertDialog(errorMessage = uiErrorModel.errorMessage, - onDismiss = { viewModel.dismissErrorDialog() }) + ErrorAlertDialog(errorMessage = uiErrorModel.errorMessage , + onDismiss = { viewModel.dismissErrorDialog() }) } Column(modifier = Modifier.fillMaxSize()) { Box( modifier = Modifier - .weight(4f) - .fillMaxWidth() + .weight(4f) + .fillMaxWidth() ) { - if (!uiState.showCleaningComposable) { - CircularDeterminateIndicator(progress = uiState.progress, - modifier = Modifier - .align(Alignment.TopCenter) - .offset(y = 98.dp), - onClick = { - viewModel.analyze() - }) + if (! uiState.analyzeState.isAnalyzeScreenVisible) { + CircularDeterminateIndicator(progress = uiState.storageUsageProgress , + modifier = Modifier + .align(Alignment.TopCenter) + .offset(y = 98.dp) , + onClick = { + viewModel.analyze() + }) } Crossfade( - targetState = uiState.showCleaningComposable, - animationSpec = tween(durationMillis = 300), + targetState = uiState.analyzeState.isAnalyzeScreenVisible , + animationSpec = tween(durationMillis = 300) , label = "" ) { showCleaningComposable -> if (showCleaningComposable) { - AnalyzeComposable(imageLoader = imageLoader, context = context, view = view) - } - } - } - } -} - -@Composable -fun AnalyzeComposable(imageLoader: ImageLoader, context: Context, view: View) { - val viewModel: HomeViewModel = viewModel() - val uiState: UiHomeModel by viewModel.uiState.collectAsState() - val coroutineScope: CoroutineScope = rememberCoroutineScope() - val enabled = uiState.selectedFileCount > 0 - val emptyFoldersString = stringResource(R.string.empty_folders) - val apkExtensions = remember { context.resources.getStringArray(R.array.apk_extensions) } - val imageExtensions = remember { context.resources.getStringArray(R.array.image_extensions) } - val videoExtensions = remember { context.resources.getStringArray(R.array.video_extensions) } - val audioExtensions = remember { context.resources.getStringArray(R.array.audio_extensions) } - val archiveExtensions = - remember { context.resources.getStringArray(R.array.archive_extensions) } - - val filesTypesStrings: List = stringArrayResource(R.array.file_types_titles).toList() - - val groupedFiles = remember( - uiState.scannedFiles, - uiState.emptyFolders, - ) { - val filesMap = uiState.scannedFiles.groupBy { file -> - when (file.extension.lowercase()) { - in imageExtensions -> { - return@groupBy filesTypesStrings[0] - } - - in apkExtensions -> { - return@groupBy filesTypesStrings[3] - } - - in videoExtensions -> { - return@groupBy filesTypesStrings[2] - } - - in audioExtensions -> { - return@groupBy filesTypesStrings[1] - } - - in archiveExtensions -> { - return@groupBy filesTypesStrings[4] - } - - else -> { - return@groupBy filesTypesStrings[5] - } - } - } - - if (uiState.emptyFolders.isNotEmpty()) { - return@remember filesMap + (emptyFoldersString to uiState.emptyFolders) - } else { - return@remember filesMap - } - } - - Column( - modifier = Modifier - .animateContentSize() - .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.End - ) { - OutlinedCard( - modifier = Modifier - .weight(1f) - .fillMaxWidth(), - ) { - when { - uiState.scannedFiles.isEmpty() -> { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } - } - - uiState.noFilesFound -> { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Icon( - imageVector = Icons.Outlined.FolderOff, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.onSurface - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(id = R.string.no_files_found), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface - ) - - OutlinedButton(modifier = Modifier.bounceClick(), onClick = { - viewModel.rescanFiles() - }) { - Icon( - modifier = Modifier.size(ButtonDefaults.IconSize), - imageVector = Icons.Outlined.Refresh, - contentDescription = "Close" - ) - Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) - Text(text = "Try again") - } - } - } - } - - else -> { - val tabs = groupedFiles.keys.toList() - val pagerState: PagerState = rememberPagerState(pageCount = { tabs.size }) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - ScrollableTabRow( - selectedTabIndex = pagerState.currentPage, - modifier = Modifier.weight(1f), - edgePadding = 0.dp, - indicator = { tabPositions -> - TabRowDefaults.PrimaryIndicator( - modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]), - shape = RoundedCornerShape( - topStart = 3.dp, - topEnd = 3.dp, - bottomEnd = 0.dp, - bottomStart = 0.dp, - ), - ) - }, - ) { - tabs.forEachIndexed { index, title -> - Tab(modifier = Modifier.bounceClick(), - selected = pagerState.currentPage == index, - onClick = { - view.playSoundEffect(SoundEffectConstants.CLICK) - coroutineScope.launch { - pagerState.animateScrollToPage(index) - } - }, - text = { Text(text = title) }) - } - } - - IconButton(modifier = Modifier.bounceClick(), onClick = { - view.playSoundEffect(SoundEffectConstants.CLICK) - viewModel.onCloseAnalyzeComposable() - }) { - Icon(imageVector = Icons.Outlined.Close, contentDescription = "Close") - } - } - - HorizontalPager( - modifier = Modifier.hapticPagerSwipe(pagerState), - state = pagerState, - ) { page -> - val filesForCurrentPage = groupedFiles[tabs[page]] ?: emptyList() - - val filesByDate = filesForCurrentPage.groupBy { file -> - SimpleDateFormat( - "yyyy-MM-dd", Locale.getDefault() - ).format(Date(file.lastModified())) - } - - FilesByDateSection( - modifier = Modifier, - filesByDate = filesByDate, - fileSelectionStates = uiState.fileSelectionStates, - imageLoader = imageLoader, - onFileSelectionChange = viewModel::onFileSelectionChange, - view = view, + key(uiState.analyzeState.fileTypesData) { + AnalyzeScreen( + imageLoader = imageLoader , + view = view , + viewModel = viewModel , + data = uiState ) } } } } - if (uiState.scannedFiles.isNotEmpty()) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - val statusText: String = if (uiState.selectedFileCount > 0) { - pluralStringResource( - id = R.plurals.status_selected_files, - count = uiState.selectedFileCount, - uiState.selectedFileCount - ) - } else { - stringResource(id = R.string.status_no_files_selected) - } - val statusColor: Color by animateColorAsState( - targetValue = if (uiState.selectedFileCount > 0) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.secondary - }, animationSpec = tween(), label = "Selected Files Status Color Animation" - ) - - Text( - text = statusText, - color = statusColor, - modifier = Modifier.animateContentSize() - ) - SelectAllComposable(viewModel = viewModel, view = view) - } - - TwoRowButtons( - modifier = Modifier, - enabled = enabled, - onStartButtonClick = { - viewModel.moveToTrash() - }, - onStartButtonIcon = Icons.Outlined.Delete, - onStartButtonText = R.string.move_to_trash, - - onEndButtonClick = { - viewModel.clean() - }, - onEndButtonIcon = Icons.Outlined.DeleteForever, - onEndButtonText = R.string.delete_forever, - view = view, - ) - } - } -} - -@Composable -fun FilesByDateSection( - modifier: Modifier, - filesByDate: Map>, - fileSelectionStates: Map, - imageLoader: ImageLoader, - onFileSelectionChange: (File, Boolean) -> Unit, - view: View -) { - LazyColumn( - modifier = modifier.fillMaxSize() - ) { - val sortedDates = filesByDate.keys.sortedByDescending { dateString -> - SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(dateString) - } - sortedDates.forEach { date -> - val files = filesByDate[date] ?: emptyList() - item(key = date) { - DateHeader( - files = files, - fileSelectionStates = fileSelectionStates, - onFileSelectionChange = onFileSelectionChange, - view = view - ) - } - - item(key = "$date-grid") { - FilesGrid( - files = files, - imageLoader = imageLoader, - fileSelectionStates = fileSelectionStates, - onFileSelectionChange = onFileSelectionChange, - view = view - ) - } - } - } -} - -@Composable -fun DateHeader( - files: List, - fileSelectionStates: Map, - onFileSelectionChange: (File, Boolean) -> Unit, - view: View -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - modifier = Modifier.padding(start = 8.dp), - text = TimeHelper.formatDate(Date(files[0].lastModified())) - ) - val allFilesForDateSelected = files.all { fileSelectionStates[it] == true } - Checkbox(modifier = Modifier.bounceClick(), - checked = allFilesForDateSelected, - onCheckedChange = { isChecked -> - view.playSoundEffect(SoundEffectConstants.CLICK) - files.forEach { file -> - onFileSelectionChange(file, isChecked) - } - }) - } -} - -@Composable -fun FilesGrid( - files: List, - imageLoader: ImageLoader, - fileSelectionStates: Map, - onFileSelectionChange: (File, Boolean) -> Unit, - view: View -) { - Box( - modifier = Modifier.fillMaxSize() - ) { - NonLazyGrid( - columns = 3, itemCount = files.size, modifier = Modifier.padding(horizontal = 8.dp) - ) { index -> - val file = files[index] - FileCard( - file = file, - imageLoader = imageLoader, - isChecked = fileSelectionStates[file] == true, - onCheckedChange = { isChecked -> onFileSelectionChange(file, isChecked) }, - view = view - ) - } - } -} - -@Composable -fun FileCard( - file: File, imageLoader: ImageLoader, onCheckedChange: (Boolean) -> Unit, - isChecked: Boolean, - view: View, -) { - val isFolder = file.isDirectory - val context: Context = LocalContext.current - val fileExtension: String = remember(file.name) { getFileExtension(file.name) } - - val imageExtensions = - remember { context.resources.getStringArray(R.array.image_extensions).toList() } - val videoExtensions = - remember { context.resources.getStringArray(R.array.video_extensions).toList() } - - Card( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(ratio = 1f) - .bounceClick() - .clickable { - if (!file.isDirectory) { - val intent = Intent(Intent.ACTION_VIEW) - val uri = FileProvider.getUriForFile( - context, - "${context.packageName}.fileprovider", - file - ) - intent.setDataAndType(uri, context.contentResolver.getType(uri)) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - context.startActivity(intent) - } - }, - ) { - Box(modifier = Modifier.fillMaxSize()) { - if (isFolder) { - Icon( - imageVector = Icons.Outlined.Folder, - contentDescription = "Folder icon", - modifier = Modifier - .size(24.dp) - .align(Alignment.Center) - ) - } else { - when (fileExtension) { - in imageExtensions -> { - AsyncImage( - model = remember(file) { - ImageRequest.Builder(context).data(file).size(64) - .crossfade(enable = true).build() - }, - imageLoader = imageLoader, - contentDescription = file.name, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize(), - ) - } - - in videoExtensions -> { - AsyncImage( - model = remember(file) { - ImageRequest.Builder(context).data(file) - .decoderFactory { result, options, _ -> - VideoFrameDecoder(result.source, options) - }.videoFramePercent(framePercent = 0.5) - .crossfade(enable = true).build() - }, - imageLoader = imageLoader, - contentDescription = file.name, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - } - - else -> { - val fileIcon = remember(fileExtension) { - getFileIcon( - fileExtension, context - ) - } - Icon( - painter = painterResource(fileIcon), - contentDescription = null, - modifier = Modifier - .size(24.dp) - .align(Alignment.Center) - ) - } - } - } - - Checkbox( - checked = isChecked, - onCheckedChange = { checked -> - view.playSoundEffect(SoundEffectConstants.CLICK) - onCheckedChange(checked) - }, - modifier = Modifier.align(Alignment.TopEnd) - ) - - Text( - text = file.name, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .fillMaxWidth() - .background( - color = Color.Black.copy(alpha = 0.4f) - ) - .padding(8.dp) - .align(Alignment.BottomCenter) - ) - } - } -} - -/** - * Composable function for selecting or deselecting all items. - * - * This composable displays a filter chip labeled "Select All". When tapped, it toggles the - * selection state and invokes the `onCheckedChange` callback. - * - * @param checked A boolean value indicating whether all items are currently selected. - * @param onCheckedChange A callback function that is invoked when the user taps the chip to change the selection state. - */ -@Composable -fun SelectAllComposable( - viewModel: HomeViewModel, - view: View, -) { - val uiState: UiHomeModel by viewModel.uiState.collectAsState() - - Row( - modifier = Modifier - .fillMaxWidth() - .animateContentSize(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End - ) { - val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } - FilterChip( - modifier = Modifier.bounceClick(), - selected = uiState.allFilesSelected, - onClick = { - view.playSoundEffect(SoundEffectConstants.CLICK) - viewModel.toggleSelectAllFiles() - }, - label = { Text(text = stringResource(id = R.string.select_all)) }, - leadingIcon = { - AnimatedContent( - targetState = uiState.allFilesSelected, label = "Checkmark Animation" - ) { targetChecked -> - if (targetChecked) { - Icon( - imageVector = Icons.Filled.Check, - contentDescription = null, - ) - } - } - }, - interactionSource = interactionSource, - ) } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeViewModel.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeViewModel.kt index 843b2be..60d60db 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeViewModel.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeViewModel.kt @@ -9,6 +9,7 @@ import com.d4rk.cleaner.ui.viewmodel.BaseViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.io.File @@ -19,12 +20,17 @@ class HomeViewModel(application: Application) : BaseViewModel(application) { init { updateStorageInfo() + populateFileTypesData() } private fun updateStorageInfo() { viewModelScope.launch(coroutineExceptionHandler) { repository.getStorageInfo { uiHomeModel -> - _uiState.value = uiHomeModel + _uiState.update { it.copy( + storageUsageProgress = uiHomeModel.storageUsageProgress, + usedStorageFormatted = uiHomeModel.usedStorageFormatted, + totalStorageFormatted = uiHomeModel.totalStorageFormatted + ) } } } } @@ -35,96 +41,119 @@ class HomeViewModel(application: Application) : BaseViewModel(application) { repository.analyzeFiles { result -> val filteredFiles = result.first val emptyFolders = result.second - - _uiState.value = _uiState.value.copy( - scannedFiles = filteredFiles, - emptyFolders = emptyFolders, - showCleaningComposable = true, - noFilesFound = filteredFiles.isEmpty() && emptyFolders.isEmpty(), - ) + _uiState.update { currentUiState -> + currentUiState.copy( + analyzeState = currentUiState.analyzeState.copy( + scannedFileList = filteredFiles, + emptyFolderList = emptyFolders, + isAnalyzeScreenVisible = true, + isFileScanEmpty = filteredFiles.isEmpty() && emptyFolders.isEmpty() + ) + ) + } } hideLoading() } } - fun onCloseAnalyzeComposable() { - viewModelScope.launch(coroutineExceptionHandler) { - _uiState.value = _uiState.value.copy(showCleaningComposable = false) - } - } - fun rescanFiles() { viewModelScope.launch(context = Dispatchers.Default + coroutineExceptionHandler) { showLoading() - _uiState.value = _uiState.value.copy(scannedFiles = emptyList()) - repository.rescanFiles { filteredFiles -> - _uiState.value = _uiState.value.copy( - scannedFiles = filteredFiles, showCleaningComposable = true + _uiState.update { currentUiState -> + currentUiState.copy( + analyzeState = currentUiState.analyzeState.copy( + scannedFileList = emptyList() + ) ) } + repository.rescanFiles { filteredFiles -> + _uiState.update { currentUiState -> + currentUiState.copy( + analyzeState = currentUiState.analyzeState.copy( + scannedFileList = filteredFiles, + isAnalyzeScreenVisible = true + ) + ) + } + } hideLoading() } } + fun onCloseAnalyzeComposable() { + viewModelScope.launch(coroutineExceptionHandler) { + _uiState.update { currentUiState -> + currentUiState.copy( + analyzeState = currentUiState.analyzeState.copy( + isAnalyzeScreenVisible = false + ) + ) + } + } + } + fun onFileSelectionChange(file: File, isChecked: Boolean) { viewModelScope.launch(coroutineExceptionHandler) { - val updatedFileSelectionStates = - _uiState.value.fileSelectionStates + (file to isChecked) + val updatedFileSelectionStates = _uiState.value.analyzeState.fileSelectionMap + (file to isChecked) val newSelectedCount = updatedFileSelectionStates.count { it.value } - _uiState.value = _uiState.value.copy( - fileSelectionStates = updatedFileSelectionStates, - selectedFileCount = newSelectedCount, - allFilesSelected = when { - newSelectedCount == _uiState.value.scannedFiles.size && newSelectedCount > 0 -> { - true - } - - newSelectedCount == 0 -> { - false - } - - isChecked -> { - _uiState.value.allFilesSelected - } - - else -> { - false - } - } - ) + _uiState.update { currentUiState -> + currentUiState.copy( + analyzeState = currentUiState.analyzeState.copy( + fileSelectionMap = updatedFileSelectionStates, + selectedFilesCount = newSelectedCount, + areAllFilesSelected = when { + newSelectedCount == currentUiState.analyzeState.scannedFileList.size && newSelectedCount > 0 -> true + newSelectedCount == 0 -> false + isChecked -> currentUiState.analyzeState.areAllFilesSelected + else -> false + } + ) + ) + } } } fun toggleSelectAllFiles() { viewModelScope.launch(context = Dispatchers.Default + coroutineExceptionHandler) { - val newState = !_uiState.value.allFilesSelected - _uiState.value = _uiState.value.copy(allFilesSelected = newState, - fileSelectionStates = if (newState) { - _uiState.value.scannedFiles.associateWith { true } - } else { - emptyMap() - }, - selectedFileCount = if (newState) { - _uiState.value.scannedFiles.size - } else { - 0 - }) + val newState = !_uiState.value.analyzeState.areAllFilesSelected + _uiState.update { currentUiState -> + currentUiState.copy( + analyzeState = currentUiState.analyzeState.copy( + areAllFilesSelected = newState, + fileSelectionMap = if (newState) { + currentUiState.analyzeState.scannedFileList.associateWith { true } + } else { + emptyMap() + }, + selectedFilesCount = if (newState) { + currentUiState.analyzeState.scannedFileList.size + } else { + 0 + } + ) + ) + } } } fun clean() { viewModelScope.launch(context = Dispatchers.Default + coroutineExceptionHandler) { - val filesToDelete = _uiState.value.fileSelectionStates.filter { it.value }.keys + val filesToDelete = _uiState.value.analyzeState.fileSelectionMap.filter { it.value }.keys showLoading() repository.deleteFiles(filesToDelete) { - _uiState.value = - _uiState.value.copy(scannedFiles = uiState.value.scannedFiles.filterNot { - filesToDelete.contains(it) - }, - selectedFileCount = 0, - allFilesSelected = false, - fileSelectionStates = emptyMap()) + _uiState.update { currentUiState -> + currentUiState.copy( + analyzeState = currentUiState.analyzeState.copy( + scannedFileList = currentUiState.analyzeState.scannedFileList.filterNot { + filesToDelete.contains(it) + }, + selectedFilesCount = 0, + areAllFilesSelected = false, + fileSelectionMap = emptyMap() + ) + ) + } updateStorageInfo() } hideLoading() @@ -133,19 +162,38 @@ class HomeViewModel(application: Application) : BaseViewModel(application) { fun moveToTrash() { viewModelScope.launch(context = Dispatchers.Default + coroutineExceptionHandler) { - val filesToMove = _uiState.value.fileSelectionStates.filter { it.value }.keys.toList() + val filesToMove = _uiState.value.analyzeState.fileSelectionMap.filter { it.value }.keys.toList() showLoading() repository.moveToTrash(filesToMove) { - _uiState.value = - _uiState.value.copy(scannedFiles = uiState.value.scannedFiles.filterNot { existingFile -> - filesToMove.any { movedFile -> existingFile.absolutePath == movedFile.absolutePath } - }, - selectedFileCount = 0, - allFilesSelected = false, - fileSelectionStates = emptyMap()) + _uiState.update { currentUiState -> + currentUiState.copy( + analyzeState = currentUiState.analyzeState.copy( + scannedFileList = currentUiState.analyzeState.scannedFileList.filterNot { existingFile -> + filesToMove.any { movedFile -> existingFile.absolutePath == movedFile.absolutePath } + }, + selectedFilesCount = 0, + areAllFilesSelected = false, + fileSelectionMap = emptyMap() + ) + ) + } updateStorageInfo() } hideLoading() } } + + private fun populateFileTypesData() { + viewModelScope.launch(coroutineExceptionHandler) { + repository.getFileTypesData { fileTypesData -> + _uiState.update { currentUiState -> + currentUiState.copy( + analyzeState = currentUiState.analyzeState.copy( + fileTypesData = fileTypesData + ) + ) + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/repository/HomeRepository.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/repository/HomeRepository.kt index ba0e626..8b95872 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/repository/HomeRepository.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/repository/HomeRepository.kt @@ -3,6 +3,7 @@ package com.d4rk.cleaner.ui.screens.home.repository import android.app.Application import android.os.Environment import com.d4rk.cleaner.data.datastore.DataStore +import com.d4rk.cleaner.data.model.ui.screens.FileTypesData import com.d4rk.cleaner.data.model.ui.screens.UiHomeModel import com.d4rk.cleaner.utils.cleaning.FileScanner import kotlinx.coroutines.Dispatchers @@ -23,6 +24,15 @@ class HomeRepository( } } + suspend fun getFileTypesData(onSuccess: (FileTypesData) -> Unit) { + withContext(Dispatchers.IO) { + val fileTypesData = getFileTypesDataFromResources() + withContext(Dispatchers.Main) { + onSuccess(fileTypesData) + } + } + } + suspend fun analyzeFiles(onSuccess : (Pair , List>) -> Unit) { withContext(Dispatchers.IO) { val (filteredFiles , emptyFolders) = fileScanner.getAllFiles() diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/repository/HomeRepositoryImplementation.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/repository/HomeRepositoryImplementation.kt index 9d70289..358051a 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/repository/HomeRepositoryImplementation.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/repository/HomeRepositoryImplementation.kt @@ -4,7 +4,9 @@ import android.app.Application import android.content.Context import android.media.MediaScannerConnection import android.os.Environment +import com.d4rk.cleaner.R import com.d4rk.cleaner.data.datastore.DataStore +import com.d4rk.cleaner.data.model.ui.screens.FileTypesData import com.d4rk.cleaner.data.model.ui.screens.UiHomeModel import com.d4rk.cleaner.utils.cleaning.StorageUtils import java.io.File @@ -27,13 +29,34 @@ abstract class HomeRepositoryImplementation( StorageUtils.getStorageInfo(application) { used , total , usageProgress -> continuation.resume( UiHomeModel( - progress = usageProgress , storageUsed = used , storageTotal = total + storageUsageProgress = usageProgress , usedStorageFormatted = used , totalStorageFormatted = total ) ) } } } + suspend fun getFileTypesDataFromResources(): FileTypesData { + return suspendCoroutine { continuation -> + val apkExtensions = application.resources.getStringArray(R.array.apk_extensions).toList() + val imageExtensions = application.resources.getStringArray(R.array.image_extensions).toList() + val videoExtensions = application.resources.getStringArray(R.array.video_extensions).toList() + val audioExtensions = application.resources.getStringArray(R.array.audio_extensions).toList() + val archiveExtensions = application.resources.getStringArray(R.array.archive_extensions).toList() + val fileTypesTitles = application.resources.getStringArray(R.array.file_types_titles).toList() + val fileTypesData = FileTypesData( + apkExtensions = apkExtensions , + imageExtensions = imageExtensions , + videoExtensions = videoExtensions , + audioExtensions = audioExtensions , // FIXME: Type mismatch: inferred type is Array<(out) String!> but IntArray was expected + archiveExtensions = archiveExtensions , + fileTypesTitles = fileTypesTitles + ) + continuation.resume(fileTypesData) + } + } + + fun deleteFiles(filesToDelete : Set) { filesToDelete.forEach { file -> if (file.exists()) {