package com.berker.herodetail import android.graphics.Bitmap import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.clickable 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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size 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.Card import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateMapOf 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.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.luminance import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInParent import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle 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 androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.zIndex import androidx.core.graphics.drawable.toBitmap import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.palette.graphics.Palette import coil.compose.AsyncImage import coil.imageLoader import coil.request.CachePolicy import coil.request.ImageRequest import com.berker.core.ui.onSuccess import com.berker.domain.model.league.heroes.detail.HeroItem import com.berker.domain.model.league.heroes.detail.SpellItem import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.SimpleExoPlayer import com.google.android.exoplayer2.ui.AspectRatioFrameLayout import com.google.android.exoplayer2.ui.PlayerView import com.google.android.exoplayer2.video.VideoSize import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay @AndroidEntryPoint class HeroDetailFragment : Fragment() { private val viewModel: HeroDetailViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { viewModel.getHero() return ComposeView(requireContext()).apply { setContent { var heroItem by remember { mutableStateOf(HeroItem()) } LaunchedEffect(Unit) { viewModel.uiItems.collect { uiState -> uiState.onSuccess { if (it == null) return@collect heroItem = it } } } DetailedHeroScreen(heroItem = heroItem) } } } private fun getDominantColors(bitmap: Bitmap, numberOfColors: Int = 3): List { return bitmap.let { val palette = Palette.from(it).generate() val dominantColors = mutableListOf() palette.dominantSwatch?.let { dominantColors.add(it.rgb) } palette.vibrantSwatch?.let { dominantColors.add(it.rgb) } palette.mutedSwatch?.let { dominantColors.add(it.rgb) } dominantColors.take(numberOfColors).map { Color(it) } } } private fun getReadableTextColor(bgColor: Color): Color { return if (bgColor.luminance() < 0.005) { Color.Black // use black text for lighter backgrounds } else { Color.White // use white text for darker backgrounds } } @Composable fun DetailedHeroScreen(heroItem: HeroItem) { val scrollState = rememberScrollState() var startAnimation by remember { mutableStateOf(false) } var colors by remember { mutableStateOf>(emptyList()) } val systemUiController = rememberSystemUiController() var useDarkIcons = remember { if (colors.isEmpty().not()) getReadableTextColor(colors.last()) else Color.White } // Provide whether dark icons should be used depending on your background color SideEffect { systemUiController.setSystemBarsColor( color = Color.Transparent, darkIcons = useDarkIcons != Color.White ) } val imageUrl = "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/" + heroItem.id + "_0.jpg" val request = ImageRequest.Builder(LocalContext.current) .data(imageUrl) .memoryCacheKey(imageUrl) .diskCacheKey(imageUrl) .diskCachePolicy(CachePolicy.ENABLED) .memoryCachePolicy(CachePolicy.ENABLED) .crossfade(true).build() LaunchedEffect(Unit) { delay(100) // 500ms delay startAnimation = true } // Animate the scale and opacity val scale by animateFloatAsState( if (startAnimation) 1f else 0f, animationSpec = tween(1000), label = "" ) val opacity by animateFloatAsState( if (startAnimation) 1f else 0f, animationSpec = tween(1500), label = "" ) Box( modifier = Modifier .fillMaxSize() .verticalScroll(scrollState) ) { Column( modifier = Modifier .fillMaxSize() .background( if (colors .isEmpty() .not() ) colors.last() else Color.White ), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, ) { // Hero Image & Name Box( modifier = Modifier .fillMaxWidth() .height(250.dp) ) { AsyncImage( request, placeholder = painterResource(id = com.berker.presentation.R.drawable.ic_back_button), // Update this to your placeholder resource contentDescription = heroItem.name, contentScale = ContentScale.Crop, modifier = Modifier .fillMaxSize() .clip(RoundedCornerShape(0.dp, 0.dp, 32.dp, 32.dp)), onSuccess = { val asd = getDominantColors( it.result.drawable.toBitmap().copy(Bitmap.Config.ARGB_8888, true), 3 ) colors = asd useDarkIcons = if (colors.isEmpty() .not() ) getReadableTextColor(colors.first()) else Color.White }, onError = { it }, onLoading = { it }, imageLoader = requireContext().imageLoader ) Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxSize() ) { Text( text = heroItem.name.orEmpty(), style = TextStyle( color = Color.White.copy(alpha = opacity), shadow = Shadow( color = Color.Black, offset = Offset(2f, 2f), blurRadius = 4f ) ), modifier = Modifier .graphicsLayer( scaleX = scale, // apply the animated scale scaleY = scale ), fontSize = 30.sp, fontWeight = FontWeight.Bold ) Text( text = heroItem.title.orEmpty(), style = TextStyle( color = Color.White.copy(alpha = opacity), shadow = Shadow( color = Color.Black, offset = Offset(2f, 2f), blurRadius = 4f ) ), modifier = Modifier .graphicsLayer( scaleX = scale, // apply the animated scale scaleY = scale ), fontSize = 20.sp ) heroItem.infoItem?.difficulty?.let { DifficultyIndicator(difficulty = it) } } } // Lore Section Text( text = "Lore", fontWeight = FontWeight.Bold, fontSize = 20.sp, modifier = Modifier.padding(16.dp) ) Text( text = heroItem.lore.orEmpty(), modifier = Modifier.padding(horizontal = 16.dp) ) // Abilities (Spells) Text( text = "Abilities", fontWeight = FontWeight.Bold, fontSize = 20.sp, modifier = Modifier.padding(16.dp) ) Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) ) { heroItem.spells?.let { AbilityList(it) } } Text( text = "Passive Ability", fontWeight = FontWeight.Bold, fontSize = 20.sp, modifier = Modifier.padding(16.dp) ) val passiveImageUrl = "https://ddragon.leagueoflegends.com/cdn/13.16.1/img/passive/" + heroItem.passiveItem?.imageItem?.full AsyncImage( model = ImageRequest.Builder(LocalContext.current) .data(passiveImageUrl) .memoryCacheKey(passiveImageUrl) .diskCacheKey(passiveImageUrl) .diskCachePolicy(CachePolicy.ENABLED) .memoryCachePolicy(CachePolicy.ENABLED) .crossfade(true).build(), placeholder = painterResource(id = com.berker.presentation.R.drawable.ic_back_button), // Update this to your placeholder resource contentDescription = heroItem.passiveItem?.imageItem?.full, contentScale = ContentScale.Crop, modifier = Modifier .clip(shape = RoundedCornerShape(12.dp)) .size(60.dp), onSuccess = { it }, onError = { it }, onLoading = { it }, imageLoader = requireContext().imageLoader ) Text( text = heroItem.passiveItem?.name.orEmpty(), modifier = Modifier.padding(16.dp) ) heroItem.allytips?.forEachIndexed { index, s -> ExpandableTipItem("Playing Tip ${index + 1}", s) } // Statistics Text( text = "Statistics", fontWeight = FontWeight.Bold, fontSize = 20.sp, modifier = Modifier.padding(16.dp) ) heroItem.stats?.allStats()?.forEach { stat -> Text( text = "${stat.first}: ${stat.second}", fontSize = 16.sp, modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) ) } } } } @Composable fun ExpandableTipItem(title: String, content: String) { var expanded by remember { mutableStateOf(false) } val height: Dp by animateDpAsState( if (expanded) 100.dp else 0.dp, label = "" ) Card( modifier = Modifier .fillMaxWidth() .padding(8.dp) .shadow(4.dp), shape = RoundedCornerShape(8.dp) ) { Column( Modifier .clickable { expanded = !expanded } .padding(16.dp) ) { Text( text = title, color = Color.Black, fontWeight = FontWeight.Bold ) Surface( color = Color.Gray.copy(alpha = 0.1f), shape = RoundedCornerShape(8.dp), modifier = Modifier .fillMaxWidth() .height(height) .padding(top = 8.dp) ) { Text( text = content, color = Color.Black, modifier = Modifier .padding(8.dp) ) } } } } @Composable fun DifficultyIndicator(difficulty: Int) { Row( modifier = Modifier .fillMaxWidth() .height(10.dp) .clip(CircleShape) .padding(16.dp), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { for (i in 1..10) { Surface( color = if (i <= difficulty) Color.Red else Color.Gray, modifier = Modifier .weight(1f) .fillMaxHeight() ) {} } } } @Composable fun AbilityList(abilities: List) { var selectedAbility by remember { mutableStateOf(null) } val abilityPositions = remember { mutableStateMapOf() } // To store positions Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth() ) { Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth() ) { abilities.forEach { spell -> AbilityCard( spell = spell, isSelected = selectedAbility == spell.id, onSelected = { selectedAbility = it }, abilityPositions = abilityPositions, // Pass the positions map modifier = Modifier.weight(1f) ) } } if (selectedAbility != null) { val spell = abilities.find { it.id == selectedAbility } val selectedPosition = abilityPositions[selectedAbility.orEmpty()] ?: 0f // Logging for debugging println("Selected ability: $selectedAbility, Position: $selectedPosition") if (spell != null) { Box( modifier = Modifier .fillMaxWidth() // Change this to a color that stands out // Explicitly setting the height .padding(16.dp), contentAlignment = Alignment.TopStart ) { Canvas( modifier = Modifier .size(50.dp, 50.dp) // Increase the size for a bigger notch .zIndex(1f) // Ensure this is drawn on top of other layers .offset( x = with(LocalDensity.current) { selectedPosition.toDp() + 8.dp }, y = with(LocalDensity.current) { -(20).dp } // Move the notch up by its height ) ) { // Make sure the bottom of the notch touches y = 50, which will align with the top of the Surface due to the negative offset drawPath( path = Path().apply { moveTo(0f, 50f) cubicTo(15f, 40f, 25f, 0f, 50f, 50f) // Curved path for notch close() }, brush = Brush.linearGradient( colors = listOf(Color.White, Color.Gray), // Gradient colors start = Offset(0f, 0f), end = Offset(50f, 50f) ) ) } // The detailed view Surface( modifier = Modifier.fillMaxWidth(), elevation = 4.dp, shape = RoundedCornerShape(16.dp) ) { // Wrap the Text and ExoPlayerVideoPlayer in a Column Column( modifier = Modifier.fillMaxWidth() // Fill the max width in the Column ) { Text( text = "Detailed info about ${spell.name}", modifier = Modifier.padding(16.dp) ) ExoPlayerVideoPlayer( url = "https://d28xe8vt774jo5.cloudfront.net/champion-abilities/0${viewModel.heroItem.key}/ability_0${viewModel.heroItem.key}_${ decideSkillSuffix(abilities, spell) }1.webm", modifier = Modifier .fillMaxWidth() .padding(0.dp) // Removes any padding ) Text( text = "Detailed info about ${spell.name}", modifier = Modifier.padding(16.dp) ) Text( text = "Detailed info about ${spell.name}", modifier = Modifier.padding(16.dp) ) Text( text = "Detailed info about ${spell.name}", modifier = Modifier.padding(16.dp) ) } } } } } } } private fun decideSkillSuffix(list: List, item: SpellItem): Char { var returnChar: Char = 'Q' list.forEachIndexed { index, spellItem -> if (spellItem != item) return@forEachIndexed returnChar = when (index) { 0 -> 'Q' 1 -> 'W' 2 -> 'E' 3 -> 'R' else -> { 'Q' } } } return returnChar } @Composable fun ExoPlayerVideoPlayer( url: String, modifier: Modifier = Modifier ) { val context = LocalContext.current val aspectRatio = remember { mutableStateOf(1f) } val exoPlayer = remember { mutableStateOf(null) } val playerViewRef = remember { mutableStateOf(null) } DisposableEffect(url) { // Create a new player whenever the URL changes val newPlayer = SimpleExoPlayer.Builder(context).build().apply { val mediaItem = MediaItem.fromUri(Uri.parse(url)) setMediaItem(mediaItem) prepare() playWhenReady = true repeatMode = Player.REPEAT_MODE_ONE // Loop the video } exoPlayer.value?.release() exoPlayer.value = newPlayer playerViewRef.value?.player = newPlayer val aspectRatioListener = object : Player.Listener { override fun onVideoSizeChanged(videoSize: VideoSize) { aspectRatio.value = if (videoSize.height != 0) videoSize.width.toFloat() / videoSize.height else 1f } } newPlayer.addListener(aspectRatioListener) onDispose { newPlayer.removeListener(aspectRatioListener) newPlayer.release() } return@DisposableEffect onDispose { } } val screenWidth = LocalConfiguration.current.screenWidthDp.dp val calculatedHeight = with(LocalDensity.current) { (screenWidth.toPx() / aspectRatio.value).toDp() } AndroidView( modifier = modifier .fillMaxWidth() .height(calculatedHeight), factory = { ctx -> PlayerView(ctx).apply { player = exoPlayer.value useController = false resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT } }, update = { view -> playerViewRef.value = view view.player = exoPlayer.value } ) } @Composable fun AbilityCard( spell: SpellItem, isSelected: Boolean, onSelected: (String) -> Unit, abilityPositions: MutableMap, // Declare the positions map modifier: Modifier = Modifier ) { val backgroundColor by animateColorAsState( targetValue = if (isSelected) Color(0xFFBBDEFB) else Color.Transparent, animationSpec = tween(300), label = "" ) Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier .background(backgroundColor, RoundedCornerShape(12.dp)) .padding(16.dp) .clickable { onSelected(spell.id.orEmpty()) } .onGloballyPositioned { layoutCoordinates -> abilityPositions[spell.id.orEmpty()] = layoutCoordinates.positionInParent().x } ) { Box( modifier = Modifier .size(60.dp) .clip(RoundedCornerShape(12.dp)) .shadow(12.dp) ) { val spellImageUrl = "https://ddragon.leagueoflegends.com/cdn/13.16.1/img/spell/" + spell.id + ".png" AsyncImage( model = ImageRequest.Builder(LocalContext.current) .data(spellImageUrl) .memoryCacheKey(spellImageUrl) .diskCacheKey(spellImageUrl) .diskCachePolicy(CachePolicy.ENABLED) .memoryCachePolicy(CachePolicy.ENABLED) .crossfade(true).build(), placeholder = painterResource(id = com.berker.presentation.R.drawable.ic_back_button), // Update this to your placeholder resource contentDescription = spell.name.orEmpty(), contentScale = ContentScale.Crop, modifier = Modifier .size(60.dp), onSuccess = { it }, onError = { it }, onLoading = { it }, imageLoader = requireContext().imageLoader ) } // Text below the icon Text( text = spell.name.orEmpty(), fontSize = 12.sp, textAlign = TextAlign.Center, color = Color.White, modifier = Modifier.padding(top = 8.dp) ) } } }