mirror of
https://github.com/owenlejeune/MYDex.git
synced 2025-11-18 13:00:54 -05:00
main screen view and basic pokedex view
This commit is contained in:
17
.idea/deploymentTargetDropDown.xml
generated
Normal file
17
.idea/deploymentTargetDropDown.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetDropDown">
|
||||||
|
<targetSelectedWithDropDown>
|
||||||
|
<Target>
|
||||||
|
<type value="QUICK_BOOT_TARGET" />
|
||||||
|
<deviceKey>
|
||||||
|
<Key>
|
||||||
|
<type value="VIRTUAL_DEVICE_PATH" />
|
||||||
|
<value value="$USER_HOME$/.android/avd/Pixel_3a_API_33.avd" />
|
||||||
|
</Key>
|
||||||
|
</deviceKey>
|
||||||
|
</Target>
|
||||||
|
</targetSelectedWithDropDown>
|
||||||
|
<timeTargetWasSelectedWithDropDown value="2022-09-15T01:01:05.349795Z" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
tools:targetApi="31"
|
tools:targetApi="31"
|
||||||
android:name=".MYDexApplication">
|
android:name=".MYDexApplication">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.MYDex">
|
android:theme="@style/Theme.MYDex">
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.owenlejeune.mydex
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Surface
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import com.kieronquinn.monetcompat.app.MonetCompatActivity
|
|
||||||
import com.owenlejeune.mydex.ui.theme.MYDexTheme
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class MainActivity : MonetCompatActivity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
lifecycleScope.launchWhenCreated {
|
|
||||||
monet.awaitMonetReady()
|
|
||||||
setContent {
|
|
||||||
MYDexTheme(monetCompat = monet) {
|
|
||||||
val appNavController = rememberNavController()
|
|
||||||
Box {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.owenlejeune.mydex.api.pokeapi
|
package com.owenlejeune.mydex.api.pokeapi
|
||||||
|
|
||||||
import com.owenlejeune.mydex.api.Client
|
import com.owenlejeune.mydex.api.Client
|
||||||
|
import com.owenlejeune.mydex.api.pokeapi.v2.PokemonApi
|
||||||
import com.owenlejeune.mydex.preferences.AppPreferences
|
import com.owenlejeune.mydex.preferences.AppPreferences
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
@@ -15,6 +16,8 @@ class PokeApiClient: KoinComponent {
|
|||||||
private val client: Client by inject { parametersOf(BASE_URL) }
|
private val client: Client by inject { parametersOf(BASE_URL) }
|
||||||
private val preferences: AppPreferences by inject()
|
private val preferences: AppPreferences by inject()
|
||||||
|
|
||||||
|
fun createPokemonService(): PokemonApi {
|
||||||
|
return client.create(PokemonApi::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.owenlejeune.mydex.api.pokeapi.v2
|
package com.owenlejeune.mydex.api.pokeapi.v2
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.owenlejeune.mydex.api.pokeapi.v2.model.misc.PaginatedResponse
|
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.Pokemon
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
@@ -10,6 +11,8 @@ class PokemonService: KoinComponent {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val DEFAULT_LIMIT = 20
|
private const val DEFAULT_LIMIT = 20
|
||||||
|
|
||||||
|
val TAG = PokemonService::class.java.simpleName
|
||||||
}
|
}
|
||||||
|
|
||||||
private val service: PokemonApi by inject()
|
private val service: PokemonApi by inject()
|
||||||
@@ -22,7 +25,11 @@ class PokemonService: KoinComponent {
|
|||||||
val limit = DEFAULT_LIMIT
|
val limit = DEFAULT_LIMIT
|
||||||
val offset = DEFAULT_LIMIT * page
|
val offset = DEFAULT_LIMIT * page
|
||||||
|
|
||||||
return service.getPaginatedPokemon(offset = offset, limit = limit)
|
Log.d(TAG, "Paginated: page=$page, limit=$limit, offset=$offset")
|
||||||
|
|
||||||
|
return service.getPaginatedPokemon(offset = offset, limit = limit).apply {
|
||||||
|
Log.d(TAG, "Response: $isSuccessful, ${body()?.results?.map { it.name }}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.owenlejeune.mydex.api.pokeapi.v2.paging
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import com.owenlejeune.mydex.api.pokeapi.v2.PokemonService
|
||||||
|
import com.owenlejeune.mydex.api.pokeapi.v2.model.misc.NameAndUrl
|
||||||
|
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.Pokemon
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
class PokemonPagingSource: PagingSource<Int, Pokemon>(), KoinComponent {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = PokemonPagingSource::class.java.simpleName
|
||||||
|
}
|
||||||
|
|
||||||
|
private val pokemonService: PokemonService by inject()
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Int, Pokemon>): Int? {
|
||||||
|
return state.anchorPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Pokemon> {
|
||||||
|
return try {
|
||||||
|
val nextPage = params.key ?: 1
|
||||||
|
val response = pokemonService.getPokemon(nextPage)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val responseBody = response.body()!!
|
||||||
|
// Log.d(TAG, "${results.map { it.name }}")
|
||||||
|
LoadResult.Page(
|
||||||
|
data = listOf(responseBody),
|
||||||
|
prevKey = if (nextPage == 1) null else nextPage - 1,
|
||||||
|
nextKey = if (responseBody == null) null else nextPage + 1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LoadResult.Invalid()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoadResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.owenlejeune.mydex.api.pokeapi.v2.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.Pager
|
||||||
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.cachedIn
|
||||||
|
import com.owenlejeune.mydex.api.pokeapi.v2.model.misc.NameAndUrl
|
||||||
|
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.Pokemon
|
||||||
|
import com.owenlejeune.mydex.api.pokeapi.v2.paging.PokemonPagingSource
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
class PokemonViewModel: ViewModel() {
|
||||||
|
val pokemon: Flow<PagingData<Pokemon>> = Pager(PagingConfig(pageSize = Int.MAX_VALUE)) {
|
||||||
|
PokemonPagingSource()
|
||||||
|
}.flow.cachedIn(viewModelScope)
|
||||||
|
}
|
||||||
@@ -5,7 +5,10 @@ import com.google.gson.TypeAdapterFactory
|
|||||||
import com.owenlejeune.mydex.BuildConfig
|
import com.owenlejeune.mydex.BuildConfig
|
||||||
import com.owenlejeune.mydex.api.*
|
import com.owenlejeune.mydex.api.*
|
||||||
import com.owenlejeune.mydex.api.pokeapi.PokeApiClient
|
import com.owenlejeune.mydex.api.pokeapi.PokeApiClient
|
||||||
|
import com.owenlejeune.mydex.api.pokeapi.v2.PokemonApi
|
||||||
|
import com.owenlejeune.mydex.api.pokeapi.v2.PokemonService
|
||||||
import com.owenlejeune.mydex.preferences.AppPreferences
|
import com.owenlejeune.mydex.preferences.AppPreferences
|
||||||
|
import com.owenlejeune.mydex.utils.ResourceUtils
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val networkModule = module {
|
val networkModule = module {
|
||||||
@@ -20,14 +23,20 @@ val networkModule = module {
|
|||||||
single {
|
single {
|
||||||
GsonBuilder().apply {
|
GsonBuilder().apply {
|
||||||
get<List<TypeAdapterFactory>>().forEach { taf -> registerTypeAdapterFactory(taf) }
|
get<List<TypeAdapterFactory>>().forEach { taf -> registerTypeAdapterFactory(taf) }
|
||||||
}
|
}.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
single { PokeApiClient() }
|
single { PokeApiClient() }
|
||||||
|
single { get<PokeApiClient>().createPokemonService() }
|
||||||
|
single { PokemonService() }
|
||||||
}
|
}
|
||||||
|
|
||||||
val preferencesModule = module {
|
val preferencesModule = module {
|
||||||
single { AppPreferences(get()) }
|
single { AppPreferences(get()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val Modules = listOf(networkModule, preferencesModule)
|
val appModule = module {
|
||||||
|
factory { ResourceUtils(get()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val Modules = listOf(networkModule, preferencesModule, appModule)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.owenlejeune.mydex.extensions
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.toComposeRect
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.window.layout.WindowMetricsCalculator
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Activity.rememberWindowSize(): Size {
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
|
||||||
|
val windowMetrics = remember(configuration) {
|
||||||
|
WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this)
|
||||||
|
}
|
||||||
|
return windowMetrics.bounds.toComposeRect().size
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class WindowSizeClass { Compact, Medium, Expanded }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Activity.rememberWindowSizeClass(): WindowSizeClass {
|
||||||
|
val windowSize = rememberWindowSize()
|
||||||
|
|
||||||
|
val windowSizeDp = with(LocalDensity.current) {
|
||||||
|
windowSize.toDpSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
return getWindowSizeClass(windowSizeDp)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getWindowSizeClass(windowDpSize: DpSize): WindowSizeClass = when {
|
||||||
|
windowDpSize.width < 0.dp -> throw IllegalArgumentException("Dp value cannot be negative")
|
||||||
|
windowDpSize.width < 600.dp -> WindowSizeClass.Compact
|
||||||
|
windowDpSize.width < 840.dp -> WindowSizeClass.Medium
|
||||||
|
else -> WindowSizeClass.Expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Activity.launchActivity(activity: Class<T>) {
|
||||||
|
val intent = Intent(this, activity)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.owenlejeune.mydex.extensions
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridScope
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.paging.compose.LazyPagingItems
|
||||||
|
|
||||||
|
fun <T: Any> LazyGridScope.lazyPagingItems(
|
||||||
|
lazyPagingItems: LazyPagingItems<T>,
|
||||||
|
itemContent: @Composable LazyGridItemScope.(value: T?) -> Unit
|
||||||
|
) {
|
||||||
|
items(lazyPagingItems.itemCount) { index ->
|
||||||
|
itemContent(lazyPagingItems[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T: Any> LazyGridScope.listItems(
|
||||||
|
items: List<T>,
|
||||||
|
itemContent: @Composable (value: T) -> Unit
|
||||||
|
) {
|
||||||
|
items(items.size) { index ->
|
||||||
|
itemContent(items[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LazyGridScope.header(
|
||||||
|
content: @Composable LazyGridItemScope.() -> Unit
|
||||||
|
) {
|
||||||
|
item(
|
||||||
|
span = {
|
||||||
|
GridItemSpan(maxLineSpan)
|
||||||
|
},
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T: Any> LazyListScope.listItems(
|
||||||
|
items: Collection<T>,
|
||||||
|
itemContent: @Composable (value: T) -> Unit
|
||||||
|
) {
|
||||||
|
items(items.size) { index ->
|
||||||
|
itemContent(items.elementAt(index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T: Any?> LazyListScope.listItems(
|
||||||
|
items: List<T?>,
|
||||||
|
key: (T?) -> Any,
|
||||||
|
itemContent: @Composable (value: T?) -> Unit
|
||||||
|
) {
|
||||||
|
items(items.size, key = { key(items[it]) }) { index ->
|
||||||
|
itemContent(items[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T: Any> LazyListScope.lazyPagingItems(
|
||||||
|
lazyPagingItems: LazyPagingItems<T>,
|
||||||
|
itemContent: @Composable LazyItemScope.(value: T?) -> Unit
|
||||||
|
) {
|
||||||
|
items(lazyPagingItems.itemCount) { index ->
|
||||||
|
itemContent(lazyPagingItems[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.owenlejeune.mydex.extensions
|
||||||
|
|
||||||
|
fun String.charAtFromEnd(index: Int): Char {
|
||||||
|
return get(length-1-index)
|
||||||
|
}
|
||||||
54
app/src/main/java/com/owenlejeune/mydex/ui/MainActivity.kt
Normal file
54
app/src/main/java/com/owenlejeune/mydex/ui/MainActivity.kt
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package com.owenlejeune.mydex.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.kieronquinn.monetcompat.app.MonetCompatActivity
|
||||||
|
import com.owenlejeune.mydex.preferences.AppPreferences
|
||||||
|
import com.owenlejeune.mydex.ui.navigation.DataNavItem
|
||||||
|
import com.owenlejeune.mydex.ui.navigation.MainNavItem
|
||||||
|
import com.owenlejeune.mydex.ui.theme.MYDexTheme
|
||||||
|
import com.owenlejeune.mydex.ui.views.AppScaffold
|
||||||
|
import com.owenlejeune.mydex.ui.views.PokedexView
|
||||||
|
import org.koin.java.KoinJavaComponent
|
||||||
|
|
||||||
|
class MainActivity : MonetCompatActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
lifecycleScope.launchWhenCreated {
|
||||||
|
monet.awaitMonetReady()
|
||||||
|
setContent {
|
||||||
|
MYDexTheme(monetCompat = monet) {
|
||||||
|
val appNavController = rememberNavController()
|
||||||
|
Box {
|
||||||
|
MainNavigationRoutes(appNavController = appNavController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MainNavigationRoutes(
|
||||||
|
startDestination: String = MainNavItem.MainView.route,
|
||||||
|
appNavController: NavHostController,
|
||||||
|
preferences: AppPreferences = KoinJavaComponent.get(AppPreferences::class.java)
|
||||||
|
) {
|
||||||
|
NavHost(navController = appNavController, startDestination = startDestination) {
|
||||||
|
composable(MainNavItem.MainView.route) {
|
||||||
|
AppScaffold(appNavController = appNavController)
|
||||||
|
}
|
||||||
|
composable(DataNavItem.Pokedex.route) {
|
||||||
|
PokedexView(appNavController = appNavController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
194
app/src/main/java/com/owenlejeune/mydex/ui/components/Widgets.kt
Normal file
194
app/src/main/java/com/owenlejeune/mydex/ui/components/Widgets.kt
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package com.owenlejeune.mydex.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||||
|
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusEvent
|
||||||
|
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.modifier.modifierLocalConsumer
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import com.owenlejeune.mydex.R
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun RoundedTextField(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||||
|
requestFocus: Boolean = false,
|
||||||
|
placeHolder: String = "",
|
||||||
|
placeHolderTextColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
backgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
textStyle: TextStyle = MaterialTheme.typography.bodySmall,
|
||||||
|
cursorColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
singleLine: Boolean = true,
|
||||||
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
readOnly: Boolean = false,
|
||||||
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
|
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||||
|
leadingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
trailingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
shape: Shape = RoundedCornerShape(25.dp)
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(shape),
|
||||||
|
shape = shape,
|
||||||
|
color = backgroundColor
|
||||||
|
) {
|
||||||
|
Row(Modifier.padding(horizontal = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
if (leadingIcon != null) {
|
||||||
|
leadingIcon()
|
||||||
|
}
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.padding(start = 8.dp, end = 8.dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(1f),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
if (value.isEmpty() && placeHolder.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = placeHolder,
|
||||||
|
style = textStyle,
|
||||||
|
color = placeHolderTextColor,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
BasicTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.bringIntoViewRequester(bringIntoViewRequester)
|
||||||
|
.onFocusEvent {
|
||||||
|
if (it.isFocused) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
delay(200)
|
||||||
|
bringIntoViewRequester.bringIntoView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
singleLine = singleLine,
|
||||||
|
textStyle = textStyle.copy(color = textColor),
|
||||||
|
cursorBrush = SolidColor(cursorColor),
|
||||||
|
maxLines = maxLines,
|
||||||
|
enabled = enabled,
|
||||||
|
readOnly = readOnly,
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (trailingIcon != null) {
|
||||||
|
trailingIcon()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestFocus) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchBar(
|
||||||
|
placeholder: String,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
RoundedTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(55.dp)
|
||||||
|
.clickable(
|
||||||
|
onClick = onClick
|
||||||
|
),
|
||||||
|
value = "",
|
||||||
|
enabled = false,
|
||||||
|
onValueChange = { },
|
||||||
|
placeHolder = placeholder,
|
||||||
|
leadingIcon = {
|
||||||
|
Image(
|
||||||
|
imageVector = Icons.Filled.Search,
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.primary)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MenuItemButton(
|
||||||
|
modifier: Modifier,
|
||||||
|
text: String,
|
||||||
|
color: Color,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.height(80.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = color),
|
||||||
|
onClick = onClick
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = R.drawable.pokeball_s,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
.offset(x = 20.dp, y = 0.dp)
|
||||||
|
.size(width = 96.dp, height = 96.dp),
|
||||||
|
colorFilter = ColorFilter.tint(Color.White.copy(alpha = 0.15f))
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterStart)
|
||||||
|
.padding(start = 16.dp),
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(color = Color.White)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.owenlejeune.mydex.ui.navigation
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import com.owenlejeune.mydex.R
|
||||||
|
import com.owenlejeune.mydex.ui.theme.*
|
||||||
|
import com.owenlejeune.mydex.utils.ResourceUtils
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
sealed class DataNavItem(
|
||||||
|
val route: String,
|
||||||
|
val color: Color,
|
||||||
|
@StringRes titleRes: Int
|
||||||
|
): KoinComponent {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val Pages by lazy { listOf(Pokedex, Moves, Abilities, Items, Locations, TypeCharts) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val resourceUtils: ResourceUtils by inject()
|
||||||
|
|
||||||
|
val title = resourceUtils.getString(titleRes)
|
||||||
|
|
||||||
|
object Pokedex: DataNavItem("pokedex_route", PokeTeal, R.string.pokedex_nav_title)
|
||||||
|
object Moves: DataNavItem("moves_route", PokeRed, R.string.moves_nav_title)
|
||||||
|
object Abilities: DataNavItem("abilities_route", PokeLightBlue, R.string.abilities_nav_title)
|
||||||
|
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)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.owenlejeune.mydex.ui.navigation
|
||||||
|
|
||||||
|
sealed class MainNavItem(val route: String) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val Items by lazy { listOf(MainView) }
|
||||||
|
}
|
||||||
|
|
||||||
|
object MainView: MainNavItem("main_route")
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,7 +2,16 @@ package com.owenlejeune.mydex.ui.theme
|
|||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
val Purple200 = Color(0xFFBB86FC)
|
val PokeBlack = Color(0xFF303943)
|
||||||
val Purple500 = Color(0xFF6200EE)
|
val PokeBlue = Color(0xFF429BED)
|
||||||
val Purple700 = Color(0xFF3700B3)
|
val PokeBrown = Color(0xFFB1736C)
|
||||||
val Teal200 = Color(0xFF03DAC5)
|
val PokeLightBlue = Color(0xFF58ABF6)
|
||||||
|
val PokeLightBrown = Color(0xFFCA8179)
|
||||||
|
val PokeLightPurple = Color(0xFF9F5BBA)
|
||||||
|
val PokeLightRed = Color(0xFFF7786B)
|
||||||
|
val PokeLightTeal = Color(0xFF2CDAB1)
|
||||||
|
val PokeLightYellow = Color(0xFFFFCE4B)
|
||||||
|
val PokePurple = Color(0xFF7C538C)
|
||||||
|
val PokeRed = Color(0xFFFA6555)
|
||||||
|
val PokeTeal = Color(0xFF4FC1A6)
|
||||||
|
val PokeYellow = Color(0xFFF6C747)
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package com.owenlejeune.mydex.ui.views
|
||||||
|
|
||||||
|
import androidx.compose.animation.rememberSplineBasedDecay
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.owenlejeune.mydex.extensions.WindowSizeClass
|
||||||
|
import com.owenlejeune.mydex.extensions.rememberWindowSizeClass
|
||||||
|
import com.owenlejeune.mydex.preferences.AppPreferences
|
||||||
|
import com.owenlejeune.mydex.ui.MainActivity
|
||||||
|
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
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MainActivity.AppScaffold(
|
||||||
|
appNavController: NavHostController,
|
||||||
|
preferences: AppPreferences = KoinJavaComponent.get(AppPreferences::class.java)
|
||||||
|
) {
|
||||||
|
val windowSize = rememberWindowSizeClass()
|
||||||
|
|
||||||
|
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
|
||||||
|
val topAppBarScrollState = rememberTopAppBarScrollState()
|
||||||
|
val scrollBehavior = remember(decayAnimationSpec) {
|
||||||
|
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec, topAppBarScrollState)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
MainContent(
|
||||||
|
windowSize = windowSize,
|
||||||
|
appNavController = appNavController,
|
||||||
|
topBarScrollBehaviour = scrollBehavior
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.MainContent(
|
||||||
|
windowSize: WindowSizeClass,
|
||||||
|
appNavController: NavHostController,
|
||||||
|
topBarScrollBehaviour: TopAppBarScrollBehavior
|
||||||
|
) {
|
||||||
|
if (windowSize != WindowSizeClass.Expanded) {
|
||||||
|
SingleColumnMainContent(
|
||||||
|
appNavController = appNavController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.SingleColumnMainContent(appNavController: NavHostController) {
|
||||||
|
val tint = MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
AsyncImage(
|
||||||
|
model = com.owenlejeune.mydex.R.drawable.pokeball,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(x = 90.dp, y = (-40).dp)
|
||||||
|
.size(width = 240.dp, height = 240.dp)
|
||||||
|
.align(Alignment.TopEnd),
|
||||||
|
colorFilter = ColorFilter.tint(tint)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(all = 36.dp)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(120.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = com.owenlejeune.mydex.R.string.main_screen_title),
|
||||||
|
style = TextStyle(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 30.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(50.dp))
|
||||||
|
|
||||||
|
SearchBar(placeholder = stringResource(id = com.owenlejeune.mydex.R.string.main_search_placeholder)) {}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
val cols = 2
|
||||||
|
val rows = DataNavItem.Pages.size/cols
|
||||||
|
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) {
|
||||||
|
DataNavItem.Pages[2*i+1]
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
MenuItemRow(appNavController = appNavController, first = first, second = second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MenuItemRow(
|
||||||
|
appNavController: NavHostController,
|
||||||
|
first: DataNavItem,
|
||||||
|
second: DataNavItem?
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
MenuItemButton(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
text = first.title,
|
||||||
|
color = first.color,
|
||||||
|
onClick = {
|
||||||
|
appNavController.navigate(first.route)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
second?.let {
|
||||||
|
MenuItemButton(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
text = second.title,
|
||||||
|
color = second.color,
|
||||||
|
onClick = {
|
||||||
|
appNavController.navigate(second.route)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
143
app/src/main/java/com/owenlejeune/mydex/ui/views/PokedexViews.kt
Normal file
143
app/src/main/java/com/owenlejeune/mydex/ui/views/PokedexViews.kt
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package com.owenlejeune.mydex.ui.views
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.blur
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.owenlejeune.mydex.R
|
||||||
|
import com.owenlejeune.mydex.api.pokeapi.v2.PokemonService
|
||||||
|
import com.owenlejeune.mydex.api.pokeapi.v2.model.misc.NameAndUrl
|
||||||
|
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.Pokemon
|
||||||
|
import com.owenlejeune.mydex.api.pokeapi.v2.viewmodel.PokemonViewModel
|
||||||
|
import com.owenlejeune.mydex.extensions.charAtFromEnd
|
||||||
|
import com.owenlejeune.mydex.extensions.header
|
||||||
|
import com.owenlejeune.mydex.extensions.lazyPagingItems
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.java.KoinJavaComponent.get
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PokedexView(
|
||||||
|
appNavController: NavController
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(color = MaterialTheme.colorScheme.background)
|
||||||
|
) {
|
||||||
|
val tint = MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
AsyncImage(
|
||||||
|
model = R.drawable.pokeball,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(x = 90.dp, y = (-40).dp)
|
||||||
|
.size(width = 240.dp, height = 240.dp)
|
||||||
|
.align(Alignment.TopEnd),
|
||||||
|
colorFilter = ColorFilter.tint(tint)
|
||||||
|
)
|
||||||
|
|
||||||
|
SmallTopAppBar(
|
||||||
|
modifier = Modifier
|
||||||
|
.statusBarsPadding()
|
||||||
|
.blur(radius = 10.dp),
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { appNavController.popBackStack() }) {
|
||||||
|
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {},
|
||||||
|
colors = TopAppBarDefaults
|
||||||
|
.smallTopAppBarColors(
|
||||||
|
containerColor = Color.Transparent.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
val lazyPokemon = PokemonViewModel().pokemon.collectAsLazyPagingItems()
|
||||||
|
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(2),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 36.dp, end = 36.dp, top = 160.dp)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
header {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.main_screen_title),
|
||||||
|
style = TextStyle(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 30.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(bottom = 50.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazyPagingItems(lazyPokemon) { item ->
|
||||||
|
item?.let {
|
||||||
|
PokedexCard(pokemon = item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun PokedexCard(
|
||||||
|
pokemon: Pokemon
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(80.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = R.drawable.pokeball_s,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.offset(x = 20.dp, y = 0.dp)
|
||||||
|
.size(width = 96.dp, height = 96.dp),
|
||||||
|
colorFilter = ColorFilter.tint(Color.White.copy(alpha = 0.15f))
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterStart)
|
||||||
|
.padding(start = 16.dp),
|
||||||
|
text = pokemon.name,
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(color = Color.White)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.owenlejeune.mydex.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
class ResourceUtils(private val context: Context) {
|
||||||
|
|
||||||
|
fun getString(stringRes: Int): String {
|
||||||
|
return context.getString(stringRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
BIN
app/src/main/res/drawable/pokeball.png
Normal file
BIN
app/src/main/res/drawable/pokeball.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
app/src/main/res/drawable/pokeball_s.png
Normal file
BIN
app/src/main/res/drawable/pokeball_s.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
@@ -1,3 +1,15 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">MYDex</string>
|
<string name="app_name">MYDex</string>
|
||||||
|
|
||||||
|
<!-- main screen -->
|
||||||
|
<string name="main_screen_title">What Pokémon\nare you looking for?</string>
|
||||||
|
<string name="main_search_placeholder">Search Pokémon, Move, Ability, etc.</string>
|
||||||
|
|
||||||
|
<!-- nav titles -->
|
||||||
|
<string name="pokedex_nav_title">Pokedex</string>
|
||||||
|
<string name="moves_nav_title">Moves</string>
|
||||||
|
<string name="abilities_nav_title">Abilities</string>
|
||||||
|
<string name="items_nav_title">Items</string>
|
||||||
|
<string name="locations_nav_title">Locations</string>
|
||||||
|
<string name="type_charts_nav_title">Type charts</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,5 +2,8 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Theme.MYDex" parent="Theme.AppCompat.NoActionBar">
|
<style name="Theme.MYDex" parent="Theme.AppCompat.NoActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="windowNoTitle">true</item>
|
||||||
|
<item name="android:windowTranslucentStatus">true</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user