mirror of
https://github.com/owenlejeune/MYDex.git
synced 2025-11-08 08:22:42 -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"
|
||||
android:name=".MYDexApplication">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
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
|
||||
|
||||
import com.owenlejeune.mydex.api.Client
|
||||
import com.owenlejeune.mydex.api.pokeapi.v2.PokemonApi
|
||||
import com.owenlejeune.mydex.preferences.AppPreferences
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
@@ -15,6 +16,8 @@ class PokeApiClient: KoinComponent {
|
||||
private val client: Client by inject { parametersOf(BASE_URL) }
|
||||
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
|
||||
|
||||
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 org.koin.core.component.KoinComponent
|
||||
@@ -10,6 +11,8 @@ class PokemonService: KoinComponent {
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_LIMIT = 20
|
||||
|
||||
val TAG = PokemonService::class.java.simpleName
|
||||
}
|
||||
|
||||
private val service: PokemonApi by inject()
|
||||
@@ -22,7 +25,11 @@ class PokemonService: KoinComponent {
|
||||
val limit = DEFAULT_LIMIT
|
||||
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.api.*
|
||||
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.utils.ResourceUtils
|
||||
import org.koin.dsl.module
|
||||
|
||||
val networkModule = module {
|
||||
@@ -20,14 +23,20 @@ val networkModule = module {
|
||||
single {
|
||||
GsonBuilder().apply {
|
||||
get<List<TypeAdapterFactory>>().forEach { taf -> registerTypeAdapterFactory(taf) }
|
||||
}
|
||||
}.create()
|
||||
}
|
||||
|
||||
single { PokeApiClient() }
|
||||
single { get<PokeApiClient>().createPokemonService() }
|
||||
single { PokemonService() }
|
||||
}
|
||||
|
||||
val preferencesModule = module {
|
||||
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
|
||||
|
||||
val Purple200 = Color(0xFFBB86FC)
|
||||
val Purple500 = Color(0xFF6200EE)
|
||||
val Purple700 = Color(0xFF3700B3)
|
||||
val Teal200 = Color(0xFF03DAC5)
|
||||
val PokeBlack = Color(0xFF303943)
|
||||
val PokeBlue = Color(0xFF429BED)
|
||||
val PokeBrown = Color(0xFFB1736C)
|
||||
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>
|
||||
<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>
|
||||
@@ -2,5 +2,8 @@
|
||||
<resources>
|
||||
|
||||
<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>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user