From 133e06db1e7d6e706d626340e0fb25cba1e830e9 Mon Sep 17 00:00:00 2001 From: Xaver Date: Wed, 11 Dec 2024 20:27:20 +0100 Subject: [PATCH] 0.0.1 --- app/build.gradle.kts | 14 ++ app/src/main/AndroidManifest.xml | 3 + .../xaxa/gamelibraryapp/LibraryApplication.kt | 28 +++ .../at/xaxa/gamelibraryapp/MainActivity.kt | 21 +- .../java/at/xaxa/gamelibraryapp/data/Game.kt | 8 + .../gamelibraryapp/data/GameRepository.kt | 43 ++++ .../xaxa/gamelibraryapp/data/db/LibraryDao.kt | 30 +++ .../gamelibraryapp/data/db/LibraryDatabase.kt | 32 +++ .../gamelibraryapp/data/db/LibraryEntity.kt | 13 ++ .../gamelibraryapp/data/remote/GameDto.kt | 25 +++ .../data/remote/GameRemoteService.kt | 22 +++ .../gamelibraryapp/ui/AppViewModelProvider.kt | 28 +++ .../xaxa/gamelibraryapp/ui/Details/Details.kt | 25 +++ .../ui/Details/DetailsViewModel.kt | 45 +++++ .../gamelibraryapp/ui/GameDetailsViewModel.kt | 38 ++++ .../gamelibraryapp/ui/GameList/GameList.kt | 49 +++++ .../ui/GameList/GameListViewModel.kt | 2 + .../at/xaxa/gamelibraryapp/ui/LibraryUi.kt | 186 +++++++++++++----- .../gamelibraryapp/ui/Search/SearchList.kt | 22 +++ .../ui/Search/SearchListViewModel.kt | 2 + gradle/libs.versions.toml | 9 + 21 files changed, 573 insertions(+), 72 deletions(-) create mode 100644 app/src/main/java/at/xaxa/gamelibraryapp/LibraryApplication.kt create mode 100644 app/src/main/java/at/xaxa/gamelibraryapp/data/Game.kt create mode 100644 app/src/main/java/at/xaxa/gamelibraryapp/data/GameRepository.kt create mode 100644 app/src/main/java/at/xaxa/gamelibraryapp/data/db/LibraryDao.kt create mode 100644 app/src/main/java/at/xaxa/gamelibraryapp/data/db/LibraryDatabase.kt create mode 100644 app/src/main/java/at/xaxa/gamelibraryapp/data/db/LibraryEntity.kt create mode 100644 app/src/main/java/at/xaxa/gamelibraryapp/data/remote/GameDto.kt create mode 100644 app/src/main/java/at/xaxa/gamelibraryapp/data/remote/GameRemoteService.kt create mode 100644 app/src/main/java/at/xaxa/gamelibraryapp/ui/AppViewModelProvider.kt create mode 100644 app/src/main/java/at/xaxa/gamelibraryapp/ui/Details/Details.kt create mode 100644 app/src/main/java/at/xaxa/gamelibraryapp/ui/Details/DetailsViewModel.kt create mode 100644 app/src/main/java/at/xaxa/gamelibraryapp/ui/GameDetailsViewModel.kt create mode 100644 app/src/main/java/at/xaxa/gamelibraryapp/ui/GameList/GameList.kt create mode 100644 app/src/main/java/at/xaxa/gamelibraryapp/ui/GameList/GameListViewModel.kt create mode 100644 app/src/main/java/at/xaxa/gamelibraryapp/ui/Search/SearchList.kt create mode 100644 app/src/main/java/at/xaxa/gamelibraryapp/ui/Search/SearchListViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2c22bca..e32b769 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,9 @@ plugins { alias(libs.plugins.android.application) 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" } android { @@ -53,6 +56,17 @@ dependencies { implementation(libs.coil.compose) implementation(libs.coil.network.okhttp) + implementation(libs.androidx.navigation.compose) + + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + ksp(libs.androidx.room.compiler) + + // Retrofit + implementation(libs.converter.kotlinx.serialization) + implementation(libs.retrofit) + implementation(libs.kotlinx.serialization.json) + implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 367ee29..84d2144 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,9 @@ + + - Greeting( - name = "Android", + LibraryApp( modifier = Modifier.padding(innerPadding) ) } @@ -29,19 +30,3 @@ class MainActivity : ComponentActivity() { } } } - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - GameLibraryAppTheme { - Greeting("Android") - } -} \ No newline at end of file diff --git a/app/src/main/java/at/xaxa/gamelibraryapp/data/Game.kt b/app/src/main/java/at/xaxa/gamelibraryapp/data/Game.kt new file mode 100644 index 0000000..bff3337 --- /dev/null +++ b/app/src/main/java/at/xaxa/gamelibraryapp/data/Game.kt @@ -0,0 +1,8 @@ +package at.xaxa.gamelibraryapp.data + +data class Game( + val id: Int, + val name: String, + val details: String, + val note: String +) diff --git a/app/src/main/java/at/xaxa/gamelibraryapp/data/GameRepository.kt b/app/src/main/java/at/xaxa/gamelibraryapp/data/GameRepository.kt new file mode 100644 index 0000000..0168f85 --- /dev/null +++ b/app/src/main/java/at/xaxa/gamelibraryapp/data/GameRepository.kt @@ -0,0 +1,43 @@ +package at.xaxa.gamelibraryapp.data + +import at.xaxa.gamelibraryapp.data.db.LibraryDao +import at.xaxa.gamelibraryapp.data.db.LibraryEntity +import at.xaxa.gamelibraryapp.data.remote.GameRemoteService +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +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) + } + } catch (e: Exception) { + Log.e("Repository", "Something went wrong! ${e.message}", e) + } + }*/ + + fun getAllGamesInLibrary(): Flow> { + return libraryDao.getAllEntries().map { + it.map {item -> Game(item._id, item.name, item.details, item.note) } + } + } + + suspend fun findGameById(id: Int): LibraryEntity { + val item = libraryDao.findEntryById(id) + return LibraryEntity(item._id, item.name, item.details, item.note) + } + + suspend fun addEntry(libraryEntity: LibraryEntity) { + libraryDao.addEntry(LibraryEntity(libraryEntity._id, libraryEntity.name, libraryEntity.details, libraryEntity.note)) + } + + suspend fun updateEntry(game: LibraryEntity) { + libraryDao.updateEntry(LibraryEntity(game._id, game.name, game.details, game.note)) + } +} \ No newline at end of file diff --git a/app/src/main/java/at/xaxa/gamelibraryapp/data/db/LibraryDao.kt b/app/src/main/java/at/xaxa/gamelibraryapp/data/db/LibraryDao.kt new file mode 100644 index 0000000..36f90fc --- /dev/null +++ b/app/src/main/java/at/xaxa/gamelibraryapp/data/db/LibraryDao.kt @@ -0,0 +1,30 @@ +package at.xaxa.gamelibraryapp.data.db + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import at.xaxa.gamelibraryapp.data.Game +import kotlinx.coroutines.flow.Flow + +@Dao +interface LibraryDao { + @Insert + suspend fun addEntry(libraryEntity: LibraryEntity) + + @Update + suspend fun updateEntry(libraryEntity: LibraryEntity) + + @Delete + suspend fun deleteEntry(libraryEntity: LibraryEntity) + + @Query("SELECT * FROM library WHERE _id = :id") + suspend fun findEntryById(id: Int): LibraryEntity + + @Query("SELECT * FROM library") + fun getAllEntries(): Flow> + + @Query("SELECT * FROM library WHERE name = :entryName") + fun getEntriesWithName(entryName: String): Flow> +} diff --git a/app/src/main/java/at/xaxa/gamelibraryapp/data/db/LibraryDatabase.kt b/app/src/main/java/at/xaxa/gamelibraryapp/data/db/LibraryDatabase.kt new file mode 100644 index 0000000..e70f19b --- /dev/null +++ b/app/src/main/java/at/xaxa/gamelibraryapp/data/db/LibraryDatabase.kt @@ -0,0 +1,32 @@ +package at.xaxa.gamelibraryapp.data.db + +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import at.xaxa.gamelibraryapp.LibraryApplication + +@Database(entities = [LibraryEntity::class], version = 2) +abstract class LibraryDatabase : RoomDatabase() { + abstract fun libraryDao(): LibraryDao + + companion object { + @Volatile + private var Instance: LibraryDatabase? = null + + fun getDatabase(context: LibraryApplication): LibraryDatabase { + // if the Instance is not null, return it, otherwise create a new database instance. + return Instance ?: synchronized(this) { + val instance = Room.databaseBuilder(context, LibraryDatabase::class.java, "library_database") + /** + * Setting this option in your app's database builder means that Room + * permanently deletes all data from the tables in your database when it + * attempts to perform a migration with no defined migration path. + */ + .fallbackToDestructiveMigration() + .build() + Instance = instance + return instance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/at/xaxa/gamelibraryapp/data/db/LibraryEntity.kt b/app/src/main/java/at/xaxa/gamelibraryapp/data/db/LibraryEntity.kt new file mode 100644 index 0000000..6e2d0a8 --- /dev/null +++ b/app/src/main/java/at/xaxa/gamelibraryapp/data/db/LibraryEntity.kt @@ -0,0 +1,13 @@ +package at.xaxa.gamelibraryapp.data.db + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "library") +data class LibraryEntity( + @PrimaryKey + val _id: Int, + val name: String, + val details: String, + val note: String +) \ No newline at end of file diff --git a/app/src/main/java/at/xaxa/gamelibraryapp/data/remote/GameDto.kt b/app/src/main/java/at/xaxa/gamelibraryapp/data/remote/GameDto.kt new file mode 100644 index 0000000..0e5f496 --- /dev/null +++ b/app/src/main/java/at/xaxa/gamelibraryapp/data/remote/GameDto.kt @@ -0,0 +1,25 @@ +package at.xaxa.gamelibraryapp.data.remote + +import kotlinx.serialization.Serializable + +@Serializable +data class ApiResponse( + val count: Int, + val next: String?, + val previous: String?, + val results: List +) + +@Serializable +data class GameDto( + val id: Int, + val name: String, + val background_image: String, + val genres: List +) + +@Serializable +data class GenreDto( + val id: Int, + val name: String +) diff --git a/app/src/main/java/at/xaxa/gamelibraryapp/data/remote/GameRemoteService.kt b/app/src/main/java/at/xaxa/gamelibraryapp/data/remote/GameRemoteService.kt new file mode 100644 index 0000000..324729e --- /dev/null +++ b/app/src/main/java/at/xaxa/gamelibraryapp/data/remote/GameRemoteService.kt @@ -0,0 +1,22 @@ +package at.xaxa.gamelibraryapp.data.remote + +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query + +// TODO Implement Remote Service +interface GameRemoteService { + @GET("games") + suspend fun getAllGames(@Query("key") apiKey: String): List + + @GET("contacts/{contactId}") + suspend fun getContactById(@Path("contactId") contactId: Int) + + @GET("contacts/search") // becomes contacts/search?filter= + suspend fun findContacts(@Query("filter") filterText : String): List + + @POST("contacts") + suspend fun addContact(@Body contactDto: ApiResponse) +} \ No newline at end of file diff --git a/app/src/main/java/at/xaxa/gamelibraryapp/ui/AppViewModelProvider.kt b/app/src/main/java/at/xaxa/gamelibraryapp/ui/AppViewModelProvider.kt new file mode 100644 index 0000000..dc13023 --- /dev/null +++ b/app/src/main/java/at/xaxa/gamelibraryapp/ui/AppViewModelProvider.kt @@ -0,0 +1,28 @@ +package at.xaxa.gamelibraryapp.ui + +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import at.xaxa.gamelibraryapp.LibraryApplication +import at.xaxa.gamelibraryapp.ui.Details.DetailsViewModel + +object AppViewModelProvider { + val Factory = viewModelFactory { + initializer { + DetailsViewModel(this.createSavedStateHandle(), (this[APPLICATION_KEY] as LibraryApplication).libraryRepository) + } + + /*initializer { + ContactsViewModel((this[APPLICATION_KEY] as ContactsApplication).contactsRepository) + } + + initializer { + ContactDetailsViewModel(this.createSavedStateHandle(), (this[APPLICATION_KEY] as ContactsApplication).contactsRepository) + } + + initializer { + ContactEditViewModel(this.createSavedStateHandle(), (this[APPLICATION_KEY] as ContactsApplication).contactsRepository) + }*/ + } +} \ No newline at end of file diff --git a/app/src/main/java/at/xaxa/gamelibraryapp/ui/Details/Details.kt b/app/src/main/java/at/xaxa/gamelibraryapp/ui/Details/Details.kt new file mode 100644 index 0000000..7d4358b --- /dev/null +++ b/app/src/main/java/at/xaxa/gamelibraryapp/ui/Details/Details.kt @@ -0,0 +1,25 @@ +package at.xaxa.gamelibraryapp.ui.Details + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavBackStackEntry +import at.xaxa.gamelibraryapp.ui.AppViewModelProvider +import at.xaxa.gamelibraryapp.ui.DetailCard + +@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 + + DetailCard(modifier, + title = "Game Title $gameId", // Use the extracted gameId + details = "details", + imageUrl = "https://media.rawg.io/media/games/b7f/b7ffc4c4776e61eca19d36d3c227f89a.jpg", + note = "note" + ) +} diff --git a/app/src/main/java/at/xaxa/gamelibraryapp/ui/Details/DetailsViewModel.kt b/app/src/main/java/at/xaxa/gamelibraryapp/ui/Details/DetailsViewModel.kt new file mode 100644 index 0000000..9292595 --- /dev/null +++ b/app/src/main/java/at/xaxa/gamelibraryapp/ui/Details/DetailsViewModel.kt @@ -0,0 +1,45 @@ +package at.xaxa.gamelibraryapp.ui.Details + +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.GameRepository +import at.xaxa.gamelibraryapp.data.db.LibraryEntity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +data class GameEditUi( + val libraryEntity: LibraryEntity = LibraryEntity(0, "", "", "") +) + +class DetailsViewModel(private val savedStateHandle: SavedStateHandle, private val gameRepository: GameRepository) : ViewModel() { + + private val gameId: Int = checkNotNull(savedStateHandle["gameId"]) + + var editUiState by mutableStateOf(GameEditUi()) + private set + + init { + viewModelScope.launch { + val game = withContext(Dispatchers.IO) { + gameRepository.findGameById(gameId) + } + editUiState = GameEditUi(game) + } + } + + fun updateGame(libraryEntity: LibraryEntity) { + editUiState = editUiState.copy(libraryEntity=libraryEntity) + } + + fun saveLibrary() { + viewModelScope.launch { + gameRepository.updateEntry(editUiState.libraryEntity) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/at/xaxa/gamelibraryapp/ui/GameDetailsViewModel.kt b/app/src/main/java/at/xaxa/gamelibraryapp/ui/GameDetailsViewModel.kt new file mode 100644 index 0000000..0cc51f2 --- /dev/null +++ b/app/src/main/java/at/xaxa/gamelibraryapp/ui/GameDetailsViewModel.kt @@ -0,0 +1,38 @@ +package at.xaxa.gamelibraryapp.ui + +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.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +data class GameDetailUi( + val game: LibraryEntity = LibraryEntity(0, "", "", "") +) + +class GameDetailsViewModel(savedStateHandle: SavedStateHandle, private val gameRepository: GameRepository): ViewModel() { + + private val gameId: Int = checkNotNull(savedStateHandle["gameId"]) + + private val _detailUiState = MutableStateFlow(GameDetailUi()) + val detailUiState = _detailUiState.asStateFlow() + + init { + viewModelScope.launch { + val libraryEntity = withContext(Dispatchers.IO) { + gameRepository.findGameById(gameId) + } + _detailUiState.update { + GameDetailUi(libraryEntity) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/at/xaxa/gamelibraryapp/ui/GameList/GameList.kt b/app/src/main/java/at/xaxa/gamelibraryapp/ui/GameList/GameList.kt new file mode 100644 index 0000000..01bea5a --- /dev/null +++ b/app/src/main/java/at/xaxa/gamelibraryapp/ui/GameList/GameList.kt @@ -0,0 +1,49 @@ +package at.xaxa.gamelibraryapp.ui.GameList + +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.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.unit.dp +import at.xaxa.gamelibraryapp.ui.HorizontalCard + +@Composable +fun GameList(modifier: Modifier = Modifier, onCardClick: (Int) -> Unit) { + var searchText by remember { mutableStateOf("") } + + Column(modifier = modifier) { + // Search Bar + OutlinedTextField( + value = searchText, + onValueChange = { searchText = it }, + label = { Text("Search games") }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + ) + + // Game List + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(16) { index -> + HorizontalCard( + modifier = modifier, + title = "Game $index", + details = "Details about game $index", + imageUrl = "https://media.rawg.io/media/games/b7f/b7ffc4c4776e61eca19d36d3c227f89a.jpg", + onClick = { + onCardClick(index) + } + ) + } + } + } +} diff --git a/app/src/main/java/at/xaxa/gamelibraryapp/ui/GameList/GameListViewModel.kt b/app/src/main/java/at/xaxa/gamelibraryapp/ui/GameList/GameListViewModel.kt new file mode 100644 index 0000000..115a3b8 --- /dev/null +++ b/app/src/main/java/at/xaxa/gamelibraryapp/ui/GameList/GameListViewModel.kt @@ -0,0 +1,2 @@ +package at.xaxa.gamelibraryapp.ui.GameList + diff --git a/app/src/main/java/at/xaxa/gamelibraryapp/ui/LibraryUi.kt b/app/src/main/java/at/xaxa/gamelibraryapp/ui/LibraryUi.kt index 1db6a53..fe1e4cd 100644 --- a/app/src/main/java/at/xaxa/gamelibraryapp/ui/LibraryUi.kt +++ b/app/src/main/java/at/xaxa/gamelibraryapp/ui/LibraryUi.kt @@ -1,47 +1,32 @@ package at.xaxa.gamelibraryapp.ui -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer 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.layout.requiredHeight -import androidx.compose.foundation.layout.requiredWidth -import androidx.compose.foundation.lazy.LazyColumn 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.material.icons.outlined.Delete -import androidx.compose.material.icons.outlined.Edit -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -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.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -53,56 +38,70 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import at.xaxa.gamelibraryapp.R +import at.xaxa.gamelibraryapp.ui.Details.DetailView +import at.xaxa.gamelibraryapp.ui.GameList.GameList +import at.xaxa.gamelibraryapp.ui.Search.SearchList import coil3.compose.AsyncImage -import java.time.format.TextStyle -enum class DemoRoutes(val route: String) { +enum class LibraryRoutes(val route: String) { List("list"), Add("add"), - Detail("task/{taskId}") + Detail("game/{gameId}") } @Composable fun LibraryApp(modifier: Modifier = Modifier) { val navController = rememberNavController() + val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route Scaffold( bottomBar = { - androidx.compose.material3.NavigationBar { - NavigationBarItem( - icon = { Icon(Icons.AutoMirrored.Filled.List, contentDescription = "List") }, - label = { Text("List") }, - selected = navController.currentBackStackEntryAsState().value?.destination?.route == "list", - onClick = { navController.navigate(DemoRoutes.List.route) } - ) - NavigationBarItem( - icon = { Icon(Icons.Default.Add, contentDescription = "Add") }, - label = { Text("Add") }, - selected = navController.currentBackStackEntryAsState().value?.destination?.route == "add", - onClick = { navController.navigate(DemoRoutes.Add.route) } - ) + // Hide bottom bar only when in the Detail view + if (currentRoute != LibraryRoutes.Detail.route) { + androidx.compose.material3.NavigationBar { + NavigationBarItem( + icon = { Icon(Icons.AutoMirrored.Filled.List, contentDescription = "List") }, + label = { Text("List") }, + selected = currentRoute == LibraryRoutes.List.route, + onClick = { navController.navigate(LibraryRoutes.List.route) } + ) + NavigationBarItem( + icon = { Icon(Icons.Default.Add, contentDescription = "Add") }, + label = { Text("Search") }, + selected = currentRoute == LibraryRoutes.Add.route, + onClick = { navController.navigate(LibraryRoutes.Add.route) } + ) + } } } ) { innerPadding -> NavHost( navController = navController, - startDestination = DemoRoutes.List.route, - modifier = Modifier.padding(innerPadding) + startDestination = LibraryRoutes.List.route, + modifier = Modifier.padding(innerPadding), + contentAlignment = Alignment.Center ) { - composable(DemoRoutes.List.route){ - // screen + composable(LibraryRoutes.List.route){ + GameList(onCardClick = { + navController.navigate("game/$it") + }) } - composable(DemoRoutes.Add.route) { - // screen + composable(LibraryRoutes.Add.route) { + SearchList(onCardClick = { + navController.navigate("game/$it") + }) } composable( - route = DemoRoutes.Detail.route, - arguments = listOf(navArgument("taskId") { + route = LibraryRoutes.Detail.route, + arguments = listOf(navArgument("gameId") { type = NavType.IntType }) ) { - // screen + backStackEntry -> + DetailView( + navBackStackEntry = backStackEntry, + modifier = Modifier + ) } } } @@ -110,23 +109,93 @@ fun LibraryApp(modifier: Modifier = Modifier) { @Composable -fun HorizontalCard(modifier: Modifier = Modifier, title: String, details: String) { +fun DetailCard( + modifier: Modifier = Modifier, + title: String, + details: String, + note: String, + imageUrl: String) { Surface( modifier = modifier - .requiredWidth(360.dp) - .requiredHeight(80.dp) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) .clip(RoundedCornerShape(12.dp)) + .border(1.dp, Color.LightGray, RoundedCornerShape(12.dp)) ) { - Row(modifier = Modifier.fillMaxSize()) { - LayoutMediaText(modifier = Modifier.weight(1f), title, details) - ShowPicture(modifier = Modifier.weight(0.5f)) + Column(modifier) { + ShowPicture(modifier = Modifier.height(200.dp), imageUrl) + LayoutMediaText(modifier = Modifier, title, details) } } } +@Composable +fun HorizontalCard( + modifier: Modifier = Modifier, + title: String, + details: String, + imageUrl: String, + onClick: () -> Unit) { + Surface( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + .requiredHeight(80.dp) + .clip(RoundedCornerShape(12.dp)) + .border(1.dp, Color.LightGray, RoundedCornerShape(12.dp)) + .clickable { onClick() } + ) { + Row(modifier = Modifier.fillMaxSize()) { + LayoutMediaTextHor(modifier = Modifier.weight(1f), title, details) + ShowPicture(modifier = Modifier.weight(0.5f), imageUrl) + } + } +} @Composable fun LayoutMediaText(modifier: Modifier = Modifier, title: String, details: String) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .clip(shape = RoundedCornerShape(12.dp)) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(all = 16.dp) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Top), + modifier = Modifier + ) { + Text( + text = title, + color = Color.DarkGray, + lineHeight = 1.5.em, + style = androidx.compose.ui.text.TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + letterSpacing = 0.15.sp + ), + modifier = Modifier + .fillMaxWidth()) + Text( + text = details, + color = Color.DarkGray, + lineHeight = 1.43.em, + style = androidx.compose.ui.text.TextStyle( + fontSize = 14.sp, + letterSpacing = 0.25.sp + )) + } + } + } +} + +@Composable +fun LayoutMediaTextHor(modifier: Modifier = Modifier, title: String, details: String) { Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier @@ -173,7 +242,7 @@ fun LayoutMediaText(modifier: Modifier = Modifier, title: String, details: Strin } @Composable -fun ShowPicture(modifier: Modifier = Modifier) { +fun ShowPicture(modifier: Modifier = Modifier, imageUrl: String) { Surface( shape = RoundedCornerShape(12.dp), color = Color(0xfffef7ff), @@ -182,9 +251,10 @@ fun ShowPicture(modifier: Modifier = Modifier) { ) { //Display Image here AsyncImage( - model = "https://media.rawg.io/media/games/b7f/b7ffc4c4776e61eca19d36d3c227f89a.jpg", + model = imageUrl, contentDescription = "Translated description of what the image contains", - Modifier.fillMaxHeight() + Modifier.fillMaxHeight(), + contentScale = ContentScale.Crop ) } } @@ -192,5 +262,13 @@ fun ShowPicture(modifier: Modifier = Modifier) { @Preview(widthDp = 360, heightDp = 80) @Composable private fun HorizontalCardPreview() { - HorizontalCard(Modifier, "Uncharted 3", "Action Adventure, 2011") + HorizontalCard(Modifier, "Uncharted 3", "Action Adventure, 2011", "https://media.rawg.io/media/games/b7f/b7ffc4c4776e61eca19d36d3c227f89a.jpg", onClick = { + println("clicked") + }) +} + +@Preview +@Composable +private fun DetailCardPreview() { + DetailCard(Modifier, "Uncharted 3", "Action Adventure, 2011", "test", "https://media.rawg.io/media/games/b7f/b7ffc4c4776e61eca19d36d3c227f89a.jpg") } \ No newline at end of file diff --git a/app/src/main/java/at/xaxa/gamelibraryapp/ui/Search/SearchList.kt b/app/src/main/java/at/xaxa/gamelibraryapp/ui/Search/SearchList.kt new file mode 100644 index 0000000..19b74e5 --- /dev/null +++ b/app/src/main/java/at/xaxa/gamelibraryapp/ui/Search/SearchList.kt @@ -0,0 +1,22 @@ +package at.xaxa.gamelibraryapp.ui.Search + +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import at.xaxa.gamelibraryapp.ui.HorizontalCard + +@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) + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/at/xaxa/gamelibraryapp/ui/Search/SearchListViewModel.kt b/app/src/main/java/at/xaxa/gamelibraryapp/ui/Search/SearchListViewModel.kt new file mode 100644 index 0000000..38f4dec --- /dev/null +++ b/app/src/main/java/at/xaxa/gamelibraryapp/ui/Search/SearchListViewModel.kt @@ -0,0 +1,2 @@ +package at.xaxa.gamelibraryapp.ui.Search + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 003bda7..961fa5c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,20 +1,27 @@ [versions] agp = "8.6.0" coilCompose = "3.0.4" +converterKotlinxSerialization = "2.11.0" kotlin = "1.9.0" coreKtx = "1.15.0" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" +kotlinxSerializationJson = "1.6.3" lifecycleRuntimeKtx = "2.8.7" activityCompose = "1.9.3" composeBom = "2024.04.01" navigationCompose = "2.8.4" +roomRuntime = "2.6.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" } +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" } +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" } coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilCompose" } +converter-kotlinx-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "converterKotlinxSerialization" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } @@ -29,6 +36,8 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "converterKotlinxSerialization" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }