diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4844e75..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") @@ -58,7 +64,9 @@ 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") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") @@ -70,24 +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") + 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") - val navVersion = "2.5.3" - implementation("androidx.navigation:navigation-compose:$navVersion") diff --git a/app/src/main/java/com/example/moviesvapp/components/MovieItem.kt b/app/src/main/java/com/example/moviesvapp/components/MovieItem.kt deleted file mode 100644 index 5bf6f8d..0000000 --- a/app/src/main/java/com/example/moviesvapp/components/MovieItem.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.example.moviesvapp.components - -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.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 - -@Composable -fun MovieItem(movie: Movie) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - val painter = rememberAsyncImagePainter(model = movie.poster) - Image( - painter = painter, - contentDescription = "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(movie.title, fontWeight = FontWeight.Bold) - Text(movie.year) - } - } -} \ No newline at end of file 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..b983758 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,5 @@ data class Movie( @SerializedName("Type") val type: String, @SerializedName("Poster") - val poster: String + val poster: String, ) \ 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/MoviesViewModel.kt b/app/src/main/java/com/example/moviesvapp/model/MoviesViewModel.kt deleted file mode 100644 index e9ae628..0000000 --- a/app/src/main/java/com/example/moviesvapp/model/MoviesViewModel.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.moviesvapp.model - -import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class MoviesViewModel : ViewModel() { - val movies = mutableStateOf>(emptyList()) - val isLoading = mutableStateOf(false) - - private val omdbApi = RetrofitInstance.omdbApi - - fun searchMovies(query: String) { - viewModelScope.launch(Dispatchers.IO) { - try { - val apiKey = "458ec391" - val trimmedQuery = query.trim() // Trim the query - val response = omdbApi.searchMovies(apiKey, trimmedQuery) - withContext(Dispatchers.Main) { - if (response.Response == "True") { - movies.value = response.Search - isLoading.value = false - } else { - movies.value = emptyList() - isLoading.value = false - } - } - } catch (e: Exception) { - withContext(Dispatchers.Main) { - movies.value = emptyList() - isLoading.value = false - } - } - } - } - -} 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..de3f56d --- /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() + 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 new file mode 100644 index 0000000..7de6e34 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/model/SharedPreferencesExtensions.kt @@ -0,0 +1,60 @@ +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 + + +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) +} + +fun SharedPreferences.clearUsername() { + edit().remove(USER_NAME).apply() +} + +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) +} + +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/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..984da92 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/model/database/MovieDao.kt @@ -0,0 +1,23 @@ +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) + + @Query("DELETE FROM favorite_movies") + suspend fun deleteAll() +} 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/MainActivity.kt b/app/src/main/java/com/example/moviesvapp/ui/MainActivity.kt index 5016548..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,19 +1,52 @@ package com.example.moviesvapp.ui +import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.lifecycle.viewmodel.compose.viewModel -import com.example.moviesvapp.components.MoviesScreen -import com.example.moviesvapp.model.MoviesViewModel +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.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 { - val viewModel: MoviesViewModel = viewModel() - MoviesScreen(viewModel) + MyApplicationTheme { + val lastLoginDate = viewModel.getLastLoginDate() + val uiState by viewModel.uiState.collectAsState() + + if (!uiState.isLoggedIn) { + LoginScreen( + loginStatus = uiState.loginStatus, + onClickedLogin = viewModel::login, + onLogin = viewModel::saveCredentials, + ) + } else { + MainScreen( + onLogout = { + viewModel.clearData() + moviesViewModel.clearFavoriteMovies() + + }, + 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 new file mode 100644 index 0000000..a3e4663 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/MainScreen.kt @@ -0,0 +1,55 @@ +package com.example.moviesvapp.ui.components + +import android.annotation.SuppressLint +import androidx.compose.material.* +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.Scaffold +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.navigation.compose.rememberNavController +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.topbar.AppTopBar +import com.example.moviesvapp.ui.theme.MyApplicationTheme +import kotlinx.coroutines.launch + +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +@Composable +fun MainScreen(onLogout: () -> Unit, username: String?, lastLoginDate: String?) { + val navController = rememberNavController() + val drawerState = rememberDrawerState(DrawerValue.Closed) + val scope = rememberCoroutineScope() + + MyApplicationTheme { + ModalNavigationDrawer( + drawerContent = { + LogoutDrawer( + onLogout = onLogout, + username = username, + lastLoginDate = lastLoginDate + ) + }, + 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) + } + } + ) + } +} + + + 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 new file mode 100644 index 0000000..7cb13b9 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/detail/DetailScreen.kt @@ -0,0 +1,28 @@ +package com.example.moviesvapp.ui.components.detail + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.example.moviesvapp.model.Movie +import com.example.moviesvapp.ui.components.MovieDetails + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DetailScreen(movie: Movie, openBottomSheet: () -> Unit) { + val sheetState = rememberModalBottomSheetState() + ModalBottomSheet( + onDismissRequest = openBottomSheet, + modifier = Modifier.fillMaxSize(), + sheetState = sheetState, + content = { + MovieDetails( + poster = movie.poster, + type = movie.type, + year = movie.year + ) + } + ) +} 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 new file mode 100644 index 0000000..f3f4fe8 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/favorite/FavoriteScreen.kt @@ -0,0 +1,62 @@ +package com.example.moviesvapp.ui.components.favorite + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerDefaults +import androidx.compose.foundation.pager.PagerSnapDistance +import androidx.compose.foundation.pager.rememberPagerState +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.platform.LocalConfiguration +import androidx.compose.ui.unit.dp +import com.example.moviesvapp.model.database.FavoriteMovie +import com.example.moviesvapp.ui.theme.MyApplicationTheme +import kotlinx.coroutines.flow.StateFlow + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun FavoriteScreen(favoriteMoviesFlow: StateFlow>) { + val favoriteMovies by favoriteMoviesFlow.collectAsState(initial = emptyList()) + + MyApplicationTheme { + 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 + ) + } + } + } +} + 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 new file mode 100644 index 0000000..83122c8 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieItem.kt @@ -0,0 +1,88 @@ +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.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 +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 +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 +import com.example.moviesvapp.ui.components.detail.DetailScreen + +@Composable +fun MovieItem( + movie: Movie, + favoriteMovies: Set, + onToggleFavorite: (Movie) -> Unit +) { + var openBottomSheet by rememberSaveable { mutableStateOf(false) } + val isFavorite = favoriteMovies.contains(movie.imdbID) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .clickable(onClick = { + openBottomSheet = !openBottomSheet + }), + + verticalAlignment = Alignment.CenterVertically + ) { + val painter = rememberAsyncImagePainter(model = movie.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( + movie.title, style = MaterialTheme.typography.bodyLarge + ) + + } + 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 new file mode 100644 index 0000000..60cc924 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/MovieUiState.kt @@ -0,0 +1,10 @@ +package com.example.moviesvapp.ui.components.home + +import com.example.moviesvapp.model.Movie + +data class MovieUiState( + val movies: List = emptyList(), + val isLoading: Boolean = false, + val favoriteMovies: Set = emptySet() +) + 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 65% 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 3fa3470..1a6ebde 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,4 +1,4 @@ -package com.example.moviesvapp.components +package com.example.moviesvapp.ui.components.home import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -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 @@ -24,16 +23,22 @@ 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.model.MoviesViewModel -import com.example.musicplayercompose.ui.theme.MyApplicationTheme +import com.example.moviesvapp.R +import com.example.moviesvapp.model.Movie +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 +fun MoviesScreen( + uiState: MovieUiState, + searchMovies: (String) -> Unit, + onToggleFavorite: (Movie) -> Unit + +) { + val movies = uiState.movies + val isLoading = uiState.isLoading var searchQuery by remember { mutableStateOf(TextFieldValue("")) } MyApplicationTheme { @@ -41,19 +46,25 @@ fun MoviesScreen(viewModel: MoviesViewModel) { Column( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(56.dp), 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 = { viewModel.searchMovies(searchQuery.text) } + onClick = { + searchMovies(searchQuery.text) + } ) { - Icon(Icons.Default.Search, contentDescription = "Search Icon") + Icon( + Icons.Default.Search, + contentDescription = stringResource(R.string.search_icon) + ) } } ) @@ -62,10 +73,13 @@ fun MoviesScreen(viewModel: MoviesViewModel) { if (isLoading) { CircularProgressIndicator(modifier = Modifier.wrapContentSize(Alignment.Center)) } else { - LazyColumn { + LazyColumn( + modifier = Modifier.padding(bottom = 56.dp) + ) { 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 new file mode 100644 index 0000000..4bdcef5 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/home/viewmodel/MoviesViewModel.kt @@ -0,0 +1,109 @@ +package com.example.moviesvapp.ui.components.home.viewmodel + +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 +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() { + + private val omdbApi = RetrofitInstance.omdbApi + 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 + } + } + } + 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( + movies = emptyList(), + isLoading = false + ) + ) + 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 { + val apiKey = sharedPreferences.getApiKey() + ?: throw IllegalStateException("API Key not found") + val trimmedQuery = query.trim() + val response = omdbApi.searchMovies(apiKey, trimmedQuery) + withContext(Dispatchers.Main) { + if (response.response == "True") { + _uiState.update { + it.copy(movies = response.search, isLoading = false) + } + } else { + _uiState.update { + it.copy(movies = emptyList(), isLoading = false) + } + } + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + _uiState.update { + it.copy(movies = emptyList(), isLoading = 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 new file mode 100644 index 0000000..92fb47b --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginScreen.kt @@ -0,0 +1,90 @@ +package com.example.moviesvapp.ui.components.login + +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.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.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 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoginScreen( + loginStatus: Resource>, + onClickedLogin: (String, String) -> Unit, + onLogin: (String, String) -> Unit +) { + + val username = remember { mutableStateOf("") } + val apiKey = remember { mutableStateOf("") } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextField( + value = username.value, + onValueChange = { username.value = it }, + label = { Text(stringResource(R.string.username)) }, + singleLine = true + ) + Spacer(modifier = Modifier.height(8.dp)) + TextField( + value = apiKey.value, + onValueChange = { apiKey.value = it }, + label = { Text(stringResource(R.string.password)) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + ) + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = { + 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( + stringResource(R.string.invalid__key_please_check_your_password), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium + ) + } + + is Resource.Idle -> { + Text( + 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/LoginUiState.kt b/app/src/main/java/com/example/moviesvapp/ui/components/login/LoginUiState.kt new file mode 100644 index 0000000..6eb9d62 --- /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? = 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 new file mode 100644 index 0000000..12feb74 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/login/viewmodel/LoginViewModel.kt @@ -0,0 +1,109 @@ +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.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + + +class LoginViewModel(context: Context) : ViewModel() { + + private val sharedPreferences: SharedPreferences = + context.getSharedPreferences("MyPrefs", Context.MODE_PRIVATE) + + private val _uiState = MutableStateFlow( + LoginUiState( + isLoggedIn = !sharedPreferences.getApiKey().isNullOrBlank(), + userName = sharedPreferences.getUsername() // Add this line + + ) + ) + val uiState = _uiState.asStateFlow() + + + fun getLastLoginDate(): String? { + return sharedPreferences.getLastLoginDate() + } + + @RequiresApi(Build.VERSION_CODES.O) + fun saveCredentials(username: String, apiKey: String) { + 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 { + _uiState.update { it.copy(loginStatus = Resource.Loading) } + try { + val response = RetrofitInstance.omdbApi.searchMovies(apiKey, "marvel") + if (response.response == "True") { + _uiState.update { + it.copy( + userName = username, + loginStatus = Resource.Success(Pair(username, apiKey)) + ) + + } + } else { + _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" + ) + ) + } + Log.d("LoginError", "Exception caught: ${e.message}") + } + } + } +} 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 new file mode 100644 index 0000000..67ba053 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomBar.kt @@ -0,0 +1,27 @@ +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 + +@Composable +fun BottomBar(navController: NavHostController) { + val screens = listOf( + BottomNavItem.Home, + BottomNavItem.Favorite, + ) + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + + NavigationBar { + 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/ui/components/navigation/BottomNavGraph.kt b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavGraph.kt new file mode 100644 index 0000000..df80aa4 --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavGraph.kt @@ -0,0 +1,41 @@ +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 +import androidx.navigation.compose.composable +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 + val moviesViewModel = remember { MoviesViewModel(context) } + + val uiState by moviesViewModel.uiState.collectAsState() + + NavHost( + navController = navController, + startDestination = BottomNavItem.Home.route + ) { + composable(route = BottomNavItem.Home.route) { + MoviesScreen( + uiState = uiState, + searchMovies = moviesViewModel::searchMovies, + onToggleFavorite = { movie -> moviesViewModel.toggleFavorite(movie) } + + ) + } + composable(route = BottomNavItem.Favorite.route) { + FavoriteScreen(favoriteMoviesFlow = moviesViewModel.favoriteMoviesFlow) + } + } +} diff --git a/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavItem.kt b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavItem.kt new file mode 100644 index 0000000..326067d --- /dev/null +++ b/app/src/main/java/com/example/moviesvapp/ui/components/navigation/BottomNavItem.kt @@ -0,0 +1,23 @@ +package com.example.moviesvapp.ui.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/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/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/res/drawable/logo.png b/app/src/main/res/drawable/logo.png new file mode 100644 index 0000000..2866e07 Binary files /dev/null and b/app/src/main/res/drawable/logo.png differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e96199a..61e60bb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,23 @@ 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 + Login + Category: + Movie Release: + Last login date icon + User: %1$s + User icon + Logo + 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