From 8ebc8ac98f4bdd4d440a7fa3aa0415f089490c5b Mon Sep 17 00:00:00 2001 From: Owen LeJeune Date: Tue, 27 Sep 2022 11:39:03 -0400 Subject: [PATCH] base stats view --- .idea/deploymentTargetDropDown.xml | 17 + app/build.gradle | 4 +- .../mydex/api/pokeapi/v2/PokemonApi.kt | 4 + .../mydex/api/pokeapi/v2/PokemonService.kt | 5 + .../api/pokeapi/v2/model/pokemon/Pokemon.kt | 2 +- .../pokeapi/v2/model/pokemon/PokemonStat.kt | 2 +- .../mydex/extensions/ColorExtensions.kt | 12 + .../mydex/ui/components/Helpers.kt | 31 ++ .../mydex/ui/components/Widgets.kt | 29 +- .../mydex/ui/navigation/DataNavItem.kt | 3 +- .../com/owenlejeune/mydex/ui/theme/Color.kt | 23 +- .../owenlejeune/mydex/ui/views/DetailView.kt | 318 +++++++++++++++++- .../mydex/ui/views/MainActivityViews.kt | 5 +- .../mydex/ui/views/PokedexViews.kt | 7 +- .../com/owenlejeune/mydex/utils/AppCache.kt | 2 + .../com/owenlejeune/mydex/utils/ColorUtils.kt | 27 +- .../owenlejeune/mydex/utils/DataManager.kt | 95 ++++++ .../com/owenlejeune/mydex/utils/PokeUtils.kt | 49 +++ app/src/main/res/values/strings.xml | 10 + 19 files changed, 624 insertions(+), 21 deletions(-) create mode 100644 .idea/deploymentTargetDropDown.xml create mode 100644 app/src/main/java/com/owenlejeune/mydex/extensions/ColorExtensions.kt create mode 100644 app/src/main/java/com/owenlejeune/mydex/ui/components/Helpers.kt create mode 100644 app/src/main/java/com/owenlejeune/mydex/utils/DataManager.kt create mode 100644 app/src/main/java/com/owenlejeune/mydex/utils/PokeUtils.kt diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..ff13dd2 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 5fe4522..59ec8f2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -80,9 +80,9 @@ dependencies { implementation "androidx.compose.ui:ui-tooling-preview:$compose" implementation "androidx.activity:activity-compose:$compose_activity" implementation "com.google.accompanist:accompanist-systemuicontroller:$compose_accompanist" -// implementation "com.google.accompanist:accompanist-pager:$compose_accompanist" + implementation "com.google.accompanist:accompanist-pager:$compose_accompanist" // implementation "com.google.accompanist:accompanist-pager-indicators:$compose_accompanist" -// implementation "com.google.accompanist:accompanist-flowlayout:$compose_accompanist" + implementation "com.google.accompanist:accompanist-flowlayout:$compose_accompanist" // implementation "com.google.accompanist:accompanist-insets:$compose_accompanist" implementation "androidx.navigation:navigation-compose:$compose_navigation" implementation "androidx.paging:paging-compose:$compose_paging" diff --git a/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/PokemonApi.kt b/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/PokemonApi.kt index fabd330..077d9f6 100644 --- a/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/PokemonApi.kt +++ b/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/PokemonApi.kt @@ -3,6 +3,7 @@ package com.owenlejeune.mydex.api.pokeapi.v2 import com.owenlejeune.mydex.api.pokeapi.v2.model.misc.PaginatedResponse import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.Pokemon import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonSpecies +import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonStat import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonType import retrofit2.Response import retrofit2.http.GET @@ -23,4 +24,7 @@ interface PokemonApi { @GET("type/{id}") suspend fun getPokemonType(@Path("id") id: Int): Response + @GET("stat/{id}") + suspend fun getPokemonStat(@Path("id") id: Int): Response + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/PokemonService.kt b/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/PokemonService.kt index 3390e2a..dbb619c 100644 --- a/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/PokemonService.kt +++ b/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/PokemonService.kt @@ -4,6 +4,7 @@ import android.util.Log import com.owenlejeune.mydex.api.pokeapi.v2.model.misc.PaginatedResponse import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.Pokemon import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonSpecies +import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonStat import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonType import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -42,4 +43,8 @@ class PokemonService: KoinComponent { return service.getPokemonType(id) } + suspend fun getPokemonStat(id: Int): Response { + return service.getPokemonStat(id) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/model/pokemon/Pokemon.kt b/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/model/pokemon/Pokemon.kt index 21e2af8..dc99356 100644 --- a/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/model/pokemon/Pokemon.kt +++ b/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/model/pokemon/Pokemon.kt @@ -21,6 +21,6 @@ class Pokemon( @SerializedName("moves") val moves: List, @SerializedName("species") val species: NameAndUrl, @SerializedName("sprites") val sprites: Sprites, - @SerializedName("stats") val state: List, + @SerializedName("stats") val stats: List, @SerializedName("types") val types: List ) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/model/pokemon/PokemonStat.kt b/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/model/pokemon/PokemonStat.kt index bae682e..c87e9ec 100644 --- a/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/model/pokemon/PokemonStat.kt +++ b/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/model/pokemon/PokemonStat.kt @@ -14,5 +14,5 @@ class PokemonStat( @SerializedName("is_battle_only") val isBattleOnly: Boolean, @SerializedName("move_damage_class") val moveDamageClass: NameAndUrl, @SerializedName("name") val name: String, - @SerializedName("names") val names: NameAndLanguage + @SerializedName("names") val names: List ) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/mydex/extensions/ColorExtensions.kt b/app/src/main/java/com/owenlejeune/mydex/extensions/ColorExtensions.kt new file mode 100644 index 0000000..9222e0a --- /dev/null +++ b/app/src/main/java/com/owenlejeune/mydex/extensions/ColorExtensions.kt @@ -0,0 +1,12 @@ +package com.owenlejeune.mydex.extensions + +import androidx.annotation.FloatRange +import androidx.compose.ui.graphics.Color + +fun Color.adjustBy(@FloatRange(from = (-1f).toDouble(), to = 1f.toDouble()) x: Float): Color { + return copy( + red = (red+x).coerceIn(minimumValue = 0f, maximumValue = 1f), + green = (green+x).coerceIn(minimumValue = 0f, maximumValue = 1f), + blue = (blue+x).coerceIn(minimumValue = 0f, maximumValue = 1f) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/mydex/ui/components/Helpers.kt b/app/src/main/java/com/owenlejeune/mydex/ui/components/Helpers.kt new file mode 100644 index 0000000..b6686ac --- /dev/null +++ b/app/src/main/java/com/owenlejeune/mydex/ui/components/Helpers.kt @@ -0,0 +1,31 @@ +package com.owenlejeune.mydex.ui.components + +import androidx.compose.ui.layout.MeasurePolicy + +//fun flowLayoutMeasurePolicy() = MeasurePolicy { measurables, constraints -> +// layout(constraints.maxWidth, constraints.maxHeight) { +// val placeables = measurables.map { measurable -> +// measurable.measure(constraints) +// } +// var yPos = 0 +// var xPos = 0 +// var maxY = 0 +// placeables.forEach { placeable -> +// if (xPos + placeable.width > +// constraints.maxWidth +// ) { +// xPos = 0 +// yPos += maxY +// maxY = 0 +// } +// placeable.placeRelative( +// x = xPos, +// y = yPos +// ) +// xPos += placeable.width +// if (maxY < placeable.height) { +// maxY = placeable.height +// } +// } +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/mydex/ui/components/Widgets.kt b/app/src/main/java/com/owenlejeune/mydex/ui/components/Widgets.kt index 7ab7976..1aa919c 100644 --- a/app/src/main/java/com/owenlejeune/mydex/ui/components/Widgets.kt +++ b/app/src/main/java/com/owenlejeune/mydex/ui/components/Widgets.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.Layout import androidx.compose.ui.modifier.modifierLocalConsumer import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign @@ -212,4 +213,30 @@ fun PokemonTypeLabel( color = Color.White ) } -} \ No newline at end of file +} + +@Composable +fun SmallTabIndicator( + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.primary +) { + Spacer( + modifier + .padding(horizontal = 28.dp) + .height(2.dp) + .background(color, RoundedCornerShape(topStartPercent = 100, topEndPercent = 100)) + ) +} + +//@Composable +//fun FlowLayout( +// modifier: Modifier = Modifier, +// content: @Composable () -> Unit +//) { +// val measurePolicy = flowLayoutMeasurePolicy() +// Layout( +// measurePolicy = measurePolicy, +// content = content, +// modifier = modifier +// ) +//} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/mydex/ui/navigation/DataNavItem.kt b/app/src/main/java/com/owenlejeune/mydex/ui/navigation/DataNavItem.kt index b698403..cee319c 100644 --- a/app/src/main/java/com/owenlejeune/mydex/ui/navigation/DataNavItem.kt +++ b/app/src/main/java/com/owenlejeune/mydex/ui/navigation/DataNavItem.kt @@ -15,7 +15,7 @@ sealed class DataNavItem( ): KoinComponent { companion object { - val Pages by lazy { listOf(Pokedex, Moves, Abilities, Items, Locations, TypeCharts) } + val Pages by lazy { listOf(Pokedex, Moves, Abilities, Items, Locations, TypeCharts, Settings) } } private val resourceUtils: ResourceUtils by inject() @@ -28,5 +28,6 @@ sealed class DataNavItem( object Items: DataNavItem("items_route", PokeYellow, R.string.items_nav_title) object Locations: DataNavItem("locations_route", PokePurple, R.string.locations_nav_title) object TypeCharts: DataNavItem("type_charts_route", PokeBrown, R.string.type_charts_nav_title) + object Settings: DataNavItem("settings_route", PokeGrey, R.string.settings_nav_title) } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/mydex/ui/theme/Color.kt b/app/src/main/java/com/owenlejeune/mydex/ui/theme/Color.kt index 995856a..b38d6a5 100644 --- a/app/src/main/java/com/owenlejeune/mydex/ui/theme/Color.kt +++ b/app/src/main/java/com/owenlejeune/mydex/ui/theme/Color.kt @@ -16,4 +16,25 @@ val PokeLightYellow = Color(0xFFFFCE4B) val PokePurple = Color(0xFF7C538C) val PokeRed = Color(0xFFFA6555) val PokeGreen = Color(0xFF4FC1A6) -val PokeYellow = Color(0xFFF6C747) \ No newline at end of file +val PokeYellow = Color(0xFFF6C747) + +val BugType = Color(0xFF98AD19) +val DarkType = Color(0xFF5C4638) +val DragonType = Color(0xFF5B10F6) +val ElectricType = Color(0xFFF7C726) +val FairyType = Color(0xFFF389D9) +val FightingType = Color(0xFF7B211E) +val FireType = Color(0xFFEA3825) +val FlyingType = Color(0xFF8C73C6) +val GhostType = Color(0xFF5C4386) +val GrassType = Color(0xFF68C13F) +val GroundType = Color(0xFFD7B456) +val IceType = Color(0xFF88D0CF) +val NormalType = Color(0xFF979965) +val PoisonType = Color(0xFF8C288E) +val PsychicType = Color(0xFFF33D75) +val RockType = Color(0xFFA8912C) +val ShadowType = Color(0xFF312536) +val SteelType = Color(0xFFAAA8C5) +val UnknownType = Color(0xFF56917E) +val WaterType = Color(0xFF5579EC) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/mydex/ui/views/DetailView.kt b/app/src/main/java/com/owenlejeune/mydex/ui/views/DetailView.kt index b06ffd4..0be3edf 100644 --- a/app/src/main/java/com/owenlejeune/mydex/ui/views/DetailView.kt +++ b/app/src/main/java/com/owenlejeune/mydex/ui/views/DetailView.kt @@ -2,7 +2,9 @@ package com.owenlejeune.mydex.ui.views import androidx.compose.foundation.background 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.filled.ArrowBack import androidx.compose.material3.* @@ -14,6 +16,7 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale @@ -21,15 +24,25 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import coil.compose.AsyncImage +import com.google.accompanist.flowlayout.FlowRow +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.rememberPagerState import com.owenlejeune.mydex.R import com.owenlejeune.mydex.api.pokeapi.v2.PokemonService +import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.Pokemon import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonSpecies +import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonStat import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonType +import com.owenlejeune.mydex.extensions.adjustBy import com.owenlejeune.mydex.extensions.getIdFromUrl import com.owenlejeune.mydex.extensions.getNameForLanguage import com.owenlejeune.mydex.ui.components.PokemonTypeLabel -import com.owenlejeune.mydex.utils.AppCache -import com.owenlejeune.mydex.utils.ColorUtils +import com.owenlejeune.mydex.ui.components.SmallTabIndicator +import com.owenlejeune.mydex.utils.* +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import org.koin.java.KoinJavaComponent.get @OptIn(ExperimentalMaterial3Api::class) @@ -69,19 +82,19 @@ fun PokemonDetailView( Card( modifier = Modifier .fillMaxSize() - .padding(top = 140.dp), + .padding(top = 150.dp), shape = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.background) ) { - + Details(pokemon = pokemon, pokemonSpecies = pokemonSpecies) } AsyncImage( - model = pokemon.sprites.frontDefault, + model = PokeUtils.spriteFromId(pokemonId), contentDescription = null, contentScale = ContentScale.FillBounds, modifier = Modifier - .size(250.dp) + .size(200.dp) .align(Alignment.TopCenter) ) } @@ -98,7 +111,7 @@ fun PokemonDetailView( title = {}, colors = TopAppBarDefaults .smallTopAppBarColors( - containerColor = Color.Transparent//.copy(alpha = 0.4f) + containerColor = Color.Transparent ) ) } @@ -177,6 +190,234 @@ private fun Header( } } +@OptIn(ExperimentalPagerApi::class) +@Composable +private fun ColumnScope.Details( + pokemon: Pokemon, + pokemonSpecies: PokemonSpecies, + service: PokemonService = get(PokemonService::class.java) +) { + val scope = rememberCoroutineScope() + val pagerState = rememberPagerState() + + Spacer(modifier = Modifier.height(50.dp)) + + val tabs = DetailTab.Items + TabRow( + selectedTabIndex = pagerState.currentPage, + divider = {} + ) { + tabs.forEachIndexed { index, tab -> + Tab( + selected = pagerState.currentPage == index, + onClick = { + scope.launch { pagerState.animateScrollToPage(index) } + } + ) { + Spacer(modifier = Modifier.height(18.dp)) + Text(text = tab.name, color = MaterialTheme.colorScheme.onBackground) + Spacer(modifier = Modifier.height(18.dp)) + } + } + } + HorizontalPager(count = tabs.size, state = pagerState) { page -> + tabs[page].screen(pokemon, pokemonSpecies, service) + } +} + +@Composable +private fun AboutView( + pokemon: Pokemon, + pokemonSpecies: PokemonSpecies, + service: PokemonService +) { + val scrollState = rememberScrollState() + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .padding(all = 24.dp) + .verticalScroll(state = scrollState) + ) { + + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun BaseStatsView( + pokemon: Pokemon, + pokemonSpecies: PokemonSpecies, + service: PokemonService +) { + val scrollState = rememberScrollState() + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .padding(all = 24.dp) + .verticalScroll(state = scrollState) + ) { + pokemon.stats.forEach { stat -> + val statId = stat.stat.url.getIdFromUrl() + + val fullStat = remember { mutableStateOf(null) } + + LaunchedEffect(key1 = fullStat.value) { + fetchPokemonStat(statId, fullStat, service) + } + fullStat.value?.let { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = it.names.getNameForLanguage() ?: stat.stat.name, + modifier = Modifier.fillMaxWidth(.2f) + ) + Text( + text = stat.baseStat.toString(), + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.tertiary + ) + LinearProgressIndicator( + progress = (stat.baseStat.toFloat() / 255f), + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(5.dp)) + ) + } + } + } + + val doubleDamageRelations = remember { mutableSetOf() } + val halfDamageRelations = remember { mutableSetOf() } + val noDamageRelations = remember { mutableSetOf() } + + val common = doubleDamageRelations.intersect(halfDamageRelations) + val common2 = doubleDamageRelations.intersect(noDamageRelations) + doubleDamageRelations.removeAll { it in common || it in common2 } + halfDamageRelations.removeAll { it in common || it in common2 } + noDamageRelations.removeAll { it in common || it in common2 } + + if (doubleDamageRelations.isNotEmpty()) { + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.type_weaknesses_title), + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + + Text(text = stringResource(R.string.type_weaknesses_description)) + } + + FlowRow( + mainAxisSpacing = 8.dp, + crossAxisSpacing = 8.dp + ) { + if (doubleDamageRelations.isEmpty()) { + pokemon.types.forEach { t -> + val typeId = t.type.url.getIdFromUrl() + DataManager.getTypeById(typeId) { type -> + type.damageRelations.doubleDamageFrom.forEach { + val id = it.url.getIdFromUrl() + DataManager.getTypeById(id) { t -> + doubleDamageRelations.add(t.names.getNameForLanguage() ?: it.name) + } + } + } + } + } + doubleDamageRelations.forEach { tr -> + TypeRelationChip(type = tr) + } + } + + if (halfDamageRelations.isNotEmpty() || noDamageRelations.isNotEmpty()) { + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.type_defences_title), + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + } + + if (halfDamageRelations.isNotEmpty()) { + Text(text = stringResource(R.string.type_defences_half_description)) + } + + FlowRow( + mainAxisSpacing = 8.dp, + crossAxisSpacing = 8.dp + ) { + if (halfDamageRelations.isEmpty()) { + pokemon.types.forEach { t -> + val typeId = t.type.url.getIdFromUrl() + DataManager.getTypeById(typeId) { type -> + type.damageRelations.halfDamageFrom.forEach { + val id = it.url.getIdFromUrl() + DataManager.getTypeById(id) { t -> + halfDamageRelations.add(t.names.getNameForLanguage() ?: it.name) + } + } + } + } + } + halfDamageRelations.forEach { tr -> + TypeRelationChip(type = tr) + } + } + + if (noDamageRelations.isNotEmpty()) { + Text(text = stringResource(R.string.type_defences_no_description)) + } + + FlowRow( + mainAxisSpacing = 8.dp, + crossAxisSpacing = 8.dp + ) { + if (noDamageRelations.isEmpty()) { + pokemon.types.forEach { t -> + val typeId = t.type.url.getIdFromUrl() + DataManager.getTypeById(typeId) { type -> + type.damageRelations.noDamageFrom.forEach { + val id = it.url.getIdFromUrl() + DataManager.getTypeById(id) { t -> + noDamageRelations.add(t.names.getNameForLanguage() ?: it.name) + } + } + } + } + } + noDamageRelations.forEach { tr -> + TypeRelationChip(type = tr) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TypeRelationChip ( + type: String +) { + val color = ColorUtils.pokeTypeNameToComposeColor(type) + Card( + modifier = Modifier + .clip(RoundedCornerShape(5.dp)), + colors = CardDefaults.cardColors( + containerColor = color + ) + ) { + Text( + text = type, + color = color.adjustBy(-.4f), + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + ) + } +} + private suspend fun fetchPokemonType( id: Int, pokemonType: MutableState, @@ -192,4 +433,67 @@ private suspend fun fetchPokemonType( } } } +} + +private suspend fun fetchPokemonStat( + statId: Int, + pokemonStat: MutableState, + service: PokemonService +) { + if (pokemonStat.value == null) { + service.getPokemonStat(statId).apply { + if (isSuccessful) { + body()?.let { + pokemonStat.value = it + AppCache.cachedStats.put(it.id, it) + } + } + } + } +} + +private sealed class DetailTab( + stringRes: Int, + route: String, + val screen: @Composable (Pokemon, PokemonSpecies, PokemonService) -> Unit +): KoinComponent { + + companion object { + val Items by lazy { listOf(About, BaseStats, Evolution, Moves) } + } + + private val resourceUtils: ResourceUtils by inject() + + val name: String = resourceUtils.getString(stringRes) + + object About: DetailTab( + stringRes = R.string.about_tab_title, + route = "about_tab", + screen = @Composable { pokemon, pokemonSpecies, service -> + + } + ) + + object BaseStats: DetailTab( + stringRes = R.string.base_stats_tab_title, + route = "base_stats_tab", + screen = @Composable { p, ps, s -> BaseStatsView(p, ps, s) } + ) + + object Evolution: DetailTab( + stringRes = R.string.evolution_tab_title, + route = "evolution_tab", + screen = @Composable { pokemon, pokemonSpecies, service -> + + } + ) + + object Moves: DetailTab( + stringRes = R.string.moves_tab_title, + route = "moves_tab", + screen = @Composable { pokemon, pokemonSpecies, service -> + + } + ) + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/mydex/ui/views/MainActivityViews.kt b/app/src/main/java/com/owenlejeune/mydex/ui/views/MainActivityViews.kt index 97eec54..e7ab338 100644 --- a/app/src/main/java/com/owenlejeune/mydex/ui/views/MainActivityViews.kt +++ b/app/src/main/java/com/owenlejeune/mydex/ui/views/MainActivityViews.kt @@ -24,6 +24,7 @@ import com.owenlejeune.mydex.ui.components.MenuItemButton import com.owenlejeune.mydex.ui.components.SearchBar import com.owenlejeune.mydex.ui.navigation.DataNavItem import org.koin.java.KoinJavaComponent +import kotlin.math.ceil @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -105,14 +106,14 @@ private fun BoxScope.SingleColumnMainContent(appNavController: NavHostController Spacer(modifier = Modifier.height(32.dp)) val cols = 2 - val rows = DataNavItem.Pages.size/cols + val rows = ceil(DataNavItem.Pages.size.toFloat()/cols.toFloat()).toInt() Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp) ) { for (i in 0 until rows) { val first = DataNavItem.Pages[2*i] - val second = if (2*i+1 <= DataNavItem.Pages.size) { + val second = if (2*i+1 < DataNavItem.Pages.size) { DataNavItem.Pages[2*i+1] } else { null diff --git a/app/src/main/java/com/owenlejeune/mydex/ui/views/PokedexViews.kt b/app/src/main/java/com/owenlejeune/mydex/ui/views/PokedexViews.kt index a6cad3a..77b32ff 100644 --- a/app/src/main/java/com/owenlejeune/mydex/ui/views/PokedexViews.kt +++ b/app/src/main/java/com/owenlejeune/mydex/ui/views/PokedexViews.kt @@ -39,6 +39,7 @@ import com.owenlejeune.mydex.ui.components.PokemonTypeLabel import com.owenlejeune.mydex.ui.navigation.MainNavItem import com.owenlejeune.mydex.utils.AppCache import com.owenlejeune.mydex.utils.ColorUtils +import com.owenlejeune.mydex.utils.PokeUtils import org.koin.java.KoinJavaComponent.get @Composable @@ -167,8 +168,6 @@ fun PokedexCard( bgColor.value = ColorUtils.pokeColorToComposeColor(color = species.color.name) val name = species.names.getNameForLanguage() ?: species.name - val dexNumber = pokemon.value!!.id.toString().padStart(3, '0') - Text( modifier = Modifier .align(Alignment.TopStart), @@ -199,13 +198,13 @@ fun PokedexCard( Text( modifier = Modifier .align(Alignment.TopEnd), - text = "#${dexNumber}", + text = PokeUtils.idToDexNumber(pokemonId), style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold), color = Color.Unspecified.copy(alpha = 0.3f) ) AsyncImage( - model = pokemon.value?.sprites?.frontDefault, + model = PokeUtils.spriteFromId(pokemonId), contentDescription = name, modifier = Modifier .align(Alignment.BottomEnd) diff --git a/app/src/main/java/com/owenlejeune/mydex/utils/AppCache.kt b/app/src/main/java/com/owenlejeune/mydex/utils/AppCache.kt index 41fa149..aee7e98 100644 --- a/app/src/main/java/com/owenlejeune/mydex/utils/AppCache.kt +++ b/app/src/main/java/com/owenlejeune/mydex/utils/AppCache.kt @@ -3,6 +3,7 @@ package com.owenlejeune.mydex.utils import android.util.SparseArray import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.Pokemon import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonSpecies +import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonStat import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonType object AppCache { @@ -10,5 +11,6 @@ object AppCache { var cachedSpecies = SparseArray() var cachedPokemon = SparseArray() var cachedTypes = SparseArray() + var cachedStats = SparseArray() } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/mydex/utils/ColorUtils.kt b/app/src/main/java/com/owenlejeune/mydex/utils/ColorUtils.kt index a1cf84e..d13a521 100644 --- a/app/src/main/java/com/owenlejeune/mydex/utils/ColorUtils.kt +++ b/app/src/main/java/com/owenlejeune/mydex/utils/ColorUtils.kt @@ -9,7 +9,7 @@ object ColorUtils { @Composable fun pokeColorToComposeColor(color: String): Color { - return when (color) { + return when (color.lowercase()) { "green" -> PokeGreen "red" -> PokeRed "blue" -> PokeBlue @@ -24,4 +24,29 @@ object ColorUtils { } } + fun pokeTypeNameToComposeColor(type: String): Color { + return when (type.lowercase()) { + "bug" -> BugType + "dark" -> DarkType + "dragon" -> DragonType + "electric" -> ElectricType + "fairy" -> FairyType + "fighting" -> FightingType + "fire" -> FireType + "flying" -> FlyingType + "ghost" -> GhostType + "grass" -> GrassType + "ground" -> GroundType + "ice" -> IceType + "normal" -> NormalType + "poison" -> PoisonType + "psychic" -> PsychicType + "rock" -> RockType + "shadow" -> ShadowType + "steel" -> SteelType + "water" -> WaterType + else -> UnknownType + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/mydex/utils/DataManager.kt b/app/src/main/java/com/owenlejeune/mydex/utils/DataManager.kt new file mode 100644 index 0000000..19c997b --- /dev/null +++ b/app/src/main/java/com/owenlejeune/mydex/utils/DataManager.kt @@ -0,0 +1,95 @@ +package com.owenlejeune.mydex.utils + +import com.owenlejeune.mydex.api.pokeapi.v2.PokemonService +import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.Pokemon +import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonSpecies +import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonStat +import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import retrofit2.Response + +object DataManager: KoinComponent { + + private val service: PokemonService by inject() + + fun getPokemonById(id: Int, callback: (Pokemon) -> Unit) { + AppCache.cachedPokemon[id]?.let(callback) ?: run { + CoroutineScope(Dispatchers.IO).launch { + service.getPokemon(id).apply { + if (isSuccessful) { + body()?.let { + AppCache.cachedPokemon.put(it.id, it) + callback(it) + } + } + } + } + } + } + + fun getPokemonSpeciesById(id: Int, callback: (PokemonSpecies) -> Unit) { + AppCache.cachedSpecies[id]?.let(callback) ?: run { + CoroutineScope(Dispatchers.IO).launch { + service.getPokemonSpecies(id).apply { + if (isSuccessful) { + body()?.let { + AppCache.cachedSpecies.put(it.id, it) + callback(it) + } + } + } + } + } + } + + fun getTypeById(id: Int, callback: (PokemonType) -> Unit) { + getById( + id = id, + callback = callback, + retriever = { AppCache.cachedTypes[it] }, + fetcher = { service.getPokemonType(it) }, + storer = { AppCache.cachedTypes.put(it.id, it) } + ) + } + + fun getStatById(id: Int, callback: (PokemonStat) -> Unit) { + AppCache.cachedStats[id]?.let(callback) ?: run { + CoroutineScope(Dispatchers.IO).launch { + service.getPokemonStat(id).apply { + if (isSuccessful) { + body()?.let { + AppCache.cachedStats.put(it.id, it) + callback(it) + } + } + } + } + } + } + + private fun getById( + id: Int, + callback: (T) -> Unit, + retriever: (Int) -> T?, + fetcher: suspend (Int) -> Response, + storer: (T) -> Unit + ) { + retriever(id)?.let(callback) ?: run { + CoroutineScope(Dispatchers.IO).launch { + fetcher(id).apply { + if (isSuccessful) { + body()?.let { + storer(it) + callback(it) + } + } + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/mydex/utils/PokeUtils.kt b/app/src/main/java/com/owenlejeune/mydex/utils/PokeUtils.kt new file mode 100644 index 0000000..01b3791 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/mydex/utils/PokeUtils.kt @@ -0,0 +1,49 @@ +package com.owenlejeune.mydex.utils + +import kotlin.math.ceil +import kotlin.math.floor + +object PokeUtils { + + private val HEC_TO_LBS = 0.22f + private val HEC_TO_KG = 0.1f + private val DEC_TO_CM = 10 + private val CM_TO_IN = 2.54 + private val IN_TO_FT = 12 + + fun idToDexNumber(id: Int, includeNumberSign: Boolean = true): String { + val padded = id.toString().padStart(3, '0') + return if (includeNumberSign) { + "#$padded" + } else { + padded + } + } + + fun spriteFromId(id: Int): String { + val paddedNumber = idToDexNumber(id, false) + return "https://assets.pokemon.com/assets/cms2/img/pokedex/full/${paddedNumber}.png" + } + + fun weightInPounds(weight: Int): Float { + return weight * HEC_TO_LBS + } + + fun weightInKg(weight: Int): Float { + return weight * HEC_TO_KG + } + + fun heightToCm(height: Int): Int { + return height * DEC_TO_CM + } + + fun heightToFtIn(height: Int): Pair { + val heightCm = height * DEC_TO_CM + + val feet = floor((height / CM_TO_IN) / IN_TO_FT).toInt() + val inches = ceil((height / CM_TO_IN) - (feet * IN_TO_FT)).toInt() + + return Pair(feet, inches) + } + +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 53bbf3d..4e04923 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,4 +12,14 @@ Items Locations Type charts + About + Base Stats + Evolution + Moves + Settings + Type weaknesses + Pokémon of these types deal double damage to this Pokémon + Type defences + Pokémon of these types deal half damage to this Pokémon + Pokémon of these types deal no damage to this Pokémon \ No newline at end of file