diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fdbfc6d..9f899ca 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,7 +14,7 @@ android { applicationId = "com.d4rk.cleaner" minSdk = 23 targetSdk = 34 - versionCode = 125 + versionCode = 127 versionName = "2.0.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" resourceConfigurations += listOf( 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 91758d7..2c12425 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 @@ -12,12 +12,15 @@ 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.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState @@ -26,6 +29,12 @@ 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.FolderOff +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator @@ -33,6 +42,7 @@ 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 @@ -148,6 +158,7 @@ fun AnalyzeComposable(imageLoader : ImageLoader) { val viewModel : HomeViewModel = viewModel() val uiState : UiHomeModel by viewModel.uiState.collectAsState() val context = LocalContext.current + val activity = LocalContext.current as Activity val coroutineScope : CoroutineScope = rememberCoroutineScope() val apkExtensions = remember { context.resources.getStringArray(R.array.apk_extensions) } @@ -204,119 +215,152 @@ fun AnalyzeComposable(imageLoader : ImageLoader) { OutlinedCard( modifier = Modifier .weight(1f) - .fillMaxWidth() , + .fillMaxWidth(), ) { - if (uiState.isAnalyzing && uiState.scannedFiles.isEmpty()) { - Box(modifier = Modifier.fillMaxSize() , contentAlignment = Alignment.Center) { - CircularProgressIndicator() + when { + uiState.isAnalyzing && uiState.scannedFiles.isEmpty() -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } } - } - 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, - ), + uiState.scannedFiles.isEmpty() -> { + 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 ) - }, - ) { - tabs.forEachIndexed { index, title -> - Tab( - modifier = Modifier.bounceClick(), - selected = pagerState.currentPage == index, - onClick = { - coroutineScope.launch { - pagerState.animateScrollToPage(index) - } - }, - text = { Text(text = title) } + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(id = R.string.no_files_found), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface ) - } - } - IconButton( - modifier = Modifier, - onClick = { - // TODO: Add close action + OutlinedButton ( + modifier = Modifier.bounceClick(), + onClick = { + // TODO: Add close action + } + ) { + Icon(modifier = Modifier.size(ButtonDefaults.IconSize) , imageVector = Icons.Outlined.Refresh , contentDescription = "Close") + Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) + Text("Try again") + } } - ) { - Icon(imageVector = Icons.Outlined.Close, contentDescription = "Close") } } + 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 = { + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + }, + text = { Text(text = title) } + ) + } + } - 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())) + IconButton( + modifier = Modifier.bounceClick(), + onClick = { + // TODO: Add close action + } + ) { + Icon(imageVector = Icons.Outlined.Close, contentDescription = "Close") + } } - LazyColumn( - modifier = Modifier.fillMaxSize() , - ) { - filesByDate.forEach { (date , files) -> - item(key = date) { - 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 { uiState.fileSelectionStates[it] == true } - Checkbox(checked = allFilesForDateSelected , - onCheckedChange = { isChecked -> - files.forEach { file -> - viewModel.onFileSelectionChange( - file , - isChecked - ) - } - }) - } - } + 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())) + } - item(key = "$date-grid") { - Box( - modifier = Modifier.fillMaxSize() - ) { - NonLazyGrid( - columns = 3 , - itemCount = files.size , - modifier = Modifier.padding(horizontal = 8.dp) - ) { index -> - FileCard( - file = files[index] , - viewModel = viewModel , - imageLoader = imageLoader + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + filesByDate.forEach { (date, files) -> + item(key = date) { + 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 { uiState.fileSelectionStates[it] == true } + Checkbox( + modifier = Modifier.bounceClick(), + checked = allFilesForDateSelected, + onCheckedChange = { isChecked -> + files.forEach { file -> + viewModel.onFileSelectionChange( + file, + isChecked + ) + } + }) + } + } + + item(key = "$date-grid") { + Box( + modifier = Modifier.fillMaxSize() + ) { + NonLazyGrid( + columns = 3, + itemCount = files.size, + modifier = Modifier.padding(horizontal = 8.dp) + ) { index -> + FileCard( + file = files[index], + viewModel = viewModel, + imageLoader = imageLoader + ) + } } } } @@ -325,30 +369,72 @@ fun AnalyzeComposable(imageLoader : ImageLoader) { } } } - Row( - modifier = Modifier.fillMaxWidth() , - verticalAlignment = Alignment.CenterVertically , - horizontalArrangement = Arrangement.SpaceBetween , - ) { - val statusText : String = if (uiState.selectedFileCount > 0) { - stringResource(id = R.string.status_selected_files , uiState.selectedFileCount) - } - else { - stringResource(id = R.string.status_no_files_selected) + if (uiState.scannedFiles.isNotEmpty()) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + val statusText: String = if (uiState.selectedFileCount > 0) { + stringResource(id = R.string.status_selected_files, 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) } - val statusColor : Color by animateColorAsState( - targetValue = if (uiState.selectedFileCount > 0) { - MaterialTheme.colorScheme.primary + + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceAround + ) { + OutlinedButton( + onClick = { + // TODO: add trash + }, + modifier = Modifier.weight(1f).bounceClick(), + colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.Black) + ) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = "Move to trash", + modifier = Modifier.size(ButtonDefaults.IconSize) + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text("Move to trash") } - else { - MaterialTheme.colorScheme.secondary - } , animationSpec = tween() , label = "Selected Files Status Color Animation" - ) - Text( - text = statusText , color = statusColor , modifier = Modifier.animateContentSize() - ) - SelectAllComposable(viewModel) + Spacer(Modifier.width(8.dp)) + + Button( + onClick = { + viewModel.clean(activity) + }, + modifier = Modifier.weight(1f).bounceClick(), + colors = ButtonDefaults.buttonColors(contentColor = Color.White) + ) { + Icon( + imageVector = Icons.Outlined.DeleteForever, + contentDescription = "Delete forever", + modifier = Modifier.size(ButtonDefaults.IconSize) + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text(text = "Delete forever") + } + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 011a313..1621261 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -271,6 +271,7 @@ + No files found Invalid input or argument provided. Please check the information and try again.