Skip to content

Commit 31a2244

Browse files
committed
feat: show detailed error log in install history items and support expanding & copying logs
1 parent 2bd0fc1 commit 31a2244

2 files changed

Lines changed: 160 additions & 53 deletions

File tree

app/src/main/java/app/pwhs/universalinstaller/presentation/install/HistoryCard.kt

Lines changed: 159 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,48 @@
11
package app.pwhs.universalinstaller.presentation.install
22

33
import android.graphics.BitmapFactory
4+
import android.widget.Toast
5+
import androidx.compose.animation.animateContentSize
46
import androidx.compose.foundation.Image
7+
import androidx.compose.foundation.background
8+
import androidx.compose.foundation.clickable
59
import androidx.compose.foundation.layout.Arrangement
10+
import androidx.compose.foundation.layout.Box
611
import androidx.compose.foundation.layout.Column
712
import androidx.compose.foundation.layout.Row
13+
import androidx.compose.foundation.layout.Spacer
814
import androidx.compose.foundation.layout.fillMaxWidth
15+
import androidx.compose.foundation.layout.height
916
import androidx.compose.foundation.layout.padding
1017
import androidx.compose.foundation.layout.size
18+
import androidx.compose.foundation.shape.RoundedCornerShape
1119
import androidx.compose.material.icons.Icons
1220
import androidx.compose.material.icons.rounded.CheckCircle
21+
import androidx.compose.material.icons.rounded.ContentCopy
1322
import androidx.compose.material.icons.rounded.Error
23+
import androidx.compose.material.icons.rounded.ExpandMore
1424
import androidx.compose.material3.CardDefaults
1525
import androidx.compose.material3.ElevatedCard
26+
import androidx.compose.material3.HorizontalDivider
1627
import androidx.compose.material3.Icon
28+
import androidx.compose.material3.IconButton
1729
import androidx.compose.material3.MaterialTheme
1830
import androidx.compose.material3.Text
1931
import androidx.compose.runtime.Composable
32+
import androidx.compose.runtime.getValue
33+
import androidx.compose.runtime.mutableStateOf
34+
import androidx.compose.runtime.saveable.rememberSaveable
35+
import androidx.compose.runtime.setValue
2036
import androidx.compose.ui.Alignment
2137
import androidx.compose.ui.Modifier
2238
import androidx.compose.ui.draw.clip
39+
import androidx.compose.ui.draw.rotate
2340
import androidx.compose.ui.graphics.asImageBitmap
41+
import androidx.compose.ui.platform.LocalClipboardManager
42+
import androidx.compose.ui.platform.LocalContext
2443
import androidx.compose.ui.res.stringResource
44+
import androidx.compose.ui.text.AnnotatedString
45+
import androidx.compose.ui.text.font.FontWeight
2546
import androidx.compose.ui.text.style.TextOverflow
2647
import androidx.compose.ui.unit.dp
2748
import app.pwhs.universalinstaller.R
@@ -37,77 +58,163 @@ internal fun HistoryCard(
3758
modifier: Modifier = Modifier,
3859
) {
3960
val dateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.getDefault())
61+
var expanded by rememberSaveable(entry.id) { mutableStateOf(false) }
62+
val canExpand = !entry.success && !entry.errorMessage.isNullOrBlank()
63+
val clipboardManager = LocalClipboardManager.current
64+
val context = LocalContext.current
4065

4166
ElevatedCard(
42-
modifier = modifier.fillMaxWidth(),
67+
modifier = modifier
68+
.fillMaxWidth()
69+
.animateContentSize()
70+
.clickable(enabled = canExpand) { expanded = !expanded },
4371
shape = MaterialTheme.shapes.large,
4472
colors = CardDefaults.elevatedCardColors(
4573
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
4674
),
4775
) {
48-
Row(
49-
modifier = Modifier
50-
.fillMaxWidth()
51-
.padding(12.dp),
52-
verticalAlignment = Alignment.CenterVertically,
53-
horizontalArrangement = Arrangement.spacedBy(12.dp),
54-
) {
55-
// Icon
56-
val iconBitmap = entry.iconPath?.let { path ->
57-
try {
58-
val file = File(path)
59-
if (file.exists()) BitmapFactory.decodeFile(path)?.asImageBitmap() else null
60-
} catch (_: Exception) { null }
61-
}
76+
Column {
77+
Row(
78+
modifier = Modifier
79+
.fillMaxWidth()
80+
.padding(12.dp),
81+
verticalAlignment = Alignment.CenterVertically,
82+
horizontalArrangement = Arrangement.spacedBy(12.dp),
83+
) {
84+
// Icon
85+
val iconBitmap = entry.iconPath?.let { path ->
86+
try {
87+
val file = File(path)
88+
if (file.exists()) BitmapFactory.decodeFile(path)?.asImageBitmap() else null
89+
} catch (_: Exception) { null }
90+
}
6291

63-
if (iconBitmap != null) {
64-
Image(
65-
bitmap = iconBitmap,
66-
contentDescription = entry.appName,
67-
modifier = Modifier
68-
.size(40.dp)
69-
.clip(MaterialTheme.shapes.medium),
70-
)
71-
} else {
92+
if (iconBitmap != null) {
93+
Image(
94+
bitmap = iconBitmap,
95+
contentDescription = entry.appName,
96+
modifier = Modifier
97+
.size(40.dp)
98+
.clip(MaterialTheme.shapes.medium),
99+
)
100+
} else {
101+
Icon(
102+
imageVector = if (entry.success) Icons.Rounded.CheckCircle else Icons.Rounded.Error,
103+
contentDescription = null,
104+
tint = if (entry.success) MaterialTheme.colorScheme.primary
105+
else MaterialTheme.colorScheme.error,
106+
modifier = Modifier.size(40.dp),
107+
)
108+
}
109+
110+
// Info
111+
Column(modifier = Modifier.weight(1f)) {
112+
Text(
113+
text = entry.appName,
114+
style = MaterialTheme.typography.titleSmall,
115+
maxLines = 1,
116+
overflow = TextOverflow.Ellipsis,
117+
color = MaterialTheme.colorScheme.onSurface,
118+
)
119+
Text(
120+
text = entry.fileName,
121+
style = MaterialTheme.typography.labelSmall,
122+
maxLines = 1,
123+
overflow = TextOverflow.Ellipsis,
124+
color = MaterialTheme.colorScheme.onSurfaceVariant,
125+
)
126+
Text(
127+
text = dateFormat.format(Date(entry.installedAt)),
128+
style = MaterialTheme.typography.labelSmall,
129+
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
130+
)
131+
}
132+
133+
// Status badge
72134
Icon(
73135
imageVector = if (entry.success) Icons.Rounded.CheckCircle else Icons.Rounded.Error,
74-
contentDescription = null,
136+
contentDescription = if (entry.success) stringResource(R.string.status_success) else stringResource(R.string.status_failed),
75137
tint = if (entry.success) MaterialTheme.colorScheme.primary
76138
else MaterialTheme.colorScheme.error,
77-
modifier = Modifier.size(40.dp),
139+
modifier = Modifier.size(20.dp),
78140
)
79141
}
80142

81-
// Info
82-
Column(modifier = Modifier.weight(1f)) {
83-
Text(
84-
text = entry.appName,
85-
style = MaterialTheme.typography.titleSmall,
86-
maxLines = 1,
87-
overflow = TextOverflow.Ellipsis,
88-
color = MaterialTheme.colorScheme.onSurface,
89-
)
90-
Text(
91-
text = entry.fileName,
92-
style = MaterialTheme.typography.labelSmall,
93-
maxLines = 1,
94-
overflow = TextOverflow.Ellipsis,
95-
color = MaterialTheme.colorScheme.onSurfaceVariant,
96-
)
97-
Text(
98-
text = dateFormat.format(Date(entry.installedAt)),
99-
style = MaterialTheme.typography.labelSmall,
100-
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
143+
if (canExpand) {
144+
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f))
145+
ErrorBlock(
146+
message = entry.errorMessage.orEmpty(),
147+
expanded = expanded,
148+
onCopy = {
149+
clipboardManager.setText(AnnotatedString(entry.errorMessage.orEmpty()))
150+
Toast.makeText(
151+
context,
152+
context.getString(R.string.dialog_failed_copied),
153+
Toast.LENGTH_SHORT
154+
).show()
155+
}
101156
)
102157
}
158+
}
159+
}
160+
}
103161

104-
// Status badge
162+
@Composable
163+
private fun ErrorBlock(
164+
message: String,
165+
expanded: Boolean,
166+
onCopy: () -> Unit
167+
) {
168+
Column(
169+
modifier = Modifier
170+
.fillMaxWidth()
171+
.padding(horizontal = 12.dp, vertical = 10.dp),
172+
) {
173+
Row(
174+
verticalAlignment = Alignment.CenterVertically,
175+
horizontalArrangement = Arrangement.spacedBy(6.dp),
176+
) {
177+
Text(
178+
text = stringResource(R.string.logs_reason),
179+
style = MaterialTheme.typography.labelSmall,
180+
fontWeight = FontWeight.SemiBold,
181+
color = MaterialTheme.colorScheme.error,
182+
)
183+
Spacer(Modifier.weight(1f))
184+
IconButton(
185+
onClick = onCopy,
186+
modifier = Modifier.size(24.dp)
187+
) {
188+
Icon(
189+
imageVector = Icons.Rounded.ContentCopy,
190+
contentDescription = "Copy Error Log",
191+
tint = MaterialTheme.colorScheme.onSurfaceVariant,
192+
modifier = Modifier.size(16.dp)
193+
)
194+
}
105195
Icon(
106-
imageVector = if (entry.success) Icons.Rounded.CheckCircle else Icons.Rounded.Error,
107-
contentDescription = if (entry.success) stringResource(R.string.status_success) else stringResource(R.string.status_failed),
108-
tint = if (entry.success) MaterialTheme.colorScheme.primary
109-
else MaterialTheme.colorScheme.error,
110-
modifier = Modifier.size(20.dp),
196+
imageVector = Icons.Rounded.ExpandMore,
197+
contentDescription = if (expanded) stringResource(R.string.logs_collapse_cd) else stringResource(R.string.logs_expand_cd),
198+
tint = MaterialTheme.colorScheme.onSurfaceVariant,
199+
modifier = Modifier
200+
.size(18.dp)
201+
.rotate(if (expanded) 180f else 0f),
202+
)
203+
}
204+
Spacer(Modifier.height(6.dp))
205+
Box(
206+
modifier = Modifier
207+
.fillMaxWidth()
208+
.clip(RoundedCornerShape(10.dp))
209+
.background(MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.35f))
210+
.padding(horizontal = 10.dp, vertical = 8.dp),
211+
) {
212+
Text(
213+
text = message,
214+
style = MaterialTheme.typography.bodySmall,
215+
color = MaterialTheme.colorScheme.onErrorContainer,
216+
maxLines = if (expanded) Int.MAX_VALUE else 2,
217+
overflow = TextOverflow.Ellipsis,
111218
)
112219
}
113220
}

app/src/main/java/app/pwhs/universalinstaller/presentation/install/controller/BaseInstallController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ abstract class BaseInstallController(
150150
if (context == null) return@launch
151151
val errorInfo = InstallErrorHelper.getErrorInfo(context, result.failure)
152152
val fullMessage = "${errorInfo.title}\n${errorInfo.guidance}"
153-
saveHistory(sessionData, success = false, errorMessage = errorInfo.title)
153+
saveHistory(sessionData, success = false, errorMessage = fullMessage)
154154
handleError(fullMessage, session.id)
155155
}
156156
}

0 commit comments

Comments
 (0)