From 871a29cc52ec600ce741da4bb213e783673322e7 Mon Sep 17 00:00:00 2001 From: kevinMartinezC Date: Wed, 26 Apr 2023 08:37:53 -0600 Subject: [PATCH 01/16] feat: add BottomNavigation bar --- app/build.gradle.kts | 1 + .../moviesvapp/components/MainScreen.kt | 63 +++++++++++++++++++ .../components/navigation/BottomBar.kt | 36 +++++++++++ .../components/navigation/BottomNavGraph.kt | 25 ++++++++ .../components/navigation/BottomNavItem.kt | 23 +++++++ .../com/example/moviesvapp/ui/MainActivity.kt | 9 ++- 6 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/example/moviesvapp/components/MainScreen.kt create mode 100644 app/src/main/java/com/example/moviesvapp/components/navigation/BottomBar.kt create mode 100644 app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavGraph.kt create mode 100644 app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavItem.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4844e75..216b124 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -59,6 +59,7 @@ dependencies { implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material:material:1.4.2") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/app/src/main/java/com/example/moviesvapp/components/MainScreen.kt b/app/src/main/java/com/example/moviesvapp/components/MainScreen.kt new file mode 100644 index 0000000..ce0f7e8 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/components/MainScreen.kt @@ -0,0 +1,63 @@ +package com.example.moviesvapp.components + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.example.moviesvapp.components.navigation.BottomBar +import com.example.moviesvapp.components.navigation.BottomNavGraph +import com.example.moviesvapp.components.navigation.BottomNavItem + +@OptIn(ExperimentalMaterial3Api::class) +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +@Composable +fun MainScreen() { + val navController = rememberNavController() + Scaffold( + bottomBar = { BottomBar(navController = navController) } + ) { + BottomNavGraph(navController = navController) + } +} + +@Composable +fun RowScope.AddItem( + screen: BottomNavItem, + currentDestination: NavDestination?, + navController: NavHostController +) { + NavigationBarItem( + label = { + Text(text = screen.title) + }, + icon = { + Icon( + imageVector = screen.icon, + contentDescription = "Navigation Icon", + tint = MaterialTheme.colorScheme.primary + + ) + }, + selected = currentDestination?.hierarchy?.any { + it.route == screen.route + } == true, + onClick = { + navController.navigate(screen.route) { + popUpTo(navController.graph.findStartDestination().id) + launchSingleTop = true + } + }, + alwaysShowLabel = false, + ) +} diff --git a/app/src/main/java/com/example/moviesvapp/components/navigation/BottomBar.kt b/app/src/main/java/com/example/moviesvapp/components/navigation/BottomBar.kt new file mode 100644 index 0000000..d67b8b1 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/components/navigation/BottomBar.kt @@ -0,0 +1,36 @@ +package com.example.moviesvapp.components.navigation + +import androidx.compose.foundation.background +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState +import com.example.moviesvapp.components.AddItem + +@Composable +fun BottomBar(navController: NavHostController) { + val screens = listOf( + BottomNavItem.Home, + BottomNavItem.Favorite, + ) + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + + NavigationBar ( + modifier = Modifier.background( + color = MaterialTheme.colorScheme.primary, + shape = MaterialTheme.shapes.medium + ) + ){ + screens.forEach { screen -> + AddItem( + screen = screen, + currentDestination = currentDestination, + navController = navController + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavGraph.kt b/app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavGraph.kt new file mode 100644 index 0000000..9998f6f --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavGraph.kt @@ -0,0 +1,25 @@ +package com.example.moviesvapp.components.navigation + +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.example.moviesvapp.components.MoviesScreen +import com.example.moviesvapp.model.MoviesViewModel + + +@Composable +fun BottomNavGraph(navController: NavHostController) { + NavHost( + navController = navController, + startDestination = BottomNavItem.Home.route + ) { + composable(route = BottomNavItem.Home.route) { + MoviesScreen(viewModel = MoviesViewModel()) + + } + composable(route = BottomNavItem.Favorite.route) { + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavItem.kt b/app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavItem.kt new file mode 100644 index 0000000..d1e1b46 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavItem.kt @@ -0,0 +1,23 @@ +package com.example.moviesvapp.components.navigation + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Star +import androidx.compose.ui.graphics.vector.ImageVector + +sealed class BottomNavItem( + val route: String, + val title: String, + val icon: ImageVector +){ + object Home : BottomNavItem( + route = "home", + title = "Home", + icon = Icons.Default.Search + ) + object Favorite : BottomNavItem( + route = "favorite", + title = "Favorite", + icon = Icons.Default.Star + ) +} diff --git a/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt b/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt index 5016548..b6d3947 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt @@ -4,17 +4,24 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.lifecycle.viewmodel.compose.viewModel +import com.example.moviesvapp.components.MainScreen import com.example.moviesvapp.components.MoviesScreen import com.example.moviesvapp.model.MoviesViewModel +import com.example.musicplayercompose.ui.theme.MyApplicationTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { + MyApplicationTheme { + MainScreen() + } + } + /*setContent { val viewModel: MoviesViewModel = viewModel() MoviesScreen(viewModel) - } + }*/ } } From fd170069b5926271647d190068c60e8b833a8796 Mon Sep 17 00:00:00 2001 From: kevinMartinezC Date: Wed, 26 Apr 2023 15:03:33 -0600 Subject: [PATCH 02/16] feat: add login screen and logic to make a request to get the correct Api key as password --- app/build.gradle.kts | 1 + .../moviesvapp/components/FavoriteScreen.kt | 9 ++ .../moviesvapp/components/LoginScreen.kt | 94 +++++++++++++++++++ .../moviesvapp/components/MovieItem.kt | 5 +- .../moviesvapp/components/MoviesScreen.kt | 12 ++- .../components/navigation/BottomBar.kt | 10 +- .../components/navigation/BottomNavGraph.kt | 12 ++- .../moviesvapp/model/MoviesResponse.kt | 10 +- .../moviesvapp/model/PreferenceRepository.kt | 14 +++ .../com/example/moviesvapp/model/Resource.kt | 8 ++ .../model/SharedPreferencesExtensions.kt | 13 +++ .../com/example/moviesvapp/ui/MainActivity.kt | 32 +++++-- .../com/example/moviesvapp/ui/theme/Color.kt | 70 +++++++------- .../com/example/moviesvapp/ui/theme/Font.kt | 2 +- .../com/example/moviesvapp/ui/theme/Theme.kt | 3 +- .../com/example/moviesvapp/ui/theme/Type.kt | 21 ++++- .../moviesvapp/viewmodel/LoginViewModel.kt | 44 +++++++++ .../{model => viewmodel}/MoviesViewModel.kt | 10 +- 18 files changed, 294 insertions(+), 76 deletions(-) create mode 100644 app/src/main/java/com/example/moviesvapp/components/FavoriteScreen.kt create mode 100644 app/src/main/java/com/example/moviesvapp/components/LoginScreen.kt create mode 100644 app/src/main/java/com/example/moviesvapp/model/PreferenceRepository.kt create mode 100644 app/src/main/java/com/example/moviesvapp/model/Resource.kt create mode 100644 app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt create mode 100644 app/src/main/java/com/example/moviesvapp/viewmodel/LoginViewModel.kt rename app/src/main/java/com/example/moviesvapp/{model => viewmodel}/MoviesViewModel.kt (79%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 216b124..5c4a800 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -89,6 +89,7 @@ dependencies { val navVersion = "2.5.3" implementation("androidx.navigation:navigation-compose:$navVersion") + implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") diff --git a/app/src/main/java/com/example/moviesvapp/components/FavoriteScreen.kt b/app/src/main/java/com/example/moviesvapp/components/FavoriteScreen.kt new file mode 100644 index 0000000..5ad0ff5 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/components/FavoriteScreen.kt @@ -0,0 +1,9 @@ +package com.example.moviesvapp.components + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable + +@Composable +fun FavoriteScreen() { + Text(text = "Favotire") +} \ No newline at end of file diff --git a/app/src/main/java/com/example/moviesvapp/components/LoginScreen.kt b/app/src/main/java/com/example/moviesvapp/components/LoginScreen.kt new file mode 100644 index 0000000..05f3d42 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/components/LoginScreen.kt @@ -0,0 +1,94 @@ +package com.example.moviesvapp.components + +import android.util.Log +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.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.example.moviesvapp.model.Resource +import com.example.moviesvapp.viewmodel.LoginViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoginScreen(viewModel: LoginViewModel, onLoginSuccess: (String) -> Unit) { + val username = remember { mutableStateOf("") } + val apiKey = remember { mutableStateOf("") } + val loginStatus by viewModel.loginStatus.collectAsState() + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextField( + value = username.value, + onValueChange = { username.value = it }, + label = { Text("Username") }, + singleLine = true + ) + Spacer(modifier = Modifier.height(8.dp)) + TextField( + value = apiKey.value, + onValueChange = { apiKey.value = it }, + label = { Text("API Key") }, + singleLine = true + ) + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = { + viewModel.login(username = username.value, apiKey= apiKey.value) }) { + Text("Login") + } + + when (loginStatus) { + is Resource.Loading -> Text("Logging in...") + is Resource.Error -> { + val errorStatus = loginStatus as Resource.Error + val userFriendlyMessage = when (errorStatus.message) { + "Invalid API key" -> "Invalid API key. Please check your Password and try again." + else -> "An error occurred. Please try again later." + } + Text( + userFriendlyMessage, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium + ) + } + + + is Resource.Success -> { + val successStatus = loginStatus as Resource.Success + LaunchedEffect(Unit) { + onLoginSuccess(successStatus.data) + } + } + + is Resource.Idle -> { + Text( + "Please enter your username and password", + + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } +} + diff --git a/app/src/main/java/com/example/moviesvapp/components/MovieItem.kt b/app/src/main/java/com/example/moviesvapp/components/MovieItem.kt index 5bf6f8d..d6a064c 100644 --- a/app/src/main/java/com/example/moviesvapp/components/MovieItem.kt +++ b/app/src/main/java/com/example/moviesvapp/components/MovieItem.kt @@ -7,12 +7,12 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter import com.example.moviesvapp.model.Movie @@ -40,7 +40,8 @@ fun MovieItem(movie: Movie) { .weight(1f) .align(Alignment.CenterVertically) ) { - Text(movie.title, fontWeight = FontWeight.Bold) + Text(movie.title, style = MaterialTheme.typography.bodyLarge + ) Text(movie.year) } } diff --git a/app/src/main/java/com/example/moviesvapp/components/MoviesScreen.kt b/app/src/main/java/com/example/moviesvapp/components/MoviesScreen.kt index 3fa3470..b225d02 100644 --- a/app/src/main/java/com/example/moviesvapp/components/MoviesScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/components/MoviesScreen.kt @@ -1,5 +1,6 @@ package com.example.moviesvapp.components +import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -26,8 +27,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp -import com.example.moviesvapp.model.MoviesViewModel -import com.example.musicplayercompose.ui.theme.MyApplicationTheme +import com.example.moviesvapp.viewmodel.MoviesViewModel +import com.example.moviesvapp.ui.theme.MyApplicationTheme @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -45,13 +46,18 @@ fun MoviesScreen(viewModel: MoviesViewModel) { horizontalAlignment = Alignment.CenterHorizontally ) { TextField( + value = searchQuery, onValueChange = { searchQuery = it }, label = { Text("Search movies") }, modifier = Modifier.fillMaxWidth(), + maxLines = 1, trailingIcon = { IconButton( - onClick = { viewModel.searchMovies(searchQuery.text) } + onClick = { + Log.d("MoviesViewModel", "Search Icon Clicked") + + viewModel.searchMovies(searchQuery.text) } ) { Icon(Icons.Default.Search, contentDescription = "Search Icon") } diff --git a/app/src/main/java/com/example/moviesvapp/components/navigation/BottomBar.kt b/app/src/main/java/com/example/moviesvapp/components/navigation/BottomBar.kt index d67b8b1..eaf7e94 100644 --- a/app/src/main/java/com/example/moviesvapp/components/navigation/BottomBar.kt +++ b/app/src/main/java/com/example/moviesvapp/components/navigation/BottomBar.kt @@ -1,11 +1,8 @@ package com.example.moviesvapp.components.navigation -import androidx.compose.foundation.background -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState import com.example.moviesvapp.components.AddItem @@ -19,12 +16,7 @@ fun BottomBar(navController: NavHostController) { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination - NavigationBar ( - modifier = Modifier.background( - color = MaterialTheme.colorScheme.primary, - shape = MaterialTheme.shapes.medium - ) - ){ + NavigationBar { screens.forEach { screen -> AddItem( screen = screen, diff --git a/app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavGraph.kt b/app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavGraph.kt index 9998f6f..7ff6f93 100644 --- a/app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavGraph.kt +++ b/app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavGraph.kt @@ -1,25 +1,27 @@ package com.example.moviesvapp.components.navigation import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import com.example.moviesvapp.components.FavoriteScreen import com.example.moviesvapp.components.MoviesScreen -import com.example.moviesvapp.model.MoviesViewModel +import com.example.moviesvapp.viewmodel.MoviesViewModel @Composable -fun BottomNavGraph(navController: NavHostController) { +fun BottomNavGraph( + navController: NavHostController) { NavHost( navController = navController, startDestination = BottomNavItem.Home.route ) { composable(route = BottomNavItem.Home.route) { MoviesScreen(viewModel = MoviesViewModel()) - - } + } composable(route = BottomNavItem.Favorite.route) { - + FavoriteScreen() } } } \ No newline at end of file diff --git a/app/src/main/java/com/example/moviesvapp/model/MoviesResponse.kt b/app/src/main/java/com/example/moviesvapp/model/MoviesResponse.kt index 87762a9..2a5da50 100644 --- a/app/src/main/java/com/example/moviesvapp/model/MoviesResponse.kt +++ b/app/src/main/java/com/example/moviesvapp/model/MoviesResponse.kt @@ -1,4 +1,12 @@ package com.example.moviesvapp.model -data class MoviesResponse(val Search: List, val totalResults: String, val Response: String) +import com.google.gson.annotations.SerializedName + +data class MoviesResponse( + @SerializedName("Search") + val search: List, + val totalResults: String, + @SerializedName("Response") + val response: String +) diff --git a/app/src/main/java/com/example/moviesvapp/model/PreferenceRepository.kt b/app/src/main/java/com/example/moviesvapp/model/PreferenceRepository.kt new file mode 100644 index 0000000..e527aa8 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/model/PreferenceRepository.kt @@ -0,0 +1,14 @@ +package com.example.moviesvapp.model + +import android.content.SharedPreferences + +class PreferenceRepository(private val sharedPreferences: SharedPreferences) { + + fun getApiKey(): String? { + return sharedPreferences.getApiKey() + } + + fun saveApiKey(apiKey: String) { + sharedPreferences.saveApiKey(apiKey) + } +} diff --git a/app/src/main/java/com/example/moviesvapp/model/Resource.kt b/app/src/main/java/com/example/moviesvapp/model/Resource.kt new file mode 100644 index 0000000..074e565 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/model/Resource.kt @@ -0,0 +1,8 @@ +package com.example.moviesvapp.model + +sealed class Resource { + data class Success(val data: T) : Resource() + data class Error(val message: String) : Resource() + object Loading : Resource() + class Idle : Resource() +} diff --git a/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt b/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt new file mode 100644 index 0000000..d5416b2 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt @@ -0,0 +1,13 @@ +package com.example.moviesvapp.model + +import android.content.SharedPreferences + + +fun SharedPreferences.saveApiKey(apiKey: String) { + edit().putString("API_KEY", apiKey).apply() +} + +fun SharedPreferences.getApiKey(): String? { + return getString("API_KEY", null) + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt b/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt index b6d3947..6fcf340 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt @@ -1,27 +1,39 @@ package com.example.moviesvapp.ui +import android.content.Context import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.activity.viewModels +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import com.example.moviesvapp.components.LoginScreen import com.example.moviesvapp.components.MainScreen -import com.example.moviesvapp.components.MoviesScreen -import com.example.moviesvapp.model.MoviesViewModel -import com.example.musicplayercompose.ui.theme.MyApplicationTheme +import com.example.moviesvapp.model.getApiKey +import com.example.moviesvapp.model.saveApiKey +import com.example.moviesvapp.ui.theme.MyApplicationTheme +import com.example.moviesvapp.viewmodel.LoginViewModel class MainActivity : ComponentActivity() { + private val viewModel: LoginViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyApplicationTheme { - MainScreen() + val sharedPreferences = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE) + val loggedInApiKey = remember { mutableStateOf(sharedPreferences.getApiKey()) } + + if (loggedInApiKey.value == null) { + LoginScreen(viewModel) { apiKey -> + loggedInApiKey.value = apiKey + sharedPreferences.saveApiKey(apiKey) + } + } else { + MainScreen() + } } } - /*setContent { - val viewModel: MoviesViewModel = viewModel() - MoviesScreen(viewModel) - }*/ } } - diff --git a/app/src/main/java/com/example/moviesvapp/ui/theme/Color.kt b/app/src/main/java/com/example/moviesvapp/ui/theme/Color.kt index 4eb5e1d..aae63d3 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/theme/Color.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/theme/Color.kt @@ -1,68 +1,68 @@ -package com.example.musicplayercompose.ui.theme +package com.example.moviesvapp.ui.theme import androidx.compose.ui.graphics.Color -//Dark colors -val DarkPrimary = Color(0xFFAEC6FF) -val DarkOnPrimary = Color(0xFF002E6A) -val DarkPrimaryContainer = Color(0xFF12448F) -val DarkOnPrimaryContainer = Color(0xFFD8E2FF) -val DarkSecondary = Color(0xFF4FD8EB) -val DarkOnSecondary = Color(0xFF00363D) -val DarkSecondaryContainer = Color(0xFF004F58) -val DarkOnSecondaryContainer = Color(0xFF97F0FF) -val DarkTertiary = Color(0xFF92CCFF) -val DarkOnTertiary = Color(0xFF003351) -val DarkTertiaryContainer = Color(0xFF004B73) -val DarkOnTertiaryContainer = Color(0xFFCCE5FF) +///Dark colors +val DarkPrimary = Color(0xFFE50914) +val DarkOnPrimary = Color(0xFFFFFFFF) +val DarkPrimaryContainer = Color(0xFFB20710) +val DarkOnPrimaryContainer = Color(0xFFFFFFFF) +val DarkSecondary = Color(0xFF221F1F) +val DarkOnSecondary = Color(0xFFE3E2E6) +val DarkSecondaryContainer = Color(0xFF44474F) +val DarkOnSecondaryContainer = Color(0xFFC5C6D0) +val DarkTertiary = Color(0xFF414141) +val DarkOnTertiary = Color(0xFFE3E2E6) +val DarkTertiaryContainer = Color(0xFF1B1B1F) +val DarkOnTertiaryContainer = Color(0xFFE3E2E6) val DarkError = Color(0xFFFFB4AB) val DarkOnError = Color(0xFF690005) val DarkErrorContainer = Color(0xFF93000A) val DarkOnErrorContainer = Color(0xFFFFDAD6) -val DarkBackground = Color(0xFF1B1B1F) +val DarkBackground = Color(0xFF000000) val DarkOnBackground = Color(0xFFE3E2E6) -val DarkSurface = Color(0xFF1B1B1F) +val DarkSurface = Color(0xFF000000) val DarkOnSurface = Color(0xFFE3E2E6) val DarkSurfaceVariant = Color(0xFF44474F) val DarkOnSurfaceVariant = Color(0xFFC5C6D0) val DarkOutline = Color(0xFF8E9099) val DarkInverseSurface = Color(0xFFE3E2E6) -val DarkInverseOnSurface = Color(0xFF1B1B1F) -val DarkInversePrimary = Color(0xFF325CA8) +val DarkInverseOnSurface = Color(0xFF000000) +val DarkInversePrimary = Color(0xFFE50914) val DarkShadow = Color(0xFF000000) -val DarkSurfaceTint = Color(0xFFAEC6FF) +val DarkSurfaceTint = Color(0xFFE50914) val DarkOutlineVariant = Color(0xFF44474F) val DarkScrim = Color(0xFF000000) - //Light Colors -val PrimaryLight = Color(0xFF325CA8) +val PrimaryLight = Color(0xFFE50914) val OnPrimary = Color(0xFFFFFFFF) -val PrimaryContainer = Color(0xFFD8E2FF) -val OnPrimaryContainer = Color(0xFF001A42) -val Secondary = Color(0xFF006874) -val OnSecondary = Color(0xFFFFFFFF) -val SecondaryContainer = Color(0xFF97F0FF) -val OnSecondaryContainer = Color(0xFF001F24) -val Tertiary = Color(0xFF006497) -val OnTertiary = Color(0xFFFFFFFF) -val TertiaryContainer = Color(0xFFCCE5FF) -val OnTertiaryContainer = Color(0xFF001E31) +val PrimaryContainer = Color(0xFFB20710) +val OnPrimaryContainer = Color(0xFFFFFFFF) +val Secondary = Color(0xFF221F1F) +val OnSecondary = Color(0xFFE3E2E6) +val SecondaryContainer = Color(0xFF44474F) +val OnSecondaryContainer = Color(0xFFC5C6D0) +val Tertiary = Color(0xFF414141) +val OnTertiary = Color(0xFFE3E2E6) +val TertiaryContainer = Color(0xFF1B1B1F) +val OnTertiaryContainer = Color(0xFFE3E2E6) val Error = Color(0xFFBA1A1A) val OnError = Color(0xFFFFFFFF) val ErrorContainer = Color(0xFFFFDAD6) val OnErrorContainer = Color(0xFF410002) -val Background = Color(0xFFFEFBFF) +val Background = Color(0xFFFFFFFF) val OnBackground = Color(0xFF1B1B1F) -val Surface = Color(0xFFFEFBFF) +val Surface = Color(0xFFFFFFFF) val OnSurface = Color(0xFF1B1B1F) val SurfaceVariant = Color(0xFFE1E2EC) val OnSurfaceVariant = Color(0xFF44474F) val Outline = Color(0xFF75777F) val InverseSurface = Color(0xFF303034) val InverseOnSurface = Color(0xFFF2F0F4) -val InversePrimary = Color(0xFFAEC6FF) +val InversePrimary = Color(0xFFE50914) val Shadow = Color(0xFF000000) -val SurfaceTint = Color(0xFF325CA8) +val SurfaceTint = Color(0xFFE50914) val OutlineVariant = Color(0xFFC5C6D0) val Scrim = Color(0xFF000000) + diff --git a/app/src/main/java/com/example/moviesvapp/ui/theme/Font.kt b/app/src/main/java/com/example/moviesvapp/ui/theme/Font.kt index fbd136b..cc28add 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/theme/Font.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/theme/Font.kt @@ -1,4 +1,4 @@ -package com.example.musicplayercompose.ui.theme +package com.example.moviesvapp.ui.theme import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily diff --git a/app/src/main/java/com/example/moviesvapp/ui/theme/Theme.kt b/app/src/main/java/com/example/moviesvapp/ui/theme/Theme.kt index d82211c..25d58c8 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/theme/Theme.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/theme/Theme.kt @@ -1,4 +1,4 @@ -package com.example.musicplayercompose.ui.theme +package com.example.moviesvapp.ui.theme import android.app.Activity import android.os.Build @@ -105,7 +105,6 @@ fun MyApplicationTheme( WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme } } - MaterialTheme( colorScheme = colorScheme, typography = Typography, diff --git a/app/src/main/java/com/example/moviesvapp/ui/theme/Type.kt b/app/src/main/java/com/example/moviesvapp/ui/theme/Type.kt index bf5db80..98f8bab 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/theme/Type.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/theme/Type.kt @@ -1,11 +1,10 @@ -package com.example.musicplayercompose.ui.theme +package com.example.moviesvapp.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp - val Typography = Typography( bodyLarge = TextStyle( fontFamily = NotoSerif, @@ -13,5 +12,19 @@ val Typography = Typography( fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.5.sp - ) -) + ), + bodyMedium = TextStyle( + fontFamily = NotoSerif, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.5.sp + ), + bodySmall = TextStyle( + fontFamily = NotoSerif, + fontWeight = FontWeight.Light, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ), +) \ No newline at end of file diff --git a/app/src/main/java/com/example/moviesvapp/viewmodel/LoginViewModel.kt b/app/src/main/java/com/example/moviesvapp/viewmodel/LoginViewModel.kt new file mode 100644 index 0000000..b49f608 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/viewmodel/LoginViewModel.kt @@ -0,0 +1,44 @@ +package com.example.moviesvapp.viewmodel + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.moviesvapp.model.Resource +import com.example.moviesvapp.model.RetrofitInstance + + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import retrofit2.HttpException + +class LoginViewModel : ViewModel() { + private val _loginStatus = MutableStateFlow>(Resource.Idle()) + val loginStatus: StateFlow> = _loginStatus + + fun login(username: String, apiKey: String) { + viewModelScope.launch { + _loginStatus.value = Resource.Loading + try { + val response = RetrofitInstance.omdbApi.searchMovies(apiKey, "test") + if (response.response == "True") { + _loginStatus.value = Resource.Success("Username: $username, API Key: $apiKey") + } else { + _loginStatus.value = Resource.Error("Invalid API key") + } + } catch (e: HttpException) { + if (e.code() == 401) { + _loginStatus.value = Resource.Error("Invalid API key") + } else { + _loginStatus.value = Resource.Error("HTTP ${e.code()}") + } + Log.d("LoginError", "HTTP Exception caught: ${e.message}") + } catch (e: Exception) { + _loginStatus.value = Resource.Error(e.message ?: "An error occurred") + Log.d("LoginError", "Exception caught: ${e.message}") + } + + } + } + +} diff --git a/app/src/main/java/com/example/moviesvapp/model/MoviesViewModel.kt b/app/src/main/java/com/example/moviesvapp/viewmodel/MoviesViewModel.kt similarity index 79% rename from app/src/main/java/com/example/moviesvapp/model/MoviesViewModel.kt rename to app/src/main/java/com/example/moviesvapp/viewmodel/MoviesViewModel.kt index e9ae628..6eff3b8 100644 --- a/app/src/main/java/com/example/moviesvapp/model/MoviesViewModel.kt +++ b/app/src/main/java/com/example/moviesvapp/viewmodel/MoviesViewModel.kt @@ -1,8 +1,10 @@ -package com.example.moviesvapp.model +package com.example.moviesvapp.viewmodel import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.example.moviesvapp.model.Movie +import com.example.moviesvapp.model.RetrofitInstance import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -17,11 +19,11 @@ class MoviesViewModel : ViewModel() { viewModelScope.launch(Dispatchers.IO) { try { val apiKey = "458ec391" - val trimmedQuery = query.trim() // Trim the query + val trimmedQuery = query.trim() val response = omdbApi.searchMovies(apiKey, trimmedQuery) withContext(Dispatchers.Main) { - if (response.Response == "True") { - movies.value = response.Search + if (response.response == "True") { + movies.value = response.search isLoading.value = false } else { movies.value = emptyList() From 0dc10a3974b11482a37e38ddd695ca340f67b019 Mon Sep 17 00:00:00 2001 From: KevinMartinezC <90873838+KevinMartinezC@users.noreply.github.com> Date: Wed, 26 Apr 2023 21:24:59 -0600 Subject: [PATCH 03/16] feat: get api key save it with SharedPreference to search movies --- .../moviesvapp/model/PreferenceRepository.kt | 14 ------ .../model/SharedPreferencesExtensions.kt | 9 +++- .../com/example/moviesvapp/ui/MainActivity.kt | 22 ++++++---- .../{ => ui}/components/MainScreen.kt | 12 ++--- .../components/favorite}/FavoriteScreen.kt | 2 +- .../components/home}/MovieItem.kt | 6 ++- .../components/home}/MoviesScreen.kt | 18 ++++---- .../home}/viewmodel/MoviesViewModel.kt | 13 ++++-- .../components/login}/LoginScreen.kt | 38 +++++++--------- .../login/viewmodel/LoginViewModel.kt | 33 ++++++++++++++ .../components/navigation/BottomBar.kt | 4 +- .../components/navigation/BottomNavGraph.kt | 14 +++--- .../components/navigation/BottomNavItem.kt | 2 +- .../moviesvapp/viewmodel/LoginViewModel.kt | 44 ------------------- app/src/main/res/values/strings.xml | 9 ++++ 15 files changed, 123 insertions(+), 117 deletions(-) delete mode 100644 app/src/main/java/com/example/moviesvapp/model/PreferenceRepository.kt rename app/src/main/java/com/example/moviesvapp/{ => ui}/components/MainScreen.kt (81%) rename app/src/main/java/com/example/moviesvapp/{components => ui/components/favorite}/FavoriteScreen.kt (73%) rename app/src/main/java/com/example/moviesvapp/{components => ui/components/home}/MovieItem.kt (88%) rename app/src/main/java/com/example/moviesvapp/{components => ui/components/home}/MoviesScreen.kt (85%) rename app/src/main/java/com/example/moviesvapp/{ => ui/components/home}/viewmodel/MoviesViewModel.kt (72%) rename app/src/main/java/com/example/moviesvapp/{components => ui/components/login}/LoginScreen.kt (70%) create mode 100644 app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt rename app/src/main/java/com/example/moviesvapp/{ => ui}/components/navigation/BottomBar.kt (87%) rename app/src/main/java/com/example/moviesvapp/{ => ui}/components/navigation/BottomNavGraph.kt (56%) rename app/src/main/java/com/example/moviesvapp/{ => ui}/components/navigation/BottomNavItem.kt (91%) delete mode 100644 app/src/main/java/com/example/moviesvapp/viewmodel/LoginViewModel.kt diff --git a/app/src/main/java/com/example/moviesvapp/model/PreferenceRepository.kt b/app/src/main/java/com/example/moviesvapp/model/PreferenceRepository.kt deleted file mode 100644 index e527aa8..0000000 --- a/app/src/main/java/com/example/moviesvapp/model/PreferenceRepository.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.moviesvapp.model - -import android.content.SharedPreferences - -class PreferenceRepository(private val sharedPreferences: SharedPreferences) { - - fun getApiKey(): String? { - return sharedPreferences.getApiKey() - } - - fun saveApiKey(apiKey: String) { - sharedPreferences.saveApiKey(apiKey) - } -} diff --git a/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt b/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt index d5416b2..8eb3573 100644 --- a/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt +++ b/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt @@ -3,11 +3,18 @@ package com.example.moviesvapp.model import android.content.SharedPreferences +fun SharedPreferences.saveUsername(username: String) { + edit().putString("USERNAME", username).apply() +} + +fun SharedPreferences.getUsername(): String? { + return getString("USERNAME", null) +} + fun SharedPreferences.saveApiKey(apiKey: String) { edit().putString("API_KEY", apiKey).apply() } fun SharedPreferences.getApiKey(): String? { return getString("API_KEY", null) - } \ No newline at end of file diff --git a/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt b/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt index 6fcf340..065c81d 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt @@ -2,17 +2,19 @@ package com.example.moviesvapp.ui import android.content.Context import android.os.Bundle +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import com.example.moviesvapp.components.LoginScreen -import com.example.moviesvapp.components.MainScreen -import com.example.moviesvapp.model.getApiKey +import com.example.moviesvapp.ui.components.login.LoginScreen +import com.example.moviesvapp.ui.components.MainScreen +import com.example.moviesvapp.model.getUsername import com.example.moviesvapp.model.saveApiKey +import com.example.moviesvapp.model.saveUsername import com.example.moviesvapp.ui.theme.MyApplicationTheme -import com.example.moviesvapp.viewmodel.LoginViewModel +import com.example.moviesvapp.ui.components.login.viewmodel.LoginViewModel class MainActivity : ComponentActivity() { @@ -20,15 +22,19 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setContent { MyApplicationTheme { val sharedPreferences = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE) - val loggedInApiKey = remember { mutableStateOf(sharedPreferences.getApiKey()) } + val loggedInUsername = remember { mutableStateOf(sharedPreferences.getUsername()) } + Log.d("MainActivity", "Retrieved username from SharedPreferences: ${loggedInUsername.value}") - if (loggedInApiKey.value == null) { - LoginScreen(viewModel) { apiKey -> - loggedInApiKey.value = apiKey + if (loggedInUsername.value == null) { + LoginScreen(viewModel) { username, apiKey -> + loggedInUsername.value = username + sharedPreferences.saveUsername(username) sharedPreferences.saveApiKey(apiKey) + Log.d("MainActivity", "Saved username and API key to SharedPreferences: $username, $apiKey") } } else { MainScreen() diff --git a/app/src/main/java/com/example/moviesvapp/components/MainScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt similarity index 81% rename from app/src/main/java/com/example/moviesvapp/components/MainScreen.kt rename to app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt index ce0f7e8..cb771c9 100644 --- a/app/src/main/java/com/example/moviesvapp/components/MainScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt @@ -1,4 +1,4 @@ -package com.example.moviesvapp.components +package com.example.moviesvapp.ui.components import android.annotation.SuppressLint import androidx.compose.foundation.layout.RowScope @@ -10,14 +10,16 @@ import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import com.example.moviesvapp.components.navigation.BottomBar -import com.example.moviesvapp.components.navigation.BottomNavGraph -import com.example.moviesvapp.components.navigation.BottomNavItem +import com.example.moviesvapp.R +import com.example.moviesvapp.ui.components.navigation.BottomBar +import com.example.moviesvapp.ui.components.navigation.BottomNavGraph +import com.example.moviesvapp.ui.components.navigation.BottomNavItem @OptIn(ExperimentalMaterial3Api::class) @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @@ -44,7 +46,7 @@ fun RowScope.AddItem( icon = { Icon( imageVector = screen.icon, - contentDescription = "Navigation Icon", + contentDescription = stringResource(R.string.navigation_icon), tint = MaterialTheme.colorScheme.primary ) diff --git a/app/src/main/java/com/example/moviesvapp/components/FavoriteScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt similarity index 73% rename from app/src/main/java/com/example/moviesvapp/components/FavoriteScreen.kt rename to app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt index 5ad0ff5..d7c9e35 100644 --- a/app/src/main/java/com/example/moviesvapp/components/FavoriteScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt @@ -1,4 +1,4 @@ -package com.example.moviesvapp.components +package com.example.moviesvapp.ui.components.favorite import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/app/src/main/java/com/example/moviesvapp/components/MovieItem.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt similarity index 88% rename from app/src/main/java/com/example/moviesvapp/components/MovieItem.kt rename to app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt index d6a064c..1833e48 100644 --- a/app/src/main/java/com/example/moviesvapp/components/MovieItem.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt @@ -1,4 +1,4 @@ -package com.example.moviesvapp.components +package com.example.moviesvapp.ui.components.home import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column @@ -13,8 +13,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter +import com.example.moviesvapp.R import com.example.moviesvapp.model.Movie @Composable @@ -28,7 +30,7 @@ fun MovieItem(movie: Movie) { val painter = rememberAsyncImagePainter(model = movie.poster) Image( painter = painter, - contentDescription = "Movie Poster", + contentDescription = stringResource(R.string.movie_poster), modifier = Modifier .height(100.dp) .width(75.dp) diff --git a/app/src/main/java/com/example/moviesvapp/components/MoviesScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt similarity index 85% rename from app/src/main/java/com/example/moviesvapp/components/MoviesScreen.kt rename to app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt index b225d02..beb5d57 100644 --- a/app/src/main/java/com/example/moviesvapp/components/MoviesScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt @@ -1,6 +1,5 @@ -package com.example.moviesvapp.components +package com.example.moviesvapp.ui.components.home -import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -25,14 +24,17 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp -import com.example.moviesvapp.viewmodel.MoviesViewModel +import com.example.moviesvapp.R +import com.example.moviesvapp.ui.components.home.viewmodel.MoviesViewModel import com.example.moviesvapp.ui.theme.MyApplicationTheme @OptIn(ExperimentalMaterial3Api::class) @Composable fun MoviesScreen(viewModel: MoviesViewModel) { + val movies by viewModel.movies val isLoading by viewModel.isLoading var searchQuery by remember { mutableStateOf(TextFieldValue("")) } @@ -46,20 +48,20 @@ fun MoviesScreen(viewModel: MoviesViewModel) { horizontalAlignment = Alignment.CenterHorizontally ) { TextField( - value = searchQuery, onValueChange = { searchQuery = it }, - label = { Text("Search movies") }, + label = { Text(stringResource(R.string.search_movies)) }, modifier = Modifier.fillMaxWidth(), maxLines = 1, trailingIcon = { IconButton( onClick = { - Log.d("MoviesViewModel", "Search Icon Clicked") - viewModel.searchMovies(searchQuery.text) } ) { - Icon(Icons.Default.Search, contentDescription = "Search Icon") + Icon( + Icons.Default.Search, + contentDescription = stringResource(R.string.search_icon) + ) } } ) diff --git a/app/src/main/java/com/example/moviesvapp/viewmodel/MoviesViewModel.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt similarity index 72% rename from app/src/main/java/com/example/moviesvapp/viewmodel/MoviesViewModel.kt rename to app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt index 6eff3b8..c1f79eb 100644 --- a/app/src/main/java/com/example/moviesvapp/viewmodel/MoviesViewModel.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt @@ -1,24 +1,31 @@ -package com.example.moviesvapp.viewmodel +package com.example.moviesvapp.ui.components.home.viewmodel +import android.content.Context +import android.content.SharedPreferences import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.moviesvapp.model.Movie import com.example.moviesvapp.model.RetrofitInstance +import com.example.moviesvapp.model.getApiKey import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class MoviesViewModel : ViewModel() { +class MoviesViewModel(context: Context) : ViewModel() { val movies = mutableStateOf>(emptyList()) val isLoading = mutableStateOf(false) private val omdbApi = RetrofitInstance.omdbApi + private val sharedPreferences: SharedPreferences = + context.getSharedPreferences("MyPrefs", Context.MODE_PRIVATE) fun searchMovies(query: String) { + viewModelScope.launch(Dispatchers.IO) { try { - val apiKey = "458ec391" + val apiKey = sharedPreferences.getApiKey() + ?: throw IllegalStateException("API Key not found") val trimmedQuery = query.trim() val response = omdbApi.searchMovies(apiKey, trimmedQuery) withContext(Dispatchers.Main) { diff --git a/app/src/main/java/com/example/moviesvapp/components/LoginScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginScreen.kt similarity index 70% rename from app/src/main/java/com/example/moviesvapp/components/LoginScreen.kt rename to app/src/main/java/com/example/moviesvapp/ui/components/login/LoginScreen.kt index 05f3d42..f664ea2 100644 --- a/app/src/main/java/com/example/moviesvapp/components/LoginScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginScreen.kt @@ -1,6 +1,5 @@ -package com.example.moviesvapp.components +package com.example.moviesvapp.ui.components.login -import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -15,21 +14,23 @@ import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.example.moviesvapp.R import com.example.moviesvapp.model.Resource -import com.example.moviesvapp.viewmodel.LoginViewModel +import com.example.moviesvapp.ui.components.login.viewmodel.LoginViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun LoginScreen(viewModel: LoginViewModel, onLoginSuccess: (String) -> Unit) { +fun LoginScreen(viewModel: LoginViewModel, onLoginSuccess: (String, String) -> Unit) { + val username = remember { mutableStateOf("") } val apiKey = remember { mutableStateOf("") } - val loginStatus by viewModel.loginStatus.collectAsState() + val loginStatus = viewModel.loginStatus.collectAsState().value Column( modifier = Modifier @@ -41,49 +42,42 @@ fun LoginScreen(viewModel: LoginViewModel, onLoginSuccess: (String) -> Unit) { TextField( value = username.value, onValueChange = { username.value = it }, - label = { Text("Username") }, + label = { Text(stringResource(R.string.username)) }, singleLine = true ) Spacer(modifier = Modifier.height(8.dp)) TextField( value = apiKey.value, onValueChange = { apiKey.value = it }, - label = { Text("API Key") }, + label = { Text(stringResource(R.string.password)) }, singleLine = true ) Spacer(modifier = Modifier.height(8.dp)) Button(onClick = { - viewModel.login(username = username.value, apiKey= apiKey.value) }) { + viewModel.login(username = username.value, apiKey = apiKey.value) + }) { Text("Login") } - when (loginStatus) { - is Resource.Loading -> Text("Logging in...") + is Resource.Loading -> Text(stringResource(R.string.logging_in)) is Resource.Error -> { - val errorStatus = loginStatus as Resource.Error - val userFriendlyMessage = when (errorStatus.message) { - "Invalid API key" -> "Invalid API key. Please check your Password and try again." - else -> "An error occurred. Please try again later." - } Text( - userFriendlyMessage, + stringResource(R.string.invalid__key_please_check_your_password), color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodyMedium ) } - is Resource.Success -> { - val successStatus = loginStatus as Resource.Success + val (loggedInUsername, loggedInApiKey) = loginStatus.data LaunchedEffect(Unit) { - onLoginSuccess(successStatus.data) + onLoginSuccess(loggedInUsername, loggedInApiKey) } } is Resource.Idle -> { Text( - "Please enter your username and password", - + stringResource(R.string.please_enter_your_username_and_password), color = MaterialTheme.colorScheme.onBackground, style = MaterialTheme.typography.bodyMedium ) diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt b/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt new file mode 100644 index 0000000..3fafac0 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt @@ -0,0 +1,33 @@ +package com.example.moviesvapp.ui.components.login.viewmodel + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.moviesvapp.model.Resource +import com.example.moviesvapp.model.RetrofitInstance +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + + +class LoginViewModel : ViewModel() { + private val loginStatusMutable = MutableStateFlow>>(Resource.Idle()) + val loginStatus: StateFlow>> = loginStatusMutable + + fun login(username: String, apiKey: String) { + viewModelScope.launch { + loginStatusMutable.value = Resource.Loading + try { + val response = RetrofitInstance.omdbApi.searchMovies(apiKey, "marvel") + if (response.response == "True") { + loginStatusMutable.value = Resource.Success(Pair(username, apiKey)) + } else { + loginStatusMutable.value = Resource.Error("Invalid API key") + } + } catch (e: Exception) { + loginStatusMutable.value = Resource.Error(e.message ?: "An error occurred") + Log.d("LoginError", "Exception caught: ${e.message}") + } + } + } +} diff --git a/app/src/main/java/com/example/moviesvapp/components/navigation/BottomBar.kt b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomBar.kt similarity index 87% rename from app/src/main/java/com/example/moviesvapp/components/navigation/BottomBar.kt rename to app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomBar.kt index eaf7e94..991c423 100644 --- a/app/src/main/java/com/example/moviesvapp/components/navigation/BottomBar.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomBar.kt @@ -1,11 +1,11 @@ -package com.example.moviesvapp.components.navigation +package com.example.moviesvapp.ui.components.navigation import androidx.compose.material3.NavigationBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState -import com.example.moviesvapp.components.AddItem +import com.example.moviesvapp.ui.components.AddItem @Composable fun BottomBar(navController: NavHostController) { diff --git a/app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavGraph.kt b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavGraph.kt similarity index 56% rename from app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavGraph.kt rename to app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavGraph.kt index 7ff6f93..bc0bd75 100644 --- a/app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavGraph.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavGraph.kt @@ -1,24 +1,26 @@ -package com.example.moviesvapp.components.navigation +package com.example.moviesvapp.ui.components.navigation import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import com.example.moviesvapp.components.FavoriteScreen -import com.example.moviesvapp.components.MoviesScreen -import com.example.moviesvapp.viewmodel.MoviesViewModel +import com.example.moviesvapp.ui.components.favorite.FavoriteScreen +import com.example.moviesvapp.ui.components.home.MoviesScreen +import com.example.moviesvapp.ui.components.home.viewmodel.MoviesViewModel @Composable fun BottomNavGraph( navController: NavHostController) { + val context = LocalContext.current + NavHost( navController = navController, startDestination = BottomNavItem.Home.route ) { composable(route = BottomNavItem.Home.route) { - MoviesScreen(viewModel = MoviesViewModel()) + MoviesScreen(viewModel = MoviesViewModel(context)) } composable(route = BottomNavItem.Favorite.route) { FavoriteScreen() diff --git a/app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavItem.kt b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavItem.kt similarity index 91% rename from app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavItem.kt rename to app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavItem.kt index d1e1b46..326067d 100644 --- a/app/src/main/java/com/example/moviesvapp/components/navigation/BottomNavItem.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavItem.kt @@ -1,4 +1,4 @@ -package com.example.moviesvapp.components.navigation +package com.example.moviesvapp.ui.components.navigation import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search diff --git a/app/src/main/java/com/example/moviesvapp/viewmodel/LoginViewModel.kt b/app/src/main/java/com/example/moviesvapp/viewmodel/LoginViewModel.kt deleted file mode 100644 index b49f608..0000000 --- a/app/src/main/java/com/example/moviesvapp/viewmodel/LoginViewModel.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.example.moviesvapp.viewmodel - -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.example.moviesvapp.model.Resource -import com.example.moviesvapp.model.RetrofitInstance - - -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import retrofit2.HttpException - -class LoginViewModel : ViewModel() { - private val _loginStatus = MutableStateFlow>(Resource.Idle()) - val loginStatus: StateFlow> = _loginStatus - - fun login(username: String, apiKey: String) { - viewModelScope.launch { - _loginStatus.value = Resource.Loading - try { - val response = RetrofitInstance.omdbApi.searchMovies(apiKey, "test") - if (response.response == "True") { - _loginStatus.value = Resource.Success("Username: $username, API Key: $apiKey") - } else { - _loginStatus.value = Resource.Error("Invalid API key") - } - } catch (e: HttpException) { - if (e.code() == 401) { - _loginStatus.value = Resource.Error("Invalid API key") - } else { - _loginStatus.value = Resource.Error("HTTP ${e.code()}") - } - Log.d("LoginError", "HTTP Exception caught: ${e.message}") - } catch (e: Exception) { - _loginStatus.value = Resource.Error(e.message ?: "An error occurred") - Log.d("LoginError", "Exception caught: ${e.message}") - } - - } - } - -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e96199a..1441031 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,12 @@ Movie SV APP + Invalid API key. Please check your Password and try again. + Logging in… + Please enter your username and password + Password + Username + Navigation Icon + Movie Poster + Search Icon + Search movies \ No newline at end of file From 38def3bbd93a824cecc38f69cefee9723ec928ba Mon Sep 17 00:00:00 2001 From: KevinMartinezC <90873838+KevinMartinezC@users.noreply.github.com> Date: Wed, 26 Apr 2023 23:06:19 -0600 Subject: [PATCH 04/16] feat: make a ModalNavigationDrawer to be able to logout --- .../com/example/moviesvapp/model/Resource.kt | 2 +- .../model/SharedPreferencesExtensions.kt | 10 +- .../com/example/moviesvapp/ui/MainActivity.kt | 11 ++- .../moviesvapp/ui/components/MainScreen.kt | 92 ++++++++++++++++++- .../ui/components/home/MoviesScreen.kt | 6 +- .../ui/components/login/LoginScreen.kt | 2 +- .../login/viewmodel/LoginViewModel.kt | 2 +- app/src/main/res/values/strings.xml | 1 + 8 files changed, 113 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/example/moviesvapp/model/Resource.kt b/app/src/main/java/com/example/moviesvapp/model/Resource.kt index 074e565..de3f56d 100644 --- a/app/src/main/java/com/example/moviesvapp/model/Resource.kt +++ b/app/src/main/java/com/example/moviesvapp/model/Resource.kt @@ -4,5 +4,5 @@ sealed class Resource { data class Success(val data: T) : Resource() data class Error(val message: String) : Resource() object Loading : Resource() - class Idle : Resource() + object Idle : Resource() } diff --git a/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt b/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt index 8eb3573..0fddbc0 100644 --- a/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt +++ b/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt @@ -17,4 +17,12 @@ fun SharedPreferences.saveApiKey(apiKey: String) { fun SharedPreferences.getApiKey(): String? { return getString("API_KEY", null) -} \ No newline at end of file +} + +fun SharedPreferences.clearUsername() { + edit().remove("USERNAME").apply() +} + +fun SharedPreferences.clearApiKey() { + edit().remove("API_KEY").apply() +} diff --git a/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt b/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt index 065c81d..e1da3bd 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt @@ -8,6 +8,8 @@ import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import com.example.moviesvapp.model.clearApiKey +import com.example.moviesvapp.model.clearUsername import com.example.moviesvapp.ui.components.login.LoginScreen import com.example.moviesvapp.ui.components.MainScreen import com.example.moviesvapp.model.getUsername @@ -36,8 +38,13 @@ class MainActivity : ComponentActivity() { sharedPreferences.saveApiKey(apiKey) Log.d("MainActivity", "Saved username and API key to SharedPreferences: $username, $apiKey") } - } else { - MainScreen() + }else { + MainScreen(onLogout = { + loggedInUsername.value = null + sharedPreferences.clearUsername() + sharedPreferences.clearApiKey() + Log.d("MainActivity", "Cleared username and API key from SharedPreferences") + }) } } } diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt index cb771c9..12feb4c 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt @@ -1,16 +1,36 @@ package com.example.moviesvapp.ui.components import android.annotation.SuppressLint +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExitToApp +import androidx.compose.material3.Button +import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.NavigationDrawerItemDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraph.Companion.findStartDestination @@ -20,19 +40,81 @@ import com.example.moviesvapp.R import com.example.moviesvapp.ui.components.navigation.BottomBar import com.example.moviesvapp.ui.components.navigation.BottomNavGraph import com.example.moviesvapp.ui.components.navigation.BottomNavItem +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable -fun MainScreen() { +fun MainScreen(onLogout: () -> Unit) { val navController = rememberNavController() - Scaffold( - bottomBar = { BottomBar(navController = navController) } - ) { - BottomNavGraph(navController = navController) + val drawerState = rememberDrawerState(DrawerValue.Closed) + val scope = rememberCoroutineScope() + + MaterialTheme { + ModalNavigationDrawer( + drawerContent = { LogoutDrawer(onLogout = onLogout) }, + modifier = Modifier, + drawerState = drawerState, + scrimColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.32f), + content = { + Scaffold( + topBar = { + AppTopBar { scope.launch { drawerState.open() } } + }, + bottomBar = { BottomBar(navController = navController) } + ) { + BottomNavGraph(navController = navController) + } + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppTopBar(onLogoutButtonClick: () -> Unit) { + TopAppBar( + title = { Text("Search") }, + actions = { + IconButton(onClick = onLogoutButtonClick) { + Icon(Icons.Default.ExitToApp, contentDescription = "Logout") + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LogoutDrawer(onLogout: () -> Unit) { + ModalDrawerSheet { + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + NavigationDrawerItem( + + icon = { Icon(Icons.Default.ExitToApp, contentDescription = "Logout") }, + label = { + Button( + onClick = onLogout, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + content = { Text("Logout") } + ) + }, + selected = false, + onClick = onLogout, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) + ) + } + } } } + @Composable fun RowScope.AddItem( screen: BottomNavItem, diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt index beb5d57..7fd3803 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt @@ -44,7 +44,7 @@ fun MoviesScreen(viewModel: MoviesViewModel) { Column( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(56.dp), horizontalAlignment = Alignment.CenterHorizontally ) { TextField( @@ -70,7 +70,9 @@ fun MoviesScreen(viewModel: MoviesViewModel) { if (isLoading) { CircularProgressIndicator(modifier = Modifier.wrapContentSize(Alignment.Center)) } else { - LazyColumn { + LazyColumn( + modifier = Modifier.padding(bottom = 56.dp) // Add padding to avoid content being hidden behind the bottom bar + ) { items(movies.size) { index -> val movie = movies[index] MovieItem(movie) diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginScreen.kt index f664ea2..fcf4863 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginScreen.kt @@ -56,7 +56,7 @@ fun LoginScreen(viewModel: LoginViewModel, onLoginSuccess: (String, String) -> U Button(onClick = { viewModel.login(username = username.value, apiKey = apiKey.value) }) { - Text("Login") + Text(stringResource(R.string.login)) } when (loginStatus) { is Resource.Loading -> Text(stringResource(R.string.logging_in)) diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt b/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt index 3fafac0..1a2c1cf 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.launch class LoginViewModel : ViewModel() { - private val loginStatusMutable = MutableStateFlow>>(Resource.Idle()) + private val loginStatusMutable = MutableStateFlow>>(Resource.Idle) val loginStatus: StateFlow>> = loginStatusMutable fun login(username: String, apiKey: String) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1441031..45b89ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,4 +9,5 @@ Movie Poster Search Icon Search movies + Login \ No newline at end of file From aceb2d60a43fd6727185b91d03658ba95f28a49d Mon Sep 17 00:00:00 2001 From: kevinMartinezC Date: Thu, 27 Apr 2023 13:10:09 -0600 Subject: [PATCH 05/16] feat: get the last login date and updating code --- .../model/SharedPreferencesExtensions.kt | 23 +++++ .../com/example/moviesvapp/ui/MainActivity.kt | 57 ++++++------- .../moviesvapp/ui/components/MainScreen.kt | 36 +++++--- .../ui/components/home/MoviesScreen.kt | 3 +- .../home/viewmodel/MoviesViewModel.kt | 27 +++++- .../ui/components/login/LoginScreen.kt | 30 ++++--- .../ui/components/login/LoginUiState.kt | 10 +++ .../login/viewmodel/LoginViewModel.kt | 85 +++++++++++++++++-- 8 files changed, 203 insertions(+), 68 deletions(-) create mode 100644 app/src/main/java/com/example/moviesvapp/ui/components/login/LoginUiState.kt diff --git a/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt b/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt index 0fddbc0..5aba944 100644 --- a/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt +++ b/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt @@ -1,6 +1,10 @@ package com.example.moviesvapp.model import android.content.SharedPreferences +import android.os.Build +import androidx.annotation.RequiresApi +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter fun SharedPreferences.saveUsername(username: String) { @@ -26,3 +30,22 @@ fun SharedPreferences.clearUsername() { fun SharedPreferences.clearApiKey() { edit().remove("API_KEY").apply() } + +fun SharedPreferences.saveLastLoginDate(date: String) { + edit().putString("LAST_LOGIN_DATE", date).apply() +} + +fun SharedPreferences.getLastLoginDate(): String? { + return getString("LAST_LOGIN_DATE", null) +} + +fun SharedPreferences.clearLastLoginDate() { + edit().remove("LAST_LOGIN_DATE").apply() +} + +@RequiresApi(Build.VERSION_CODES.O) +fun getCurrentDateTime(): String { + val currentDateTime = LocalDateTime.now() + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + return currentDateTime.format(formatter) +} diff --git a/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt b/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt index e1da3bd..1b71dc2 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt @@ -1,52 +1,47 @@ package com.example.moviesvapp.ui -import android.content.Context +import android.os.Build import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.viewModels -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import com.example.moviesvapp.model.clearApiKey -import com.example.moviesvapp.model.clearUsername -import com.example.moviesvapp.ui.components.login.LoginScreen +import androidx.annotation.RequiresApi +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import com.example.moviesvapp.ui.components.MainScreen -import com.example.moviesvapp.model.getUsername -import com.example.moviesvapp.model.saveApiKey -import com.example.moviesvapp.model.saveUsername -import com.example.moviesvapp.ui.theme.MyApplicationTheme +import com.example.moviesvapp.ui.components.login.LoginScreen import com.example.moviesvapp.ui.components.login.viewmodel.LoginViewModel +import com.example.moviesvapp.ui.theme.MyApplicationTheme class MainActivity : ComponentActivity() { - private val viewModel: LoginViewModel by viewModels() + private lateinit var viewModel: LoginViewModel + @RequiresApi(Build.VERSION_CODES.O) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + viewModel = LoginViewModel(this) setContent { MyApplicationTheme { - val sharedPreferences = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE) - val loggedInUsername = remember { mutableStateOf(sharedPreferences.getUsername()) } - Log.d("MainActivity", "Retrieved username from SharedPreferences: ${loggedInUsername.value}") + val lastLoginDate = viewModel.getLastLoginDate() + val uiState by viewModel.uiState.collectAsState() - if (loggedInUsername.value == null) { - LoginScreen(viewModel) { username, apiKey -> - loggedInUsername.value = username - sharedPreferences.saveUsername(username) - sharedPreferences.saveApiKey(apiKey) - Log.d("MainActivity", "Saved username and API key to SharedPreferences: $username, $apiKey") - } - }else { - MainScreen(onLogout = { - loggedInUsername.value = null - sharedPreferences.clearUsername() - sharedPreferences.clearApiKey() - Log.d("MainActivity", "Cleared username and API key from SharedPreferences") - }) + if (!uiState.isLoggedIn) { + LoginScreen( + loginStatus = uiState.loginStatus, + onClickedLogin = viewModel::login, + onLogin = viewModel::saveCredentials, + ) + } else { + MainScreen( + onLogout = { + viewModel.clearData() + }, + username = uiState.userName, + lastLoginDate = lastLoginDate + ) } } } } } + diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt index 12feb4c..44dfe10 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt @@ -5,7 +5,9 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.* import androidx.compose.material.icons.Icons @@ -45,14 +47,16 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable -fun MainScreen(onLogout: () -> Unit) { +fun MainScreen(onLogout: () -> Unit, username: String?, lastLoginDate: String?) { val navController = rememberNavController() val drawerState = rememberDrawerState(DrawerValue.Closed) val scope = rememberCoroutineScope() MaterialTheme { ModalNavigationDrawer( - drawerContent = { LogoutDrawer(onLogout = onLogout) }, + drawerContent = { + LogoutDrawer(onLogout = onLogout, username = username, lastLoginDate = lastLoginDate) + }, modifier = Modifier, drawerState = drawerState, scrimColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.32f), @@ -70,6 +74,7 @@ fun MainScreen(onLogout: () -> Unit) { } } + @OptIn(ExperimentalMaterial3Api::class) @Composable fun AppTopBar(onLogoutButtonClick: () -> Unit) { @@ -85,36 +90,45 @@ fun AppTopBar(onLogoutButtonClick: () -> Unit) { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun LogoutDrawer(onLogout: () -> Unit) { +fun LogoutDrawer(onLogout: () -> Unit, username: String?, lastLoginDate: String?) { ModalDrawerSheet { Box(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier - .align(Alignment.BottomCenter) .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - NavigationDrawerItem( + if (username != null && lastLoginDate != null) { + Text(text = "User: $username") + Text(text = "Last Login: $lastLoginDate") + } + Spacer(modifier = Modifier.height(16.dp)) + + NavigationDrawerItem( icon = { Icon(Icons.Default.ExitToApp, contentDescription = "Logout") }, label = { - Button( - onClick = onLogout, - modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), - content = { Text("Logout") } - ) + Text(text = "User: $username") }, selected = false, - onClick = onLogout, + onClick = {}, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) ) + Button( + onClick = onLogout, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + content = { Text("Logout") } + ) } } } } + + + @Composable fun RowScope.AddItem( screen: BottomNavItem, diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt index 7fd3803..774d33f 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.material3.TextField 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 @@ -35,7 +36,7 @@ import com.example.moviesvapp.ui.theme.MyApplicationTheme @Composable fun MoviesScreen(viewModel: MoviesViewModel) { - val movies by viewModel.movies + val movies by viewModel.movies.collectAsState() val isLoading by viewModel.isLoading var searchQuery by remember { mutableStateOf(TextFieldValue("")) } diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt index c1f79eb..a458626 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt @@ -2,6 +2,7 @@ package com.example.moviesvapp.ui.components.home.viewmodel import android.content.Context import android.content.SharedPreferences +import android.util.Log import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -9,11 +10,15 @@ import com.example.moviesvapp.model.Movie import com.example.moviesvapp.model.RetrofitInstance import com.example.moviesvapp.model.getApiKey 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 class MoviesViewModel(context: Context) : ViewModel() { - val movies = mutableStateOf>(emptyList()) + val _movies = MutableStateFlow>(emptyList()) + val movies = _movies.asStateFlow() val isLoading = mutableStateOf(false) private val omdbApi = RetrofitInstance.omdbApi @@ -21,25 +26,39 @@ class MoviesViewModel(context: Context) : ViewModel() { context.getSharedPreferences("MyPrefs", Context.MODE_PRIVATE) fun searchMovies(query: String) { + Log.wtf("MOVIS","${query}") viewModelScope.launch(Dispatchers.IO) { try { val apiKey = sharedPreferences.getApiKey() ?: throw IllegalStateException("API Key not found") + Log.wtf("MOVIS","${apiKey}") + val trimmedQuery = query.trim() val response = omdbApi.searchMovies(apiKey, trimmedQuery) withContext(Dispatchers.Main) { if (response.response == "True") { - movies.value = response.search + _movies.update { + response.search + } + Log.wtf("MOVIS","${response.search}") + isLoading.value = false } else { - movies.value = emptyList() + _movies.update { + emptyList() + } + isLoading.value = false } } } catch (e: Exception) { + Log.wtf("MOVIS","${e.message}") + withContext(Dispatchers.Main) { - movies.value = emptyList() + _movies.update { + emptyList() + } isLoading.value = false } } diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginScreen.kt index fcf4863..92fb47b 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginScreen.kt @@ -6,31 +6,33 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.example.moviesvapp.R import com.example.moviesvapp.model.Resource -import com.example.moviesvapp.ui.components.login.viewmodel.LoginViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun LoginScreen(viewModel: LoginViewModel, onLoginSuccess: (String, String) -> Unit) { +fun LoginScreen( + loginStatus: Resource>, + onClickedLogin: (String, String) -> Unit, + onLogin: (String, String) -> Unit +) { val username = remember { mutableStateOf("") } val apiKey = remember { mutableStateOf("") } - val loginStatus = viewModel.loginStatus.collectAsState().value Column( modifier = Modifier @@ -50,15 +52,22 @@ fun LoginScreen(viewModel: LoginViewModel, onLoginSuccess: (String, String) -> U value = apiKey.value, onValueChange = { apiKey.value = it }, label = { Text(stringResource(R.string.password)) }, - singleLine = true + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) ) Spacer(modifier = Modifier.height(8.dp)) Button(onClick = { - viewModel.login(username = username.value, apiKey = apiKey.value) + onClickedLogin( username.value, apiKey.value) }) { Text(stringResource(R.string.login)) } + when (loginStatus) { + is Resource.Success -> { + val (loggedInUsername, loggedInApiKey) = loginStatus.data + onLogin(loggedInUsername, loggedInApiKey) + } + is Resource.Loading -> Text(stringResource(R.string.logging_in)) is Resource.Error -> { Text( @@ -68,13 +77,6 @@ fun LoginScreen(viewModel: LoginViewModel, onLoginSuccess: (String, String) -> U ) } - is Resource.Success -> { - val (loggedInUsername, loggedInApiKey) = loginStatus.data - LaunchedEffect(Unit) { - onLoginSuccess(loggedInUsername, loggedInApiKey) - } - } - is Resource.Idle -> { Text( stringResource(R.string.please_enter_your_username_and_password), diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginUiState.kt b/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginUiState.kt new file mode 100644 index 0000000..7643190 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginUiState.kt @@ -0,0 +1,10 @@ +package com.example.moviesvapp.ui.components.login + +import com.example.moviesvapp.model.Resource + +data class LoginUiState( + val isLoggedIn: Boolean = false, + val userName: String = "", + val loginStatus: Resource> = Resource.Idle +) + diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt b/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt index 1a2c1cf..7e7e9dd 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt @@ -1,31 +1,102 @@ package com.example.moviesvapp.ui.components.login.viewmodel +import android.content.Context +import android.content.SharedPreferences +import android.os.Build import android.util.Log +import androidx.annotation.RequiresApi import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.moviesvapp.model.Resource import com.example.moviesvapp.model.RetrofitInstance +import com.example.moviesvapp.model.clearApiKey +import com.example.moviesvapp.model.clearLastLoginDate +import com.example.moviesvapp.model.clearUsername +import com.example.moviesvapp.model.getApiKey +import com.example.moviesvapp.model.getCurrentDateTime +import com.example.moviesvapp.model.getLastLoginDate +import com.example.moviesvapp.model.getUsername +import com.example.moviesvapp.model.saveApiKey +import com.example.moviesvapp.model.saveLastLoginDate +import com.example.moviesvapp.model.saveUsername +import com.example.moviesvapp.ui.components.login.LoginUiState +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -class LoginViewModel : ViewModel() { - private val loginStatusMutable = MutableStateFlow>>(Resource.Idle) - val loginStatus: StateFlow>> = loginStatusMutable +class LoginViewModel(context: Context) : ViewModel() { + private val sharedPreferences: SharedPreferences = + context.getSharedPreferences("MyPrefs", Context.MODE_PRIVATE) + + private val _uiState = MutableStateFlow( + LoginUiState( + isLoggedIn = !sharedPreferences.getApiKey().isNullOrBlank().not() + ) + ) + val uiState = _uiState.asStateFlow() + + + fun getLastLoginDate(): String? { + return sharedPreferences.getLastLoginDate() + } + + @RequiresApi(Build.VERSION_CODES.O) + fun saveCredentials(username: String, apiKey: String) { + Log.wtf("MOVIS","${username}${apiKey}") + sharedPreferences.saveUsername(username) + sharedPreferences.saveApiKey(apiKey) + sharedPreferences.saveLastLoginDate(getCurrentDateTime()) + _uiState.update { + it.copy( + isLoggedIn = true + ) + } + } + + fun clearData() { + viewModelScope.launch(Dispatchers.IO) { + clearCredentials() + _uiState.update { + it.copy( + isLoggedIn = false, + loginStatus = Resource.Idle + + ) + } + } + } + + private fun clearCredentials() { + sharedPreferences.clearUsername() + sharedPreferences.clearApiKey() + sharedPreferences.clearLastLoginDate() + } + + @RequiresApi(Build.VERSION_CODES.O) fun login(username: String, apiKey: String) { viewModelScope.launch { - loginStatusMutable.value = Resource.Loading + _uiState.update { it.copy(loginStatus = Resource.Loading) } try { val response = RetrofitInstance.omdbApi.searchMovies(apiKey, "marvel") if (response.response == "True") { - loginStatusMutable.value = Resource.Success(Pair(username, apiKey)) + Log.wtf("MOVIS","${response.search}") + _uiState.update { + it.copy( + userName = username, + loginStatus = Resource.Success(Pair(username, apiKey)) + ) + + } } else { - loginStatusMutable.value = Resource.Error("Invalid API key") + _uiState.update { it.copy(loginStatus = Resource.Error("Invalid API key")) } } } catch (e: Exception) { - loginStatusMutable.value = Resource.Error(e.message ?: "An error occurred") + _uiState.update { it.copy(loginStatus = Resource.Error(e.message ?: "An error occurred")) } Log.d("LoginError", "Exception caught: ${e.message}") } } From cba39a5715c6dbada3a17d9d47dc09494eb039ac Mon Sep 17 00:00:00 2001 From: kevinMartinezC Date: Thu, 27 Apr 2023 14:54:37 -0600 Subject: [PATCH 06/16] feat: add icon and fixing to don't close the session when the app restart --- .../com/example/moviesvapp/ui/MainActivity.kt | 19 ++++++- .../moviesvapp/ui/components/MainScreen.kt | 47 ++++++++++++------ .../ui/components/login/LoginUiState.kt | 2 +- .../login/viewmodel/LoginViewModel.kt | 4 +- app/src/main/res/drawable/logo.png | Bin 0 -> 25413 bytes 5 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 app/src/main/res/drawable/logo.png diff --git a/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt b/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt index 1b71dc2..d0cf4b2 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt @@ -1,16 +1,31 @@ package com.example.moviesvapp.ui +import android.content.Context import android.os.Build import android.os.Bundle +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.viewModels import androidx.annotation.RequiresApi +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import com.example.moviesvapp.ui.components.MainScreen +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import com.example.moviesvapp.model.clearApiKey +import com.example.moviesvapp.model.clearLastLoginDate +import com.example.moviesvapp.model.clearUsername +import com.example.moviesvapp.model.getCurrentDateTime +import com.example.moviesvapp.model.getLastLoginDate import com.example.moviesvapp.ui.components.login.LoginScreen -import com.example.moviesvapp.ui.components.login.viewmodel.LoginViewModel +import com.example.moviesvapp.ui.components.MainScreen +import com.example.moviesvapp.model.getUsername +import com.example.moviesvapp.model.saveApiKey +import com.example.moviesvapp.model.saveLastLoginDate +import com.example.moviesvapp.model.saveUsername import com.example.moviesvapp.ui.theme.MyApplicationTheme +import com.example.moviesvapp.ui.components.login.viewmodel.LoginViewModel class MainActivity : ComponentActivity() { diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt index 44dfe10..f820c44 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt @@ -1,7 +1,7 @@ package com.example.moviesvapp.ui.components import android.annotation.SuppressLint -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope @@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.DateRange import androidx.compose.material.icons.filled.ExitToApp import androidx.compose.material3.Button import androidx.compose.material3.DrawerValue @@ -31,6 +33,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavDestination @@ -88,6 +92,7 @@ fun AppTopBar(onLogoutButtonClick: () -> Unit) { ) } + @OptIn(ExperimentalMaterial3Api::class) @Composable fun LogoutDrawer(onLogout: () -> Unit, username: String?, lastLoginDate: String?) { @@ -96,25 +101,37 @@ fun LogoutDrawer(onLogout: () -> Unit, username: String?, lastLoginDate: String? Column( modifier = Modifier .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + horizontalAlignment = Alignment.CenterHorizontally ) { + Image( + painter = painterResource(id = R.drawable.logo), + contentDescription = "Logo", + contentScale = ContentScale.Fit, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + Spacer(modifier = Modifier.height(16.dp)) + if (username != null && lastLoginDate != null) { - Text(text = "User: $username") - Text(text = "Last Login: $lastLoginDate") + NavigationDrawerItem( + icon = { Icon(Icons.Default.AccountCircle, contentDescription = "User icon") }, + label = { Text(text = "User: $username") }, + selected = false, + onClick = {}, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) + ) + Spacer(modifier = Modifier.height(8.dp)) + NavigationDrawerItem( + icon = { Icon(Icons.Default.DateRange, contentDescription = "Last login date icon") }, + label = { Text(text = "Date: $lastLoginDate") }, + selected = false, + onClick = {}, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) + ) } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.weight(1f)) - NavigationDrawerItem( - icon = { Icon(Icons.Default.ExitToApp, contentDescription = "Logout") }, - label = { - Text(text = "User: $username") - }, - selected = false, - onClick = {}, - modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) - ) Button( onClick = onLogout, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginUiState.kt b/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginUiState.kt index 7643190..6eb9d62 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginUiState.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginUiState.kt @@ -4,7 +4,7 @@ import com.example.moviesvapp.model.Resource data class LoginUiState( val isLoggedIn: Boolean = false, - val userName: String = "", + val userName: String? = null, val loginStatus: Resource> = Resource.Idle ) diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt b/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt index 7e7e9dd..5a7d4a7 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt @@ -35,7 +35,9 @@ class LoginViewModel(context: Context) : ViewModel() { private val _uiState = MutableStateFlow( LoginUiState( - isLoggedIn = !sharedPreferences.getApiKey().isNullOrBlank().not() + isLoggedIn = !sharedPreferences.getApiKey().isNullOrBlank(), + userName = sharedPreferences.getUsername() // Add this line + ) ) val uiState = _uiState.asStateFlow() diff --git a/app/src/main/res/drawable/logo.png b/app/src/main/res/drawable/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2866e07422ec7edff396206695a9020286b5d2d5 GIT binary patch literal 25413 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 ITFx*22k_l~8~^|S literal 0 HcmV?d00001 From 95aa867efe19a5875dc6469d72edd1706094a547 Mon Sep 17 00:00:00 2001 From: kevinMartinezC Date: Thu, 27 Apr 2023 15:44:00 -0600 Subject: [PATCH 07/16] feat: add extra data to get from the API to show in the DetailScreen --- .../com/example/moviesvapp/model/Movie.kt | 8 +++++- .../moviesvapp/ui/components/MainScreen.kt | 26 +++++++++++++------ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/example/moviesvapp/model/Movie.kt b/app/src/main/java/com/example/moviesvapp/model/Movie.kt index 73fc63e..6c048a0 100644 --- a/app/src/main/java/com/example/moviesvapp/model/Movie.kt +++ b/app/src/main/java/com/example/moviesvapp/model/Movie.kt @@ -12,5 +12,11 @@ data class Movie( @SerializedName("Type") val type: String, @SerializedName("Poster") - val poster: String + val poster: String, + @SerializedName("Plot") + val plot: String , + @SerializedName("Director") + val director: String + + ) \ No newline at end of file diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt index f820c44..86295d5 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt @@ -59,7 +59,11 @@ fun MainScreen(onLogout: () -> Unit, username: String?, lastLoginDate: String?) MaterialTheme { ModalNavigationDrawer( drawerContent = { - LogoutDrawer(onLogout = onLogout, username = username, lastLoginDate = lastLoginDate) + LogoutDrawer( + onLogout = onLogout, + username = username, + lastLoginDate = lastLoginDate + ) }, modifier = Modifier, drawerState = drawerState, @@ -114,7 +118,12 @@ fun LogoutDrawer(onLogout: () -> Unit, username: String?, lastLoginDate: String? if (username != null && lastLoginDate != null) { NavigationDrawerItem( - icon = { Icon(Icons.Default.AccountCircle, contentDescription = "User icon") }, + icon = { + Icon( + Icons.Default.AccountCircle, + contentDescription = "User icon" + ) + }, label = { Text(text = "User: $username") }, selected = false, onClick = {}, @@ -122,7 +131,12 @@ fun LogoutDrawer(onLogout: () -> Unit, username: String?, lastLoginDate: String? ) Spacer(modifier = Modifier.height(8.dp)) NavigationDrawerItem( - icon = { Icon(Icons.Default.DateRange, contentDescription = "Last login date icon") }, + icon = { + Icon( + Icons.Default.DateRange, + contentDescription = "Last login date icon" + ) + }, label = { Text(text = "Date: $lastLoginDate") }, selected = false, onClick = {}, @@ -142,10 +156,6 @@ fun LogoutDrawer(onLogout: () -> Unit, username: String?, lastLoginDate: String? } } - - - - @Composable fun RowScope.AddItem( screen: BottomNavItem, @@ -174,5 +184,5 @@ fun RowScope.AddItem( } }, alwaysShowLabel = false, - ) + ) } From 44c006d999c8d504f248cb32cbc07961da1d2516 Mon Sep 17 00:00:00 2001 From: KevinMartinezC <90873838+KevinMartinezC@users.noreply.github.com> Date: Thu, 27 Apr 2023 20:58:32 -0600 Subject: [PATCH 08/16] feat: remove the viewModel from the MoviesScreen and add MovieUiState --- .../moviesvapp/ui/components/MainScreen.kt | 3 +- .../ui/components/home/MovieItem.kt | 3 +- .../ui/components/home/MovieUiState.kt | 8 ++++ .../ui/components/home/MoviesScreen.kt | 17 ++++---- .../home/viewmodel/MoviesViewModel.kt | 43 ++++++++----------- .../login/viewmodel/LoginViewModel.kt | 13 +++--- .../components/navigation/BottomNavGraph.kt | 16 +++++-- 7 files changed, 60 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/com/example/moviesvapp/ui/components/home/MovieUiState.kt diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt index 86295d5..3b29133 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt @@ -46,6 +46,7 @@ import com.example.moviesvapp.R import com.example.moviesvapp.ui.components.navigation.BottomBar import com.example.moviesvapp.ui.components.navigation.BottomNavGraph import com.example.moviesvapp.ui.components.navigation.BottomNavItem +import com.example.moviesvapp.ui.theme.MyApplicationTheme import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -56,7 +57,7 @@ fun MainScreen(onLogout: () -> Unit, username: String?, lastLoginDate: String?) val drawerState = rememberDrawerState(DrawerValue.Closed) val scope = rememberCoroutineScope() - MaterialTheme { + MyApplicationTheme { ModalNavigationDrawer( drawerContent = { LogoutDrawer( diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt index 1833e48..0be56f8 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt @@ -42,7 +42,8 @@ fun MovieItem(movie: Movie) { .weight(1f) .align(Alignment.CenterVertically) ) { - Text(movie.title, style = MaterialTheme.typography.bodyLarge + Text( + movie.title, style = MaterialTheme.typography.bodyLarge ) Text(movie.year) } diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieUiState.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieUiState.kt new file mode 100644 index 0000000..7471144 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieUiState.kt @@ -0,0 +1,8 @@ +package com.example.moviesvapp.ui.components.home + +import com.example.moviesvapp.model.Movie + +data class MovieUiState( + val movies: List = emptyList(), + val isLoading: Boolean = false +) diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt index 774d33f..f4833be 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.material3.TextField 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 @@ -29,15 +28,16 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import com.example.moviesvapp.R -import com.example.moviesvapp.ui.components.home.viewmodel.MoviesViewModel import com.example.moviesvapp.ui.theme.MyApplicationTheme @OptIn(ExperimentalMaterial3Api::class) @Composable -fun MoviesScreen(viewModel: MoviesViewModel) { - - val movies by viewModel.movies.collectAsState() - val isLoading by viewModel.isLoading +fun MoviesScreen( + uiState: MovieUiState, + searchMovies: (String) -> Unit +) { + val movies = uiState.movies + val isLoading = uiState.isLoading var searchQuery by remember { mutableStateOf(TextFieldValue("")) } MyApplicationTheme { @@ -57,7 +57,8 @@ fun MoviesScreen(viewModel: MoviesViewModel) { trailingIcon = { IconButton( onClick = { - viewModel.searchMovies(searchQuery.text) } + searchMovies(searchQuery.text) + } ) { Icon( Icons.Default.Search, @@ -72,7 +73,7 @@ fun MoviesScreen(viewModel: MoviesViewModel) { CircularProgressIndicator(modifier = Modifier.wrapContentSize(Alignment.Center)) } else { LazyColumn( - modifier = Modifier.padding(bottom = 56.dp) // Add padding to avoid content being hidden behind the bottom bar + modifier = Modifier.padding(bottom = 56.dp) ) { items(movies.size) { index -> val movie = movies[index] diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt index a458626..7ad97f2 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt @@ -2,13 +2,11 @@ package com.example.moviesvapp.ui.components.home.viewmodel import android.content.Context import android.content.SharedPreferences -import android.util.Log -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.moviesvapp.model.Movie import com.example.moviesvapp.model.RetrofitInstance import com.example.moviesvapp.model.getApiKey +import com.example.moviesvapp.ui.components.home.MovieUiState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -17,52 +15,47 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class MoviesViewModel(context: Context) : ViewModel() { - val _movies = MutableStateFlow>(emptyList()) - val movies = _movies.asStateFlow() - val isLoading = mutableStateOf(false) private val omdbApi = RetrofitInstance.omdbApi private val sharedPreferences: SharedPreferences = context.getSharedPreferences("MyPrefs", Context.MODE_PRIVATE) - fun searchMovies(query: String) { - Log.wtf("MOVIS","${query}") + private val _uiState = MutableStateFlow( + MovieUiState( + movies = emptyList(), + isLoading = false + ) + ) + val uiState = _uiState.asStateFlow() + fun searchMovies(query: String) { viewModelScope.launch(Dispatchers.IO) { try { val apiKey = sharedPreferences.getApiKey() ?: throw IllegalStateException("API Key not found") - Log.wtf("MOVIS","${apiKey}") - val trimmedQuery = query.trim() val response = omdbApi.searchMovies(apiKey, trimmedQuery) withContext(Dispatchers.Main) { if (response.response == "True") { - _movies.update { - response.search + _uiState.update { + it.copy(movies = response.search, isLoading = false) } - Log.wtf("MOVIS","${response.search}") - - isLoading.value = false } else { - _movies.update { - emptyList() + _uiState.update { + it.copy(movies = emptyList(), isLoading = false) } - - isLoading.value = false } } } catch (e: Exception) { - Log.wtf("MOVIS","${e.message}") - withContext(Dispatchers.Main) { - _movies.update { - emptyList() + _uiState.update { + it.copy(movies = emptyList(), isLoading = false) } - isLoading.value = false } } } } - } + + + diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt b/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt index 5a7d4a7..12feb74 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt @@ -22,7 +22,6 @@ import com.example.moviesvapp.model.saveUsername import com.example.moviesvapp.ui.components.login.LoginUiState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -49,7 +48,6 @@ class LoginViewModel(context: Context) : ViewModel() { @RequiresApi(Build.VERSION_CODES.O) fun saveCredentials(username: String, apiKey: String) { - Log.wtf("MOVIS","${username}${apiKey}") sharedPreferences.saveUsername(username) sharedPreferences.saveApiKey(apiKey) sharedPreferences.saveLastLoginDate(getCurrentDateTime()) @@ -86,7 +84,6 @@ class LoginViewModel(context: Context) : ViewModel() { try { val response = RetrofitInstance.omdbApi.searchMovies(apiKey, "marvel") if (response.response == "True") { - Log.wtf("MOVIS","${response.search}") _uiState.update { it.copy( userName = username, @@ -95,10 +92,16 @@ class LoginViewModel(context: Context) : ViewModel() { } } else { - _uiState.update { it.copy(loginStatus = Resource.Error("Invalid API key")) } + _uiState.update { it.copy(loginStatus = Resource.Error("Invalid API key")) } } } catch (e: Exception) { - _uiState.update { it.copy(loginStatus = Resource.Error(e.message ?: "An error occurred")) } + _uiState.update { + it.copy( + loginStatus = Resource.Error( + e.message ?: "An error occurred" + ) + ) + } Log.d("LoginError", "Exception caught: ${e.message}") } } diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavGraph.kt b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavGraph.kt index bc0bd75..f81533d 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavGraph.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavGraph.kt @@ -1,6 +1,9 @@ package com.example.moviesvapp.ui.components.navigation import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost @@ -12,18 +15,25 @@ import com.example.moviesvapp.ui.components.home.viewmodel.MoviesViewModel @Composable fun BottomNavGraph( - navController: NavHostController) { + navController: NavHostController +) { val context = LocalContext.current + val moviesViewModel = remember { MoviesViewModel(context) } + + val uiState by moviesViewModel.uiState.collectAsState() NavHost( navController = navController, startDestination = BottomNavItem.Home.route ) { composable(route = BottomNavItem.Home.route) { - MoviesScreen(viewModel = MoviesViewModel(context)) + MoviesScreen( + uiState = uiState, + searchMovies = moviesViewModel::searchMovies + ) } composable(route = BottomNavItem.Favorite.route) { FavoriteScreen() } } -} \ No newline at end of file +} From dbc10f64a4a6ccf0d7fbdc69c3bdc1134539a8c2 Mon Sep 17 00:00:00 2001 From: KevinMartinezC <90873838+KevinMartinezC@users.noreply.github.com> Date: Thu, 27 Apr 2023 22:03:51 -0600 Subject: [PATCH 09/16] feat: show BottomSheet when a item is clicked --- app/build.gradle.kts | 5 +- .../com/example/moviesvapp/model/Movie.kt | 6 -- .../ui/components/home/MovieItem.kt | 75 ++++++++++++++++++- .../ui/components/home/MoviesScreen.kt | 1 - 4 files changed, 75 insertions(+), 12 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5c4a800..4726295 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,7 +58,8 @@ dependencies { implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") - implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material3:material3:1.1.0-rc01") + implementation("androidx.compose.material3:material3-window-size-class:1.0.1") implementation("androidx.compose.material:material:1.4.2") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") @@ -89,7 +90,7 @@ dependencies { val navVersion = "2.5.3" implementation("androidx.navigation:navigation-compose:$navVersion") - implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") + implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") diff --git a/app/src/main/java/com/example/moviesvapp/model/Movie.kt b/app/src/main/java/com/example/moviesvapp/model/Movie.kt index 6c048a0..b983758 100644 --- a/app/src/main/java/com/example/moviesvapp/model/Movie.kt +++ b/app/src/main/java/com/example/moviesvapp/model/Movie.kt @@ -13,10 +13,4 @@ data class Movie( val type: String, @SerializedName("Poster") val poster: String, - @SerializedName("Plot") - val plot: String , - @SerializedName("Director") - val director: String - - ) \ No newline at end of file diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt index 0be56f8..b412e7c 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt @@ -1,15 +1,24 @@ package com.example.moviesvapp.ui.components.home import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +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.width +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale @@ -19,12 +28,18 @@ import coil.compose.rememberAsyncImagePainter import com.example.moviesvapp.R import com.example.moviesvapp.model.Movie +@OptIn(ExperimentalMaterial3Api::class) @Composable fun MovieItem(movie: Movie) { + var openBottomSheet by rememberSaveable { mutableStateOf(false) } + Row( modifier = Modifier .fillMaxWidth() - .padding(8.dp), + .padding(8.dp) + .clickable(onClick = { + openBottomSheet = !openBottomSheet}), + verticalAlignment = Alignment.CenterVertically ) { val painter = rememberAsyncImagePainter(model = movie.poster) @@ -45,7 +60,61 @@ fun MovieItem(movie: Movie) { Text( movie.title, style = MaterialTheme.typography.bodyLarge ) - Text(movie.year) + } } -} \ No newline at end of file + if (openBottomSheet){ + val sheetState = rememberModalBottomSheetState() + ModalBottomSheet( + onDismissRequest = { openBottomSheet = false }, + modifier = Modifier.fillMaxSize(), + sheetState = sheetState, + content = { + Column(modifier = Modifier.padding(16.dp)) { + Image( + painter = rememberAsyncImagePainter(model = movie.poster), + contentDescription = stringResource(R.string.movie_poster), + modifier = Modifier.height(200.dp), + contentScale = ContentScale.Crop + ) + + Text( + movie.type, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 16.dp) + ) + Text(movie.year, modifier = Modifier.padding(top = 8.dp)) + } + } + ) + } +} + + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DetailScreen(movie: Movie) { + val sheetState = rememberModalBottomSheetState() + ModalBottomSheet( + onDismissRequest = {}, + modifier = Modifier.fillMaxSize(), + sheetState = sheetState, + content = { + Column(modifier = Modifier.padding(16.dp)) { + Image( + painter = rememberAsyncImagePainter(model = movie.poster), + contentDescription = stringResource(R.string.movie_poster), + modifier = Modifier.height(200.dp), + contentScale = ContentScale.Crop + ) + Text( + movie.title, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 16.dp) + ) + Text(movie.year, modifier = Modifier.padding(top = 8.dp)) + } + } + ) +} diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt index f4833be..070bca6 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.unit.dp import com.example.moviesvapp.R import com.example.moviesvapp.ui.theme.MyApplicationTheme -@OptIn(ExperimentalMaterial3Api::class) @Composable fun MoviesScreen( uiState: MovieUiState, From b1c76513c4e1245aa8fbfc54167251ce5f1aa62d Mon Sep 17 00:00:00 2001 From: kevinMartinezC Date: Fri, 28 Apr 2023 08:19:51 -0600 Subject: [PATCH 10/16] feat: improve DetailScreen Design and organize code --- .../moviesvapp/ui/components/MainScreen.kt | 138 +----------------- .../ui/components/detail/DetailScreen.kt | 78 ++++++++++ .../ui/components/home/MovieItem.kt | 56 +------ .../moviesvapp/ui/components/logout/Logout.kt | 84 +++++++++++ .../ui/components/navigation/BottomBar.kt | 1 - .../navigation/NavigationBarItem.kt | 45 ++++++ .../moviesvapp/ui/components/topbar/TopBar.kt | 26 ++++ app/src/main/res/values/strings.xml | 9 ++ 8 files changed, 246 insertions(+), 191 deletions(-) create mode 100644 app/src/main/java/com/example/moviesvapp/ui/components/detail/DetailScreen.kt create mode 100644 app/src/main/java/com/example/moviesvapp/ui/components/logout/Logout.kt create mode 100644 app/src/main/java/com/example/moviesvapp/ui/components/navigation/NavigationBarItem.kt create mode 100644 app/src/main/java/com/example/moviesvapp/ui/components/topbar/TopBar.kt diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt index 3b29133..a3e4663 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt @@ -1,55 +1,23 @@ package com.example.moviesvapp.ui.components import android.annotation.SuppressLint -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.material.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AccountCircle -import androidx.compose.material.icons.filled.DateRange -import androidx.compose.material.icons.filled.ExitToApp -import androidx.compose.material3.Button import androidx.compose.material3.DrawerValue -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.ModalNavigationDrawer -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationDrawerItem -import androidx.compose.material3.NavigationDrawerItemDefaults import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.navigation.NavDestination -import androidx.navigation.NavDestination.Companion.hierarchy -import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import com.example.moviesvapp.R +import com.example.moviesvapp.ui.components.logout.LogoutDrawer import com.example.moviesvapp.ui.components.navigation.BottomBar import com.example.moviesvapp.ui.components.navigation.BottomNavGraph -import com.example.moviesvapp.ui.components.navigation.BottomNavItem +import com.example.moviesvapp.ui.components.topbar.AppTopBar import com.example.moviesvapp.ui.theme.MyApplicationTheme import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3Api::class) @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun MainScreen(onLogout: () -> Unit, username: String?, lastLoginDate: String?) { @@ -84,106 +52,4 @@ fun MainScreen(onLogout: () -> Unit, username: String?, lastLoginDate: String?) } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AppTopBar(onLogoutButtonClick: () -> Unit) { - TopAppBar( - title = { Text("Search") }, - actions = { - IconButton(onClick = onLogoutButtonClick) { - Icon(Icons.Default.ExitToApp, contentDescription = "Logout") - } - } - ) -} - - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun LogoutDrawer(onLogout: () -> Unit, username: String?, lastLoginDate: String?) { - ModalDrawerSheet { - Box(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - painter = painterResource(id = R.drawable.logo), - contentDescription = "Logo", - contentScale = ContentScale.Fit, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - if (username != null && lastLoginDate != null) { - NavigationDrawerItem( - icon = { - Icon( - Icons.Default.AccountCircle, - contentDescription = "User icon" - ) - }, - label = { Text(text = "User: $username") }, - selected = false, - onClick = {}, - modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) - ) - Spacer(modifier = Modifier.height(8.dp)) - NavigationDrawerItem( - icon = { - Icon( - Icons.Default.DateRange, - contentDescription = "Last login date icon" - ) - }, - label = { Text(text = "Date: $lastLoginDate") }, - selected = false, - onClick = {}, - modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) - ) - } - Spacer(modifier = Modifier.weight(1f)) - - Button( - onClick = onLogout, - modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), - content = { Text("Logout") } - ) - } - } - } -} - -@Composable -fun RowScope.AddItem( - screen: BottomNavItem, - currentDestination: NavDestination?, - navController: NavHostController -) { - NavigationBarItem( - label = { - Text(text = screen.title) - }, - icon = { - Icon( - imageVector = screen.icon, - contentDescription = stringResource(R.string.navigation_icon), - tint = MaterialTheme.colorScheme.primary - - ) - }, - selected = currentDestination?.hierarchy?.any { - it.route == screen.route - } == true, - onClick = { - navController.navigate(screen.route) { - popUpTo(navController.graph.findStartDestination().id) - launchSingleTop = true - } - }, - alwaysShowLabel = false, - ) -} diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/detail/DetailScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/detail/DetailScreen.kt new file mode 100644 index 0000000..e8bd5b7 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/detail/DetailScreen.kt @@ -0,0 +1,78 @@ +package com.example.moviesvapp.ui.components.detail + +import androidx.compose.foundation.Image +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.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter +import com.example.moviesvapp.R +import com.example.moviesvapp.model.Movie + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DetailScreen(movie: Movie, openBottomSheet: () -> Unit) { + val sheetState = rememberModalBottomSheetState() + ModalBottomSheet( + onDismissRequest = openBottomSheet, + modifier = Modifier.fillMaxSize(), + sheetState = sheetState, + content = { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = rememberAsyncImagePainter(model = movie.poster), + contentDescription = stringResource(R.string.movie_poster), + modifier = Modifier.height(200.dp), + contentScale = ContentScale.Crop + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.category), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold + + ) + Text( + movie.type, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 8.dp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.release_date), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold + + ) + Text( + movie.year, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 8.dp) + ) + } + } + ) +} diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt index b412e7c..4a931b1 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt @@ -4,16 +4,12 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -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.width -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -27,8 +23,8 @@ import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter import com.example.moviesvapp.R import com.example.moviesvapp.model.Movie +import com.example.moviesvapp.ui.components.detail.DetailScreen -@OptIn(ExperimentalMaterial3Api::class) @Composable fun MovieItem(movie: Movie) { var openBottomSheet by rememberSaveable { mutableStateOf(false) } @@ -64,57 +60,9 @@ fun MovieItem(movie: Movie) { } } if (openBottomSheet){ - val sheetState = rememberModalBottomSheetState() - ModalBottomSheet( - onDismissRequest = { openBottomSheet = false }, - modifier = Modifier.fillMaxSize(), - sheetState = sheetState, - content = { - Column(modifier = Modifier.padding(16.dp)) { - Image( - painter = rememberAsyncImagePainter(model = movie.poster), - contentDescription = stringResource(R.string.movie_poster), - modifier = Modifier.height(200.dp), - contentScale = ContentScale.Crop - ) - - Text( - movie.type, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(top = 16.dp) - ) - Text(movie.year, modifier = Modifier.padding(top = 8.dp)) - } - } - ) + DetailScreen(movie = movie, openBottomSheet = { openBottomSheet = !openBottomSheet }) } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun DetailScreen(movie: Movie) { - val sheetState = rememberModalBottomSheetState() - ModalBottomSheet( - onDismissRequest = {}, - modifier = Modifier.fillMaxSize(), - sheetState = sheetState, - content = { - Column(modifier = Modifier.padding(16.dp)) { - Image( - painter = rememberAsyncImagePainter(model = movie.poster), - contentDescription = stringResource(R.string.movie_poster), - modifier = Modifier.height(200.dp), - contentScale = ContentScale.Crop - ) - Text( - movie.title, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(top = 16.dp) - ) - Text(movie.year, modifier = Modifier.padding(top = 8.dp)) - } - } - ) -} diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/logout/Logout.kt b/app/src/main/java/com/example/moviesvapp/ui/components/logout/Logout.kt new file mode 100644 index 0000000..0e0ad9f --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/logout/Logout.kt @@ -0,0 +1,84 @@ +package com.example.moviesvapp.ui.components.logout + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.DateRange +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.example.moviesvapp.R + +@Composable +fun LogoutDrawer(onLogout: () -> Unit, username: String?, lastLoginDate: String?) { + ModalDrawerSheet { + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.logo), + contentDescription = stringResource(R.string.logo), + contentScale = ContentScale.Fit, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + if (username != null && lastLoginDate != null) { + NavigationDrawerItem( + icon = { + Icon( + Icons.Default.AccountCircle, + contentDescription = stringResource(R.string.user_icon) + ) + }, + label = { Text(text = stringResource(R.string.user, username)) }, + selected = false, + onClick = {}, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) + ) + Spacer(modifier = Modifier.height(8.dp)) + NavigationDrawerItem( + icon = { + Icon( + Icons.Default.DateRange, + contentDescription = stringResource(R.string.last_login_date_icon) + ) + }, + label = { Text(text = stringResource(R.string.date, lastLoginDate)) }, + selected = false, + onClick = {}, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Button( + onClick = onLogout, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + content = { Text(stringResource(R.string.logout)) } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomBar.kt b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomBar.kt index 991c423..67ba053 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomBar.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomBar.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState -import com.example.moviesvapp.ui.components.AddItem @Composable fun BottomBar(navController: NavHostController) { diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/navigation/NavigationBarItem.kt b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/NavigationBarItem.kt new file mode 100644 index 0000000..4904e5c --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/NavigationBarItem.kt @@ -0,0 +1,45 @@ +package com.example.moviesvapp.ui.components.navigation + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import com.example.moviesvapp.R + +@Composable +fun RowScope.AddItem( + screen: BottomNavItem, + currentDestination: NavDestination?, + navController: NavHostController +) { + NavigationBarItem( + label = { + Text(text = screen.title) + }, + icon = { + Icon( + imageVector = screen.icon, + contentDescription = stringResource(R.string.navigation_icon), + tint = MaterialTheme.colorScheme.primary + + ) + }, + selected = currentDestination?.hierarchy?.any { + it.route == screen.route + } == true, + onClick = { + navController.navigate(screen.route) { + popUpTo(navController.graph.findStartDestination().id) + launchSingleTop = true + } + }, + alwaysShowLabel = false, + ) +} diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/topbar/TopBar.kt b/app/src/main/java/com/example/moviesvapp/ui/components/topbar/TopBar.kt new file mode 100644 index 0000000..7a575ec --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/topbar/TopBar.kt @@ -0,0 +1,26 @@ +package com.example.moviesvapp.ui.components.topbar + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExitToApp +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.example.moviesvapp.R + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppTopBar(onLogoutButtonClick: () -> Unit) { + TopAppBar( + title = { Text(stringResource(R.string.search)) }, + actions = { + IconButton(onClick = onLogoutButtonClick) { + Icon(Icons.Default.ExitToApp, contentDescription = (stringResource(R.string.logout))) + } + } + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 45b89ad..896d5fe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,4 +10,13 @@ Search Icon Search movies Login + Category: + Movie Release: + Last login date icon + User: %1$s + User icon + Logo + Date: %1$s + Logout + Search \ No newline at end of file From 239b7fc2d6fae4d0e3d306b899f82c1b4b3c90d8 Mon Sep 17 00:00:00 2001 From: kevinMartinezC Date: Fri, 28 Apr 2023 10:15:53 -0600 Subject: [PATCH 11/16] feat: add fravorite screen and save item selected in room database --- app/build.gradle.kts | 31 ++++---- .../model/database/FavoriteMovie.kt | 15 ++++ .../moviesvapp/model/database/MovieDao.kt | 20 +++++ .../model/database/MovieDatabase.kt | 9 +++ .../ui/components/favorite/FavoriteScreen.kt | 73 ++++++++++++++++++- .../ui/components/home/MovieItem.kt | 17 ++++- .../ui/components/home/MovieUiState.kt | 4 +- .../ui/components/home/MoviesScreen.kt | 9 ++- .../home/viewmodel/MoviesViewModel.kt | 39 +++++++++- .../components/navigation/BottomNavGraph.kt | 6 +- app/src/main/res/values/strings.xml | 1 + build.gradle.kts | 2 + 12 files changed, 200 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/com/example/moviesvapp/model/database/FavoriteMovie.kt create mode 100644 app/src/main/java/com/example/moviesvapp/model/database/MovieDao.kt create mode 100644 app/src/main/java/com/example/moviesvapp/model/database/MovieDatabase.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4726295..addc9b3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,10 @@ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") + id("com.google.devtools.ksp") + + + } android { @@ -30,11 +34,11 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } buildFeatures { compose = true @@ -50,7 +54,9 @@ android { } dependencies { - + val navVersion = "2.5.3" + val lifecycleVersion = "2.6.1" + val roomVersion = "2.5.1" implementation("androidx.core:core-ktx:1.10.0") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") implementation("androidx.activity:activity-compose:1.7.1") @@ -72,25 +78,20 @@ dependencies { implementation ("com.squareup.retrofit2:converter-gson:2.9.0") implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") - // Lifecycle - val lifecycleVersion = "2.6.1" - // ViewModel implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion") - // ViewModel utilities for Compose implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion") - // LiveData implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion") - // Lifecycles only (without ViewModel or LiveData) implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") - // Lifecycle utilities for Compose implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion") - implementation ("io.coil-kt:coil-compose:2.3.0") - - val navVersion = "2.5.3" - implementation("androidx.navigation:navigation-compose:$navVersion") implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("androidx.room:room-runtime:$roomVersion") + annotationProcessor("androidx.room:room-compiler:$roomVersion") + implementation("androidx.room:room-ktx:$roomVersion") + ksp("androidx.room:room-compiler:2.5.1") + + diff --git a/app/src/main/java/com/example/moviesvapp/model/database/FavoriteMovie.kt b/app/src/main/java/com/example/moviesvapp/model/database/FavoriteMovie.kt new file mode 100644 index 0000000..c50e2ea --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/model/database/FavoriteMovie.kt @@ -0,0 +1,15 @@ +package com.example.moviesvapp.model.database + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "favorite_movies") +data class FavoriteMovie( + @PrimaryKey + @ColumnInfo(name = "imdbID") val imdbID: String, + @ColumnInfo(name = "title") val title: String, + @ColumnInfo(name = "year") val year: String, + @ColumnInfo(name = "type") val type: String, + @ColumnInfo(name = "poster") val poster: String, +) diff --git a/app/src/main/java/com/example/moviesvapp/model/database/MovieDao.kt b/app/src/main/java/com/example/moviesvapp/model/database/MovieDao.kt new file mode 100644 index 0000000..fbffa1a --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/model/database/MovieDao.kt @@ -0,0 +1,20 @@ +package com.example.moviesvapp.model.database + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface MovieDao { + @Query("SELECT * FROM favorite_movies") + fun getAll(): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(favoriteMovie: FavoriteMovie) + + @Delete + suspend fun delete(favoriteMovie: FavoriteMovie) +} diff --git a/app/src/main/java/com/example/moviesvapp/model/database/MovieDatabase.kt b/app/src/main/java/com/example/moviesvapp/model/database/MovieDatabase.kt new file mode 100644 index 0000000..57a0e20 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/model/database/MovieDatabase.kt @@ -0,0 +1,9 @@ +package com.example.moviesvapp.model.database + +import androidx.room.Database +import androidx.room.RoomDatabase + +@Database(entities = [FavoriteMovie::class], version = 1) +abstract class MovieDatabase : RoomDatabase() { + abstract fun movieDao(): MovieDao +} diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt index d7c9e35..118a71a 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt @@ -1,9 +1,76 @@ package com.example.moviesvapp.ui.components.favorite +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter +import com.example.moviesvapp.R +import com.example.moviesvapp.model.database.FavoriteMovie +import com.example.moviesvapp.ui.theme.MyApplicationTheme +import kotlinx.coroutines.flow.StateFlow @Composable -fun FavoriteScreen() { - Text(text = "Favotire") -} \ No newline at end of file +fun FavoriteScreen(favoriteMoviesFlow: StateFlow>) { + val favoriteMovies by favoriteMoviesFlow.collectAsState(initial = emptyList()) + + MyApplicationTheme { + LazyColumn { + items(favoriteMovies.size) { index -> + val favoriteMovie = favoriteMovies[index] + FavoriteMovieItem(favoriteMovie) + Divider() + } + } + } +} + +@Composable +fun FavoriteMovieItem(favoriteMovie: FavoriteMovie) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val painter = rememberAsyncImagePainter(model = favoriteMovie.poster) + Image( + painter = painter, + contentDescription = stringResource(R.string.movie_poster), + modifier = Modifier + .height(100.dp) + .width(75.dp) + .padding(end = 16.dp), + contentScale = ContentScale.Crop + ) + Column( + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically) + ) { + Text( + favoriteMovie.title, + style = MaterialTheme.typography.bodyLarge + ) + Text( + favoriteMovie.year, + style = MaterialTheme.typography.bodySmall + ) + } + } +} diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt index 4a931b1..9181020 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt @@ -7,7 +7,12 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.FavoriteBorder +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -26,8 +31,9 @@ import com.example.moviesvapp.model.Movie import com.example.moviesvapp.ui.components.detail.DetailScreen @Composable -fun MovieItem(movie: Movie) { +fun MovieItem(movie: Movie, favoriteMovies: Set, onToggleFavorite: (Movie) -> Unit) { var openBottomSheet by rememberSaveable { mutableStateOf(false) } + val isFavorite = favoriteMovies.contains(movie.imdbID) Row( modifier = Modifier @@ -58,6 +64,15 @@ fun MovieItem(movie: Movie) { ) } + Icon( + imageVector = if (isFavorite) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder, + contentDescription = stringResource(R.string.favorite_movie), + modifier = Modifier + .size(24.dp) + .clickable(onClick = { + onToggleFavorite(movie) + }) + ) } if (openBottomSheet){ DetailScreen(movie = movie, openBottomSheet = { openBottomSheet = !openBottomSheet }) diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieUiState.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieUiState.kt index 7471144..60cc924 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieUiState.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieUiState.kt @@ -4,5 +4,7 @@ import com.example.moviesvapp.model.Movie data class MovieUiState( val movies: List = emptyList(), - val isLoading: Boolean = false + val isLoading: Boolean = false, + val favoriteMovies: Set = emptySet() ) + diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt index 070bca6..1a6ebde 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/MoviesScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Divider -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text @@ -28,12 +27,15 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import com.example.moviesvapp.R +import com.example.moviesvapp.model.Movie import com.example.moviesvapp.ui.theme.MyApplicationTheme @Composable fun MoviesScreen( uiState: MovieUiState, - searchMovies: (String) -> Unit + searchMovies: (String) -> Unit, + onToggleFavorite: (Movie) -> Unit + ) { val movies = uiState.movies val isLoading = uiState.isLoading @@ -76,7 +78,8 @@ fun MoviesScreen( ) { items(movies.size) { index -> val movie = movies[index] - MovieItem(movie) + MovieItem(movie,favoriteMovies = uiState.favoriteMovies, + onToggleFavorite = onToggleFavorite) Divider() } } diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt index 7ad97f2..0ed1257 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt @@ -4,7 +4,11 @@ import android.content.Context import android.content.SharedPreferences import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.room.Room +import com.example.moviesvapp.model.Movie import com.example.moviesvapp.model.RetrofitInstance +import com.example.moviesvapp.model.database.FavoriteMovie +import com.example.moviesvapp.model.database.MovieDatabase import com.example.moviesvapp.model.getApiKey import com.example.moviesvapp.ui.components.home.MovieUiState import kotlinx.coroutines.Dispatchers @@ -20,6 +24,24 @@ class MoviesViewModel(context: Context) : ViewModel() { private val sharedPreferences: SharedPreferences = context.getSharedPreferences("MyPrefs", Context.MODE_PRIVATE) + private val movieDatabase = Room.databaseBuilder( + context.applicationContext, + MovieDatabase::class.java, "movie-database" + ).build() + + private val _favoriteMoviesFlow = MutableStateFlow>(emptyList()) + val favoriteMoviesFlow = _favoriteMoviesFlow.asStateFlow() + + init { + viewModelScope.launch { + movieDatabase.movieDao().getAll().collect { favoriteMovies -> + val favoriteImdbIDs = favoriteMovies.map { it.imdbID }.toSet() + _uiState.update { it.copy(favoriteMovies = favoriteImdbIDs) } + _favoriteMoviesFlow.value = favoriteMovies + } + } + } + private val _uiState = MutableStateFlow( MovieUiState( movies = emptyList(), @@ -27,7 +49,22 @@ class MoviesViewModel(context: Context) : ViewModel() { ) ) val uiState = _uiState.asStateFlow() - + fun toggleFavorite(movie: Movie) { + viewModelScope.launch(Dispatchers.IO) { + val favoriteMovie = FavoriteMovie( + imdbID = movie.imdbID, + title = movie.title, + year = movie.year, + type = movie.type, + poster = movie.poster + ) + if (_uiState.value.favoriteMovies.contains(movie.imdbID)) { + movieDatabase.movieDao().delete(favoriteMovie) + } else { + movieDatabase.movieDao().insert(favoriteMovie) + } + } + } fun searchMovies(query: String) { viewModelScope.launch(Dispatchers.IO) { try { diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavGraph.kt b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavGraph.kt index f81533d..df80aa4 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavGraph.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavGraph.kt @@ -29,11 +29,13 @@ fun BottomNavGraph( composable(route = BottomNavItem.Home.route) { MoviesScreen( uiState = uiState, - searchMovies = moviesViewModel::searchMovies + searchMovies = moviesViewModel::searchMovies, + onToggleFavorite = { movie -> moviesViewModel.toggleFavorite(movie) } + ) } composable(route = BottomNavItem.Favorite.route) { - FavoriteScreen() + FavoriteScreen(favoriteMoviesFlow = moviesViewModel.favoriteMoviesFlow) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 896d5fe..61e60bb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,4 +19,5 @@ Date: %1$s Logout Search + favorite movie\n \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index b116a83..ae7d584 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,4 +2,6 @@ plugins { id("com.android.application") version "8.1.0-beta01" apply false id("org.jetbrains.kotlin.android") version "1.8.10" apply false + id("com.google.devtools.ksp") version "1.8.10-1.0.9" apply false + } \ No newline at end of file From 8b545d47b537f93536ea0f52ff24ee6b7e275fde Mon Sep 17 00:00:00 2001 From: kevinMartinezC Date: Fri, 28 Apr 2023 11:00:08 -0600 Subject: [PATCH 12/16] feat: add functionality to delete the favorite movies add it when the user logout --- .../moviesvapp/model/database/MovieDao.kt | 3 +++ .../com/example/moviesvapp/ui/MainActivity.kt | 25 ++++++------------- .../ui/components/favorite/FavoriteScreen.kt | 2 +- .../home/viewmodel/MoviesViewModel.kt | 11 ++++++++ 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/example/moviesvapp/model/database/MovieDao.kt b/app/src/main/java/com/example/moviesvapp/model/database/MovieDao.kt index fbffa1a..984da92 100644 --- a/app/src/main/java/com/example/moviesvapp/model/database/MovieDao.kt +++ b/app/src/main/java/com/example/moviesvapp/model/database/MovieDao.kt @@ -17,4 +17,7 @@ interface MovieDao { @Delete suspend fun delete(favoriteMovie: FavoriteMovie) + + @Query("DELETE FROM favorite_movies") + suspend fun deleteAll() } diff --git a/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt b/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt index d0cf4b2..6695a96 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt @@ -1,40 +1,29 @@ package com.example.moviesvapp.ui -import android.content.Context import android.os.Build import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.viewModels import androidx.annotation.RequiresApi -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import com.example.moviesvapp.model.clearApiKey -import com.example.moviesvapp.model.clearLastLoginDate -import com.example.moviesvapp.model.clearUsername -import com.example.moviesvapp.model.getCurrentDateTime -import com.example.moviesvapp.model.getLastLoginDate -import com.example.moviesvapp.ui.components.login.LoginScreen import com.example.moviesvapp.ui.components.MainScreen -import com.example.moviesvapp.model.getUsername -import com.example.moviesvapp.model.saveApiKey -import com.example.moviesvapp.model.saveLastLoginDate -import com.example.moviesvapp.model.saveUsername -import com.example.moviesvapp.ui.theme.MyApplicationTheme +import com.example.moviesvapp.ui.components.home.viewmodel.MoviesViewModel +import com.example.moviesvapp.ui.components.login.LoginScreen import com.example.moviesvapp.ui.components.login.viewmodel.LoginViewModel +import com.example.moviesvapp.ui.theme.MyApplicationTheme class MainActivity : ComponentActivity() { private lateinit var viewModel: LoginViewModel + private lateinit var moviesViewModel: MoviesViewModel @RequiresApi(Build.VERSION_CODES.O) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = LoginViewModel(this) + moviesViewModel = MoviesViewModel(this) + setContent { MyApplicationTheme { val lastLoginDate = viewModel.getLastLoginDate() @@ -50,6 +39,8 @@ class MainActivity : ComponentActivity() { MainScreen( onLogout = { viewModel.clearData() + moviesViewModel.clearFavoriteMovies() + }, username = uiState.userName, lastLoginDate = lastLoginDate diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt index 118a71a..952fdfd 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt @@ -45,7 +45,7 @@ fun FavoriteMovieItem(favoriteMovie: FavoriteMovie) { Row( modifier = Modifier .fillMaxWidth() - .padding(8.dp), + .padding(56.dp), verticalAlignment = Alignment.CenterVertically ) { val painter = rememberAsyncImagePainter(model = favoriteMovie.poster) diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt index 0ed1257..4bdcef5 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt @@ -41,6 +41,17 @@ class MoviesViewModel(context: Context) : ViewModel() { } } } + fun clearFavoriteMovies() { + viewModelScope.launch(Dispatchers.IO) { + movieDatabase.movieDao().deleteAll() + withContext(Dispatchers.Main) { + _favoriteMoviesFlow.value = emptyList() + _uiState.update { + it.copy(favoriteMovies = emptySet()) + } + } + } + } private val _uiState = MutableStateFlow( MovieUiState( From be37459fd1aa9facd847f3d3ecaf059675f29e9d Mon Sep 17 00:00:00 2001 From: kevinMartinezC Date: Fri, 28 Apr 2023 14:26:40 -0600 Subject: [PATCH 13/16] feat: show the movies add it in the FavoriteScreen with animation --- .../ui/components/favorite/FavoriteScreen.kt | 115 ++++++++++++------ .../ui/components/home/MovieItem.kt | 11 +- 2 files changed, 84 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt index 952fdfd..93fb919 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt @@ -1,76 +1,113 @@ package com.example.moviesvapp.ui.components.favorite +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Divider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerDefaults +import androidx.compose.foundation.pager.PagerSnapDistance +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.Card import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter import com.example.moviesvapp.R import com.example.moviesvapp.model.database.FavoriteMovie import com.example.moviesvapp.ui.theme.MyApplicationTheme import kotlinx.coroutines.flow.StateFlow +import kotlin.math.absoluteValue +@OptIn(ExperimentalFoundationApi::class) @Composable fun FavoriteScreen(favoriteMoviesFlow: StateFlow>) { val favoriteMovies by favoriteMoviesFlow.collectAsState(initial = emptyList()) MyApplicationTheme { - LazyColumn { - items(favoriteMovies.size) { index -> - val favoriteMovie = favoriteMovies[index] - FavoriteMovieItem(favoriteMovie) - Divider() + val pagerState = rememberPagerState() + val fling = PagerDefaults.flingBehavior( + state = pagerState, + pagerSnapDistance = PagerSnapDistance.atMost(4) + ) + + val screenWidth = LocalConfiguration.current.screenWidthDp.dp + val screenHeight = LocalConfiguration.current.screenHeightDp.dp + val cardWidth = screenWidth * 0.7f + val cardHeight = screenHeight * 0.7f + val padding = (screenWidth - cardWidth) / 2 + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + HorizontalPager( + pageCount = favoriteMovies.size, + state = pagerState, + flingBehavior = fling, + contentPadding = PaddingValues(start = padding, end = padding) + ) { page -> + val favoriteMovie = favoriteMovies[page] + FavoriteMovieItem( + favoriteMovie = favoriteMovie, + pagerState = pagerState, + currentPage = page, + cardWidth = cardWidth, + cardHeight = cardHeight + ) } } } } +@OptIn(ExperimentalFoundationApi::class) @Composable -fun FavoriteMovieItem(favoriteMovie: FavoriteMovie) { - Row( +fun FavoriteMovieItem( + favoriteMovie: FavoriteMovie, + pagerState: PagerState, + currentPage: Int, + cardWidth: Dp, + cardHeight: Dp +) { + val painter = rememberAsyncImagePainter(model = favoriteMovie.poster) + + val pageOffset = ( + (pagerState.currentPage - currentPage) + pagerState.currentPageOffsetFraction + ).absoluteValue + + val scale = lerp( + start = 0.8f, + stop = 1f, + fraction = 1f - pageOffset.coerceIn(0f, 1f) + ) + + Card( modifier = Modifier - .fillMaxWidth() - .padding(56.dp), - verticalAlignment = Alignment.CenterVertically + .size(cardWidth, cardHeight) + .scale(scale) + .animateContentSize() ) { - val painter = rememberAsyncImagePainter(model = favoriteMovie.poster) Image( painter = painter, contentDescription = stringResource(R.string.movie_poster), - modifier = Modifier - .height(100.dp) - .width(75.dp) - .padding(end = 16.dp), + modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) - Column( - modifier = Modifier - .weight(1f) - .align(Alignment.CenterVertically) - ) { - Text( - favoriteMovie.title, - style = MaterialTheme.typography.bodyLarge - ) - Text( - favoriteMovie.year, - style = MaterialTheme.typography.bodySmall - ) - } } } + +fun lerp(start: Float, stop: Float, fraction: Float): Float { + return (1 - fraction) * start + fraction * stop +} \ No newline at end of file diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt index 9181020..83122c8 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt @@ -31,7 +31,11 @@ import com.example.moviesvapp.model.Movie import com.example.moviesvapp.ui.components.detail.DetailScreen @Composable -fun MovieItem(movie: Movie, favoriteMovies: Set, onToggleFavorite: (Movie) -> Unit) { +fun MovieItem( + movie: Movie, + favoriteMovies: Set, + onToggleFavorite: (Movie) -> Unit +) { var openBottomSheet by rememberSaveable { mutableStateOf(false) } val isFavorite = favoriteMovies.contains(movie.imdbID) @@ -40,7 +44,8 @@ fun MovieItem(movie: Movie, favoriteMovies: Set, onToggleFavorite: (Movi .fillMaxWidth() .padding(8.dp) .clickable(onClick = { - openBottomSheet = !openBottomSheet}), + openBottomSheet = !openBottomSheet + }), verticalAlignment = Alignment.CenterVertically ) { @@ -74,7 +79,7 @@ fun MovieItem(movie: Movie, favoriteMovies: Set, onToggleFavorite: (Movi }) ) } - if (openBottomSheet){ + if (openBottomSheet) { DetailScreen(movie = movie, openBottomSheet = { openBottomSheet = !openBottomSheet }) } } From a0cec018c51e844f84aab94ee9fb6ce0aed2bae6 Mon Sep 17 00:00:00 2001 From: kevinMartinezC Date: Fri, 28 Apr 2023 14:43:34 -0600 Subject: [PATCH 14/16] feat: show a bottomsheet with the details of movies when the image is clicked in the favorite screen --- .../ui/components/detail/DetailScreen.kt | 1 - .../ui/components/favorite/FavoriteScreen.kt | 71 ++++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/detail/DetailScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/detail/DetailScreen.kt index e8bd5b7..6944d93 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/detail/DetailScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/detail/DetailScreen.kt @@ -1,7 +1,6 @@ package com.example.moviesvapp.ui.components.detail import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt index 93fb919..62ffee5 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt @@ -3,9 +3,14 @@ package com.example.moviesvapp.ui.components.favorite import androidx.compose.animation.animateContentSize import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerDefaults @@ -13,15 +18,24 @@ import androidx.compose.foundation.pager.PagerSnapDistance import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter @@ -72,7 +86,7 @@ fun FavoriteScreen(favoriteMoviesFlow: StateFlow>) { } } -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable fun FavoriteMovieItem( favoriteMovie: FavoriteMovie, @@ -82,6 +96,7 @@ fun FavoriteMovieItem( cardHeight: Dp ) { val painter = rememberAsyncImagePainter(model = favoriteMovie.poster) + var openBottomSheet by rememberSaveable { mutableStateOf(false) } val pageOffset = ( (pagerState.currentPage - currentPage) + pagerState.currentPageOffsetFraction @@ -98,6 +113,9 @@ fun FavoriteMovieItem( .size(cardWidth, cardHeight) .scale(scale) .animateContentSize() + .clickable(onClick = { + openBottomSheet = !openBottomSheet + }), ) { Image( painter = painter, @@ -106,6 +124,57 @@ fun FavoriteMovieItem( contentScale = ContentScale.Crop ) } + if (openBottomSheet) { + val sheetState = rememberModalBottomSheetState() + ModalBottomSheet( + onDismissRequest = { openBottomSheet = false }, + modifier = Modifier.fillMaxSize(), + sheetState = sheetState, + content = { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = rememberAsyncImagePainter(model = favoriteMovie.poster), + contentDescription = stringResource(R.string.movie_poster), + modifier = Modifier.height(200.dp), + contentScale = ContentScale.Crop + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.category), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold + + ) + Text( + favoriteMovie.type, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 8.dp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.release_date), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold + + ) + Text( + favoriteMovie.year, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 8.dp) + ) + } + } + ) + } } fun lerp(start: Float, stop: Float, fraction: Float): Float { From fb9c7d0371bdd3a9017f7ee4cce8c54b02ef777b Mon Sep 17 00:00:00 2001 From: kevinMartinezC Date: Fri, 28 Apr 2023 14:55:22 -0600 Subject: [PATCH 15/16] feat: add MovieDetails to the design to the BottomSheet information --- .../moviesvapp/ui/components/MovieDetails.kt | 62 +++++++++++++++++++ .../ui/components/detail/DetailScreen.kt | 61 ++---------------- .../ui/components/favorite/FavoriteScreen.kt | 54 ++-------------- 3 files changed, 74 insertions(+), 103 deletions(-) create mode 100644 app/src/main/java/com/example/moviesvapp/ui/components/MovieDetails.kt diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/MovieDetails.kt b/app/src/main/java/com/example/moviesvapp/ui/components/MovieDetails.kt new file mode 100644 index 0000000..b32b43f --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/MovieDetails.kt @@ -0,0 +1,62 @@ +package com.example.moviesvapp.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter +import com.example.moviesvapp.R + +@Composable +fun MovieDetails(poster: String, type: String, year: String) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = rememberAsyncImagePainter(model = poster), + contentDescription = stringResource(R.string.movie_poster), + modifier = Modifier.height(200.dp), + contentScale = ContentScale.Crop + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.category), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold + ) + Text( + type, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 8.dp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.release_date), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold + ) + Text( + year, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 8.dp) + ) + } +} diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/detail/DetailScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/detail/DetailScreen.kt index 6944d93..7cb13b9 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/detail/DetailScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/detail/DetailScreen.kt @@ -1,26 +1,13 @@ package com.example.moviesvapp.ui.components.detail -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import coil.compose.rememberAsyncImagePainter -import com.example.moviesvapp.R import com.example.moviesvapp.model.Movie +import com.example.moviesvapp.ui.components.MovieDetails @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -31,47 +18,11 @@ fun DetailScreen(movie: Movie, openBottomSheet: () -> Unit) { modifier = Modifier.fillMaxSize(), sheetState = sheetState, content = { - Column( - modifier = Modifier - .padding(16.dp) - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - painter = rememberAsyncImagePainter(model = movie.poster), - contentDescription = stringResource(R.string.movie_poster), - modifier = Modifier.height(200.dp), - contentScale = ContentScale.Crop - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = stringResource(R.string.category), - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Bold - - ) - Text( - movie.type, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(top = 8.dp) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = stringResource(R.string.release_date), - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Bold - - ) - Text( - movie.year, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(top = 8.dp) - ) - } + MovieDetails( + poster = movie.poster, + type = movie.type, + year = movie.year + ) } ) } diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt index 62ffee5..2262a96 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt @@ -5,12 +5,8 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerDefaults @@ -19,9 +15,7 @@ import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -35,12 +29,12 @@ import androidx.compose.ui.draw.scale import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter import com.example.moviesvapp.R import com.example.moviesvapp.model.database.FavoriteMovie +import com.example.moviesvapp.ui.components.MovieDetails import com.example.moviesvapp.ui.theme.MyApplicationTheme import kotlinx.coroutines.flow.StateFlow import kotlin.math.absoluteValue @@ -131,47 +125,11 @@ fun FavoriteMovieItem( modifier = Modifier.fillMaxSize(), sheetState = sheetState, content = { - Column( - modifier = Modifier - .padding(16.dp) - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - painter = rememberAsyncImagePainter(model = favoriteMovie.poster), - contentDescription = stringResource(R.string.movie_poster), - modifier = Modifier.height(200.dp), - contentScale = ContentScale.Crop - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = stringResource(R.string.category), - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Bold - - ) - Text( - favoriteMovie.type, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(top = 8.dp) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = stringResource(R.string.release_date), - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Bold - - ) - Text( - favoriteMovie.year, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(top = 8.dp) - ) - } + MovieDetails( + poster = favoriteMovie.poster, + type = favoriteMovie.type, + year = favoriteMovie.year + ) } ) } From a5dabf871da4d490c54c0f02b2657563e248aaf0 Mon Sep 17 00:00:00 2001 From: kevinMartinezC Date: Fri, 28 Apr 2023 15:05:17 -0600 Subject: [PATCH 16/16] feat: organize code --- .../model/SharedPreferencesExtensions.kt | 21 +++-- .../components/favorite/FavoriteMovieItem.kt | 87 +++++++++++++++++++ .../ui/components/favorite/FavoriteScreen.kt | 78 ----------------- 3 files changed, 102 insertions(+), 84 deletions(-) create mode 100644 app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteMovieItem.kt diff --git a/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt b/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt index 5aba944..7de6e34 100644 --- a/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt +++ b/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt @@ -3,6 +3,9 @@ package com.example.moviesvapp.model import android.content.SharedPreferences import android.os.Build import androidx.annotation.RequiresApi +import com.example.moviesvapp.model.SharedPreferencesExtensionsKey.API_KEY +import com.example.moviesvapp.model.SharedPreferencesExtensionsKey.LAST_LOGIN_DATE +import com.example.moviesvapp.model.SharedPreferencesExtensionsKey.USER_NAME import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -20,27 +23,27 @@ fun SharedPreferences.saveApiKey(apiKey: String) { } fun SharedPreferences.getApiKey(): String? { - return getString("API_KEY", null) + return getString(API_KEY, null) } fun SharedPreferences.clearUsername() { - edit().remove("USERNAME").apply() + edit().remove(USER_NAME).apply() } fun SharedPreferences.clearApiKey() { - edit().remove("API_KEY").apply() + edit().remove(API_KEY).apply() } fun SharedPreferences.saveLastLoginDate(date: String) { - edit().putString("LAST_LOGIN_DATE", date).apply() + edit().putString(LAST_LOGIN_DATE, date).apply() } fun SharedPreferences.getLastLoginDate(): String? { - return getString("LAST_LOGIN_DATE", null) + return getString(LAST_LOGIN_DATE, null) } fun SharedPreferences.clearLastLoginDate() { - edit().remove("LAST_LOGIN_DATE").apply() + edit().remove(LAST_LOGIN_DATE).apply() } @RequiresApi(Build.VERSION_CODES.O) @@ -49,3 +52,9 @@ fun getCurrentDateTime(): String { val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") return currentDateTime.format(formatter) } + +object SharedPreferencesExtensionsKey{ + const val API_KEY ="API_KEY" + const val USER_NAME = "USERNAME" + const val LAST_LOGIN_DATE = "LAST_LOGIN_DATE" +} diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteMovieItem.kt b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteMovieItem.kt new file mode 100644 index 0000000..32799f4 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteMovieItem.kt @@ -0,0 +1,87 @@ +package com.example.moviesvapp.ui.components.favorite + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.PagerState +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import coil.compose.rememberAsyncImagePainter +import com.example.moviesvapp.R +import com.example.moviesvapp.model.database.FavoriteMovie +import com.example.moviesvapp.ui.components.MovieDetails +import kotlin.math.absoluteValue + +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) +@Composable +fun FavoriteMovieItem( + favoriteMovie: FavoriteMovie, + pagerState: PagerState, + currentPage: Int, + cardWidth: Dp, + cardHeight: Dp +) { + val painter = rememberAsyncImagePainter(model = favoriteMovie.poster) + var openBottomSheet by rememberSaveable { mutableStateOf(false) } + + val pageOffset = ( + (pagerState.currentPage - currentPage) + pagerState.currentPageOffsetFraction + ).absoluteValue + + val scale = lerp( + start = 0.8f, + stop = 1f, + fraction = 1f - pageOffset.coerceIn(0f, 1f) + ) + + Card( + modifier = Modifier + .size(cardWidth, cardHeight) + .scale(scale) + .animateContentSize() + .clickable(onClick = { + openBottomSheet = !openBottomSheet + }), + ) { + Image( + painter = painter, + contentDescription = stringResource(R.string.movie_poster), + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + } + if (openBottomSheet) { + val sheetState = rememberModalBottomSheetState() + ModalBottomSheet( + onDismissRequest = { openBottomSheet = false }, + modifier = Modifier.fillMaxSize(), + sheetState = sheetState, + content = { + MovieDetails( + poster = favoriteMovie.poster, + type = favoriteMovie.type, + year = favoriteMovie.year + ) + } + ) + } +} + +fun lerp(start: Float, stop: Float, fraction: Float): Float { + return (1 - fraction) * start + fraction * stop +} \ No newline at end of file diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt index 2262a96..f3f4fe8 100644 --- a/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt +++ b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt @@ -1,43 +1,23 @@ package com.example.moviesvapp.ui.components.favorite -import androidx.compose.animation.animateContentSize import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerDefaults import androidx.compose.foundation.pager.PagerSnapDistance -import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import coil.compose.rememberAsyncImagePainter -import com.example.moviesvapp.R import com.example.moviesvapp.model.database.FavoriteMovie -import com.example.moviesvapp.ui.components.MovieDetails import com.example.moviesvapp.ui.theme.MyApplicationTheme import kotlinx.coroutines.flow.StateFlow -import kotlin.math.absoluteValue @OptIn(ExperimentalFoundationApi::class) @Composable @@ -80,61 +60,3 @@ fun FavoriteScreen(favoriteMoviesFlow: StateFlow>) { } } -@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) -@Composable -fun FavoriteMovieItem( - favoriteMovie: FavoriteMovie, - pagerState: PagerState, - currentPage: Int, - cardWidth: Dp, - cardHeight: Dp -) { - val painter = rememberAsyncImagePainter(model = favoriteMovie.poster) - var openBottomSheet by rememberSaveable { mutableStateOf(false) } - - val pageOffset = ( - (pagerState.currentPage - currentPage) + pagerState.currentPageOffsetFraction - ).absoluteValue - - val scale = lerp( - start = 0.8f, - stop = 1f, - fraction = 1f - pageOffset.coerceIn(0f, 1f) - ) - - Card( - modifier = Modifier - .size(cardWidth, cardHeight) - .scale(scale) - .animateContentSize() - .clickable(onClick = { - openBottomSheet = !openBottomSheet - }), - ) { - Image( - painter = painter, - contentDescription = stringResource(R.string.movie_poster), - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } - if (openBottomSheet) { - val sheetState = rememberModalBottomSheetState() - ModalBottomSheet( - onDismissRequest = { openBottomSheet = false }, - modifier = Modifier.fillMaxSize(), - sheetState = sheetState, - content = { - MovieDetails( - poster = favoriteMovie.poster, - type = favoriteMovie.type, - year = favoriteMovie.year - ) - } - ) - } -} - -fun lerp(start: Float, stop: Float, fraction: Float): Float { - return (1 - fraction) * start + fraction * stop -} \ No newline at end of file