UI: extract a reusable InfoAlertDialog

This commit is contained in:
Han Yin 2025-09-03 10:14:50 -07:00
parent a4459b22d1
commit 0c6ce7b9a3
3 changed files with 156 additions and 81 deletions

View File

@ -3,10 +3,12 @@ package com.example.llama.ui.components
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -18,18 +20,95 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
data class InfoAction( data class InfoAction(
val label: String,
val icon: ImageVector, val icon: ImageVector,
val label: String,
val onAction: () -> Unit val onAction: () -> Unit
) )
@Composable
fun InfoAlertDialog(
modifier: Modifier = Modifier,
isCritical: Boolean = false,
allowDismiss: Boolean = false,
onDismiss: () -> Unit = {},
icon: ImageVector,
title: String,
message: String? = null,
action: InfoAction? = null,
confirmButton: @Composable () -> Unit = {},
) {
AlertDialog(
modifier = modifier,
onDismissRequest = onDismiss,
properties = DialogProperties(
dismissOnBackPress = allowDismiss,
dismissOnClickOutside = allowDismiss,
),
icon = {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(64.dp),
)
},
title = {
Text(
modifier = Modifier.fillMaxWidth(),
text = title,
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
fontWeight = FontWeight.SemiBold
)
},
text = {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
message?.let {
Text(
modifier = Modifier.padding(top = 8.dp),
text = it,
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
action?.let {
Button(
modifier = Modifier.padding(top = 24.dp),
onClick = it.onAction,
) {
Icon(
imageVector = it.icon,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(it.label)
}
}
}
},
containerColor = if (isCritical) MaterialTheme.colorScheme.errorContainer else AlertDialogDefaults.containerColor,
iconContentColor = if (isCritical) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
titleContentColor = if (isCritical) MaterialTheme.colorScheme.onErrorContainer else AlertDialogDefaults.titleContentColor,
textContentColor = if (isCritical) MaterialTheme.colorScheme.onErrorContainer else AlertDialogDefaults.textContentColor,
confirmButton = confirmButton,
)
}
@Composable @Composable
fun InfoView( fun InfoView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String,
icon: ImageVector, icon: ImageVector,
title: String,
message: String? = null, message: String? = null,
action: InfoAction? = null action: InfoAction? = null
) { ) {
@ -52,8 +131,8 @@ fun InfoView(
@Composable @Composable
fun InfoView( fun InfoView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String,
icon: @Composable () -> Unit, icon: @Composable () -> Unit,
title: String,
message: String? = null, message: String? = null,
action: InfoAction? = null action: InfoAction? = null
) { ) {
@ -64,9 +143,8 @@ fun InfoView(
) { ) {
icon() icon()
Spacer(modifier = Modifier.height(16.dp))
Text( Text(
modifier = Modifier.padding(top = 16.dp),
text = title, text = title,
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -74,9 +152,8 @@ fun InfoView(
) )
message?.let { message?.let {
Spacer(modifier = Modifier.height(8.dp))
Text( Text(
modifier = Modifier.padding(top = 8.dp),
text = it, text = it,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -85,16 +162,17 @@ fun InfoView(
} }
action?.let { action?.let {
Spacer(modifier = Modifier.height(24.dp)) Button(
modifier = Modifier.padding(top = 24.dp),
Button(onClick = action.onAction) { onClick = it.onAction,
) {
Icon( Icon(
imageVector = action.icon, imageVector = it.icon,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text(action.label) Text(it.label)
} }
} }
} }

View File

@ -22,7 +22,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.automirrored.filled.Help import androidx.compose.material.icons.automirrored.filled.Help
import androidx.compose.material.icons.automirrored.outlined.ContactSupport import androidx.compose.material.icons.automirrored.outlined.ContactSupport
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material.icons.filled.Attribution import androidx.compose.material.icons.filled.Attribution
import androidx.compose.material.icons.filled.Celebration import androidx.compose.material.icons.filled.Celebration
import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Download
@ -31,11 +30,11 @@ import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Today import androidx.compose.material.icons.filled.Today
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -64,6 +63,7 @@ import androidx.core.net.toUri
import com.example.llama.data.model.ModelInfo import com.example.llama.data.model.ModelInfo
import com.example.llama.data.source.remote.HuggingFaceModel import com.example.llama.data.source.remote.HuggingFaceModel
import com.example.llama.ui.components.InfoAction import com.example.llama.ui.components.InfoAction
import com.example.llama.ui.components.InfoAlertDialog
import com.example.llama.ui.components.InfoView import com.example.llama.ui.components.InfoView
import com.example.llama.ui.components.ModelCardFullExpandable import com.example.llama.ui.components.ModelCardFullExpandable
import com.example.llama.ui.scaffold.ScaffoldEvent import com.example.llama.ui.scaffold.ScaffoldEvent
@ -626,25 +626,17 @@ private fun DownloadHuggingFaceDispatchedDialog(
modelId: String, modelId: String,
onConfirm: () -> Unit, onConfirm: () -> Unit,
) { ) {
AlertDialog( InfoAlertDialog(
onDismissRequest = {},
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false
),
title = {},
text = {
InfoView(
title = "Download has started", title = "Download has started",
icon = Icons.Default.Download, icon = Icons.Default.Download,
message = "Your Android system download manager has started downloading the model: $modelId.\n\n" message = "Your Android system download manager has started downloading the model: $modelId.\n\n"
+ "You can track its progress in your notification drawer.\n" + "You can track its progress in your notification drawer.\n"
+ "Feel free to stay on this screen, or come back to import it after complete.", + "Feel free to stay on this screen, or come back to import it after complete.",
action = InfoAction(
icon = Icons.AutoMirrored.Default.ArrowForward,
label = "Okay",
onAction = onConfirm
) )
},
confirmButton = {
Button(onClick = onConfirm) { Text("Okay") }
},
) )
} }
@ -652,27 +644,16 @@ private fun DownloadHuggingFaceDispatchedDialog(
private fun FirstModelImportSuccessDialog( private fun FirstModelImportSuccessDialog(
onConfirm: () -> Unit, onConfirm: () -> Unit,
) { ) {
AlertDialog( InfoAlertDialog(
// Prevent dismissal via back button during deletion
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false
),
onDismissRequest = {},
text = {
InfoView(
title = "Congratulations", title = "Congratulations",
icon = Icons.Default.Celebration, icon = Icons.Default.Celebration,
message = "You have just installed your first Large Language Model!", message = "You have just installed your first Large Language Model!",
action = InfoAction( action = InfoAction(
label = "Check it out",
icon = Icons.AutoMirrored.Default.ArrowForward, icon = Icons.AutoMirrored.Default.ArrowForward,
label = "Check it out",
onAction = onConfirm onAction = onConfirm
) )
) )
},
confirmButton = {}
)
} }
@Composable @Composable
@ -765,21 +746,16 @@ private fun ErrorDialog(
) )
} }
AlertDialog( InfoAlertDialog(
onDismissRequest = onDismiss, isCritical = true,
text = {
InfoView(
modifier = Modifier.fillMaxWidth(),
title = title, title = title,
allowDismiss = true,
onDismiss = onDismiss,
icon = Icons.Default.Error, icon = Icons.Default.Error,
message = message, message = message,
action = action action = action,
)
},
confirmButton = { confirmButton = {
TextButton(onClick = onDismiss) { FilledTonalButton(onClick = onDismiss) { Text("Dismiss") }
Text("OK")
}
} }
) )
} }

View File

@ -3,13 +3,16 @@ package com.example.llama.ui.screens
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -20,6 +23,8 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.llama.data.model.ModelInfo import com.example.llama.data.model.ModelInfo
import com.example.llama.ui.components.InfoView import com.example.llama.ui.components.InfoView
@ -149,12 +154,26 @@ private fun RamErrorDialog(
val availableRam = formatFileByteSize(ramError.availableRam) val availableRam = formatFileByteSize(ramError.availableRam)
AlertDialog( AlertDialog(
icon = {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = "Warning icon",
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.error
)
},
title = {
Text(
modifier = Modifier.padding(top = 16.dp),
text = "Insufficient RAM",
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
fontWeight = FontWeight.SemiBold
)
},
text = { text = {
InfoView( Text(
modifier = Modifier.fillMaxWidth(), "You are trying to run a $requiredRam size model, " +
title = "Insufficient RAM",
icon = Icons.Default.Warning,
message = "You are trying to run a $requiredRam size model, " +
"but currently there's only $availableRam memory available!", "but currently there's only $availableRam memory available!",
) )
}, },
@ -162,17 +181,19 @@ private fun RamErrorDialog(
titleContentColor = MaterialTheme.colorScheme.onErrorContainer, titleContentColor = MaterialTheme.colorScheme.onErrorContainer,
textContentColor = MaterialTheme.colorScheme.onErrorContainer, textContentColor = MaterialTheme.colorScheme.onErrorContainer,
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { TextButton(
Text("Cancel") onClick = onConfirm,
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error
)
) {
Text("Proceed")
} }
}, },
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
confirmButton = { confirmButton = {
TextButton(onClick = onConfirm) { FilledTonalButton(onClick = onDismiss) {
Text( Text("Cancel")
text = "Proceed",
color = MaterialTheme.colorScheme.error
)
} }
} }
) )