Merge branch 'addFavourites' into 'master'
Add favourites See merge request ti/2023-2024/s4/mobile-security/students/joren-schipman/pokesearch!2
This commit is contained in:
commit
3af9a606ae
@ -1,6 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.androidApplication)
|
alias(libs.plugins.androidApplication)
|
||||||
alias(libs.plugins.jetbrainsKotlinAndroid)
|
alias(libs.plugins.jetbrainsKotlinAndroid)
|
||||||
|
id("com.google.devtools.ksp") version "1.9.20-1.0.14"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -69,4 +70,9 @@ dependencies {
|
|||||||
implementation("com.squareup.retrofit2:retrofit:2.11.0")
|
implementation("com.squareup.retrofit2:retrofit:2.11.0")
|
||||||
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
|
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
|
||||||
implementation("io.coil-kt:coil-compose:2.6.0")
|
implementation("io.coil-kt:coil-compose:2.6.0")
|
||||||
|
|
||||||
|
implementation("androidx.room:room-runtime:${rootProject.extra["room_version"]}")
|
||||||
|
implementation("androidx.core:core-ktx:1.12.0")
|
||||||
|
ksp("androidx.room:room-compiler:${rootProject.extra["room_version"]}")
|
||||||
|
implementation("androidx.room:room-ktx:${rootProject.extra["room_version"]}")
|
||||||
}
|
}
|
@ -4,6 +4,7 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".PokeSearch"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
21
app/src/main/java/com/ti/mobpo/AppViewModelProvider.kt
Normal file
21
app/src/main/java/com/ti/mobpo/AppViewModelProvider.kt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package com.ti.mobpo
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.createSavedStateHandle
|
||||||
|
import androidx.lifecycle.viewmodel.CreationExtras
|
||||||
|
import androidx.lifecycle.viewmodel.initializer
|
||||||
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
|
import com.ti.mobpo.ui.pokesearch.PokeSearchViewModel
|
||||||
|
|
||||||
|
object AppViewModelProvider {
|
||||||
|
val Factory = viewModelFactory {
|
||||||
|
initializer {
|
||||||
|
PokeSearchViewModel(
|
||||||
|
pokesearchApplication().appContainer.favouritesRepository
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CreationExtras.pokesearchApplication(): PokeSearch =
|
||||||
|
(this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as PokeSearch)
|
19
app/src/main/java/com/ti/mobpo/PokeSearch.kt
Normal file
19
app/src/main/java/com/ti/mobpo/PokeSearch.kt
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package com.ti.mobpo
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.ti.mobpo.data.AppContainer
|
||||||
|
import com.ti.mobpo.data.AppDataContainer
|
||||||
|
|
||||||
|
class PokeSearch : Application() {
|
||||||
|
lateinit var appContainer: AppContainer
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
appContainer = createAppContainer()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAppContainer(): AppContainer {
|
||||||
|
return AppDataContainer(this)
|
||||||
|
}
|
||||||
|
}
|
16
app/src/main/java/com/ti/mobpo/data/AppContainer.kt
Normal file
16
app/src/main/java/com/ti/mobpo/data/AppContainer.kt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package com.ti.mobpo.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
|
||||||
|
interface AppContainer {
|
||||||
|
val favouritesRepository: FavouritesRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AppDataContainer(private val context: Context) : AppContainer {
|
||||||
|
|
||||||
|
override val favouritesRepository: FavouritesRepository by lazy {
|
||||||
|
OfflineFavouritesRepository(PokemonDatabase.getDatabase(context).FavouriteDao())
|
||||||
|
}
|
||||||
|
}
|
12
app/src/main/java/com/ti/mobpo/data/Favourite.kt
Normal file
12
app/src/main/java/com/ti/mobpo/data/Favourite.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package com.ti.mobpo.data
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.ti.mobpo.model.Sprites
|
||||||
|
import com.ti.mobpo.model.Type
|
||||||
|
|
||||||
|
@Entity(tableName = "favourites")
|
||||||
|
data class Favourite(
|
||||||
|
@PrimaryKey val id: Int,
|
||||||
|
val name: String
|
||||||
|
)
|
23
app/src/main/java/com/ti/mobpo/data/FavouriteDao.kt
Normal file
23
app/src/main/java/com/ti/mobpo/data/FavouriteDao.kt
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package com.ti.mobpo.data
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface FavouriteDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
suspend fun insert(favourite: Favourite)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(favourite: Favourite)
|
||||||
|
|
||||||
|
@Query("SELECT id FROM favourites")
|
||||||
|
fun getAllFavoriteIds(): Flow<List<Int>>
|
||||||
|
|
||||||
|
@Query("SELECT EXISTS(SELECT 1 FROM favourites WHERE id = :id LIMIT 1)")
|
||||||
|
suspend fun isFavourite(id: Int): Boolean
|
||||||
|
}
|
29
app/src/main/java/com/ti/mobpo/data/FavouritesRepository.kt
Normal file
29
app/src/main/java/com/ti/mobpo/data/FavouritesRepository.kt
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package com.ti.mobpo.data
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository that provides insert, update, delete, and retrieve of [Item] from a given data source.
|
||||||
|
*/
|
||||||
|
interface FavouritesRepository {
|
||||||
|
/**
|
||||||
|
* Retrieve all the items from the the given data source.
|
||||||
|
*/
|
||||||
|
fun getAllFavoriteIds(): Flow<List<Int>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert item in the data source
|
||||||
|
*/
|
||||||
|
suspend fun insertItem(item: Favourite)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete item from the data source
|
||||||
|
*/
|
||||||
|
suspend fun deleteItem(item: Favourite)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the item is a favourite
|
||||||
|
*/
|
||||||
|
|
||||||
|
suspend fun isFavourite(id: Int): Boolean
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package com.ti.mobpo.data
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
class OfflineFavouritesRepository(private val favouriteDao: FavouriteDao) : FavouritesRepository {
|
||||||
|
override fun getAllFavoriteIds(): Flow<List<Int>> {
|
||||||
|
return favouriteDao.getAllFavoriteIds()
|
||||||
|
}
|
||||||
|
override suspend fun insertItem(item: Favourite) {
|
||||||
|
favouriteDao.insert(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteItem(item: Favourite) {
|
||||||
|
favouriteDao.delete(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun isFavourite(id: Int): Boolean {
|
||||||
|
return favouriteDao.isFavourite(id)
|
||||||
|
}
|
||||||
|
}
|
23
app/src/main/java/com/ti/mobpo/data/PokemonDatabase.kt
Normal file
23
app/src/main/java/com/ti/mobpo/data/PokemonDatabase.kt
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package com.ti.mobpo.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
|
||||||
|
@Database(entities = [Favourite::class], version = 1, exportSchema = false)
|
||||||
|
abstract class PokemonDatabase : RoomDatabase() {
|
||||||
|
abstract fun FavouriteDao(): FavouriteDao
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Volatile
|
||||||
|
private var Instance: PokemonDatabase? = null
|
||||||
|
fun getDatabase(context: Context): PokemonDatabase {
|
||||||
|
return Instance ?: synchronized(this) {
|
||||||
|
Room.databaseBuilder(context, PokemonDatabase::class.java, "favorites_database")
|
||||||
|
.build()
|
||||||
|
.also { Instance = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,8 @@ data class PokemonDetails(
|
|||||||
val id: Int,
|
val id: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val types: List<Type>,
|
val types: List<Type>,
|
||||||
@SerializedName("sprites") val sprites: Sprites
|
@SerializedName("sprites") val sprites: Sprites,
|
||||||
|
var isFavorite: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Type(
|
data class Type(
|
||||||
|
@ -8,10 +8,8 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Favorite
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
import androidx.compose.material.icons.filled.Home
|
import androidx.compose.material.icons.filled.Home
|
||||||
import androidx.compose.material.icons.filled.Person
|
|
||||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||||
import androidx.compose.material.icons.outlined.Home
|
import androidx.compose.material.icons.outlined.Home
|
||||||
import androidx.compose.material.icons.outlined.Person
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
@ -22,6 +20,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@ -34,8 +33,6 @@ import androidx.navigation.compose.currentBackStackEntryAsState
|
|||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.ti.mobpo.ui.screens.Favourites
|
import com.ti.mobpo.ui.screens.Favourites
|
||||||
import com.ti.mobpo.ui.screens.PokeSearchScreen
|
import com.ti.mobpo.ui.screens.PokeSearchScreen
|
||||||
import com.ti.mobpo.ui.screens.Profile
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Navigation() {
|
fun Navigation() {
|
||||||
@ -51,13 +48,7 @@ fun Navigation() {
|
|||||||
selectedIcon = Icons.Filled.Favorite,
|
selectedIcon = Icons.Filled.Favorite,
|
||||||
unselectedItem = Icons.Outlined.FavoriteBorder,
|
unselectedItem = Icons.Outlined.FavoriteBorder,
|
||||||
route = Screen.Favourites.route
|
route = Screen.Favourites.route
|
||||||
),
|
)
|
||||||
BottomNavigationItem(
|
|
||||||
title = "Profile",
|
|
||||||
selectedIcon = Icons.Filled.Person,
|
|
||||||
unselectedItem = Icons.Outlined.Person,
|
|
||||||
route = Screen.Profile.route
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var selectedItemIndex by rememberSaveable {
|
var selectedItemIndex by rememberSaveable {
|
||||||
@ -70,8 +61,6 @@ fun Navigation() {
|
|||||||
val updatedSelectedIndex = items.indexOfFirst { it.route == currentRoute }
|
val updatedSelectedIndex = items.indexOfFirst { it.route == currentRoute }
|
||||||
selectedItemIndex = rememberUpdatedState(updatedSelectedIndex).value
|
selectedItemIndex = rememberUpdatedState(updatedSelectedIndex).value
|
||||||
|
|
||||||
val pokeSearchViewModel: PokeSearchViewModel = viewModel()
|
|
||||||
|
|
||||||
Scaffold (
|
Scaffold (
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
NavigationBar {
|
NavigationBar {
|
||||||
@ -115,16 +104,12 @@ fun Navigation() {
|
|||||||
ExitTransition.None
|
ExitTransition.None
|
||||||
}) {
|
}) {
|
||||||
composable(route = Screen.PokeSearch.route) {
|
composable(route = Screen.PokeSearch.route) {
|
||||||
PokeSearchScreen(pokeSearchViewModel)
|
PokeSearchScreen()
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
route = Screen.Favourites.route) {
|
route = Screen.Favourites.route) {
|
||||||
Favourites()
|
Favourites()
|
||||||
}
|
}
|
||||||
composable(
|
|
||||||
route = Screen.Profile.route) {
|
|
||||||
Profile()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -137,7 +122,6 @@ fun Navigation() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun NavigationPreview() {
|
fun NavigationPreview() {
|
||||||
Surface {
|
Surface {
|
||||||
val pokeSearchViewModel: PokeSearchViewModel = viewModel()
|
PokeSearchScreen()
|
||||||
PokeSearchScreen(pokeSearchViewModel)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,7 +2,5 @@ package com.ti.mobpo.ui
|
|||||||
|
|
||||||
sealed class Screen (val route: String) {
|
sealed class Screen (val route: String) {
|
||||||
object PokeSearch : Screen("default_screen")
|
object PokeSearch : Screen("default_screen")
|
||||||
object TopNav : Screen("navigation_bar")
|
|
||||||
object Favourites : Screen("favourites_page")
|
object Favourites : Screen("favourites_page")
|
||||||
object Profile : Screen("profile_page")
|
|
||||||
}
|
}
|
@ -1,3 +1,3 @@
|
|||||||
package com.ti.mobpo.ui
|
package com.ti.mobpo.ui.pokesearch
|
||||||
|
|
||||||
data class PokeSearchUiState(val searchQuery: String = "")
|
data class PokeSearchUiState(val searchQuery: String = "")
|
@ -1,7 +1,9 @@
|
|||||||
package com.ti.mobpo.ui
|
package com.ti.mobpo.ui.pokesearch
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.ti.mobpo.data.Favourite
|
||||||
|
import com.ti.mobpo.data.FavouritesRepository
|
||||||
import com.ti.mobpo.model.PokemonDetails
|
import com.ti.mobpo.model.PokemonDetails
|
||||||
import com.ti.mobpo.network.PokeApi
|
import com.ti.mobpo.network.PokeApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@ -12,7 +14,7 @@ import java.io.IOException
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PokeSearchViewModel : ViewModel() {
|
class PokeSearchViewModel(private val favouritesRepository: FavouritesRepository) : ViewModel() {
|
||||||
private val service = PokeApi.retrofitService;
|
private val service = PokeApi.retrofitService;
|
||||||
|
|
||||||
private val _pokemonDetails = MutableStateFlow<List<PokemonDetails>?>(null)
|
private val _pokemonDetails = MutableStateFlow<List<PokemonDetails>?>(null)
|
||||||
@ -26,7 +28,8 @@ class PokeSearchViewModel : ViewModel() {
|
|||||||
val detailsList = mutableListOf<PokemonDetails>()
|
val detailsList = mutableListOf<PokemonDetails>()
|
||||||
for (pokemonSpecies in filteredList) {
|
for (pokemonSpecies in filteredList) {
|
||||||
val details = service.getPokemonDetails(extractPokemonId(pokemonSpecies.url))
|
val details = service.getPokemonDetails(extractPokemonId(pokemonSpecies.url))
|
||||||
detailsList.add(details)
|
val isFavorite = favouritesRepository.isFavourite(details.id)
|
||||||
|
detailsList.add(details.copy(isFavorite = isFavorite))
|
||||||
}
|
}
|
||||||
_pokemonDetails.value = detailsList
|
_pokemonDetails.value = detailsList
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
@ -39,4 +42,26 @@ class PokeSearchViewModel : ViewModel() {
|
|||||||
val parts = url.split("/")
|
val parts = url.split("/")
|
||||||
return parts[parts.size - 2].toInt()
|
return parts[parts.size - 2].toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toggleFavorite(pokemonId: Int) {
|
||||||
|
_pokemonDetails.value = _pokemonDetails.value?.map { pokemon ->
|
||||||
|
if (pokemon.id == pokemonId) {
|
||||||
|
pokemon.copy(isFavorite = !pokemon.isFavorite)
|
||||||
|
} else {
|
||||||
|
pokemon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
if (_pokemonDetails.value != null) {
|
||||||
|
val pokemon = _pokemonDetails.value!!.find { it.id == pokemonId }
|
||||||
|
pokemon?.let {
|
||||||
|
if (it.isFavorite) {
|
||||||
|
favouritesRepository.insertItem(Favourite(it.id, it.name))
|
||||||
|
} else {
|
||||||
|
favouritesRepository.deleteItem(Favourite(it.id, it.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,9 +1,9 @@
|
|||||||
package com.ti.mobpo.ui.screens
|
package com.ti.mobpo.ui.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import com.ti.mobpo.ui.pokesearch.PokeSearchViewModel
|
||||||
import com.ti.mobpo.ui.PokeSearchViewModel
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@ -14,19 +14,30 @@ import androidx.compose.foundation.layout.wrapContentSize
|
|||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
|
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.layout.layout
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.ti.mobpo.R
|
import com.ti.mobpo.R
|
||||||
import com.ti.mobpo.ui.SearchBar
|
import com.ti.mobpo.ui.SearchBar
|
||||||
@ -34,13 +45,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import com.ti.mobpo.model.PokemonDetails
|
import com.ti.mobpo.model.PokemonDetails
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
|
import com.ti.mobpo.AppViewModelProvider
|
||||||
import com.ti.mobpo.capitalizeFirstLetterAfterHyphens
|
import com.ti.mobpo.capitalizeFirstLetterAfterHyphens
|
||||||
import com.ti.mobpo.ui.theme.MobileSecurityTheme
|
import com.ti.mobpo.ui.theme.MobileSecurityTheme
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PokeSearchScreen(pokeSearchViewModel: PokeSearchViewModel) {
|
fun PokeSearchScreen(viewModel: PokeSearchViewModel = viewModel(factory = AppViewModelProvider.Factory)) {
|
||||||
PokeSearch(pokeSearchViewModel)
|
PokeSearch(viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -68,7 +79,9 @@ fun PokeSearch(pokeSearchViewModel: PokeSearchViewModel) {
|
|||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
items(items = results, key = { pokemon -> pokemon.id }) { pokemon ->
|
items(items = results, key = { pokemon -> pokemon.id }) { pokemon ->
|
||||||
PokemonCard(pokemon)
|
PokemonCard(pokemon, toggleFavorite = { pokemonId ->
|
||||||
|
pokeSearchViewModel.toggleFavorite(pokemonId)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -80,14 +93,15 @@ fun PokeSearch(pokeSearchViewModel: PokeSearchViewModel) {
|
|||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PokemonCard(pokemon: PokemonDetails) {
|
fun PokemonCard(pokemon: PokemonDetails, toggleFavorite: (Int) -> Unit) {
|
||||||
|
var isFavourite by remember { mutableStateOf(false) }
|
||||||
Card(
|
Card(
|
||||||
shape = MaterialTheme.shapes.medium,
|
shape = MaterialTheme.shapes.medium,
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
|
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(1f)
|
.aspectRatio(0.75f)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@ -102,14 +116,38 @@ fun PokemonCard(pokemon: PokemonDetails) {
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
)
|
)
|
||||||
Text(
|
Row(
|
||||||
text = capitalizeFirstLetterAfterHyphens(pokemon.name),
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
modifier = Modifier.fillMaxWidth()
|
||||||
modifier = Modifier
|
) {
|
||||||
.fillMaxWidth()
|
Text(
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp) // Adjust padding as needed
|
text = capitalizeFirstLetterAfterHyphens(pokemon.name),
|
||||||
.wrapContentSize(Alignment.Center)
|
modifier = Modifier
|
||||||
)
|
.padding(8.dp)
|
||||||
|
.weight(1f),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
IconButton(
|
||||||
|
onClick = { toggleFavorite(pokemon.id) },
|
||||||
|
modifier = Modifier.wrapContentSize()
|
||||||
|
.layout { measurable, constraints ->
|
||||||
|
if (constraints.maxHeight == Constraints.Infinity) {
|
||||||
|
layout(0, 0) {}
|
||||||
|
} else {
|
||||||
|
val placeable = measurable.measure(constraints)
|
||||||
|
layout(placeable.width, placeable.height) {
|
||||||
|
placeable.place(0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (pokemon.isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
|
||||||
|
contentDescription = "Favourite"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
package com.ti.mobpo.ui.screens
|
|
||||||
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Profile(){
|
|
||||||
Text("Profile Page")
|
|
||||||
}
|
|
@ -3,3 +3,9 @@ plugins {
|
|||||||
alias(libs.plugins.androidApplication) apply false
|
alias(libs.plugins.androidApplication) apply false
|
||||||
alias(libs.plugins.jetbrainsKotlinAndroid) apply false
|
alias(libs.plugins.jetbrainsKotlinAndroid) apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
extra.apply {
|
||||||
|
set("room_version", "2.6.0")
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user