Skip to content

Commit 9976ade

Browse files
committed
feat: Add Clean Build feature to find and delete build folders
This commit introduces a new "Clean Build" feature, allowing users to scan for and delete `build` directories within their projects to free up disk space. The feature includes a dedicated screen with the following functionalities: * Browsing for a root project directory (e.g., `AndroidStudioProjects`). * Scanning the selected directory to identify Gradle projects and their `build` folders. * Displaying a detailed, hierarchical view of projects and their modules, along with the size of each `build` folder. * Functionality to select/deselect all, expand/collapse all, and individually select projects or modules for deletion. * A floating action button to initiate the deletion process, showing the number of selected folders and the total space that will be freed. * Confirmation and result dialogs to ensure a safe and clear user experience. ### Key Changes: * **`composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/screen/cleanbuild`**: Added a new screen package containing the UI (`CleanBuildScreen.kt`), ViewModel (`CleanBuildViewModel.kt`), UI state (`CleanBuildUiState.kt`), and user intents (`CleanBuildIntent.kt`). * **`composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/data`**: * Created `CleanBuildRepository` and its implementation to handle the logic for scanning projects and deleting folders. * Defined new data models `ProjectBuildInfo` and `ModuleBuild` for the feature. * **`composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/di`**: Updated `RepositoryModule.kt` and `ViewModule.kt` to provide dependencies for the new feature. * **`composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/navigation`**: * Integrated the "Clean Build" screen into the app's navigation graph (`AppNavigation.kt`, `AppRoute.kt`). * Added a new "Clean Build" item to the main navigation rail (`NavigationItem.kt`). * **`composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/core/utility/Utils.kt`**: Added a `formatElapsedTime` utility function.
1 parent 0363d58 commit 9976ade

File tree

14 files changed

+1496
-0
lines changed

14 files changed

+1496
-0
lines changed

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/core/utility/Utils.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ object Utils {
102102
}
103103
}
104104

105+
fun formatElapsedTime(startTime: Long): String {
106+
val seconds = (System.currentTimeMillis() - startTime) / 1000
107+
val min = seconds / 60
108+
val sec = seconds % 60
109+
return "%02d:%02d".format(Locale.US, min, sec)
110+
}
111+
112+
105113
fun String.openFile() {
106114
val file = File(this)
107115
file.openFile()
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
@file:OptIn(ExperimentalUuidApi::class)
2+
3+
package com.meet.dev.analyzer.data.models.cleanbuild
4+
5+
import kotlin.uuid.ExperimentalUuidApi
6+
import kotlin.uuid.Uuid
7+
8+
data class ProjectBuildInfo(
9+
val uniqueId: String = Uuid.random().toString(),
10+
val projectName: String,
11+
val projectPath: String,
12+
val modules: List<ModuleBuild>,
13+
val sizeBytes: Long,
14+
val sizeFormatted: String
15+
) {
16+
val totalSize: Long get() = modules.sumOf { it.sizeBytes }
17+
val selectedModules: List<ModuleBuild> get() = modules.filter { it.isSelected }
18+
val selectedSize: Long get() = selectedModules.sumOf { it.sizeBytes }
19+
}
20+
21+
data class ModuleBuild(
22+
val uniqueId: String = Uuid.random().toString(),
23+
val moduleName: String,
24+
val path: String,
25+
val sizeBytes: Long,
26+
val sizeFormatted: String,
27+
val isSelected: Boolean = false
28+
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.meet.dev.analyzer.data.repository.cleanbuild
2+
3+
import com.meet.dev.analyzer.data.models.cleanbuild.ProjectBuildInfo
4+
5+
interface CleanBuildRepository {
6+
7+
suspend fun scanProjects(
8+
rootPath: String,
9+
updateProgress: (progress: Float, status: String) -> Unit
10+
): List<ProjectBuildInfo>
11+
12+
suspend fun deleteBuildFolder(path: String): Boolean
13+
14+
}
15+
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package com.meet.dev.analyzer.data.repository.cleanbuild
2+
3+
import com.meet.dev.analyzer.core.utility.AppLogger
4+
import com.meet.dev.analyzer.core.utility.Utils.calculateFolderSize
5+
import com.meet.dev.analyzer.core.utility.Utils.formatSize
6+
import com.meet.dev.analyzer.core.utility.Utils.tagName
7+
import com.meet.dev.analyzer.data.models.cleanbuild.ModuleBuild
8+
import com.meet.dev.analyzer.data.models.cleanbuild.ProjectBuildInfo
9+
import com.meet.dev.analyzer.data.models.project.BuildFileType
10+
import kotlinx.coroutines.Dispatchers
11+
import kotlinx.coroutines.withContext
12+
import java.io.File
13+
14+
class CleanBuildRepositoryImpl : CleanBuildRepository {
15+
16+
private val TAG = tagName(javaClass = javaClass)
17+
18+
override suspend fun scanProjects(
19+
rootPath: String,
20+
updateProgress: (progress: Float, status: String) -> Unit
21+
): List<ProjectBuildInfo> = withContext(Dispatchers.IO) {
22+
23+
AppLogger.i(TAG) { "Scanning projects..." }
24+
25+
try {
26+
val rootDir = File(rootPath)
27+
val projectDirs = rootDir.listFiles()?.filter { it.isDirectory }.orEmpty()
28+
val totalProjects = projectDirs.size.coerceAtLeast(1)
29+
30+
val projects = mutableListOf<ProjectBuildInfo>()
31+
var scannedCount = 0
32+
33+
projectDirs.forEach { projectDir ->
34+
35+
updateProgress(
36+
scannedCount.toFloat() / totalProjects,
37+
"Scanning ${projectDir.name}"
38+
)
39+
40+
// Detect Gradle project
41+
val isGradleProject = BuildFileType.entries.any {
42+
File(projectDir, it.fileName).exists()
43+
}
44+
if (!isGradleProject) {
45+
scannedCount++
46+
return@forEach
47+
}
48+
49+
val modules = mutableListOf<ModuleBuild>()
50+
51+
// Root build
52+
val rootBuildDir = File(projectDir, "build")
53+
if (rootBuildDir.exists()) {
54+
val size = calculateFolderSize(rootBuildDir, false)
55+
modules.add(
56+
ModuleBuild(
57+
moduleName = projectDir.name,
58+
path = rootBuildDir.absolutePath,
59+
sizeBytes = size,
60+
sizeFormatted = formatSize(size)
61+
)
62+
)
63+
}
64+
65+
// Module builds
66+
projectDir.listFiles()?.forEach { moduleDir ->
67+
val buildDir = File(moduleDir, "build")
68+
if (buildDir.exists()) {
69+
val size = calculateFolderSize(buildDir, false)
70+
modules.add(
71+
ModuleBuild(
72+
moduleName = moduleDir.name,
73+
path = buildDir.absolutePath,
74+
sizeBytes = size,
75+
sizeFormatted = formatSize(size)
76+
)
77+
)
78+
}
79+
}
80+
81+
if (modules.isNotEmpty()) {
82+
val totalSize = modules.sumOf { it.sizeBytes }
83+
projects.add(
84+
ProjectBuildInfo(
85+
projectName = projectDir.name,
86+
projectPath = projectDir.absolutePath,
87+
modules = modules.sortedByDescending { it.sizeBytes },
88+
sizeBytes = totalSize,
89+
sizeFormatted = formatSize(totalSize)
90+
)
91+
)
92+
}
93+
94+
scannedCount++
95+
}
96+
97+
updateProgress(1f, "Scan completed")
98+
projects.sortedByDescending { it.sizeBytes }
99+
100+
} catch (e: Exception) {
101+
AppLogger.e(TAG, e) { "Error scanning projects" }
102+
emptyList()
103+
}
104+
}
105+
106+
107+
override suspend fun deleteBuildFolder(path: String): Boolean {
108+
return try {
109+
val file = File(path)
110+
if (file.exists() && file.isDirectory) {
111+
file.deleteRecursively()
112+
} else {
113+
false
114+
}
115+
} catch (e: Exception) {
116+
AppLogger.e(TAG, e) { "Error deleting folder: $path" }
117+
false
118+
}
119+
}
120+
}

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/di/RepositoryModule.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.meet.dev.analyzer.di
22

3+
import com.meet.dev.analyzer.data.repository.cleanbuild.CleanBuildRepository
4+
import com.meet.dev.analyzer.data.repository.cleanbuild.CleanBuildRepositoryImpl
35
import com.meet.dev.analyzer.data.repository.project.ProjectAnalyzerRepository
46
import com.meet.dev.analyzer.data.repository.project.ProjectAnalyzerRepositoryImpl
57
import com.meet.dev.analyzer.data.repository.storage.StorageAnalyzerRepository
@@ -12,6 +14,7 @@ val repositoryModule = module {
1214

1315
singleOf(::ProjectAnalyzerRepositoryImpl).bind(ProjectAnalyzerRepository::class)
1416
singleOf(::StorageAnalyzerRepositoryImpl).bind(StorageAnalyzerRepository::class)
17+
singleOf(::CleanBuildRepositoryImpl).bind(CleanBuildRepository::class)
1518

1619
}
1720

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/di/ViewModule.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.meet.dev.analyzer.di
22

33
import com.meet.dev.analyzer.presentation.screen.app.AppViewModel
4+
import com.meet.dev.analyzer.presentation.screen.cleanbuild.CleanBuildViewModel
45
import com.meet.dev.analyzer.presentation.screen.onboarding.OnboardingViewModel
56
import com.meet.dev.analyzer.presentation.screen.project.ProjectAnalyzerViewModel
67
import com.meet.dev.analyzer.presentation.screen.setting.SettingsViewModel
@@ -16,4 +17,5 @@ val viewModule = module {
1617
viewModelOf(::ProjectAnalyzerViewModel)
1718
viewModelOf(::StorageAnalyzerViewModel)
1819
viewModelOf(::SettingsViewModel)
20+
viewModelOf(::CleanBuildViewModel)
1921
}

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/navigation/AppNavigation.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import androidx.navigation.compose.navigation
2121
import androidx.navigation.compose.rememberNavController
2222
import com.meet.dev.analyzer.presentation.navigation.navigation_bar.NavigationItem
2323
import com.meet.dev.analyzer.presentation.navigation.navigation_bar.NavigationRailLayout
24+
import com.meet.dev.analyzer.presentation.screen.cleanbuild.CleanBuildScreen
2425
import com.meet.dev.analyzer.presentation.screen.onboarding.OnboardingScreen
2526
import com.meet.dev.analyzer.presentation.screen.project.ProjectAnalyzerScreen
2627
import com.meet.dev.analyzer.presentation.screen.setting.SettingsScreen
@@ -128,6 +129,14 @@ fun AppNavigation(
128129
parentEntry = parentEntry
129130
)
130131
}
132+
composable<AppRoute.CleanBuild> {
133+
val parentEntry = remember(navController) {
134+
navController.getBackStackEntry(AppRoute.MainGraph)
135+
}
136+
CleanBuildScreen(
137+
parentEntry = parentEntry
138+
)
139+
}
131140

132141
}
133142
}

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/navigation/AppRoute.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,7 @@ sealed interface AppRoute {
2525
@Serializable
2626
data object Settings : AppRoute
2727

28+
@Serializable
29+
data object CleanBuild : AppRoute
30+
2831
}

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/navigation/navigation_bar/NavigationItem.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package com.meet.dev.analyzer.presentation.navigation.navigation_bar
22

33
import androidx.compose.material.icons.Icons
44
import androidx.compose.material.icons.filled.AccountTree
5+
import androidx.compose.material.icons.filled.Delete
56
import androidx.compose.material.icons.filled.Settings
67
import androidx.compose.material.icons.filled.Storage
78
import androidx.compose.material.icons.outlined.AccountTree
9+
import androidx.compose.material.icons.outlined.Delete
810
import androidx.compose.material.icons.outlined.Settings
911
import androidx.compose.material.icons.outlined.Storage
1012
import androidx.compose.ui.graphics.vector.ImageVector
@@ -31,6 +33,13 @@ enum class NavigationItem(
3133
unSelectedIcon = Icons.Outlined.Storage,
3234
description = "Analyze SDK, IDE, Gradle, and library storage usage."
3335
),
36+
CleanBuild(
37+
title = "Clean Build",
38+
appRoute = AppRoute.CleanBuild,
39+
selectedIcon = Icons.Filled.Delete,
40+
unSelectedIcon = Icons.Outlined.Delete,
41+
description = "Find and delete build folders to free up space."
42+
),
3443
Settings(
3544
title = "Settings",
3645
appRoute = AppRoute.Settings,
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.meet.dev.analyzer.presentation.screen.cleanbuild
2+
3+
sealed interface CleanBuildIntent {
4+
data class OnPathSelected(val path: String) : CleanBuildIntent
5+
data object OnAnalyzeProjects : CleanBuildIntent
6+
data class OnExpandChange(val uniqueId: String, val isExpanded: Boolean) : CleanBuildIntent
7+
data object OnExpandAll : CleanBuildIntent
8+
data object OnCollapseAll : CleanBuildIntent
9+
data class OnModuleSelectionChange(
10+
val uniqueId: String,
11+
val moduleIndex: Int,
12+
val isSelected: Boolean
13+
) : CleanBuildIntent
14+
15+
data class OnSelectAllInProject(val uniqueId: String, val isSelected: Boolean) :
16+
CleanBuildIntent
17+
18+
data object OnSelectAllProjects : CleanBuildIntent
19+
data object OnDeselectAllProjects : CleanBuildIntent
20+
data object OnDeleteClicked : CleanBuildIntent
21+
data object OnConfirmDelete : CleanBuildIntent
22+
data object OnConfirmDismissDialog : CleanBuildIntent
23+
data object OnResultDismissDialog : CleanBuildIntent
24+
data object OnClearError : CleanBuildIntent
25+
}

0 commit comments

Comments
 (0)