owenclave / app/src/main/java/io/nekohasekai/sagernet/ui/compose/components/GroupCard.kt

github - dev - back to tree - upstream
package io.nekohasekai.sagernet.ui.compose.components import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.DropdownMenu import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.LinearWavyProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.nekohasekai.sagernet.database.ProxyGroup @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun GroupCard( group: ProxyGroup, selected: Boolean, profileCount: Int, updating: Boolean = false, onClick: () -> Unit, onEdit: () -> Unit, onUpdate: (() -> Unit)? = null, menuItems: @Composable (MenuScope) -> Unit = {}, modifier: Modifier = Modifier, ) { val containerColor by animateColorAsState( targetValue = if (selected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface, label = "groupColor" ) val onContainerColor by animateColorAsState( targetValue = if (selected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface, label = "groupContentColor" ) val interactionSource = remember { MutableInteractionSource() } val pressed by interactionSource.collectIsPressedAsState() val cardScale by animateFloatAsState( targetValue = if (pressed) 0.97f else 1f, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMedium, ), label = "groupScale", ) val isSubscription = group.type == io.nekohasekai.sagernet.GroupType.SUBSCRIPTION val seedShape = remember(group.displayName()) { shapeForSeed(group.displayName()) } val iconContainer by animateColorAsState( targetValue = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.tertiaryContainer, label = "gIconContainer", ) val iconContent by animateColorAsState( targetValue = if (selected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onTertiaryContainer, label = "gIconContent", ) var menuExpanded by remember { mutableStateOf(false) } Card( modifier = modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 3.dp) .graphicsLayer { scaleX = cardScale scaleY = cardScale }, shape = MaterialTheme.shapes.extraLarge, colors = CardDefaults.cardColors(containerColor = containerColor), elevation = CardDefaults.cardElevation(defaultElevation = if (selected) 3.dp else 0.dp), interactionSource = interactionSource, onClick = onClick, ) { Column { if (updating) { LinearWavyProgressIndicator( modifier = Modifier.fillMaxWidth().height(3.dp), color = MaterialTheme.colorScheme.primary, ) } Row( modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { ShapedIcon( icon = if (isSubscription) Icons.Filled.Refresh else Icons.Filled.Folder, containerColor = iconContainer, contentColor = iconContent, size = 48.dp, shape = seedShape, pressed = pressed, ) Spacer(Modifier.width(14.dp)) Column( modifier = Modifier.weight(1f), ) { Text( text = group.displayName(), style = MaterialTheme.typography.titleMedium, color = onContainerColor, maxLines = 1, overflow = TextOverflow.Ellipsis, ) Spacer(Modifier.height(2.dp)) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Text( text = "$profileCount profiles", style = MaterialTheme.typography.bodySmall, color = onContainerColor.copy(alpha = 0.7f), ) if (isSubscription) { Text( text = group.subscription?.link?.takeIf { it.isNotEmpty() } ?: "", style = MaterialTheme.typography.bodySmall, color = onContainerColor.copy(alpha = 0.5f), maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.width(120.dp), ) } } } Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), ) { if (onUpdate != null && isSubscription) { IconButton( onClick = onUpdate, modifier = Modifier.size(44.dp), colors = IconButtonDefaults.iconButtonColors( containerColor = MaterialTheme.colorScheme.surfaceBright, ), ) { Icon( Icons.Default.Refresh, contentDescription = "Update", tint = onContainerColor.copy(alpha = 0.7f), modifier = Modifier.size(20.dp), ) } } IconButton( onClick = onEdit, modifier = Modifier.size(44.dp), colors = IconButtonDefaults.iconButtonColors( containerColor = MaterialTheme.colorScheme.surfaceBright, ), ) { Icon( Icons.Default.Edit, contentDescription = "Edit", tint = onContainerColor.copy(alpha = 0.7f), modifier = Modifier.size(20.dp), ) } Box { IconButton( onClick = { menuExpanded = true }, modifier = Modifier.size(44.dp), colors = IconButtonDefaults.iconButtonColors( containerColor = MaterialTheme.colorScheme.surfaceBright, ), ) { Icon( Icons.Default.MoreVert, contentDescription = "More", tint = onContainerColor.copy(alpha = 0.7f), modifier = Modifier.size(20.dp), ) } DropdownMenu( expanded = menuExpanded, onDismissRequest = { menuExpanded = false }, shape = androidx.compose.foundation.shape.RoundedCornerShape(24.dp), containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, ) { menuItems(MenuScope { menuExpanded = false }) } } } } } } } class MenuScope(val dismiss: () -> Unit) @Composable fun RouteCard( name: String, type: String, outbound: String, enabled: Boolean, onEnabledChange: (Boolean) -> Unit, onEdit: () -> Unit, modifier: Modifier = Modifier, ) { Card( modifier = modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 4.dp), shape = MaterialTheme.shapes.extraLarge, colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceContainerLow ), elevation = CardDefaults.cardElevation(defaultElevation = 0.5.dp), ) { Row( modifier = Modifier.fillMaxWidth().padding(16.dp), verticalAlignment = Alignment.CenterVertically, ) { Column( modifier = Modifier.weight(1f), ) { Text( text = name, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis, ) Spacer(Modifier.height(2.dp)) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Text( text = type, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) Text( text = outbound, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } androidx.compose.material3.Switch( checked = enabled, onCheckedChange = onEnabledChange, ) IconButton( onClick = onEdit, modifier = Modifier.size(36.dp), colors = IconButtonDefaults.iconButtonColors( containerColor = MaterialTheme.colorScheme.surfaceBright, ), ) { Icon( Icons.Default.Edit, contentDescription = "Edit", tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(20.dp), ) } } } }