This commit is contained in:
Xaver 2024-12-16 22:36:17 +01:00
parent 133e06db1e
commit 82ecbb2caa
17 changed files with 334 additions and 75 deletions

1
.gitignore vendored
View File

@ -112,3 +112,4 @@ fabric.properties
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
GameLibraryApp.zip

View File

@ -3,7 +3,7 @@ plugins {
alias(libs.plugins.kotlin.android)
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10"
id("com.google.devtools.ksp") version "1.6.21-1.0.6"
id("com.google.devtools.ksp") version "1.9.0-1.0.12"
}
android {

View File

@ -4,5 +4,7 @@ data class Game(
val id: Int,
val name: String,
val details: String,
val note: String
val note: String,
val imageUrl :String,
val inLibrary: Boolean
)

View File

@ -1,43 +1,56 @@
package at.xaxa.gamelibraryapp.data
import android.util.Log
import at.xaxa.gamelibraryapp.data.db.LibraryDao
import at.xaxa.gamelibraryapp.data.db.LibraryEntity
import at.xaxa.gamelibraryapp.data.remote.GameDto
import at.xaxa.gamelibraryapp.data.remote.GameRemoteService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import kotlin.math.log
class GameRepository(private val libraryDao: LibraryDao, private val gameRemoteService: GameRemoteService) {
private val apiKey = "7a2a800a085d41e88621a8a59dc5ea82"
/*suspend fun loadInitialContacts() {
try {
val contactDtoList = gameRemoteService.getAllContacts()
contactDtoList.map {
Game(0, it., "${it.telephoneNumber}", it.age)
}.forEach {
insertContact(it)
suspend fun searchGames(searchText: String): List<GameDto> {
return withContext(Dispatchers.IO) {
try {
print(gameRemoteService.searchGames(apiKey, searchText).toString())
gameRemoteService.searchGames(apiKey, searchText).results
} catch (e: Exception) {
Log.e("GameRepository", "Error searching games from API: ${e.message}", e)
emptyList<GameDto>()
}
} catch (e: Exception) {
Log.e("Repository", "Something went wrong! ${e.message}", e)
}
}*/
}
fun getAllGamesInLibrary(): Flow<List<Game>> {
return libraryDao.getAllEntries().map {
it.map {item -> Game(item._id, item.name, item.details, item.note) }
it.map {item -> Game(item._id, item.name, item.details, item.note, item.imageUrl, item.inLibrary) }
}
}
fun searchGameInDB(searchText: String):Flow<List<Game>>{
return libraryDao.getEntriesWithName(searchText).map {
it.map {item -> Game(item._id, item.name, item.details, item.note, item.imageUrl, item.inLibrary) }
}
}
suspend fun findGameById(id: Int): LibraryEntity {
val item = libraryDao.findEntryById(id)
return LibraryEntity(item._id, item.name, item.details, item.note)
//?: throw GameNotFoundException("Game with id $id not found.")
return item
}
suspend fun addEntry(libraryEntity: LibraryEntity) {
libraryDao.addEntry(LibraryEntity(libraryEntity._id, libraryEntity.name, libraryEntity.details, libraryEntity.note))
libraryDao.addEntry(LibraryEntity(libraryEntity._id, libraryEntity.name, libraryEntity.details, libraryEntity.note, libraryEntity.imageUrl, libraryEntity.inLibrary))
}
suspend fun updateEntry(game: LibraryEntity) {
libraryDao.updateEntry(LibraryEntity(game._id, game.name, game.details, game.note))
libraryDao.updateEntry(LibraryEntity(game._id, game.name, game.details, game.note, game.imageUrl, game.inLibrary))
}
}

View File

@ -22,9 +22,9 @@ interface LibraryDao {
@Query("SELECT * FROM library WHERE _id = :id")
suspend fun findEntryById(id: Int): LibraryEntity
@Query("SELECT * FROM library")
@Query("SELECT * FROM library WHERE inLibrary = 1")
fun getAllEntries(): Flow<List<LibraryEntity>>
@Query("SELECT * FROM library WHERE name = :entryName")
@Query("SELECT * FROM library WHERE name LIKE :entryName AND inLibrary = 1")
fun getEntriesWithName(entryName: String): Flow<List<LibraryEntity>>
}

View File

@ -5,7 +5,7 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import at.xaxa.gamelibraryapp.LibraryApplication
@Database(entities = [LibraryEntity::class], version = 2)
@Database(entities = [LibraryEntity::class], version = 3)
abstract class LibraryDatabase : RoomDatabase() {
abstract fun libraryDao(): LibraryDao

View File

@ -9,5 +9,7 @@ data class LibraryEntity(
val _id: Int,
val name: String,
val details: String,
val note: String
val note: String,
val imageUrl :String,
val inLibrary: Boolean
)

View File

@ -9,14 +9,5 @@ import retrofit2.http.Query
// TODO Implement Remote Service
interface GameRemoteService {
@GET("games")
suspend fun getAllGames(@Query("key") apiKey: String): List<ApiResponse>
@GET("contacts/{contactId}")
suspend fun getContactById(@Path("contactId") contactId: Int)
@GET("contacts/search") // becomes contacts/search?filter=<content of filterText>
suspend fun findContacts(@Query("filter") filterText : String): List<ApiResponse>
@POST("contacts")
suspend fun addContact(@Body contactDto: ApiResponse)
suspend fun searchGames(@Query("key") apiKey: String, @Query("search") searchText: String): ApiResponse
}

View File

@ -6,6 +6,8 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import at.xaxa.gamelibraryapp.LibraryApplication
import at.xaxa.gamelibraryapp.ui.Details.DetailsViewModel
import at.xaxa.gamelibraryapp.ui.GameList.GameListViewModel
import at.xaxa.gamelibraryapp.ui.Search.SearchListViewModel
object AppViewModelProvider {
val Factory = viewModelFactory {
@ -13,16 +15,20 @@ object AppViewModelProvider {
DetailsViewModel(this.createSavedStateHandle(), (this[APPLICATION_KEY] as LibraryApplication).libraryRepository)
}
/*initializer {
ContactsViewModel((this[APPLICATION_KEY] as ContactsApplication).contactsRepository)
initializer {
GameListViewModel(this.createSavedStateHandle(), (this[APPLICATION_KEY] as LibraryApplication).libraryRepository)
}
initializer {
ContactDetailsViewModel(this.createSavedStateHandle(), (this[APPLICATION_KEY] as ContactsApplication).contactsRepository)
SearchListViewModel(this.createSavedStateHandle(), (this[APPLICATION_KEY] as LibraryApplication).libraryRepository)
}
/*
initializer {
ContactDetailsViewModel(this.createSavedStateHandle(), (this[APPLICATION_KEY] as ContactsApplication).contactsRepository)
}
initializer {
ContactEditViewModel(this.createSavedStateHandle(), (this[APPLICATION_KEY] as ContactsApplication).contactsRepository)
}*/
initializer {
ContactEditViewModel(this.createSavedStateHandle(), (this[APPLICATION_KEY] as ContactsApplication).contactsRepository)
}*/
}
}

View File

@ -1,25 +1,66 @@
package at.xaxa.gamelibraryapp.ui.Details
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavBackStackEntry
import at.xaxa.gamelibraryapp.ui.AppViewModelProvider
import at.xaxa.gamelibraryapp.ui.DetailCard
import at.xaxa.gamelibraryapp.ui.LayoutMediaText
import at.xaxa.gamelibraryapp.ui.ShowPicture
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailView(
navBackStackEntry: NavBackStackEntry,
viewModel: DetailsViewModel = viewModel(factory = AppViewModelProvider.Factory),
modifier: Modifier = Modifier
) {
// Extract arguments here
val gameId = navBackStackEntry.arguments?.getInt("gameId") // Or `Int` if needed
val libraryEntity = viewModel.editUiState.libraryEntity
DetailCard(modifier,
title = "Game Title $gameId", // Use the extracted gameId
details = "details",
imageUrl = "https://media.rawg.io/media/games/b7f/b7ffc4c4776e61eca19d36d3c227f89a.jpg",
note = "note"
)
val buttonText = if (libraryEntity.inLibrary) "Remove from library" else "Add to library"
Column {
DetailCard(modifier,
title = libraryEntity.name,
details = libraryEntity.details,
imageUrl = libraryEntity.imageUrl,
note = libraryEntity.note,
onClick = {
viewModel.updateGame(libraryEntity.copy(inLibrary = !libraryEntity.inLibrary))
},
buttonText = buttonText
)
OutlinedTextField(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 4.dp)
.fillMaxSize(),
shape = RoundedCornerShape(12.dp),
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.DarkGray,
unfocusedBorderColor = Color.LightGray),
value = libraryEntity.note,
onValueChange = {
viewModel.updateGame(libraryEntity.copy(note = it))
},
label = { Text("Notes") }
)
}
}

View File

@ -13,10 +13,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
data class GameEditUi(
val libraryEntity: LibraryEntity = LibraryEntity(0, "", "", "")
val libraryEntity: LibraryEntity = LibraryEntity(0, "", "", "", "",true)
)
class DetailsViewModel(private val savedStateHandle: SavedStateHandle, private val gameRepository: GameRepository) : ViewModel() {
class DetailsViewModel(
private val savedStateHandle: SavedStateHandle,
private val gameRepository: GameRepository
) : ViewModel() {
private val gameId: Int = checkNotNull(savedStateHandle["gameId"])
@ -33,13 +36,12 @@ class DetailsViewModel(private val savedStateHandle: SavedStateHandle, private v
}
fun updateGame(libraryEntity: LibraryEntity) {
editUiState = editUiState.copy(libraryEntity=libraryEntity)
}
editUiState = editUiState.copy(libraryEntity = libraryEntity)
fun saveLibrary() {
viewModelScope.launch {
gameRepository.updateEntry(editUiState.libraryEntity)
withContext(Dispatchers.IO) {
gameRepository.updateEntry(libraryEntity)
}
}
}
}

View File

@ -14,7 +14,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
data class GameDetailUi(
val game: LibraryEntity = LibraryEntity(0, "", "", "")
val game: LibraryEntity = LibraryEntity(0, "", "", "", "", false)
)
class GameDetailsViewModel(savedStateHandle: SavedStateHandle, private val gameRepository: GameRepository): ViewModel() {

View File

@ -5,20 +5,36 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import at.xaxa.gamelibraryapp.ui.AppViewModelProvider
import at.xaxa.gamelibraryapp.ui.Details.DetailsViewModel
import at.xaxa.gamelibraryapp.ui.HorizontalCard
import kotlinx.coroutines.flow.count
import androidx.compose.foundation.lazy.items
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GameList(modifier: Modifier = Modifier, onCardClick: (Int) -> Unit) {
fun GameList(modifier: Modifier = Modifier,
viewModel: GameListViewModel = viewModel(factory = AppViewModelProvider.Factory),
onCardClick: (Int) -> Unit) {
//val gameList = viewModel.editUiState.libraryEntity
val gameListState by viewModel.editUiState.libraryEntity.collectAsState(initial = emptyList())
var searchText by remember { mutableStateOf("") }
viewModel.searchGamesInDB(searchText)
Column(modifier = modifier) {
// Search Bar
@ -28,19 +44,22 @@ fun GameList(modifier: Modifier = Modifier, onCardClick: (Int) -> Unit) {
label = { Text("Search games") },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
.padding(horizontal = 16.dp, vertical = 4.dp),
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.DarkGray,
unfocusedBorderColor = Color.LightGray)
)
// Game List
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(16) { index ->
items(gameListState) { game ->
HorizontalCard(
modifier = modifier,
title = "Game $index",
details = "Details about game $index",
imageUrl = "https://media.rawg.io/media/games/b7f/b7ffc4c4776e61eca19d36d3c227f89a.jpg",
title = game.name,
details = game.details,
imageUrl = game.imageUrl,
onClick = {
onCardClick(index)
onCardClick(game.id)
}
)
}

View File

@ -1,2 +1,47 @@
package at.xaxa.gamelibraryapp.ui.GameList
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.xaxa.gamelibraryapp.data.Game
import at.xaxa.gamelibraryapp.data.GameRepository
import at.xaxa.gamelibraryapp.data.db.LibraryEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
data class GameListUi(
val libraryEntity: Flow<List<Game>> = flowOf(emptyList())
)
class GameListViewModel(private val savedStateHandle: SavedStateHandle, private val gameRepository: GameRepository) : ViewModel() {
var editUiState by mutableStateOf(GameListUi())
private set
init {
viewModelScope.launch {
val games = withContext(Dispatchers.IO) {
gameRepository.getAllGamesInLibrary()
}
editUiState = GameListUi(games)
}
}
fun searchGamesInDB(searchString: String) {
viewModelScope.launch {
val searchResults = withContext(Dispatchers.IO) {
gameRepository.searchGameInDB("%$searchString%")
}
editUiState = GameListUi(searchResults)
}
}
}

View File

@ -16,6 +16,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
@ -99,7 +100,6 @@ fun LibraryApp(modifier: Modifier = Modifier) {
) {
backStackEntry ->
DetailView(
navBackStackEntry = backStackEntry,
modifier = Modifier
)
}
@ -114,7 +114,9 @@ fun DetailCard(
title: String,
details: String,
note: String,
imageUrl: String) {
imageUrl: String,
onClick: () -> Unit,
buttonText: String) {
Surface(
modifier = modifier
.fillMaxWidth()
@ -125,6 +127,13 @@ fun DetailCard(
Column(modifier) {
ShowPicture(modifier = Modifier.height(200.dp), imageUrl)
LayoutMediaText(modifier = Modifier, title, details)
FilledTonalButton(
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
onClick = { onClick() }) {
Text(buttonText)
}
}
}
}
@ -270,5 +279,7 @@ private fun HorizontalCardPreview() {
@Preview
@Composable
private fun DetailCardPreview() {
DetailCard(Modifier, "Uncharted 3", "Action Adventure, 2011", "test", "https://media.rawg.io/media/games/b7f/b7ffc4c4776e61eca19d36d3c227f89a.jpg")
DetailCard(Modifier, "Uncharted 3", "Action Adventure, 2011", "test", "https://media.rawg.io/media/games/b7f/b7ffc4c4776e61eca19d36d3c227f89a.jpg", onClick = {
println("clicked")
}, "Add to library")
}

View File

@ -1,22 +1,70 @@
package at.xaxa.gamelibraryapp.ui.Search
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import at.xaxa.gamelibraryapp.ui.AppViewModelProvider
import at.xaxa.gamelibraryapp.ui.HorizontalCard
import androidx.compose.foundation.lazy.items
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchList(modifier: Modifier = Modifier, onCardClick: (Int) -> Unit){
LazyColumn{
items(16) { index ->
HorizontalCard(modifier,
"test $index",
"details $index",
"https://media.rawg.io/media/games/6e0/6e0c19bb111bd4fa20cf0eb72a049519.jpg",
onClick = {
onCardClick(index)
}
fun SearchList(
modifier: Modifier = Modifier,
viewModel: SearchListViewModel = viewModel(factory = AppViewModelProvider.Factory),
onCardClick: (Int) -> Unit
) {
var searchText by remember { mutableStateOf("") }
val gamesList by viewModel.editUiState.games.collectAsState(initial = emptyList())
Column(modifier = modifier) {
// Search field
OutlinedTextField(
value = searchText,
onValueChange = {
searchText = it
// Call searchGames when text changes
viewModel.searchGames(searchText)
},
label = { Text("Search games") },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.DarkGray,
unfocusedBorderColor = Color.LightGray
)
)
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(gamesList) { game ->
HorizontalCard(modifier,
game.name,
game.details,
game.imageUrl,//"https://media.rawg.io/media/games/6e0/6e0c19bb111bd4fa20cf0eb72a049519.jpg",
onClick = {
viewModel.addGameToDB(game)
onCardClick(game.id)
}
)
}
}
}
}

View File

@ -1,2 +1,80 @@
package at.xaxa.gamelibraryapp.ui.Search
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.xaxa.gamelibraryapp.data.Game
import at.xaxa.gamelibraryapp.data.GameRepository
import at.xaxa.gamelibraryapp.data.db.LibraryEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
// UI State to hold a list of games
data class GameListUi(
val games: Flow<List<Game>> = flow{emit(emptyList()) }
)
class SearchListViewModel(
private val savedStateHandle: SavedStateHandle,
private val gameRepository: GameRepository
) : ViewModel() {
var editUiState by mutableStateOf(GameListUi())
private set
// Function to search games based on search text
fun searchGames(searchText: String) {
viewModelScope.launch {
// Launching a background task to fetch the data from the repository
val games = withContext(Dispatchers.IO) {
gameRepository.searchGames(searchText) // Get the list of games
}
// Now transform the data before emitting
val mappedGames = games.map { game ->
// Transform each game as needed
Game(
id = game.id,
name = game.name,
details = game.genres.firstOrNull()?.name ?: "Genre Unknown",
note = "",
imageUrl = game.background_image,
inLibrary = false
)
}
editUiState = GameListUi(games = flow { emit(mappedGames) })
}
}
fun addGameToDB(game: Game){
viewModelScope.launch {
try {
val libraryEntity = LibraryEntity(
_id = game.id,
name = game.name,
details = game.details,
note = game.note,
imageUrl = game.imageUrl,
inLibrary = false
)
withContext(Dispatchers.IO) {
gameRepository.addEntry(libraryEntity) // Add game to the database
}
// Optionally, log or update UI state to reflect that the game was added
Log.d("GameLibraryApp", "Game added to DB: ${game.name}")
} catch (e: Exception) {
Log.e("GameLibraryApp", "Error adding game to DB", e)
}
}
}
}