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 077d9f6..8d02f2b 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 @@ -5,6 +5,7 @@ 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.api.pokeapi.v2.model.pokemon.egggroup.EggGroup import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path @@ -27,4 +28,7 @@ interface PokemonApi { @GET("stat/{id}") suspend fun getPokemonStat(@Path("id") id: Int): Response + @GET("egg-group/{id}") + suspend fun getEggGroup(@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 dbb619c..3eaf41a 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 @@ -6,6 +6,7 @@ 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.api.pokeapi.v2.model.pokemon.egggroup.EggGroup import org.koin.core.component.KoinComponent import org.koin.core.component.inject import retrofit2.Response @@ -47,4 +48,8 @@ class PokemonService: KoinComponent { return service.getPokemonStat(id) } + suspend fun getEggGroup(id: Int): Response { + return service.getEggGroup(id) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/model/pokemon/egggroup/EggGroup.kt b/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/model/pokemon/egggroup/EggGroup.kt index be34027..b371fd6 100644 --- a/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/model/pokemon/egggroup/EggGroup.kt +++ b/app/src/main/java/com/owenlejeune/mydex/api/pokeapi/v2/model/pokemon/egggroup/EggGroup.kt @@ -7,6 +7,6 @@ import com.owenlejeune.mydex.api.pokeapi.v2.model.misc.NameAndUrl class EggGroup( @SerializedName("id") val id: Int, @SerializedName("name") val name: String, - @SerializedName("name") val names: List, + @SerializedName("names") val names: List, @SerializedName("pokemon_species") val pokemonSpecies: List ) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/mydex/extensions/ListExtensions.kt b/app/src/main/java/com/owenlejeune/mydex/extensions/ListExtensions.kt index 3184332..37a7353 100644 --- a/app/src/main/java/com/owenlejeune/mydex/extensions/ListExtensions.kt +++ b/app/src/main/java/com/owenlejeune/mydex/extensions/ListExtensions.kt @@ -5,5 +5,6 @@ import com.owenlejeune.mydex.api.pokeapi.v2.model.misc.NameAndLanguage fun List.getNameForLanguage(): String? { val lang = Locale.current.language - return find { it.language.name == lang }?.name + val defLang = "en" + return find { it.language.name == lang }?.name ?: find { it.language.name == defLang}?.name } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/mydex/preferences/AppPreferences.kt b/app/src/main/java/com/owenlejeune/mydex/preferences/AppPreferences.kt index 64b845e..da5f7fe 100644 --- a/app/src/main/java/com/owenlejeune/mydex/preferences/AppPreferences.kt +++ b/app/src/main/java/com/owenlejeune/mydex/preferences/AppPreferences.kt @@ -20,6 +20,7 @@ class AppPreferences(context: Context) { private val SELECTED_COLOR = "selected_color" private val USE_WALLPAPER_COLORS = "use_wallpaper_colors" private val DARK_THEME = "dark_theme" + private val USE_METRIC = "use_metric" } private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE) @@ -50,6 +51,12 @@ class AppPreferences(context: Context) { get() = preferences.getInt(SELECTED_COLOR, selectedColorDefault) set(value) { preferences.put(SELECTED_COLOR, value) } + /******* Config ********/ + val useMetricDefault: Boolean = false + var useMetric: Boolean + get() = preferences.getBoolean(USE_METRIC, useMetricDefault) + set(value) { preferences.put(USE_METRIC, value) } + /********* Helpers ********/ private fun SharedPreferences.put(key: String, value: Any?) { edit().apply { 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 0be3edf..8172cb5 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 @@ -16,10 +16,12 @@ 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.painterResource 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 +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController @@ -34,11 +36,16 @@ 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.api.pokeapi.v2.model.pokemon.egggroup.EggGroup import com.owenlejeune.mydex.extensions.adjustBy import com.owenlejeune.mydex.extensions.getIdFromUrl import com.owenlejeune.mydex.extensions.getNameForLanguage +import com.owenlejeune.mydex.preferences.AppPreferences import com.owenlejeune.mydex.ui.components.PokemonTypeLabel import com.owenlejeune.mydex.ui.components.SmallTabIndicator +import com.owenlejeune.mydex.ui.theme.PokeBlue +import com.owenlejeune.mydex.ui.theme.PokeGrey +import com.owenlejeune.mydex.ui.theme.PokeLightRed import com.owenlejeune.mydex.utils.* import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent @@ -225,20 +232,132 @@ private fun ColumnScope.Details( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun AboutView( pokemon: Pokemon, pokemonSpecies: PokemonSpecies, - service: PokemonService + service: PokemonService, + preferences: AppPreferences = get(AppPreferences::class.java) ) { val scrollState = rememberScrollState() - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), + Column ( + verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier + .fillMaxHeight() + .fillMaxWidth() .padding(all = 24.dp) .verticalScroll(state = scrollState) ) { + val lang = Locale.current.language + val flavorText = pokemonSpecies.flavorTextEntries.filter { it.language.name == lang }[0] + Text( + text = flavorText.flavorText.replace("\n", " "), + color = MaterialTheme.colorScheme.onBackground + ) + + Card( + elevation = CardDefaults.cardElevation(defaultElevation = 10.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) + ) { + Row(modifier = Modifier + .fillMaxWidth() + .padding(start = 12.dp, end = 12.dp, top = 12.dp) + ) { + Text( + text = stringResource(R.string.poke_details_height_title), + modifier = Modifier.weight(1f), + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + Text( + text = stringResource(R.string.poke_details_weight_title), + modifier = Modifier.weight(1f), + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Row(modifier = Modifier + .fillMaxWidth() + .padding(start = 12.dp, end = 12.dp, bottom = 12.dp) + ) { + val height = if (preferences.useMetric) { + PokeUtils.heightToCm(pokemon.height) + } else { + PokeUtils.heightToFtIn(pokemon.height) + } + Text( + text = height, + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center + ) + val weight = if (preferences.useMetric) { + PokeUtils.weightInKg(pokemon.weight) + } else { + PokeUtils.weightInPounds(pokemon.weight) + } + Text( + text = weight, + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center + ) + } + } + + Text( + text = stringResource(R.string.poke_details_breeding_title), + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text(text = stringResource(R.string.poke_details_gender_subtitle), color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(text = stringResource(R.string.poke_details_egg_groups_subtitle), color = MaterialTheme.colorScheme.onSurfaceVariant) + } + Column( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + val percentageFemale = PokeUtils.genderRateToPercentage(pokemonSpecies.genderRate) + val percentageMale = 100f - percentageFemale + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon(painter = painterResource(id = R.drawable.male_symbol), contentDescription = null, tint = PokeBlue) + Text(text = "$percentageMale%") + } + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon(painter = painterResource(id = R.drawable.female_symbol), contentDescription = null, tint = PokeLightRed) + Text(text = "$percentageFemale%") + } + } + + val eggGroups = remember { mutableListOf() } + if (eggGroups.isEmpty()) { + pokemonSpecies.eggGroups.forEach { + val id = it.url.getIdFromUrl() + DataManager.getEggGroupById(id) { eggGroup -> eggGroups.add(eggGroup) } + } + } + if (eggGroups.isNotEmpty()) { + Text(text = eggGroups.joinToString(separator = ", ") { it.names.getNameForLanguage() ?: it.name } ) + } + } + } } } @@ -397,6 +516,44 @@ private fun BaseStatsView( } } +@Composable +private fun EvolutionView( + pokemon: Pokemon, + pokemonSpecies: PokemonSpecies, + service: PokemonService +) { + val scrollState = rememberScrollState() + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .padding(all = 24.dp) + .verticalScroll(state = scrollState) + .background(color = Color.Red) + .fillMaxSize() + ) { + Text(text = "Evolution") + } +} + +@Composable +private fun MovesView( + pokemon: Pokemon, + pokemonSpecies: PokemonSpecies, + service: PokemonService +) { + val scrollState = rememberScrollState() + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .padding(all = 24.dp) + .verticalScroll(state = scrollState) + .background(color = Color.Red) + .fillMaxSize() + ) { + Text(text = "Moves") + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun TypeRelationChip ( @@ -469,9 +626,7 @@ private sealed class DetailTab( object About: DetailTab( stringRes = R.string.about_tab_title, route = "about_tab", - screen = @Composable { pokemon, pokemonSpecies, service -> - - } + screen = @Composable { p, ps, s -> AboutView(p, ps, s) } ) object BaseStats: DetailTab( @@ -483,17 +638,13 @@ private sealed class DetailTab( object Evolution: DetailTab( stringRes = R.string.evolution_tab_title, route = "evolution_tab", - screen = @Composable { pokemon, pokemonSpecies, service -> - - } + screen = @Composable { p, ps, s -> EvolutionView(p, ps, s) } ) object Moves: DetailTab( stringRes = R.string.moves_tab_title, route = "moves_tab", - screen = @Composable { pokemon, pokemonSpecies, service -> - - } + screen = @Composable { p, ps, s -> MovesView(p, ps, s) } ) } \ No newline at end of file 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 aee7e98..281f30d 100644 --- a/app/src/main/java/com/owenlejeune/mydex/utils/AppCache.kt +++ b/app/src/main/java/com/owenlejeune/mydex/utils/AppCache.kt @@ -5,6 +5,7 @@ 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.api.pokeapi.v2.model.pokemon.egggroup.EggGroup object AppCache { @@ -12,5 +13,6 @@ object AppCache { var cachedPokemon = SparseArray() var cachedTypes = SparseArray() var cachedStats = SparseArray() + var cachedEggGroups = SparseArray() } \ 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 index 19c997b..b2794cb 100644 --- a/app/src/main/java/com/owenlejeune/mydex/utils/DataManager.kt +++ b/app/src/main/java/com/owenlejeune/mydex/utils/DataManager.kt @@ -5,6 +5,7 @@ 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.api.pokeapi.v2.model.pokemon.egggroup.EggGroup import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -17,33 +18,23 @@ 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) - } - } - } - } - } + getById( + id = id, + callback = callback, + retriever = { AppCache.cachedPokemon[it] }, + fetcher = { service.getPokemon(it) }, + storer = { AppCache.cachedPokemon.put(it.id, 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) - } - } - } - } - } + getById( + id = id, + callback = callback, + retriever = { AppCache.cachedSpecies[it] }, + fetcher = { service.getPokemonSpecies(it) }, + storer = { AppCache.cachedSpecies.put(it.id, it) } + ) } fun getTypeById(id: Int, callback: (PokemonType) -> Unit) { @@ -57,18 +48,23 @@ object DataManager: KoinComponent { } 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) - } - } - } - } - } + getById( + id = id, + callback = callback, + retriever = { AppCache.cachedStats[id] }, + fetcher = { service.getPokemonStat(id) }, + storer = { AppCache.cachedStats.put(it.id, it) } + ) + } + + fun getEggGroupById(id: Int, callback: (EggGroup) -> Unit) { + getById( + id = id, + callback = callback, + retriever = { AppCache.cachedEggGroups[id] }, + fetcher = { service.getEggGroup(id) }, + storer = { AppCache.cachedEggGroups.put(it.id, it) } + ) } private fun getById( diff --git a/app/src/main/java/com/owenlejeune/mydex/utils/PokeUtils.kt b/app/src/main/java/com/owenlejeune/mydex/utils/PokeUtils.kt index 01b3791..c6b43ed 100644 --- a/app/src/main/java/com/owenlejeune/mydex/utils/PokeUtils.kt +++ b/app/src/main/java/com/owenlejeune/mydex/utils/PokeUtils.kt @@ -10,6 +10,7 @@ object PokeUtils { private val DEC_TO_CM = 10 private val CM_TO_IN = 2.54 private val IN_TO_FT = 12 + private val GENDER_RATE = 1f/8f fun idToDexNumber(id: Int, includeNumberSign: Boolean = true): String { val padded = id.toString().padStart(3, '0') @@ -25,25 +26,29 @@ object PokeUtils { return "https://assets.pokemon.com/assets/cms2/img/pokedex/full/${paddedNumber}.png" } - fun weightInPounds(weight: Int): Float { - return weight * HEC_TO_LBS + fun weightInPounds(weight: Int): String { + return "${weight * HEC_TO_LBS} lbs" } - fun weightInKg(weight: Int): Float { - return weight * HEC_TO_KG + fun weightInKg(weight: Int): String { + return "${weight * HEC_TO_KG} kg" } - fun heightToCm(height: Int): Int { - return height * DEC_TO_CM + fun heightToCm(height: Int): String { + return "${height * DEC_TO_CM} cm" } - fun heightToFtIn(height: Int): Pair { + fun heightToFtIn(height: Int): String { 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() + val feet = floor((heightCm / CM_TO_IN) / IN_TO_FT).toInt() + val inches = ceil((heightCm / CM_TO_IN) - (feet * IN_TO_FT)).toInt() - return Pair(feet, inches) + return "${feet}' ${inches}\"" + } + + fun genderRateToPercentage(genderRate: Int): Float { + return genderRate.toFloat() * GENDER_RATE * 100 } } \ No newline at end of file diff --git a/app/src/main/res/drawable/female_symbol.xml b/app/src/main/res/drawable/female_symbol.xml new file mode 100644 index 0000000..678bb80 --- /dev/null +++ b/app/src/main/res/drawable/female_symbol.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/male_symbol.xml b/app/src/main/res/drawable/male_symbol.xml new file mode 100644 index 0000000..87e3789 --- /dev/null +++ b/app/src/main/res/drawable/male_symbol.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4e04923..89641dc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,4 +22,9 @@ 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 + Height + Height + Breeding + Gender + Egg Groups \ No newline at end of file