Skip to content

Commit dbe57eb

Browse files
committed
refactor: Add scan timer and improve error handling
This commit introduces a timer to display the elapsed time during scans in both the Project and Storage Analyzers. It also enhances the ViewModels with more robust error handling for `IOException` and `SecurityException`, and refactors UI components for better consistency. ### Key Changes: * **Scan Timer and Elapsed Time:** * A timer has been added to the `ProgressStatusLayout`, displaying the elapsed scan time (e.g., `01:23`). * `ProjectAnalyzerViewModel` and `StorageAnalyzerViewModel` now track and update the elapsed time during analysis. * The respective UI states (`ProjectAnalyzerUiState`, `StorageAnalyzerUiState`) have been updated to include `scanElapsedTime`. * **Improved ViewModel Logic:** * The analysis logic in both `ProjectAnalyzerViewModel` and `StorageAnalyzerViewModel` is moved to an `IO` dispatcher. * Added specific `try-catch` blocks to handle `IOException` and `SecurityException`, providing more precise error messages to the user (e.g., "Failed to access some files" or "Permission denied"). * UI updates are now correctly dispatched back to the `Main` thread. * **UI and Component Refactoring:** * The composable `SummaryExpandableSection.kt` has been renamed to `SummaryExpandableSectionLayout.kt` for consistency. * The `type` parameter in `SummaryExpandableSectionLayout` has been renamed to `expandableSection` to improve clarity and self-documentation. This change is reflected across all its usages in various tab content files.
1 parent 76528ba commit dbe57eb

15 files changed

+208
-82
lines changed

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/components/ProgressStatusLayout.kt

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,75 @@ import androidx.compose.animation.fadeOut
77
import androidx.compose.animation.shrinkVertically
88
import androidx.compose.foundation.layout.Arrangement
99
import androidx.compose.foundation.layout.Column
10+
import androidx.compose.foundation.layout.Row
1011
import androidx.compose.foundation.layout.fillMaxWidth
12+
import androidx.compose.foundation.layout.size
13+
import androidx.compose.material.icons.Icons
14+
import androidx.compose.material.icons.filled.Timer
15+
import androidx.compose.material3.Icon
1116
import androidx.compose.material3.LinearProgressIndicator
1217
import androidx.compose.material3.MaterialTheme
1318
import androidx.compose.material3.Text
1419
import androidx.compose.runtime.Composable
20+
import androidx.compose.ui.Alignment
1521
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.text.font.FontWeight
1623
import androidx.compose.ui.unit.dp
1724

1825
@Composable
1926
fun ProgressStatusLayout(
2027
isScanning: Boolean,
2128
scanProgress: Float,
22-
scanStatus: String
29+
scanStatus: String,
30+
scanElapsedTime: String,
31+
modifier: Modifier = Modifier
2332
) {
2433
AnimatedVisibility(
2534
visible = isScanning,
2635
enter = fadeIn() + expandVertically(),
2736
exit = fadeOut() + shrinkVertically()
2837
) {
2938
Column(
39+
modifier = modifier,
3040
verticalArrangement = Arrangement.spacedBy(8.dp)
3141
) {
3242
LinearProgressIndicator(
3343
progress = { scanProgress },
3444
modifier = Modifier.fillMaxWidth(),
3545
color = MaterialTheme.colorScheme.primary,
3646
)
37-
Text(
38-
scanStatus,
39-
style = MaterialTheme.typography.bodySmall,
40-
color = MaterialTheme.colorScheme.onSurfaceVariant
41-
)
47+
Row(
48+
modifier = Modifier.fillMaxWidth(),
49+
horizontalArrangement = Arrangement.SpaceBetween,
50+
verticalAlignment = Alignment.CenterVertically
51+
) {
52+
// Scan Status
53+
Text(
54+
text = scanStatus,
55+
style = MaterialTheme.typography.bodySmall,
56+
color = MaterialTheme.colorScheme.onSurfaceVariant,
57+
modifier = Modifier.weight(1f)
58+
)
59+
60+
// Timer
61+
Row(
62+
horizontalArrangement = Arrangement.spacedBy(4.dp),
63+
verticalAlignment = Alignment.CenterVertically
64+
) {
65+
Icon(
66+
imageVector = Icons.Default.Timer,
67+
contentDescription = "Timer",
68+
modifier = Modifier.size(16.dp),
69+
tint = MaterialTheme.colorScheme.primary
70+
)
71+
Text(
72+
text = scanElapsedTime,
73+
style = MaterialTheme.typography.bodySmall,
74+
fontWeight = FontWeight.Medium,
75+
color = MaterialTheme.colorScheme.primary
76+
)
77+
}
78+
}
4279
}
4380
}
4481
}

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/components/SummaryExpandableSection.kt renamed to composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/components/SummaryExpandableSectionLayout.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ import java.awt.Cursor
3434

3535
@Composable
3636
fun SummaryExpandableSectionLayout(
37-
type: ExpandableSection,
37+
expandableSection: ExpandableSection,
3838
isExpanded: Boolean = true,
3939
onExpandChange: (() -> Unit)? = null,
4040
content: @Composable RowScope.(ExpandableSection) -> Unit,
4141
) {
4242
Card(
4343
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
44-
colors = type.cardColors(),
44+
colors = expandableSection.cardColors(),
4545
) {
4646
Column(
4747
modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp)
@@ -50,23 +50,23 @@ fun SummaryExpandableSectionLayout(
5050
verticalAlignment = Alignment.CenterVertically
5151
) {
5252
Icon(
53-
type.icon,
53+
expandableSection.icon,
5454
contentDescription = null,
5555
modifier = Modifier
5656
.size(24.dp)
5757
)
5858
Spacer(Modifier.width(10.dp))
5959
Text(
60-
text = type.title,
60+
text = expandableSection.title,
6161
style = MaterialTheme.typography.headlineSmall,
6262
fontWeight = FontWeight.Bold,
63-
color = type.titleColor(),
63+
color = expandableSection.titleColor(),
6464
)
6565
Spacer(Modifier.width(1.dp))
6666
// Info
6767
CustomToolTip(
68-
title = type.title,
69-
description = type.description
68+
title = expandableSection.title,
69+
description = expandableSection.description
7070
) {
7171
IconButton(
7272
onClick = {},
@@ -115,7 +115,7 @@ fun SummaryExpandableSectionLayout(
115115
modifier = Modifier.fillMaxWidth(),
116116
horizontalArrangement = Arrangement.SpaceEvenly,
117117
content = {
118-
content(type)
118+
content(expandableSection)
119119
}
120120
)
121121
}

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/screen/project/ProjectAnalyzerUiState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ data class ProjectAnalyzerUiState(
1313
val isScanning: Boolean = false,
1414
val scanProgress: Float = 0f,
1515
val scanStatus: String = "",
16+
val scanElapsedTime: String = "00:00",
1617
)

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/screen/project/ProjectAnalyzerViewModel.kt

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@ import androidx.lifecycle.viewModelScope
55
import com.meet.dev.analyzer.core.utility.AppLogger
66
import com.meet.dev.analyzer.core.utility.Utils.tagName
77
import com.meet.dev.analyzer.data.repository.project.ProjectAnalyzerRepository
8+
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.cancelAndJoin
10+
import kotlinx.coroutines.delay
811
import kotlinx.coroutines.flow.MutableStateFlow
912
import kotlinx.coroutines.flow.asStateFlow
1013
import kotlinx.coroutines.flow.update
14+
import kotlinx.coroutines.isActive
1115
import kotlinx.coroutines.launch
16+
import kotlinx.coroutines.withContext
17+
import kotlinx.io.IOException
1218
import java.io.File
1319

1420
class ProjectAnalyzerViewModel(
@@ -57,40 +63,91 @@ class ProjectAnalyzerViewModel(
5763

5864
AppLogger.i(TAG) { "Starting project analysis for: $currentPath" }
5965

60-
viewModelScope.launch {
66+
viewModelScope.launch(Dispatchers.IO) {
6167
try {
6268
_uiState.update {
6369
it.copy(
6470
isScanning = true,
6571
error = null,
6672
scanProgress = 0f,
67-
scanStatus = "Initializing scan..."
73+
scanStatus = "Initializing scan...",
74+
scanElapsedTime = "00:00"
6875
)
6976
}
7077

78+
val startTime = System.currentTimeMillis()
79+
80+
// Start elapsed time counter
81+
val timerJob = launch {
82+
while (isActive) {
83+
val elapsedMillis = System.currentTimeMillis() - startTime
84+
val seconds = (elapsedMillis / 1000) % 60
85+
val minutes = (elapsedMillis / 1000) / 60
86+
val formatted = String.format("%02d:%02d", minutes, seconds)
87+
_uiState.update { it.copy(scanElapsedTime = formatted) }
88+
delay(1000)
89+
}
90+
}
91+
92+
// 🧩 Main analysis logic
7193
if (validateProject(currentPath)) {
7294
val projectInfo = repository.analyzeProject(currentPath) { progress, status ->
7395
AppLogger.d(TAG) { "Progress: $progress, Status: $status" }
7496
_uiState.update {
7597
it.copy(
76-
scanProgress = progress,
98+
scanProgress = progress.coerceIn(0f, 1f),
7799
scanStatus = status
78100
)
79101
}
80102
}
81103

104+
timerJob.cancelAndJoin()
105+
withContext(Dispatchers.Main) {
106+
_uiState.update {
107+
it.copy(
108+
isScanning = false,
109+
projectInfo = projectInfo,
110+
scanProgress = 1f,
111+
scanStatus = "Analysis complete"
112+
)
113+
}
114+
}
115+
116+
// 🕓 Final elapsed log
117+
val totalMillis = System.currentTimeMillis() - startTime
118+
val totalSeconds = totalMillis / 1000
119+
val minutes = totalSeconds / 60
120+
val seconds = totalSeconds % 60
121+
AppLogger.i(TAG) { "Project analysis completed successfully in ${minutes}m ${seconds}s" }
122+
} else {
82123
_uiState.update {
83124
it.copy(
84125
isScanning = false,
85-
projectInfo = projectInfo,
86-
scanProgress = 1f,
87-
scanStatus = "Analysis complete"
126+
scanProgress = 0f,
127+
scanStatus = "",
128+
error = "Invalid project directory"
88129
)
89130
}
90-
91-
AppLogger.i(TAG) { "Project analysis completed successfully" }
92131
}
93132

133+
} catch (e: IOException) {
134+
AppLogger.e(TAG, e) { "File read error" }
135+
_uiState.update {
136+
it.copy(
137+
isScanning = false,
138+
scanStatus = "",
139+
error = "Failed to access some files"
140+
)
141+
}
142+
} catch (e: SecurityException) {
143+
AppLogger.e(TAG, e) { "Permission denied" }
144+
_uiState.update {
145+
it.copy(
146+
isScanning = false,
147+
scanStatus = "",
148+
error = "Permission denied for storage access"
149+
)
150+
}
94151
} catch (e: Exception) {
95152
AppLogger.e(TAG, e) { "Error analyzing project" }
96153
_uiState.update {

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/screen/project/components/ProjectSelectionSection.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ fun ProjectSelectionSection(
129129
ProgressStatusLayout(
130130
isScanning = uiState.isScanning,
131131
scanProgress = uiState.scanProgress,
132-
scanStatus = uiState.scanStatus
132+
scanStatus = uiState.scanStatus,
133+
scanElapsedTime = uiState.scanElapsedTime
133134
)
134135

135136
// Error display

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/screen/storage/StorageAnalyzerScreen.kt

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
package com.meet.dev.analyzer.presentation.screen.storage
22

3-
import androidx.compose.animation.AnimatedVisibility
4-
import androidx.compose.animation.expandVertically
5-
import androidx.compose.animation.fadeIn
6-
import androidx.compose.animation.fadeOut
7-
import androidx.compose.animation.shrinkVertically
83
import androidx.compose.foundation.layout.Arrangement
94
import androidx.compose.foundation.layout.Column
105
import androidx.compose.foundation.layout.Row
@@ -102,22 +97,13 @@ fun StorageAnalyzerContent(
10297
modifier = Modifier.fillMaxSize().padding(it)
10398
) {
10499
// Progress and status
105-
AnimatedVisibility(
106-
visible = uiState.isScanning,
107-
enter = fadeIn() + expandVertically(),
108-
exit = fadeOut() + shrinkVertically()
109-
) {
110-
Column(
111-
modifier = Modifier.padding(10.dp),
112-
verticalArrangement = Arrangement.spacedBy(12.dp)
113-
) {
114-
ProgressStatusLayout(
115-
isScanning = uiState.isScanning,
116-
scanProgress = uiState.scanProgress,
117-
scanStatus = uiState.scanStatus
118-
)
119-
}
120-
}
100+
ProgressStatusLayout(
101+
isScanning = uiState.isScanning,
102+
scanProgress = uiState.scanProgress,
103+
scanStatus = uiState.scanStatus,
104+
scanElapsedTime = uiState.scanElapsedTime,
105+
modifier = Modifier.padding(10.dp)
106+
)
121107

122108
// Error Layout
123109
if (uiState.error != null) {

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/screen/storage/StorageAnalyzerUiState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ data class StorageAnalyzerUiState(
1313
val storageAnalyzerInfo: StorageAnalyzerInfo? = null,
1414
val isScanning: Boolean = false,
1515
val scanProgress: Float = 0f,
16+
val scanElapsedTime: String = "00:00",
1617
val scanStatus: String = "",
1718
)

0 commit comments

Comments
 (0)