add about details

This commit is contained in:
Owen LeJeune
2022-09-28 10:18:04 -04:00
parent 8ebc8ac98f
commit 9dd598bafb
12 changed files with 246 additions and 60 deletions

View File

@@ -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.PokemonSpecies
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonStat 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.PokemonType
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.egggroup.EggGroup
import retrofit2.Response import retrofit2.Response
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Path import retrofit2.http.Path
@@ -27,4 +28,7 @@ interface PokemonApi {
@GET("stat/{id}") @GET("stat/{id}")
suspend fun getPokemonStat(@Path("id") id: Int): Response<PokemonStat> suspend fun getPokemonStat(@Path("id") id: Int): Response<PokemonStat>
@GET("egg-group/{id}")
suspend fun getEggGroup(@Path("id") id: Int): Response<EggGroup>
} }

View File

@@ -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.PokemonSpecies
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonStat 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.PokemonType
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.egggroup.EggGroup
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import retrofit2.Response import retrofit2.Response
@@ -47,4 +48,8 @@ class PokemonService: KoinComponent {
return service.getPokemonStat(id) return service.getPokemonStat(id)
} }
suspend fun getEggGroup(id: Int): Response<EggGroup> {
return service.getEggGroup(id)
}
} }

View File

@@ -7,6 +7,6 @@ import com.owenlejeune.mydex.api.pokeapi.v2.model.misc.NameAndUrl
class EggGroup( class EggGroup(
@SerializedName("id") val id: Int, @SerializedName("id") val id: Int,
@SerializedName("name") val name: String, @SerializedName("name") val name: String,
@SerializedName("name") val names: List<NameAndLanguage>, @SerializedName("names") val names: List<NameAndLanguage>,
@SerializedName("pokemon_species") val pokemonSpecies: List<NameAndUrl> @SerializedName("pokemon_species") val pokemonSpecies: List<NameAndUrl>
) )

View File

@@ -5,5 +5,6 @@ import com.owenlejeune.mydex.api.pokeapi.v2.model.misc.NameAndLanguage
fun List<NameAndLanguage>.getNameForLanguage(): String? { fun List<NameAndLanguage>.getNameForLanguage(): String? {
val lang = Locale.current.language 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
} }

View File

@@ -20,6 +20,7 @@ class AppPreferences(context: Context) {
private val SELECTED_COLOR = "selected_color" private val SELECTED_COLOR = "selected_color"
private val USE_WALLPAPER_COLORS = "use_wallpaper_colors" private val USE_WALLPAPER_COLORS = "use_wallpaper_colors"
private val DARK_THEME = "dark_theme" private val DARK_THEME = "dark_theme"
private val USE_METRIC = "use_metric"
} }
private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE) 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) get() = preferences.getInt(SELECTED_COLOR, selectedColorDefault)
set(value) { preferences.put(SELECTED_COLOR, value) } 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 ********/ /********* Helpers ********/
private fun SharedPreferences.put(key: String, value: Any?) { private fun SharedPreferences.put(key: String, value: Any?) {
edit().apply { edit().apply {

View File

@@ -16,10 +16,12 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController 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.PokemonSpecies
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonStat 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.PokemonType
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.egggroup.EggGroup
import com.owenlejeune.mydex.extensions.adjustBy import com.owenlejeune.mydex.extensions.adjustBy
import com.owenlejeune.mydex.extensions.getIdFromUrl import com.owenlejeune.mydex.extensions.getIdFromUrl
import com.owenlejeune.mydex.extensions.getNameForLanguage 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.PokemonTypeLabel
import com.owenlejeune.mydex.ui.components.SmallTabIndicator 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 com.owenlejeune.mydex.utils.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@@ -225,20 +232,132 @@ private fun ColumnScope.Details(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun AboutView( private fun AboutView(
pokemon: Pokemon, pokemon: Pokemon,
pokemonSpecies: PokemonSpecies, pokemonSpecies: PokemonSpecies,
service: PokemonService service: PokemonService,
preferences: AppPreferences = get(AppPreferences::class.java)
) { ) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Column( Column (
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.padding(all = 24.dp) .padding(all = 24.dp)
.verticalScroll(state = scrollState) .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<EggGroup>() }
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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun TypeRelationChip ( private fun TypeRelationChip (
@@ -469,9 +626,7 @@ private sealed class DetailTab(
object About: DetailTab( object About: DetailTab(
stringRes = R.string.about_tab_title, stringRes = R.string.about_tab_title,
route = "about_tab", route = "about_tab",
screen = @Composable { pokemon, pokemonSpecies, service -> screen = @Composable { p, ps, s -> AboutView(p, ps, s) }
}
) )
object BaseStats: DetailTab( object BaseStats: DetailTab(
@@ -483,17 +638,13 @@ private sealed class DetailTab(
object Evolution: DetailTab( object Evolution: DetailTab(
stringRes = R.string.evolution_tab_title, stringRes = R.string.evolution_tab_title,
route = "evolution_tab", route = "evolution_tab",
screen = @Composable { pokemon, pokemonSpecies, service -> screen = @Composable { p, ps, s -> EvolutionView(p, ps, s) }
}
) )
object Moves: DetailTab( object Moves: DetailTab(
stringRes = R.string.moves_tab_title, stringRes = R.string.moves_tab_title,
route = "moves_tab", route = "moves_tab",
screen = @Composable { pokemon, pokemonSpecies, service -> screen = @Composable { p, ps, s -> MovesView(p, ps, s) }
}
) )
} }

View File

@@ -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.PokemonSpecies
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonStat 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.PokemonType
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.egggroup.EggGroup
object AppCache { object AppCache {
@@ -12,5 +13,6 @@ object AppCache {
var cachedPokemon = SparseArray<Pokemon>() var cachedPokemon = SparseArray<Pokemon>()
var cachedTypes = SparseArray<PokemonType>() var cachedTypes = SparseArray<PokemonType>()
var cachedStats = SparseArray<PokemonStat>() var cachedStats = SparseArray<PokemonStat>()
var cachedEggGroups = SparseArray<EggGroup>()
} }

View File

@@ -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.PokemonSpecies
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.PokemonStat 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.PokemonType
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.egggroup.EggGroup
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -17,33 +18,23 @@ object DataManager: KoinComponent {
private val service: PokemonService by inject() private val service: PokemonService by inject()
fun getPokemonById(id: Int, callback: (Pokemon) -> Unit) { fun getPokemonById(id: Int, callback: (Pokemon) -> Unit) {
AppCache.cachedPokemon[id]?.let(callback) ?: run { getById(
CoroutineScope(Dispatchers.IO).launch { id = id,
service.getPokemon(id).apply { callback = callback,
if (isSuccessful) { retriever = { AppCache.cachedPokemon[it] },
body()?.let { fetcher = { service.getPokemon(it) },
AppCache.cachedPokemon.put(it.id, it) storer = { AppCache.cachedPokemon.put(it.id, it) }
callback(it) )
}
}
}
}
}
} }
fun getPokemonSpeciesById(id: Int, callback: (PokemonSpecies) -> Unit) { fun getPokemonSpeciesById(id: Int, callback: (PokemonSpecies) -> Unit) {
AppCache.cachedSpecies[id]?.let(callback) ?: run { getById(
CoroutineScope(Dispatchers.IO).launch { id = id,
service.getPokemonSpecies(id).apply { callback = callback,
if (isSuccessful) { retriever = { AppCache.cachedSpecies[it] },
body()?.let { fetcher = { service.getPokemonSpecies(it) },
AppCache.cachedSpecies.put(it.id, it) storer = { AppCache.cachedSpecies.put(it.id, it) }
callback(it) )
}
}
}
}
}
} }
fun getTypeById(id: Int, callback: (PokemonType) -> Unit) { fun getTypeById(id: Int, callback: (PokemonType) -> Unit) {
@@ -57,18 +48,23 @@ object DataManager: KoinComponent {
} }
fun getStatById(id: Int, callback: (PokemonStat) -> Unit) { fun getStatById(id: Int, callback: (PokemonStat) -> Unit) {
AppCache.cachedStats[id]?.let(callback) ?: run { getById(
CoroutineScope(Dispatchers.IO).launch { id = id,
service.getPokemonStat(id).apply { callback = callback,
if (isSuccessful) { retriever = { AppCache.cachedStats[id] },
body()?.let { fetcher = { service.getPokemonStat(id) },
AppCache.cachedStats.put(it.id, it) storer = { AppCache.cachedStats.put(it.id, it) }
callback(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 <T> getById( private fun <T> getById(

View File

@@ -10,6 +10,7 @@ object PokeUtils {
private val DEC_TO_CM = 10 private val DEC_TO_CM = 10
private val CM_TO_IN = 2.54 private val CM_TO_IN = 2.54
private val IN_TO_FT = 12 private val IN_TO_FT = 12
private val GENDER_RATE = 1f/8f
fun idToDexNumber(id: Int, includeNumberSign: Boolean = true): String { fun idToDexNumber(id: Int, includeNumberSign: Boolean = true): String {
val padded = id.toString().padStart(3, '0') 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" return "https://assets.pokemon.com/assets/cms2/img/pokedex/full/${paddedNumber}.png"
} }
fun weightInPounds(weight: Int): Float { fun weightInPounds(weight: Int): String {
return weight * HEC_TO_LBS return "${weight * HEC_TO_LBS} lbs"
} }
fun weightInKg(weight: Int): Float { fun weightInKg(weight: Int): String {
return weight * HEC_TO_KG return "${weight * HEC_TO_KG} kg"
} }
fun heightToCm(height: Int): Int { fun heightToCm(height: Int): String {
return height * DEC_TO_CM return "${height * DEC_TO_CM} cm"
} }
fun heightToFtIn(height: Int): Pair<Int, Int> { fun heightToFtIn(height: Int): String {
val heightCm = height * DEC_TO_CM val heightCm = height * DEC_TO_CM
val feet = floor((height / CM_TO_IN) / IN_TO_FT).toInt() val feet = floor((heightCm / CM_TO_IN) / IN_TO_FT).toInt()
val inches = ceil((height / CM_TO_IN) - (feet * 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
} }
} }

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#F7786B"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M17.5,9.5C17.5,6.46 15.04,4 12,4S6.5,6.46 6.5,9.5c0,2.7 1.94,4.93 4.5,5.4V17H9v2h2v2h2v-2h2v-2h-2v-2.1C15.56,14.43 17.5,12.2 17.5,9.5zM8.5,9.5C8.5,7.57 10.07,6 12,6s3.5,1.57 3.5,3.5S13.93,13 12,13S8.5,11.43 8.5,9.5z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#429BED"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9.5,11c1.93,0 3.5,1.57 3.5,3.5S11.43,18 9.5,18S6,16.43 6,14.5S7.57,11 9.5,11zM9.5,9C6.46,9 4,11.46 4,14.5S6.46,20 9.5,20s5.5,-2.46 5.5,-5.5c0,-1.16 -0.36,-2.23 -0.97,-3.12L18,7.42V10h2V4h-6v2h2.58l-3.97,3.97C11.73,9.36 10.66,9 9.5,9z"/>
</vector>

View File

@@ -22,4 +22,9 @@
<string name="type_defences_title">Type defences</string> <string name="type_defences_title">Type defences</string>
<string name="type_defences_half_description">Pokémon of these types deal half damage to this Pokémon</string> <string name="type_defences_half_description">Pokémon of these types deal half damage to this Pokémon</string>
<string name="type_defences_no_description">Pokémon of these types deal no damage to this Pokémon</string> <string name="type_defences_no_description">Pokémon of these types deal no damage to this Pokémon</string>
<string name="poke_details_height_title">Height</string>
<string name="poke_details_weight_title">Height</string>
<string name="poke_details_breeding_title">Breeding</string>
<string name="poke_details_gender_subtitle">Gender</string>
<string name="poke_details_egg_groups_subtitle">Egg Groups</string>
</resources> </resources>