11package app.pwhs.universalinstaller.presentation.install
22
33import android.graphics.BitmapFactory
4+ import android.widget.Toast
5+ import androidx.compose.animation.animateContentSize
46import androidx.compose.foundation.Image
7+ import androidx.compose.foundation.background
8+ import androidx.compose.foundation.clickable
59import androidx.compose.foundation.layout.Arrangement
10+ import androidx.compose.foundation.layout.Box
611import androidx.compose.foundation.layout.Column
712import androidx.compose.foundation.layout.Row
13+ import androidx.compose.foundation.layout.Spacer
814import androidx.compose.foundation.layout.fillMaxWidth
15+ import androidx.compose.foundation.layout.height
916import androidx.compose.foundation.layout.padding
1017import androidx.compose.foundation.layout.size
18+ import androidx.compose.foundation.shape.RoundedCornerShape
1119import androidx.compose.material.icons.Icons
1220import androidx.compose.material.icons.rounded.CheckCircle
21+ import androidx.compose.material.icons.rounded.ContentCopy
1322import androidx.compose.material.icons.rounded.Error
23+ import androidx.compose.material.icons.rounded.ExpandMore
1424import androidx.compose.material3.CardDefaults
1525import androidx.compose.material3.ElevatedCard
26+ import androidx.compose.material3.HorizontalDivider
1627import androidx.compose.material3.Icon
28+ import androidx.compose.material3.IconButton
1729import androidx.compose.material3.MaterialTheme
1830import androidx.compose.material3.Text
1931import 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
2036import androidx.compose.ui.Alignment
2137import androidx.compose.ui.Modifier
2238import androidx.compose.ui.draw.clip
39+ import androidx.compose.ui.draw.rotate
2340import androidx.compose.ui.graphics.asImageBitmap
41+ import androidx.compose.ui.platform.LocalClipboardManager
42+ import androidx.compose.ui.platform.LocalContext
2443import androidx.compose.ui.res.stringResource
44+ import androidx.compose.ui.text.AnnotatedString
45+ import androidx.compose.ui.text.font.FontWeight
2546import androidx.compose.ui.text.style.TextOverflow
2647import androidx.compose.ui.unit.dp
2748import 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 }
0 commit comments