Merge branch 'simple_sub_system' into 'master'
Simple sub system See merge request ti/2023-2024/s4/mobile-security/students/joren-schipman/pokesearch!3
This commit is contained in:
		@@ -1,11 +1,11 @@
 | 
				
			|||||||
package com.ti.mobpo
 | 
					package com.ti.mobpo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import androidx.lifecycle.ViewModelProvider
 | 
					import androidx.lifecycle.ViewModelProvider
 | 
				
			||||||
import androidx.lifecycle.createSavedStateHandle
 | 
					 | 
				
			||||||
import androidx.lifecycle.viewmodel.CreationExtras
 | 
					import androidx.lifecycle.viewmodel.CreationExtras
 | 
				
			||||||
import androidx.lifecycle.viewmodel.initializer
 | 
					import androidx.lifecycle.viewmodel.initializer
 | 
				
			||||||
import androidx.lifecycle.viewmodel.viewModelFactory
 | 
					import androidx.lifecycle.viewmodel.viewModelFactory
 | 
				
			||||||
import com.ti.mobpo.ui.pokesearch.PokeSearchViewModel
 | 
					import com.ti.mobpo.ui.viewmodels.FavouritesViewModel
 | 
				
			||||||
 | 
					import com.ti.mobpo.ui.viewmodels.PokeSearchViewModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object AppViewModelProvider {
 | 
					object AppViewModelProvider {
 | 
				
			||||||
    val Factory = viewModelFactory {
 | 
					    val Factory = viewModelFactory {
 | 
				
			||||||
@@ -13,9 +13,17 @@ object AppViewModelProvider {
 | 
				
			|||||||
            PokeSearchViewModel(
 | 
					            PokeSearchViewModel(
 | 
				
			||||||
                pokesearchApplication().appContainer.favouritesRepository
 | 
					                pokesearchApplication().appContainer.favouritesRepository
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        initializer {
 | 
				
			||||||
 | 
					            FavouritesViewModel(
 | 
				
			||||||
 | 
					                pokesearchApplication().appContainer.favouritesRepository, pokesearchApplication().featureManager
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun CreationExtras.pokesearchApplication(): PokeSearch =
 | 
					fun CreationExtras.pokesearchApplication(): PokeSearch =
 | 
				
			||||||
    (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as PokeSearch)
 | 
					    (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as PokeSearch)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,17 +3,26 @@ package com.ti.mobpo
 | 
				
			|||||||
import android.app.Application
 | 
					import android.app.Application
 | 
				
			||||||
import com.ti.mobpo.data.AppContainer
 | 
					import com.ti.mobpo.data.AppContainer
 | 
				
			||||||
import com.ti.mobpo.data.AppDataContainer
 | 
					import com.ti.mobpo.data.AppDataContainer
 | 
				
			||||||
 | 
					import com.ti.mobpo.ui.util.FeatureManager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PokeSearch : Application() {
 | 
					class PokeSearch : Application() {
 | 
				
			||||||
    lateinit var appContainer: AppContainer
 | 
					    lateinit var appContainer: AppContainer
 | 
				
			||||||
        private set
 | 
					        private set
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    lateinit var featureManager: FeatureManager
 | 
				
			||||||
 | 
					        private set
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun onCreate() {
 | 
					    override fun onCreate() {
 | 
				
			||||||
        super.onCreate()
 | 
					        super.onCreate()
 | 
				
			||||||
        appContainer = createAppContainer()
 | 
					        appContainer = createAppContainer()
 | 
				
			||||||
 | 
					        featureManager = createFeatrureManager()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun createAppContainer(): AppContainer {
 | 
					    private fun createAppContainer(): AppContainer {
 | 
				
			||||||
        return AppDataContainer(this)
 | 
					        return AppDataContainer(this)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun createFeatrureManager(): FeatureManager {
 | 
				
			||||||
 | 
					        return FeatureManager(this)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,8 +2,6 @@ package com.ti.mobpo.data
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import androidx.room.Entity
 | 
					import androidx.room.Entity
 | 
				
			||||||
import androidx.room.PrimaryKey
 | 
					import androidx.room.PrimaryKey
 | 
				
			||||||
import com.ti.mobpo.model.Sprites
 | 
					 | 
				
			||||||
import com.ti.mobpo.model.Type
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Entity(tableName = "favourites")
 | 
					@Entity(tableName = "favourites")
 | 
				
			||||||
data class Favourite(
 | 
					data class Favourite(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,9 +14,8 @@ interface FavouriteDao {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @Delete
 | 
					    @Delete
 | 
				
			||||||
    suspend fun delete(favourite: Favourite)
 | 
					    suspend fun delete(favourite: Favourite)
 | 
				
			||||||
 | 
					    @Query("SELECT * from favourites ORDER BY id ASC")
 | 
				
			||||||
    @Query("SELECT id FROM favourites")
 | 
					    fun getAllItems(): Flow<List<Favourite>>
 | 
				
			||||||
    fun getAllFavoriteIds(): Flow<List<Int>>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Query("SELECT EXISTS(SELECT 1 FROM favourites WHERE id = :id LIMIT 1)")
 | 
					    @Query("SELECT EXISTS(SELECT 1 FROM favourites WHERE id = :id LIMIT 1)")
 | 
				
			||||||
    suspend fun isFavourite(id: Int): Boolean
 | 
					    suspend fun isFavourite(id: Int): Boolean
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@ interface FavouritesRepository {
 | 
				
			|||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Retrieve all the items from the the given data source.
 | 
					     * Retrieve all the items from the the given data source.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    fun getAllFavoriteIds(): Flow<List<Int>>
 | 
					    fun getAllItems(): Flow<List<Favourite>>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Insert item in the data source
 | 
					     * Insert item in the data source
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,8 +3,8 @@ package com.ti.mobpo.data
 | 
				
			|||||||
import kotlinx.coroutines.flow.Flow
 | 
					import kotlinx.coroutines.flow.Flow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class OfflineFavouritesRepository(private val favouriteDao: FavouriteDao) : FavouritesRepository {
 | 
					class OfflineFavouritesRepository(private val favouriteDao: FavouriteDao) : FavouritesRepository {
 | 
				
			||||||
    override fun getAllFavoriteIds(): Flow<List<Int>> {
 | 
					    override fun getAllItems(): Flow<List<Favourite>> {
 | 
				
			||||||
        return favouriteDao.getAllFavoriteIds()
 | 
					        return favouriteDao.getAllItems()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    override suspend fun insertItem(item: Favourite) {
 | 
					    override suspend fun insertItem(item: Favourite) {
 | 
				
			||||||
        favouriteDao.insert(item)
 | 
					        favouriteDao.insert(item)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								app/src/main/java/com/ti/mobpo/model/FavouriteUiState.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/java/com/ti/mobpo/model/FavouriteUiState.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					package com.ti.mobpo.model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.ti.mobpo.data.Favourite
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					data class FavouriteUiState(val favourites: List<Favourite> = listOf())
 | 
				
			||||||
@@ -20,7 +20,6 @@ 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
 | 
				
			||||||
@@ -31,8 +30,11 @@ import androidx.navigation.compose.NavHost
 | 
				
			|||||||
import androidx.navigation.compose.composable
 | 
					import androidx.navigation.compose.composable
 | 
				
			||||||
import androidx.navigation.compose.currentBackStackEntryAsState
 | 
					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.AppViewModelProvider
 | 
				
			||||||
 | 
					import com.ti.mobpo.ui.screens.FavoritesScreen
 | 
				
			||||||
import com.ti.mobpo.ui.screens.PokeSearchScreen
 | 
					import com.ti.mobpo.ui.screens.PokeSearchScreen
 | 
				
			||||||
 | 
					import com.ti.mobpo.ui.viewmodels.FavouritesViewModel
 | 
				
			||||||
 | 
					import com.ti.mobpo.ui.viewmodels.PokeSearchViewModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Composable
 | 
					@Composable
 | 
				
			||||||
fun Navigation() {
 | 
					fun Navigation() {
 | 
				
			||||||
@@ -61,6 +63,9 @@ 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 pokeSearchVM = viewModel<PokeSearchViewModel>(factory = AppViewModelProvider.Factory)
 | 
				
			||||||
 | 
					    val favoritesVM = viewModel<FavouritesViewModel>(factory = AppViewModelProvider.Factory)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Scaffold (
 | 
					    Scaffold (
 | 
				
			||||||
        bottomBar = {
 | 
					        bottomBar = {
 | 
				
			||||||
            NavigationBar {
 | 
					            NavigationBar {
 | 
				
			||||||
@@ -104,11 +109,11 @@ fun Navigation() {
 | 
				
			|||||||
                            ExitTransition.None
 | 
					                            ExitTransition.None
 | 
				
			||||||
                        }) {
 | 
					                        }) {
 | 
				
			||||||
                        composable(route = Screen.PokeSearch.route) {
 | 
					                        composable(route = Screen.PokeSearch.route) {
 | 
				
			||||||
                            PokeSearchScreen()
 | 
					                            PokeSearchScreen(pokeSearchVM)
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        composable(
 | 
					                        composable(
 | 
				
			||||||
                            route = Screen.Favourites.route) {
 | 
					                            route = Screen.Favourites.route) {
 | 
				
			||||||
                            Favourites()
 | 
					                            FavoritesScreen(favoritesVM)
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@@ -122,6 +127,6 @@ fun Navigation() {
 | 
				
			|||||||
@Composable
 | 
					@Composable
 | 
				
			||||||
fun NavigationPreview() {
 | 
					fun NavigationPreview() {
 | 
				
			||||||
    Surface {
 | 
					    Surface {
 | 
				
			||||||
        PokeSearchScreen()
 | 
					        PokeSearchScreen(viewModel(factory = AppViewModelProvider.Factory))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,3 +0,0 @@
 | 
				
			|||||||
package com.ti.mobpo.ui.pokesearch
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
data class PokeSearchUiState(val searchQuery: String = "")
 | 
					 | 
				
			||||||
@@ -1,10 +1,89 @@
 | 
				
			|||||||
package com.ti.mobpo.ui.screens
 | 
					package com.ti.mobpo.ui.screens
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import androidx.compose.foundation.layout.Arrangement
 | 
				
			||||||
 | 
					import androidx.compose.foundation.layout.Column
 | 
				
			||||||
 | 
					import androidx.compose.foundation.layout.Spacer
 | 
				
			||||||
 | 
					import androidx.compose.foundation.layout.fillMaxSize
 | 
				
			||||||
 | 
					import androidx.compose.foundation.lazy.grid.GridCells
 | 
				
			||||||
 | 
					import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
 | 
				
			||||||
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.getValue
 | 
				
			||||||
 | 
					import androidx.compose.ui.Alignment
 | 
				
			||||||
 | 
					import androidx.compose.ui.unit.dp
 | 
				
			||||||
 | 
					import com.ti.mobpo.ui.viewmodels.FavouritesViewModel
 | 
				
			||||||
 | 
					import androidx.compose.foundation.layout.height
 | 
				
			||||||
 | 
					import androidx.compose.foundation.layout.padding
 | 
				
			||||||
 | 
					import androidx.compose.foundation.lazy.grid.items
 | 
				
			||||||
 | 
					import androidx.compose.material3.AlertDialog
 | 
				
			||||||
 | 
					import androidx.compose.material3.Button
 | 
				
			||||||
 | 
					import androidx.compose.runtime.LaunchedEffect
 | 
				
			||||||
 | 
					import androidx.compose.ui.Modifier
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Composable
 | 
					@Composable
 | 
				
			||||||
fun Favourites() {
 | 
					fun FavoritesScreen(viewModel: FavouritesViewModel) {
 | 
				
			||||||
    println("Favourites")
 | 
					    Favorites(viewModel)
 | 
				
			||||||
    Text("Favourites Page")
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Composable
 | 
				
			||||||
 | 
					fun Favorites(favoritesViewModel: FavouritesViewModel) {
 | 
				
			||||||
 | 
					    LaunchedEffect(Unit) {
 | 
				
			||||||
 | 
					        favoritesViewModel.loadFavourites()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val favorites by favoritesViewModel.pokemonDetails.collectAsState()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Column(
 | 
				
			||||||
 | 
					        verticalArrangement = Arrangement.Top,
 | 
				
			||||||
 | 
					        horizontalAlignment = Alignment.CenterHorizontally,
 | 
				
			||||||
 | 
					        modifier = Modifier
 | 
				
			||||||
 | 
					            .fillMaxSize()
 | 
				
			||||||
 | 
					            .padding(20.dp)
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        Spacer(modifier = Modifier.height(16.dp))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Show AlertDialog if access check failed
 | 
				
			||||||
 | 
					        if (favoritesViewModel.accessCheckFailed.value) {
 | 
				
			||||||
 | 
					            AlertDialog(
 | 
				
			||||||
 | 
					                onDismissRequest = {
 | 
				
			||||||
 | 
					                    // Dismiss the dialog
 | 
				
			||||||
 | 
					                    favoritesViewModel.accessCheckFailed.value = false
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                title = {
 | 
				
			||||||
 | 
					                    Text("Access Denied")
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                text = {
 | 
				
			||||||
 | 
					                    Text("You do not have access to this feature.")
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                confirmButton = {
 | 
				
			||||||
 | 
					                    Button(
 | 
				
			||||||
 | 
					                        onClick = {
 | 
				
			||||||
 | 
					                            favoritesViewModel.accessCheckFailed.value = false
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    ) {
 | 
				
			||||||
 | 
					                        Text("OK")
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        favorites?.let { favoritesList ->
 | 
				
			||||||
 | 
					            if (favoritesList.isNotEmpty()) {
 | 
				
			||||||
 | 
					                LazyVerticalGrid(
 | 
				
			||||||
 | 
					                    columns = GridCells.Adaptive(150.dp),
 | 
				
			||||||
 | 
					                    verticalArrangement = Arrangement.spacedBy(8.dp),
 | 
				
			||||||
 | 
					                    horizontalArrangement = Arrangement.spacedBy(8.dp)
 | 
				
			||||||
 | 
					                ) {
 | 
				
			||||||
 | 
					                    items(items = favoritesList, key = { pokemon -> pokemon.id }) { pokemon ->
 | 
				
			||||||
 | 
					                        PokemonCard(pokemon, toggleFavorite = { pokemonId ->
 | 
				
			||||||
 | 
					                            favoritesViewModel.toggleFavorite(pokemonId)
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Text("No favorites found")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
package com.ti.mobpo.ui.screens
 | 
					package com.ti.mobpo.ui.screens
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import com.ti.mobpo.ui.pokesearch.PokeSearchViewModel
 | 
					import com.ti.mobpo.ui.viewmodels.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.Row
 | 
				
			||||||
@@ -19,6 +19,7 @@ import androidx.compose.material.icons.filled.Favorite
 | 
				
			|||||||
import androidx.compose.material.icons.outlined.FavoriteBorder
 | 
					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.CircularProgressIndicator
 | 
				
			||||||
import androidx.compose.material3.Icon
 | 
					import androidx.compose.material3.Icon
 | 
				
			||||||
import androidx.compose.material3.IconButton
 | 
					import androidx.compose.material3.IconButton
 | 
				
			||||||
import androidx.compose.material3.MaterialTheme
 | 
					import androidx.compose.material3.MaterialTheme
 | 
				
			||||||
@@ -26,9 +27,6 @@ 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
 | 
				
			||||||
@@ -45,18 +43,18 @@ 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Composable
 | 
					@Composable
 | 
				
			||||||
fun PokeSearchScreen(viewModel: PokeSearchViewModel = viewModel(factory = AppViewModelProvider.Factory)) {
 | 
					fun PokeSearchScreen(viewModel: PokeSearchViewModel) {
 | 
				
			||||||
    PokeSearch(viewModel)
 | 
					    PokeSearch(viewModel)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Composable
 | 
					@Composable
 | 
				
			||||||
fun PokeSearch(pokeSearchViewModel: PokeSearchViewModel) {
 | 
					fun PokeSearch(pokeSearchViewModel: PokeSearchViewModel) {
 | 
				
			||||||
    val searchResults by pokeSearchViewModel.pokemonDetails.collectAsState()
 | 
					    val searchResults by pokeSearchViewModel.pokemonDetails.collectAsState()
 | 
				
			||||||
 | 
					    val isLoading by pokeSearchViewModel.isLoading.collectAsState()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Column(
 | 
					    Column(
 | 
				
			||||||
        verticalArrangement = Arrangement.Top,
 | 
					        verticalArrangement = Arrangement.Top,
 | 
				
			||||||
@@ -71,21 +69,25 @@ fun PokeSearch(pokeSearchViewModel: PokeSearchViewModel) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        Spacer(modifier = Modifier.height(16.dp))
 | 
					        Spacer(modifier = Modifier.height(16.dp))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        searchResults?.let { results ->
 | 
					        if(isLoading) {
 | 
				
			||||||
            if (results.isNotEmpty()) {
 | 
					            CircularProgressIndicator()
 | 
				
			||||||
                LazyVerticalGrid(
 | 
					        } else {
 | 
				
			||||||
                    columns = GridCells.Adaptive(150.dp),
 | 
					            searchResults?.let { results ->
 | 
				
			||||||
                    verticalArrangement = Arrangement.spacedBy(8.dp),
 | 
					                if (results.isNotEmpty()) {
 | 
				
			||||||
                    horizontalArrangement = Arrangement.spacedBy(8.dp)
 | 
					                    LazyVerticalGrid(
 | 
				
			||||||
                ) {
 | 
					                        columns = GridCells.Adaptive(150.dp),
 | 
				
			||||||
                    items(items = results, key = { pokemon -> pokemon.id }) { pokemon ->
 | 
					                        verticalArrangement = Arrangement.spacedBy(8.dp),
 | 
				
			||||||
                        PokemonCard(pokemon, toggleFavorite = { pokemonId ->
 | 
					                        horizontalArrangement = Arrangement.spacedBy(8.dp)
 | 
				
			||||||
                            pokeSearchViewModel.toggleFavorite(pokemonId)
 | 
					                    ) {
 | 
				
			||||||
                        })
 | 
					                        items(items = results, key = { pokemon -> pokemon.id }) { pokemon ->
 | 
				
			||||||
 | 
					                            PokemonCard(pokemon, toggleFavorite = { pokemonId ->
 | 
				
			||||||
 | 
					                                pokeSearchViewModel.toggleFavorite(pokemonId)
 | 
				
			||||||
 | 
					                            })
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    Text("No results found")
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                Text("No results found")
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -94,7 +96,6 @@ fun PokeSearch(pokeSearchViewModel: PokeSearchViewModel) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@Composable
 | 
					@Composable
 | 
				
			||||||
fun PokemonCard(pokemon: PokemonDetails, toggleFavorite: (Int) -> Unit) {
 | 
					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),
 | 
				
			||||||
@@ -130,7 +131,8 @@ fun PokemonCard(pokemon: PokemonDetails, toggleFavorite: (Int) -> Unit) {
 | 
				
			|||||||
                )
 | 
					                )
 | 
				
			||||||
                IconButton(
 | 
					                IconButton(
 | 
				
			||||||
                    onClick = { toggleFavorite(pokemon.id) },
 | 
					                    onClick = { toggleFavorite(pokemon.id) },
 | 
				
			||||||
                    modifier = Modifier.wrapContentSize()
 | 
					                    modifier = Modifier
 | 
				
			||||||
 | 
					                        .wrapContentSize()
 | 
				
			||||||
                        .layout { measurable, constraints ->
 | 
					                        .layout { measurable, constraints ->
 | 
				
			||||||
                            if (constraints.maxHeight == Constraints.Infinity) {
 | 
					                            if (constraints.maxHeight == Constraints.Infinity) {
 | 
				
			||||||
                                layout(0, 0) {}
 | 
					                                layout(0, 0) {}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										18
									
								
								app/src/main/java/com/ti/mobpo/ui/util/FeatureManager.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								app/src/main/java/com/ti/mobpo/ui/util/FeatureManager.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					package com.ti.mobpo.ui.util
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.content.Context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FeatureManager(private val context: Context) {
 | 
				
			||||||
 | 
					    private val PREFS_NAME = "FeaturePrefs"
 | 
				
			||||||
 | 
					    private val KEY_PAID_FEATURE_ENABLED = "paid_feature_enabled"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun hasAccessToPaidFeature(): Boolean {
 | 
				
			||||||
 | 
					        val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
 | 
				
			||||||
 | 
					        return prefs.getBoolean(KEY_PAID_FEATURE_ENABLED, false)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun setPaidFeatureEnabled(enabled: Boolean) {
 | 
				
			||||||
 | 
					        val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
 | 
				
			||||||
 | 
					        prefs.edit().putBoolean(KEY_PAID_FEATURE_ENABLED, enabled).apply()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					package com.ti.mobpo.ui.viewmodels
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import androidx.compose.runtime.MutableState
 | 
				
			||||||
 | 
					import androidx.compose.runtime.mutableStateOf
 | 
				
			||||||
 | 
					import androidx.lifecycle.ViewModel
 | 
				
			||||||
 | 
					import androidx.lifecycle.viewModelScope
 | 
				
			||||||
 | 
					import com.ti.mobpo.data.Favourite
 | 
				
			||||||
 | 
					import com.ti.mobpo.data.FavouritesRepository
 | 
				
			||||||
 | 
					import com.ti.mobpo.model.FavouriteUiState
 | 
				
			||||||
 | 
					import com.ti.mobpo.model.PokemonDetails
 | 
				
			||||||
 | 
					import com.ti.mobpo.network.PokeApi
 | 
				
			||||||
 | 
					import com.ti.mobpo.ui.util.FeatureManager
 | 
				
			||||||
 | 
					import kotlinx.coroutines.flow.MutableStateFlow
 | 
				
			||||||
 | 
					import kotlinx.coroutines.flow.SharingStarted
 | 
				
			||||||
 | 
					import kotlinx.coroutines.flow.StateFlow
 | 
				
			||||||
 | 
					import kotlinx.coroutines.flow.asStateFlow
 | 
				
			||||||
 | 
					import kotlinx.coroutines.flow.map
 | 
				
			||||||
 | 
					import kotlinx.coroutines.flow.stateIn
 | 
				
			||||||
 | 
					import kotlinx.coroutines.launch
 | 
				
			||||||
 | 
					import java.io.IOException
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FavouritesViewModel(private val favouritesRepository: FavouritesRepository,
 | 
				
			||||||
 | 
					                          private val featureManager: FeatureManager
 | 
				
			||||||
 | 
					) : ViewModel() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val service = PokeApi.retrofitService
 | 
				
			||||||
 | 
					    private val _pokemonDetails = MutableStateFlow<List<PokemonDetails>?>(null)
 | 
				
			||||||
 | 
					    val pokemonDetails: StateFlow<List<PokemonDetails>?> = _pokemonDetails.asStateFlow()
 | 
				
			||||||
 | 
					    val accessCheckFailed: MutableState<Boolean> = mutableStateOf(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun loadFavourites() {
 | 
				
			||||||
 | 
					        // featureManager.setPaidFeatureEnabled(false) enable and disable acccess
 | 
				
			||||||
 | 
					        // Ugly workaround to make sure all the favourites are loaded before displaying them
 | 
				
			||||||
 | 
					        viewModelScope.launch {
 | 
				
			||||||
 | 
					            if(!featureManager.hasAccessToPaidFeature()) {
 | 
				
			||||||
 | 
					                accessCheckFailed.value = true
 | 
				
			||||||
 | 
					                return@launch
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                val favouritesList: StateFlow<FavouriteUiState> =
 | 
				
			||||||
 | 
					                    favouritesRepository.getAllItems().map { FavouriteUiState(it) }
 | 
				
			||||||
 | 
					                        .stateIn(
 | 
				
			||||||
 | 
					                            scope = viewModelScope,
 | 
				
			||||||
 | 
					                            started = SharingStarted.WhileSubscribed(5_000L),
 | 
				
			||||||
 | 
					                            initialValue = FavouriteUiState()
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                favouritesList.collect { state ->
 | 
				
			||||||
 | 
					                    val detailsList = mutableListOf<PokemonDetails>()
 | 
				
			||||||
 | 
					                    for (favourite in state.favourites) {
 | 
				
			||||||
 | 
					                        val details = service.getPokemonDetails(favourite.id)
 | 
				
			||||||
 | 
					                        detailsList.add(details.copy(isFavorite = true))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    _pokemonDetails.value = detailsList
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (e: IOException) {
 | 
				
			||||||
 | 
					                /* Handle error */
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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,10 +1,11 @@
 | 
				
			|||||||
package com.ti.mobpo.ui.pokesearch
 | 
					package com.ti.mobpo.ui.viewmodels
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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.Favourite
 | 
				
			||||||
import com.ti.mobpo.data.FavouritesRepository
 | 
					import com.ti.mobpo.data.FavouritesRepository
 | 
				
			||||||
import com.ti.mobpo.model.PokemonDetails
 | 
					import com.ti.mobpo.model.PokemonDetails
 | 
				
			||||||
 | 
					import com.ti.mobpo.model.PokemonSpecies
 | 
				
			||||||
import com.ti.mobpo.network.PokeApi
 | 
					import com.ti.mobpo.network.PokeApi
 | 
				
			||||||
import kotlinx.coroutines.flow.MutableStateFlow
 | 
					import kotlinx.coroutines.flow.MutableStateFlow
 | 
				
			||||||
import kotlinx.coroutines.flow.StateFlow
 | 
					import kotlinx.coroutines.flow.StateFlow
 | 
				
			||||||
@@ -13,27 +14,56 @@ import kotlinx.coroutines.launch
 | 
				
			|||||||
import java.io.IOException
 | 
					import java.io.IOException
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private const val SHOW_LIMIT = 20
 | 
				
			||||||
class PokeSearchViewModel(private val favouritesRepository: FavouritesRepository) : 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)
 | 
				
			||||||
    val pokemonDetails: StateFlow<List<PokemonDetails>?> = _pokemonDetails.asStateFlow()
 | 
					    val pokemonDetails: StateFlow<List<PokemonDetails>?> = _pokemonDetails.asStateFlow()
 | 
				
			||||||
 | 
					    private val _initialPokemonList = MutableStateFlow<List<PokemonSpecies>?>(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fun search(query: String) {
 | 
					    private val _isLoading = MutableStateFlow(false)
 | 
				
			||||||
 | 
					    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
 | 
				
			||||||
 | 
					    init {
 | 
				
			||||||
 | 
					        fetchPokemonSpecies()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun fetchPokemonSpecies() {
 | 
				
			||||||
        viewModelScope.launch {
 | 
					        viewModelScope.launch {
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                val response = service.getPokemon()
 | 
					                val response = service.getPokemon()
 | 
				
			||||||
                val filteredList = response.results.filter { it.name.contains(query, ignoreCase = true) }
 | 
					                _initialPokemonList.value = response.results.sortedBy { it.name.lowercase() }
 | 
				
			||||||
 | 
					            } catch (e: IOException) {
 | 
				
			||||||
 | 
					                /*TODO*/
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun search(query: String) {
 | 
				
			||||||
 | 
					        _isLoading.value = true
 | 
				
			||||||
 | 
					        viewModelScope.launch {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                val firstIndex = _initialPokemonList.value?.indexOfFirst { it.name.startsWith(query, ignoreCase = true) }
 | 
				
			||||||
 | 
					                val lastIndex = _initialPokemonList.value?.indexOfLast { it.name.startsWith(query, ignoreCase = true) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                val detailsList = mutableListOf<PokemonDetails>()
 | 
					                val detailsList = mutableListOf<PokemonDetails>()
 | 
				
			||||||
                for (pokemonSpecies in filteredList) {
 | 
					
 | 
				
			||||||
                    val details = service.getPokemonDetails(extractPokemonId(pokemonSpecies.url))
 | 
					                if (firstIndex != null && lastIndex != null) {
 | 
				
			||||||
                    val isFavorite = favouritesRepository.isFavourite(details.id)
 | 
					                    val endIndex = minOf(firstIndex + SHOW_LIMIT, lastIndex + 1)
 | 
				
			||||||
                    detailsList.add(details.copy(isFavorite = isFavorite))
 | 
					                    val startIndex = maxOf(firstIndex, endIndex - SHOW_LIMIT)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    for (index in startIndex until endIndex) {
 | 
				
			||||||
 | 
					                        val pokemonSpecies = _initialPokemonList.value!![index]
 | 
				
			||||||
 | 
					                        val details = service.getPokemonDetails(extractPokemonId(pokemonSpecies.url))
 | 
				
			||||||
 | 
					                        val isFavorite = favouritesRepository.isFavourite(details.id)
 | 
				
			||||||
 | 
					                        detailsList.add(details.copy(isFavorite = isFavorite))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                _pokemonDetails.value = detailsList
 | 
					                _pokemonDetails.value = detailsList
 | 
				
			||||||
            } catch (e: IOException) {
 | 
					            } catch (e: IOException) {
 | 
				
			||||||
                /* Handle error */
 | 
					                /* Handle error */
 | 
				
			||||||
 | 
					            } finally {
 | 
				
			||||||
 | 
					                _isLoading.value = false
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
		Reference in New Issue
	
	Block a user