Created
November 27, 2025 06:13
-
-
Save webianks/1d116035608ceebc4ef7174890ff424e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.webianks.miniapps | |
| import android.graphics.Color as AndroidColor | |
| import android.os.Bundle | |
| import androidx.activity.ComponentActivity | |
| import androidx.activity.SystemBarStyle | |
| import androidx.activity.compose.BackHandler | |
| import androidx.activity.compose.setContent | |
| import androidx.activity.enableEdgeToEdge | |
| import androidx.annotation.DrawableRes | |
| import androidx.compose.foundation.ExperimentalFoundationApi | |
| import androidx.compose.foundation.Image | |
| import androidx.compose.foundation.background | |
| import androidx.compose.foundation.border | |
| import androidx.compose.foundation.combinedClickable | |
| import androidx.compose.foundation.layout.* | |
| import androidx.compose.foundation.lazy.grid.GridCells | |
| import androidx.compose.foundation.lazy.grid.LazyVerticalGrid | |
| import androidx.compose.foundation.lazy.grid.items | |
| import androidx.compose.foundation.rememberScrollState | |
| import androidx.compose.foundation.shape.CircleShape | |
| import androidx.compose.foundation.shape.RoundedCornerShape | |
| import androidx.compose.foundation.verticalScroll | |
| import androidx.compose.material.icons.Icons | |
| import androidx.compose.material.icons.automirrored.filled.ArrowBack | |
| import androidx.compose.material.icons.filled.Menu | |
| import androidx.compose.material3.* | |
| import androidx.compose.runtime.* | |
| import androidx.compose.ui.Alignment | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.draw.clip | |
| import androidx.compose.ui.draw.dropShadow | |
| import androidx.compose.ui.graphics.Color | |
| import androidx.compose.ui.graphics.shadow.Shadow | |
| import androidx.compose.ui.hapticfeedback.HapticFeedbackType | |
| import androidx.compose.ui.layout.ContentScale | |
| import androidx.compose.ui.platform.LocalHapticFeedback | |
| import androidx.compose.ui.res.painterResource | |
| import androidx.compose.ui.text.TextStyle | |
| import androidx.compose.ui.text.font.Font | |
| import androidx.compose.ui.text.font.FontFamily | |
| import androidx.compose.ui.text.font.FontWeight | |
| import androidx.compose.ui.text.style.TextDecoration | |
| import androidx.compose.ui.text.style.TextOverflow | |
| import androidx.compose.ui.unit.DpOffset | |
| import androidx.compose.ui.unit.dp | |
| import androidx.compose.ui.unit.sp | |
| // Typography | |
| val HostGrotesk = FontFamily( | |
| Font(R.font.host_grotesk_medium, FontWeight.Medium), | |
| Font(R.font.host_grotesk_semibold, FontWeight.SemiBold) | |
| ) | |
| val TechStoreTypography = Typography( | |
| titleLarge = TextStyle( | |
| fontFamily = HostGrotesk, | |
| fontWeight = FontWeight.SemiBold, | |
| fontSize = 22.sp, | |
| lineHeight = 28.sp, | |
| letterSpacing = 0.sp | |
| ), | |
| titleMedium = TextStyle( | |
| fontFamily = HostGrotesk, | |
| fontWeight = FontWeight.SemiBold, | |
| fontSize = 18.sp, | |
| lineHeight = 24.sp, | |
| letterSpacing = 0.sp | |
| ), | |
| titleSmall = TextStyle( | |
| fontFamily = HostGrotesk, | |
| fontWeight = FontWeight.SemiBold, | |
| fontSize = 16.sp, | |
| lineHeight = 20.sp, | |
| letterSpacing = 0.sp | |
| ), | |
| bodyMedium = TextStyle( | |
| fontFamily = HostGrotesk, | |
| fontWeight = FontWeight.Medium, | |
| fontSize = 14.sp, | |
| lineHeight = 16.sp, | |
| letterSpacing = 0.sp | |
| ), | |
| labelLarge = TextStyle( | |
| fontFamily = HostGrotesk, | |
| fontWeight = FontWeight.Normal, | |
| fontSize = 14.sp, | |
| lineHeight = 16.sp, | |
| letterSpacing = 0.sp | |
| ), | |
| labelMedium = TextStyle( | |
| fontFamily = HostGrotesk, | |
| fontWeight = FontWeight.Normal, | |
| fontSize = 12.sp, | |
| lineHeight = 16.sp, | |
| letterSpacing = 0.sp | |
| ), | |
| labelSmall = TextStyle( | |
| fontFamily = HostGrotesk, | |
| fontWeight = FontWeight.Medium, | |
| fontSize = 10.sp, | |
| lineHeight = 14.sp, | |
| letterSpacing = 0.5.sp | |
| ) | |
| ) | |
| // Models | |
| data class ProductSpecs( | |
| val display: String, | |
| val mainCam: String, | |
| val frontCam: String, | |
| val processor: String, | |
| val ram: String, | |
| val storage: String, | |
| val battery: String | |
| ) | |
| data class Product( | |
| val id: Int, | |
| val name: String, | |
| val price: Int, | |
| val salePrice: Int? = null, | |
| val specs: ProductSpecs, | |
| @DrawableRes val imageRes: Int | |
| ) | |
| // Data | |
| val PRODUCTS = listOf( | |
| Product( | |
| 1, "Google Pixel 9", 799, null, | |
| ProductSpecs("6.3\"", "50MP", "12MP", "Tensor G4", "8GB", "128GB", "4600mAh"), | |
| R.drawable.google_pixel_9 | |
| ), | |
| Product( | |
| 2, "Samsung Galaxy S24+", 999, 899, | |
| ProductSpecs("6.7\"", "50MP", "12MP", "Snapdragon 8 Gen 3", "12GB", "256GB", "4900mAh"), | |
| R.drawable.samsung_galaxy_s24 | |
| ), | |
| Product( | |
| 3, "iPhone 15 Pro", 1099, null, | |
| ProductSpecs("6.1\"", "48MP", "12MP", "A17 Pro", "8GB", "256GB", "4350mAh"), | |
| R.drawable.iphone_15_pro | |
| ), | |
| Product( | |
| 4, "Google Pixel 9 Pro", 999, null, | |
| ProductSpecs("6.7\"", "50MP", "12MP", "Tensor G4", "12GB", "256GB", "5100mAh"), | |
| R.drawable.google_pixel_9_pro | |
| ), | |
| Product( | |
| 5, "OnePlus 12", 899, 799, | |
| ProductSpecs("6.8\"", "50MP", "32MP", "Snapdragon 8 Gen 3", "12GB", "256GB", "5400mAh"), | |
| R.drawable.oneplus_12 | |
| ), | |
| Product( | |
| 6, "Xiaomi 14", 799, 699, | |
| ProductSpecs("6.4\"", "50MP", "32MP", "Snapdragon 8 Gen 3", "12GB", "256GB", "4610mAh"), | |
| R.drawable.xiaomi_14 | |
| ) | |
| ) | |
| // Screen | |
| class MainActivity : ComponentActivity() { | |
| override fun onCreate(savedInstanceState: Bundle?) { | |
| super.onCreate(savedInstanceState) | |
| enableEdgeToEdge( | |
| statusBarStyle = SystemBarStyle.light( | |
| AndroidColor.TRANSPARENT, AndroidColor.TRANSPARENT | |
| ), | |
| navigationBarStyle = SystemBarStyle.light( | |
| AndroidColor.TRANSPARENT, AndroidColor.TRANSPARENT | |
| ) | |
| ) | |
| setContent { | |
| MaterialTheme( | |
| colorScheme = lightColorScheme( | |
| primary = Color(0xFF4291FF), // Brand | |
| background = Color(0xFFEDF0F6), | |
| surface = Color(0xFFFFFFFF), | |
| onSurface = Color(0xFF041221), // Text-Primary | |
| onSurfaceVariant = Color(0xFF526881), // Text-Secondary & Text-Disabled | |
| outline = Color(0xFFD8E4EA), | |
| error = Color(0xFFE6556F) // Discount | |
| ), | |
| typography = TechStoreTypography | |
| ) { | |
| TechStoreApp() | |
| } | |
| } | |
| } | |
| } | |
| // State and Navigation | |
| enum class Screen { | |
| ProductList, | |
| Comparison | |
| } | |
| @Composable | |
| fun TechStoreApp() { | |
| var currentScreen by remember { mutableStateOf(Screen.ProductList) } | |
| var selectedIds by remember { mutableStateOf(listOf<Int>()) } | |
| val haptics = LocalHapticFeedback.current | |
| fun toggleSelection(productId: Int) { | |
| if (selectedIds.contains(productId)) { | |
| selectedIds = selectedIds - productId | |
| } else { | |
| when (selectedIds.size) { | |
| 0, 1 -> { | |
| selectedIds = selectedIds + productId | |
| haptics.performHapticFeedback(HapticFeedbackType.LongPress) | |
| } | |
| 2 -> { | |
| val first = selectedIds[0] | |
| selectedIds = listOf(first, productId) | |
| haptics.performHapticFeedback(HapticFeedbackType.LongPress) | |
| } | |
| } | |
| } | |
| } | |
| fun clearSelection() { | |
| selectedIds = emptyList() | |
| } | |
| Scaffold( | |
| topBar = { | |
| TopBar( | |
| currentScreen = currentScreen, | |
| selectionCount = selectedIds.size, | |
| onBack = { | |
| if (currentScreen == Screen.Comparison) { | |
| currentScreen = Screen.ProductList | |
| } else { | |
| clearSelection() | |
| } | |
| }, | |
| onCompare = { currentScreen = Screen.Comparison } | |
| ) | |
| } | |
| ) { paddingValues -> | |
| Box(modifier = Modifier.padding(paddingValues)) { | |
| when (currentScreen) { | |
| Screen.ProductList -> { | |
| ProductListScreen( | |
| products = PRODUCTS, | |
| selectedIds = selectedIds, | |
| onProductLongPress = { toggleSelection(it) }, | |
| onProductClick = { id -> | |
| if (selectedIds.contains(id)) { | |
| toggleSelection(id) | |
| } | |
| } | |
| ) | |
| } | |
| Screen.Comparison -> { | |
| val p1 = PRODUCTS.find { it.id == selectedIds.getOrNull(0) } | |
| val p2 = PRODUCTS.find { it.id == selectedIds.getOrNull(1) } | |
| if (p1 != null && p2 != null) { | |
| ComparisonScreen(p1, p2) | |
| } else { | |
| // Fallback should rarely happen | |
| Text("Error: Invalid selection") | |
| } | |
| // Handle hardware back button in Comparison view | |
| BackHandler { | |
| currentScreen = Screen.ProductList | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Components | |
| @OptIn(ExperimentalMaterial3Api::class) | |
| @Composable | |
| fun TopBar( | |
| currentScreen: Screen, | |
| selectionCount: Int, | |
| onBack: () -> Unit, | |
| onCompare: () -> Unit | |
| ) { | |
| val isSelectionMode = selectionCount > 0 | |
| val isComparison = currentScreen == Screen.Comparison | |
| CenterAlignedTopAppBar( | |
| colors = TopAppBarDefaults.centerAlignedTopAppBarColors( | |
| containerColor = MaterialTheme.colorScheme.background | |
| ), | |
| title = { | |
| if (isComparison) { | |
| Text("Comparison", style = MaterialTheme.typography.titleLarge) | |
| } else if (isSelectionMode) { | |
| Text( | |
| "$selectionCount item${if (selectionCount != 1) "s" else ""} selected", | |
| style = MaterialTheme.typography.titleSmall, | |
| color = Color(0xFFA8B7BF) | |
| ) | |
| } else { | |
| Text("TechStore", style = MaterialTheme.typography.titleLarge) | |
| } | |
| }, | |
| navigationIcon = { | |
| if (isComparison || isSelectionMode) { | |
| IconButton(onClick = onBack) { | |
| Icon( | |
| imageVector = Icons.AutoMirrored.Filled.ArrowBack, | |
| contentDescription = "Back" | |
| ) | |
| } | |
| } else { | |
| IconButton(onClick = { /* Menu Action */ }) { | |
| Icon(imageVector = Icons.Default.Menu, contentDescription = "Menu") | |
| } | |
| } | |
| }, | |
| actions = { | |
| if (isComparison) { | |
| Spacer(modifier = Modifier.width(48.dp)) | |
| } else if (isSelectionMode) { | |
| val canCompare = selectionCount >= 2 | |
| IconButton( | |
| onClick = onCompare, | |
| enabled = canCompare | |
| ) { | |
| Icon( | |
| painter = painterResource(R.drawable.ic_scale), | |
| contentDescription = "Compare", | |
| tint = if (canCompare) MaterialTheme.colorScheme.onSurface else Color.LightGray | |
| ) | |
| } | |
| } else { | |
| IconButton(onClick = { /* Cart Action */ }) { | |
| Icon( | |
| painterResource(R.drawable.ic_cart), | |
| modifier = Modifier.size(20.dp), | |
| tint = MaterialTheme.colorScheme.onSurface, | |
| contentDescription = "Cart" | |
| ) | |
| } | |
| } | |
| } | |
| ) | |
| } | |
| @Composable | |
| fun ProductListScreen( | |
| products: List<Product>, | |
| selectedIds: List<Int>, | |
| onProductLongPress: (Int) -> Unit, | |
| onProductClick: (Int) -> Unit | |
| ) { | |
| LazyVerticalGrid( | |
| columns = GridCells.Fixed(2), | |
| contentPadding = PaddingValues(16.dp), | |
| horizontalArrangement = Arrangement.spacedBy(12.dp), | |
| verticalArrangement = Arrangement.spacedBy(12.dp), | |
| modifier = Modifier | |
| .fillMaxSize() | |
| .background(MaterialTheme.colorScheme.background) | |
| ) { | |
| items(products) { product -> | |
| ProductItem( | |
| product = product, | |
| isSelected = selectedIds.contains(product.id), | |
| onLongPress = { onProductLongPress(product.id) }, | |
| onClick = { onProductClick(product.id) } | |
| ) | |
| } | |
| } | |
| } | |
| @OptIn(ExperimentalFoundationApi::class) | |
| @Composable | |
| fun ProductItem( | |
| product: Product, | |
| isSelected: Boolean, | |
| onLongPress: () -> Unit, | |
| onClick: () -> Unit | |
| ) { | |
| val borderColor = | |
| if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline | |
| val borderWidth = if (isSelected) 2.dp else 0.dp | |
| Card( | |
| colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), | |
| elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), | |
| modifier = Modifier | |
| .fillMaxWidth() | |
| .dropShadow( | |
| shape = RoundedCornerShape(12.dp), | |
| shadow = Shadow( | |
| radius = 4.dp, | |
| spread = 0.dp, | |
| offset = DpOffset(0.dp, 4.dp), | |
| alpha = 0.04f, | |
| color = MaterialTheme.colorScheme.onSurface | |
| ) | |
| ) | |
| .border(borderWidth, borderColor, RoundedCornerShape(12.dp)) | |
| .clip(RoundedCornerShape(12.dp)) | |
| .combinedClickable( | |
| onClick = onClick, | |
| onLongClick = onLongPress | |
| ) | |
| ) { | |
| Column(modifier = Modifier.padding(12.dp)) { | |
| Box( | |
| modifier = Modifier | |
| .fillMaxWidth() | |
| .height(120.dp) | |
| ) { | |
| Image( | |
| painter = painterResource(id = product.imageRes), | |
| contentDescription = product.name, | |
| modifier = Modifier.fillMaxSize(), | |
| contentScale = ContentScale.Fit | |
| ) | |
| // Sale Badge | |
| if (product.salePrice != null) { | |
| Badge( | |
| modifier = Modifier | |
| .align(Alignment.TopEnd) | |
| .padding(4.dp), | |
| containerColor = MaterialTheme.colorScheme.error, | |
| contentColor = Color.White | |
| ) { | |
| Text( | |
| "-38%", | |
| style = MaterialTheme.typography.labelSmall, | |
| ) | |
| } | |
| } | |
| } | |
| Spacer(modifier = Modifier.height(12.dp)) | |
| Text( | |
| text = product.name, | |
| style = MaterialTheme.typography.bodyMedium, | |
| color = MaterialTheme.colorScheme.onSurfaceVariant, | |
| maxLines = 1, | |
| overflow = TextOverflow.Ellipsis | |
| ) | |
| Spacer(modifier = Modifier.height(4.dp)) | |
| // Price Row | |
| Row(verticalAlignment = Alignment.Bottom) { | |
| if (product.salePrice != null) { | |
| Text( | |
| text = "$${product.salePrice}", | |
| color = MaterialTheme.colorScheme.error, | |
| style = MaterialTheme.typography.titleMedium | |
| ) | |
| Spacer(modifier = Modifier.width(6.dp)) | |
| Text( | |
| text = "$${product.price}", | |
| textDecoration = TextDecoration.LineThrough, | |
| color = MaterialTheme.colorScheme.onSurfaceVariant, | |
| style = MaterialTheme.typography.bodyMedium, | |
| modifier = Modifier.padding(bottom = 2.dp) | |
| ) | |
| } else { | |
| Text( | |
| text = "$${product.price}", | |
| color = MaterialTheme.colorScheme.onSurface, | |
| style = MaterialTheme.typography.titleMedium | |
| ) | |
| } | |
| } | |
| Spacer(modifier = Modifier.height(12.dp)) | |
| Button( | |
| onClick = { /* No-op */ }, | |
| modifier = Modifier | |
| .fillMaxWidth() | |
| .height(28.dp), | |
| contentPadding = PaddingValues(0.dp), | |
| shape = CircleShape | |
| ) { | |
| Text("Buy", style = MaterialTheme.typography.bodyMedium) | |
| } | |
| } | |
| } | |
| } | |
| @Composable | |
| fun ComparisonScreen(p1: Product, p2: Product) { | |
| Column( | |
| modifier = Modifier | |
| .fillMaxSize() | |
| .verticalScroll(rememberScrollState()) | |
| .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) | |
| .background(MaterialTheme.colorScheme.surface) | |
| .padding(16.dp) | |
| ) { | |
| Row( | |
| modifier = Modifier.fillMaxWidth(), | |
| horizontalArrangement = Arrangement.spacedBy(16.dp) | |
| ) { | |
| ComparisonHeaderItem(product = p1, modifier = Modifier.weight(1f)) | |
| ComparisonHeaderItem(product = p2, modifier = Modifier.weight(1f)) | |
| } | |
| Spacer(modifier = Modifier.height(12.dp)) | |
| HorizontalDivider(color = MaterialTheme.colorScheme.outline) | |
| // Specs Table | |
| SpecRow("Display", p1.specs.display, p2.specs.display) | |
| HorizontalDivider(color = MaterialTheme.colorScheme.outline) | |
| SpecRow("Main Camera", p1.specs.mainCam, p2.specs.mainCam) | |
| SpecRow("Front Camera", p1.specs.frontCam, p2.specs.frontCam) | |
| HorizontalDivider(color = MaterialTheme.colorScheme.outline) | |
| SpecRow("Processor", p1.specs.processor, p2.specs.processor) | |
| SpecRow("RAM", p1.specs.ram, p2.specs.ram) | |
| SpecRow("Storage", p1.specs.storage, p2.specs.storage) | |
| HorizontalDivider(color = MaterialTheme.colorScheme.outline) | |
| SpecRow("Battery", p1.specs.battery, p2.specs.battery) | |
| HorizontalDivider() | |
| } | |
| } | |
| @Composable | |
| fun ComparisonHeaderItem(product: Product, modifier: Modifier = Modifier) { | |
| Column( | |
| modifier = modifier, | |
| horizontalAlignment = Alignment.Start | |
| ) { | |
| Box( | |
| modifier = Modifier | |
| .border(1.dp, MaterialTheme.colorScheme.outline, RoundedCornerShape(12.dp)) | |
| ) { | |
| Image( | |
| painter = painterResource(id = product.imageRes), | |
| contentDescription = product.name, | |
| modifier = Modifier | |
| .fillMaxWidth() | |
| .height(184.dp) | |
| .padding(8.dp), | |
| contentScale = ContentScale.Fit | |
| ) | |
| // Sale Badge | |
| if (product.salePrice != null) { | |
| Badge( | |
| modifier = Modifier | |
| .align(Alignment.TopEnd) | |
| .padding(4.dp), | |
| containerColor = MaterialTheme.colorScheme.error, | |
| contentColor = Color.White | |
| ) { | |
| Text( | |
| "-38%", | |
| style = MaterialTheme.typography.labelSmall, | |
| ) | |
| } | |
| } | |
| } | |
| Spacer(modifier = Modifier.height(8.dp)) | |
| Text( | |
| text = product.name, | |
| style = MaterialTheme.typography.bodyMedium, | |
| color = MaterialTheme.colorScheme.onSurfaceVariant, | |
| maxLines = 1, | |
| overflow = TextOverflow.Ellipsis | |
| ) | |
| Spacer(modifier = Modifier.height(4.dp)) | |
| if (product.salePrice != null) { | |
| Row(verticalAlignment = Alignment.Bottom) { | |
| Text( | |
| text = "$${product.salePrice}", | |
| color = MaterialTheme.colorScheme.error, | |
| style = MaterialTheme.typography.titleMedium | |
| ) | |
| Spacer(modifier = Modifier.width(6.dp)) | |
| Text( | |
| text = "$${product.price}", | |
| textDecoration = TextDecoration.LineThrough, | |
| color = MaterialTheme.colorScheme.onSurfaceVariant, | |
| style = MaterialTheme.typography.bodyMedium, | |
| modifier = Modifier.padding(bottom = 2.dp) | |
| ) | |
| } | |
| } else { | |
| Text( | |
| text = "$${product.price}", | |
| color = MaterialTheme.colorScheme.onSurface, | |
| style = MaterialTheme.typography.titleMedium | |
| ) | |
| } | |
| Spacer(modifier = Modifier.height(8.dp)) | |
| Button( | |
| onClick = { }, | |
| modifier = Modifier | |
| .fillMaxWidth() | |
| .height(28.dp), | |
| contentPadding = PaddingValues(0.dp), | |
| shape = CircleShape | |
| ) { | |
| Text("Buy", style = MaterialTheme.typography.bodyMedium) | |
| } | |
| } | |
| } | |
| @Composable | |
| fun SpecRow(label: String, val1: String, val2: String) { | |
| Column(modifier = Modifier.padding(vertical = 12.dp)) { | |
| Text( | |
| text = label, | |
| style = MaterialTheme.typography.labelMedium, | |
| color = MaterialTheme.colorScheme.onSurfaceVariant, | |
| modifier = Modifier.padding(bottom = 4.dp) | |
| ) | |
| Row(modifier = Modifier.fillMaxWidth()) { | |
| Text( | |
| text = val1, | |
| style = MaterialTheme.typography.labelLarge, | |
| color = MaterialTheme.colorScheme.onSurface, | |
| modifier = Modifier.weight(1f) | |
| ) | |
| Spacer(modifier = Modifier.width(12.dp)) | |
| Text( | |
| text = val2, | |
| style = MaterialTheme.typography.labelLarge, | |
| color = MaterialTheme.colorScheme.onSurface, | |
| modifier = Modifier.weight(1f) | |
| ) | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment