package com.craigvg.lichun_android.ui.screens.activities import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ExitToApp import androidx.compose.material.icons.automirrored.filled.TrendingUp import androidx.compose.material.icons.filled.* 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.scale import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import coil3.compose.AsyncImage import com.craigvg.lichun_android.domain.models.* import com.craigvg.lichun_android.ui.navigation.activityIconSharedKey import com.craigvg.lichun_android.ui.navigation.personAvatarSharedKey import com.craigvg.lichun_android.ui.navigation.sharedElementKey import com.craigvg.lichun_android.ui.theme.AppColors import com.craigvg.lichun_android.ui.theme.AppSpacing import com.craigvg.lichun_android.ui.theme.AppTypography import com.craigvg.lichun_android.viewmodel.PlayerViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle /** * Activity detail screen with hero image, performance, focus, people, quit * Ported from iOS ActivityView.swift */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun ActivityDetailScreen( activityId: String, playerViewModel: PlayerViewModel = hiltViewModel(), onBack: () -> Unit = {}, onPersonClick: (String) -> Unit = {} ) { val person by playerViewModel.person.collectAsStateWithLifecycle() val player by playerViewModel.player.collectAsStateWithLifecycle() val activity = person.activities.find { it.id == activityId } val record = person.activityRecords.find { it.id == activityId } val educationRecord = person.currentEducation?.takeIf { it.id == activityId } // Hero scale animation val infiniteTransition = rememberInfiniteTransition(label = "hero") val headerScale by infiniteTransition.animateFloat( initialValue = 1.0f, targetValue = 1.05f, animationSpec = infiniteRepeatable( animation = tween(4000, easing = EaseInOut), repeatMode = RepeatMode.Reverse ), label = "headerScale" ) // Content appear animation var showContent by remember { mutableStateOf(false) } LaunchedEffect(Unit) { showContent = true } val contentAlpha by animateFloatAsState( targetValue = if (showContent) 1f else 0f, animationSpec = tween(600), label = "contentAlpha" ) if (activity == null) { Box( modifier = Modifier.fillMaxSize().background(AppColors.background), contentAlignment = Alignment.Center ) { CircularProgressIndicator(color = AppColors.primary) } return } val relatedPeople = remember(player.r, activity.type) { player.r.filter { p -> when { activity.type in listOf("high_school", "elementary_school", "college") -> p.relationships.any { it == "classmate" || it == "teacher" } activity.type == "job" -> p.relationships.any { it == "coworker" || it == "boss" } else -> false } }.sortedByDescending { p -> p.relationships.any { it == "boss" || it == "teacher" } } } Scaffold( topBar = { TopAppBar( title = {}, navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.Default.Close, "Close", tint = AppColors.secondaryText) } }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent) ) }, containerColor = AppColors.background ) { paddingValues -> Column( modifier = Modifier .fillMaxSize() .padding(paddingValues) .verticalScroll(rememberScrollState()) ) { // Hero Header Box( modifier = Modifier .fillMaxWidth() .height(280.dp) ) { AsyncImage( model = activity.image, contentDescription = activity.title, modifier = Modifier .fillMaxSize() .sharedElementKey(activityIconSharedKey(activity.id)) .scale(headerScale), contentScale = ContentScale.Crop ) // Gradient overlay Box( modifier = Modifier .fillMaxSize() .background( Brush.verticalGradient( colors = listOf( Color.Transparent, Color.Black.copy(alpha = 0.3f), Color.Black.copy(alpha = 0.7f) ) ) ) ) // Title overlay Column( modifier = Modifier .align(Alignment.BottomStart) .padding(AppSpacing.md) .padding(bottom = AppSpacing.sm) ) { Text( text = activity.title, style = AppTypography.title, color = Color.White ) if (record != null) { Spacer(modifier = Modifier.height(AppSpacing.xs)) ActivityTypeLabel(record.type) } } } Spacer(modifier = Modifier.height(AppSpacing.md)) // Performance Card if (record != null) { PerformanceCard( record = record, educationRecord = educationRecord, modifier = Modifier .padding(horizontal = AppSpacing.md) .graphicsLayer { alpha = contentAlpha } ) Spacer(modifier = Modifier.height(AppSpacing.md)) } // Focus Selection Card if (player.focuses.isNotEmpty()) { FocusCard( focuses = player.focuses, currentFocus = record?.focus ?: "", onFocusSelect = { focus -> playerViewModel.setFocus(activityId, focus.id) }, modifier = Modifier .padding(horizontal = AppSpacing.md) .graphicsLayer { alpha = contentAlpha } ) Spacer(modifier = Modifier.height(AppSpacing.md)) } // People Section if (relatedPeople.isNotEmpty()) { PeopleSection( people = relatedPeople, activityType = activity.type, onPersonClick = { personId -> playerViewModel.retrievePerson(personId) onPersonClick(personId) }, modifier = Modifier .padding(horizontal = AppSpacing.md) .graphicsLayer { alpha = contentAlpha } ) Spacer(modifier = Modifier.height(AppSpacing.md)) } // Quit Button if (record != null) { QuitButton( type = record.type, onQuit = { playerViewModel.dropActivity(activityId) onBack() }, modifier = Modifier .padding(horizontal = AppSpacing.md) .graphicsLayer { alpha = contentAlpha } ) } Spacer(modifier = Modifier.height(AppSpacing.xl)) } } } @Composable private fun ActivityTypeLabel(type: String) { val (icon, label) = when (type) { "job" -> Icons.Default.Work to "Occupation" "high_school" -> Icons.Default.School to "High School" "elementary_school" -> Icons.Default.School to "Elementary School" "college" -> Icons.Default.School to "College" "extracurricular" -> Icons.Default.Star to "Extracurricular" else -> Icons.Default.GridView to "Activity" } Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier .clip(RoundedCornerShape(AppSpacing.smallCornerRadius)) .background(Color.White.copy(alpha = 0.2f)) .padding(horizontal = 12.dp, vertical = 6.dp) ) { Icon(icon, null, tint = Color.White, modifier = Modifier.size(12.dp)) Text(label, style = AppTypography.caption, color = Color.White) } } @Composable private fun PerformanceCard( record: ActivityRecord, educationRecord: EducationRecord?, modifier: Modifier = Modifier ) { Card( modifier = modifier.fillMaxWidth(), shape = RoundedCornerShape(AppSpacing.cornerRadius), colors = CardDefaults.cardColors(containerColor = AppColors.surfaceElevated) ) { Column(modifier = Modifier.padding(AppSpacing.md)) { // Header Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.AutoMirrored.Filled.TrendingUp, null, tint = AppColors.primary, modifier = Modifier.size(20.dp)) Spacer(modifier = Modifier.width(AppSpacing.xs)) Text("Performance", style = AppTypography.headline, color = AppColors.primaryText) } Spacer(modifier = Modifier.height(AppSpacing.md)) // Job level stats record.level?.let { level -> StatRow(Icons.Default.Work, "Position", level.level, AppColors.primary) Spacer(modifier = Modifier.height(AppSpacing.sm)) StatRow(Icons.Default.AttachMoney, "Salary", "$${level.salary} / month", AppColors.money) Spacer(modifier = Modifier.height(AppSpacing.sm)) // Performance progress bar PerformanceProgressBar( icon = Icons.Default.Star, label = "Performance", value = record.performance, startColor = AppColors.success, endColor = AppColors.energy ) } // Education stats if (educationRecord != null) { StatRow(Icons.Default.School, "Education Level", educationRecord.educationLevel, AppColors.intelligence) Spacer(modifier = Modifier.height(AppSpacing.sm)) val gpa = gpPercentToGPA(educationRecord.GPA.toInt()) StatRow(Icons.Default.BarChart, "GPA", String.format("%.1f", gpa), AppColors.prestige) Spacer(modifier = Modifier.height(AppSpacing.sm)) PerformanceProgressBar( icon = Icons.Default.Psychology, label = "Academic Performance", value = educationRecord.GPA.toInt(), startColor = AppColors.intelligence, endColor = AppColors.prestige ) } // Current focus display if (record.focus.isNotEmpty()) { Spacer(modifier = Modifier.height(AppSpacing.sm)) HorizontalDivider(color = AppColors.primaryText.copy(alpha = 0.1f)) Spacer(modifier = Modifier.height(AppSpacing.sm)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm) ) { Icon(Icons.Default.FilterCenterFocus, null, tint = AppColors.secondary, modifier = Modifier.size(14.dp)) Text("Current Focus:", style = AppTypography.body, color = AppColors.secondaryText) } Text(record.focus, style = AppTypography.bodyBold, color = AppColors.primaryText, maxLines = 1) } } } } } @Composable private fun StatRow(icon: ImageVector, label: String, value: String, color: Color) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { Icon(icon, null, tint = color, modifier = Modifier.size(14.dp)) Spacer(modifier = Modifier.width(AppSpacing.sm)) Text(label, style = AppTypography.body, color = AppColors.secondaryText, modifier = Modifier.weight(1f)) Text(value, style = AppTypography.bodyBold, color = AppColors.primaryText) } } @Composable private fun PerformanceProgressBar( icon: ImageVector, label: String, value: Int, startColor: Color, endColor: Color ) { Column { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs)) { Icon(icon, null, tint = startColor, modifier = Modifier.size(12.dp)) Text(label, style = AppTypography.body, color = AppColors.primaryText) } Text("$value%", style = AppTypography.bodyBold, color = AppColors.primaryText) } Spacer(modifier = Modifier.height(AppSpacing.xs)) Box( modifier = Modifier .fillMaxWidth() .height(12.dp) .clip(RoundedCornerShape(6.dp)) .background(AppColors.background) ) { Box( modifier = Modifier .fillMaxWidth(value / 100f) .fillMaxHeight() .clip(RoundedCornerShape(6.dp)) .background(Brush.horizontalGradient(listOf(startColor, endColor))) ) } } } @Composable private fun FocusCard( focuses: List, currentFocus: String, onFocusSelect: (FocusOption) -> Unit, modifier: Modifier = Modifier ) { Card( modifier = modifier.fillMaxWidth(), shape = RoundedCornerShape(AppSpacing.cornerRadius), colors = CardDefaults.cardColors(containerColor = AppColors.surfaceElevated) ) { Column(modifier = Modifier.padding(AppSpacing.md)) { Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.FilterCenterFocus, null, tint = AppColors.accent, modifier = Modifier.size(20.dp)) Spacer(modifier = Modifier.width(AppSpacing.xs)) Text("Change Focus", style = AppTypography.headline, color = AppColors.primaryText) } Spacer(modifier = Modifier.height(AppSpacing.md)) // Focus chips @OptIn(ExperimentalLayoutApi::class) androidx.compose.foundation.layout.FlowRow( horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), verticalArrangement = Arrangement.spacedBy(AppSpacing.sm) ) { focuses.forEach { focus -> val isSelected = currentFocus == focus.focus_name FocusChip( name = focus.focus_name, isSelected = isSelected, onClick = { onFocusSelect(focus) } ) } } } } } @Composable private fun FocusChip( name: String, isSelected: Boolean, onClick: () -> Unit ) { val scale by animateFloatAsState( targetValue = if (isSelected) 1.05f else 1.0f, animationSpec = spring(dampingRatio = 0.6f), label = "chipScale" ) Box( modifier = Modifier .scale(scale) .clip(RoundedCornerShape(AppSpacing.pillCornerRadius)) .then( if (isSelected) { Modifier.background( Brush.horizontalGradient(listOf(AppColors.primary, AppColors.accent)) ) } else { Modifier .background(AppColors.background) .border(1.5.dp, AppColors.primaryText.copy(alpha = 0.3f), RoundedCornerShape(AppSpacing.pillCornerRadius)) } ) .clickable(onClick = onClick) .padding(horizontal = AppSpacing.md, vertical = AppSpacing.sm) ) { Text( text = name, style = AppTypography.body, color = if (isSelected) Color.White else AppColors.primaryText ) } } @Composable private fun PeopleSection( people: List, activityType: String, onPersonClick: (String) -> Unit, modifier: Modifier = Modifier ) { val title = when { activityType in listOf("high_school", "elementary_school", "college") -> "Classmates & Teachers" activityType == "job" -> "Coworkers & Bosses" else -> "People" } Card( modifier = modifier.fillMaxWidth(), shape = RoundedCornerShape(AppSpacing.cornerRadius), colors = CardDefaults.cardColors(containerColor = AppColors.surfaceElevated) ) { Column(modifier = Modifier.padding(AppSpacing.md)) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs)) { Icon(Icons.Default.People, null, tint = AppColors.secondary, modifier = Modifier.size(20.dp)) Text(title, style = AppTypography.headline, color = AppColors.primaryText) } Text( text = "${people.size}", style = AppTypography.bodyBold, color = AppColors.secondaryText, modifier = Modifier .clip(RoundedCornerShape(8.dp)) .background(AppColors.background) .padding(horizontal = 10.dp, vertical = 4.dp) ) } Spacer(modifier = Modifier.height(AppSpacing.md)) // People grid (2 columns) val rows = people.chunked(2) Column(verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)) { rows.forEach { row -> Row( horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), modifier = Modifier.fillMaxWidth() ) { row.forEach { p -> PersonGridCard( person = p, onClick = { onPersonClick(p.id) }, modifier = Modifier.weight(1f) ) } if (row.size == 1) { Spacer(modifier = Modifier.weight(1f)) } } } } } } } @Composable private fun PersonGridCard( person: Person, onClick: () -> Unit, modifier: Modifier = Modifier ) { val relationship = person.relationships.firstOrNull() ?: "Person" val color = when { relationship.contains("boss") || relationship.contains("teacher") -> AppColors.prestige relationship.contains("coworker") || relationship.contains("classmate") -> AppColors.friend else -> AppColors.acquaintance } Card( modifier = modifier.clickable(onClick = onClick), shape = RoundedCornerShape(AppSpacing.cornerRadius), colors = CardDefaults.cardColors(containerColor = AppColors.background) ) { Column( modifier = Modifier.padding(AppSpacing.sm).fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { com.craigvg.lichun_android.ui.components.images.CharacterAvatar( imageUrl = person.image, firstName = person.firstname, lastName = person.lastname, size = 50.dp, modifier = Modifier.sharedElementKey(personAvatarSharedKey(person.id)) ) Spacer(modifier = Modifier.height(AppSpacing.xs)) Text( text = person.firstname, style = AppTypography.caption, color = AppColors.primaryText, maxLines = 1, overflow = TextOverflow.Ellipsis ) Box( modifier = Modifier .clip(RoundedCornerShape(6.dp)) .background(color.copy(alpha = 0.15f)) .padding(horizontal = 8.dp, vertical = 3.dp) ) { Text( text = relationship.replaceFirstChar { it.uppercase() }, style = AppTypography.caption.copy(fontSize = 10.sp), color = AppColors.primaryText ) } } } } @Composable private fun QuitButton( type: String, onQuit: () -> Unit, modifier: Modifier = Modifier ) { val (icon, text) = when (type) { "job" -> Icons.AutoMirrored.Filled.ExitToApp to "Quit Job" "extracurricular" -> Icons.Default.Cancel to "Quit Activity" else -> Icons.Default.Cancel to "Leave Activity" } Button( onClick = onQuit, modifier = modifier .fillMaxWidth() .height(AppSpacing.buttonHeight), shape = RoundedCornerShape(AppSpacing.cornerRadius), colors = ButtonDefaults.buttonColors(containerColor = AppColors.error) ) { Icon(icon, null, modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.width(AppSpacing.sm)) Text(text, style = AppTypography.bodyBold, color = Color.White) } } /** Convert GPA percentage (0-100) to 4.0 scale */ private fun gpPercentToGPA(gpPercent: Int): Double { return (gpPercent / 25.0).coerceIn(0.0, 4.0) }