From a2a86240ae72fe09057a11b011367361010efb91 Mon Sep 17 00:00:00 2001 From: webianks Date: Sat, 10 Jan 2026 20:01:05 +0530 Subject: [PATCH 01/25] Init Compose HomeScreen --- .../tech/salroid/filmy/theme/LimeGreen.kt | 0 .../java/tech/salroid/filmy/theme/Theme.kt | 4 + .../salroid/filmy/ui/composables/Error.kt | 29 +++++ .../salroid/filmy/ui/composables/Loading.kt | 4 + .../salroid/filmy/ui/composables/MovieItem.kt | 70 ++++++++++++ .../filmy/ui/home/MainActivityCompose.kt | 106 ++++++++++++++++++ .../salroid/filmy/ui/screens/HomeScreen.kt | 4 + .../salroid/filmy/ui/screens/MoviesScreen.kt | 56 +++++++++ .../filmy/ui/screens/MoviesViewModel.kt | 4 + 9 files changed, 277 insertions(+) create mode 100644 app/src/main/java/tech/salroid/filmy/theme/LimeGreen.kt create mode 100644 app/src/main/java/tech/salroid/filmy/theme/Theme.kt create mode 100644 app/src/main/java/tech/salroid/filmy/ui/composables/Error.kt create mode 100644 app/src/main/java/tech/salroid/filmy/ui/composables/Loading.kt create mode 100644 app/src/main/java/tech/salroid/filmy/ui/composables/MovieItem.kt create mode 100644 app/src/main/java/tech/salroid/filmy/ui/home/MainActivityCompose.kt create mode 100644 app/src/main/java/tech/salroid/filmy/ui/screens/HomeScreen.kt create mode 100644 app/src/main/java/tech/salroid/filmy/ui/screens/MoviesScreen.kt create mode 100644 app/src/main/java/tech/salroid/filmy/ui/screens/MoviesViewModel.kt diff --git a/app/src/main/java/tech/salroid/filmy/theme/LimeGreen.kt b/app/src/main/java/tech/salroid/filmy/theme/LimeGreen.kt new file mode 100644 index 00000000..e69de29b diff --git a/app/src/main/java/tech/salroid/filmy/theme/Theme.kt b/app/src/main/java/tech/salroid/filmy/theme/Theme.kt new file mode 100644 index 00000000..12e707cd --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/theme/Theme.kt @@ -0,0 +1,4 @@ +package tech.salroid.filmy.theme + +class Theme { +} \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/composables/Error.kt b/app/src/main/java/tech/salroid/filmy/ui/composables/Error.kt new file mode 100644 index 00000000..93d337d0 --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/ui/composables/Error.kt @@ -0,0 +1,29 @@ +package tech.salroid.filmy.ui.composables + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.LoadingIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview + + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun LoadingWidget(modifier: Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + LoadingIndicator() + } +} + + +@Composable +@Preview(showBackground = true) +fun LoadingWidgetPreview() { + LoadingWidget(modifier = Modifier) +} \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/composables/Loading.kt b/app/src/main/java/tech/salroid/filmy/ui/composables/Loading.kt new file mode 100644 index 00000000..d98161a5 --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/ui/composables/Loading.kt @@ -0,0 +1,4 @@ +package tech.salroid.filmy.ui.composables + +class Loading { +} \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/composables/MovieItem.kt b/app/src/main/java/tech/salroid/filmy/ui/composables/MovieItem.kt new file mode 100644 index 00000000..39a9c404 --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/ui/composables/MovieItem.kt @@ -0,0 +1,70 @@ +package tech.salroid.filmy.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color.Companion.Transparent +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import tech.salroid.filmy.R +import tech.salroid.filmy.data.local.db.entity.Movie +import tech.salroid.filmy.ui.screens.dummyMovie +import tech.salroid.filmy.utility.toReadableDate + +@Composable +fun MovieItem( + modifier: Modifier, + movie: Movie +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Card( + shape = RoundedCornerShape(corner = CornerSize(16.dp)), + colors = CardDefaults.cardColors(containerColor = Transparent) + ) { + AsyncImage( + modifier = Modifier + .fillMaxWidth() + .height(170.dp), + //.clip(RoundedCornerShape(corner = CornerSize(16.dp))), + contentScale = ContentScale.Fit, + model = stringResource(R.string.movie_poster_url, movie.posterPath.orEmpty()), + contentDescription = "${movie.title} movie poster}" + ) + } + Text( + modifier = Modifier.padding(top = 8.dp), + textAlign = TextAlign.Center, + text = movie.title.orEmpty() + ) + Text( + modifier = Modifier.padding(top = 4.dp), + textAlign = TextAlign.Center, + text = movie.releaseDate?.toReadableDate().orEmpty() + ) + } +} + +@Preview(showBackground = true) +@Composable +fun MovieItemPreview() { + MovieItem( + modifier = Modifier, + movie = dummyMovie + ) +} \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/home/MainActivityCompose.kt b/app/src/main/java/tech/salroid/filmy/ui/home/MainActivityCompose.kt new file mode 100644 index 00000000..87e3e04c --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/ui/home/MainActivityCompose.kt @@ -0,0 +1,106 @@ +package tech.salroid.filmy.ui.home + +import android.content.Intent +import android.os.Bundle +import androidx.activity.OnBackPressedCallback +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import androidx.navigation.findNavController +import androidx.navigation.ui.NavigationUI +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import tech.salroid.filmy.R +import tech.salroid.filmy.databinding.ActivityMainBinding +import tech.salroid.filmy.ui.intro.FilmyIntroActivity +import tech.salroid.filmy.ui.search.SearchViewModel +import tech.salroid.filmy.utility.PreferenceHelper.isColdStart +import tech.salroid.filmy.utility.PreferenceHelper.setColdStartDone +import tech.salroid.filmy.utility.themeSystemBars + +@AndroidEntryPoint +class MainActivity : AppCompatActivity() { + + private val viewModel: MainViewModel by viewModels() + private val viewModelSearch: SearchViewModel by viewModels() + private var throughShortcut: Boolean? = null + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + setTheme(R.style.AppTheme) + super.onCreate(savedInstanceState) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + throughShortcut = intent.getBooleanExtra("throughShortcut", false) + themeSystemBars(lightStatusBar = true, navigationColorAsStatus = false) + + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (viewModelSearch.isSearchOpen.value) { + viewModelSearch.closeSearch() + } else { + if (isEnabled) { + isEnabled = false + onBackPressedDispatcher.onBackPressed() + } + } + } + }) + introLogic() + setupNavigation() + observerUiStates() + } + + private fun observerUiStates() { + lifecycleScope.launch { + /*viewModelSearch.uiStateSearchView.collect { + it?.let { + val margin = when (it) { + SearchViewUiState.Hidden -> 0 + SearchViewUiState.Visible -> binding.navigationBarView.height + } + animateBottomNavigation(margin) + } + }*/ + } + + lifecycleScope.launch { + viewModel.uiStateNavigationVisibility.collect { + // binding.mainAppBarContent.root.isVisible = it + // animateTopNavigation(it) + } + } + } + + private fun animateBottomNavigation(margin: Int) { + /*binding.navigationBarView.clearAnimation() + binding.navigationBarView + .animate() + .translationY(margin.toFloat()).apply { + duration = 175 + startDelay = if (margin == 0) 100 else 0 + interpolator = + if (margin == 0) LinearOutSlowInInterpolator() else FastOutLinearInInterpolator() + }*/ + } + + private fun setupNavigation() { + val navController = findNavController(R.id.navHostFragment) + NavigationUI.setupWithNavController(binding.navigationBarView, navController) + + if (throughShortcut == true) { + navController.navigate(R.id.collections) + binding.navigationBarView.selectedItemId = R.id.collections + } + } + + private fun introLogic() { + if (isColdStart(this)) { + val intent = Intent(this@MainActivity, FilmyIntroActivity::class.java) + startActivity(intent) + setColdStartDone(this) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/screens/HomeScreen.kt b/app/src/main/java/tech/salroid/filmy/ui/screens/HomeScreen.kt new file mode 100644 index 00000000..ddd457e1 --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/ui/screens/HomeScreen.kt @@ -0,0 +1,4 @@ +package tech.salroid.filmy.ui.screens + +class HomeScreen { +} \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesScreen.kt b/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesScreen.kt new file mode 100644 index 00000000..208cb7b4 --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesScreen.kt @@ -0,0 +1,56 @@ +package tech.salroid.filmy.ui.screens + +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.viewmodel.compose.viewModel +import tech.salroid.filmy.data.local.db.entity.Movie +import tech.salroid.filmy.ui.components.MovieItem + +@Composable +fun MoviesScreen( + modifier: Modifier, + viewModel: MoviesViewModel = viewModel() +) { + + val moviesUiState by viewModel. + +} + + +@Composable +fun MoviesList(movies: List) { + LazyVerticalGrid(columns = GridCells.Fixed(3)) { + items(movies) { + MovieItem(modifier = Modifier, movie = it) + } + } +} + +@Preview(showBackground = true) +@Composable +fun MoviesListPreview() { + MoviesList( + movies = listOf( + dummyMovie, + dummyMovie, + dummyMovie, + dummyMovie, + dummyMovie, + dummyMovie, + dummyMovie, + dummyMovie + ) + ) +} + + +val dummyMovie = Movie( + id = 12141, + title = "People We Meet On Vacation", + releaseDate = "2026-01-06", + +) \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesViewModel.kt b/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesViewModel.kt new file mode 100644 index 00000000..d8bf7c5b --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesViewModel.kt @@ -0,0 +1,4 @@ +package tech.salroid.filmy.ui.screens + +class MoviesViewModel { +} \ No newline at end of file From 79bfee336662c45dd21c50f3e71fd3861f0437d0 Mon Sep 17 00:00:00 2001 From: webianks Date: Sat, 10 Jan 2026 22:16:37 +0530 Subject: [PATCH 02/25] Compose Init: Bootstrap HomeScreen --- app/build.gradle | 51 ++++++-- app/src/main/AndroidManifest.xml | 2 +- .../filmy/data/network/MoviesApiHelper.kt | 3 + .../filmy/data/network/MoviesApiHelperImpl.kt | 7 ++ .../tech/salroid/filmy/theme/LimeGreen.kt | 51 ++++++++ .../java/tech/salroid/filmy/theme/Theme.kt | 49 +++++++- .../salroid/filmy/ui/composables/Error.kt | 30 +++-- .../salroid/filmy/ui/composables/Loading.kt | 27 ++++- .../salroid/filmy/ui/composables/MovieItem.kt | 21 ++-- .../filmy/ui/home/MainActivityCompose.kt | 112 ++++-------------- .../salroid/filmy/ui/home/MoviesRepository.kt | 3 + .../filmy/ui/intro/FilmyIntroActivity.kt | 22 +++- .../salroid/filmy/ui/intro/IntroFragmentA.kt | 16 ++- .../salroid/filmy/ui/intro/IntroFragmentB.kt | 15 ++- .../salroid/filmy/ui/intro/IntroFragmentC.kt | 15 ++- .../salroid/filmy/ui/intro/IntroFragmentD.kt | 15 ++- .../salroid/filmy/ui/screens/HomeScreen.kt | 13 +- .../salroid/filmy/ui/screens/MoviesScreen.kt | 42 +++++-- .../filmy/ui/screens/MoviesViewModel.kt | 39 +++++- build.gradle | 7 +- 20 files changed, 389 insertions(+), 151 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9cc713b0..a7ede3a5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.google.devtools.ksp' id 'com.google.dagger.hilt.android' + id 'org.jetbrains.kotlin.plugin.compose' } apply plugin: 'com.android.application' @@ -50,8 +51,9 @@ android { } buildFeatures { - viewBinding = true - buildConfig = true + viewBinding true + buildConfig true + compose true } namespace 'tech.salroid.filmy' } @@ -93,30 +95,31 @@ dependencies { implementation "androidx.paging:paging-runtime-ktx:$paging_version" implementation 'androidx.browser:browser:1.9.0' - implementation "androidx.work:work-runtime:2.10.5" + implementation "androidx.work:work-runtime:2.11.0" implementation 'androidx.palette:palette-ktx:1.0.0' implementation "androidx.concurrent:concurrent-futures-ktx:1.3.0" implementation 'com.github.bumptech.glide:glide:5.0.5' - implementation 'com.github.paolorotolo:appintro:4.1.0' + implementation 'com.github.paolorotolo:appintro:6.3.1' implementation "androidx.preference:preference-ktx:1.2.1" implementation "androidx.fragment:fragment-ktx:1.8.9" - implementation "androidx.core:core-splashscreen:1.0.1" + implementation "androidx.core:core-splashscreen:1.2.0" implementation 'com.squareup.retrofit2:retrofit:3.0.0' implementation 'com.squareup.retrofit2:converter-gson:3.0.0' - implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0' - implementation 'com.squareup.okhttp3:okhttp:4.10.0' + implementation 'com.squareup.okhttp3:logging-interceptor:5.3.2' + implementation 'com.squareup.okhttp3:okhttp:5.3.2' implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation 'com.facebook.shimmer:shimmer:0.5.0' - def lottieVersion = "3.4.0" - implementation "com.airbnb.android:lottie:$lottieVersion" + implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0" - def lifecycle_version = "2.9.4" + def lifecycle_version = "2.10.0" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" - def roomVersion = "2.8.1" + def roomVersion = "2.8.4" implementation "androidx.room:room-runtime:$roomVersion" + implementation "androidx.room:room-ktx:$roomVersion" + implementation "androidx.room:room-paging:$roomVersion" ksp "androidx.room:room-compiler:$roomVersion" // Dagger Hilt @@ -124,14 +127,36 @@ dependencies { ksp "com.google.dagger:hilt-compiler:2.60.1" // Navigation - def nav_version = "2.9.5" + def nav_version = "2.9.6" implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" - implementation platform('com.google.firebase:firebase-bom:34.3.0') + def lottieVersion = "6.7.1" + implementation "com.airbnb.android:lottie:$lottieVersion" + + // Compose + implementation "com.google.android.material:compose-theme-adapter:1.2.1" + + // Firebase + implementation platform('com.google.firebase:firebase-bom:34.7.0') implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-crashlytics' + // Compose + def composeBom = platform('androidx.compose:compose-bom:2025.12.01') + implementation composeBom + androidTestImplementation composeBom + implementation("androidx.compose.material3:material3:1.5.0-alpha04") + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose' + implementation 'androidx.activity:activity-compose' + implementation("androidx.hilt:hilt-navigation-compose:1.3.0") + implementation("io.coil-kt.coil3:coil-compose:3.3.0") + implementation("io.coil-kt.coil3:coil-network-okhttp:3.3.0") + implementation 'androidx.compose.ui:ui-tooling-preview' + debugImplementation 'androidx.compose.ui:ui-tooling' + + //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'junit:junit:4.13.2' androidTestImplementation "androidx.test.espresso:espresso-core:3.7.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cf10dae7..03aa92b6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ tools:targetApi="n"> diff --git a/app/src/main/java/tech/salroid/filmy/data/network/MoviesApiHelper.kt b/app/src/main/java/tech/salroid/filmy/data/network/MoviesApiHelper.kt index f843866f..3f6c1c2a 100644 --- a/app/src/main/java/tech/salroid/filmy/data/network/MoviesApiHelper.kt +++ b/app/src/main/java/tech/salroid/filmy/data/network/MoviesApiHelper.kt @@ -10,6 +10,9 @@ import tech.salroid.filmy.data.local.model.watch_providers.WatchProviderResponse interface MoviesApiHelper { fun getMovies(type: String, isTrending: Boolean): Flow> + + fun getMoviesFlow(type: String, isTrending: Boolean): Flow + fun getTvShows(type: String, isTrending: Boolean): Flow> fun getMovieDetails(id: String): Flow fun getTvShowDetails(id: String): Flow diff --git a/app/src/main/java/tech/salroid/filmy/data/network/MoviesApiHelperImpl.kt b/app/src/main/java/tech/salroid/filmy/data/network/MoviesApiHelperImpl.kt index 6ba89c5e..af3d5656 100644 --- a/app/src/main/java/tech/salroid/filmy/data/network/MoviesApiHelperImpl.kt +++ b/app/src/main/java/tech/salroid/filmy/data/network/MoviesApiHelperImpl.kt @@ -31,6 +31,13 @@ class MoviesApiHelperImpl(private val apiService: MoviesApiService) : MoviesApiH } ).flow + override fun getMoviesFlow( + type: String, + isTrending: Boolean + ): Flow = flow { + emit(apiService.getTrendingMovies("day", 1)) + } + override fun getTvShows(type: String, isTrending: Boolean): Flow> = Pager( config = PagingConfig( pageSize = 20, diff --git a/app/src/main/java/tech/salroid/filmy/theme/LimeGreen.kt b/app/src/main/java/tech/salroid/filmy/theme/LimeGreen.kt index e69de29b..824dd4d0 100644 --- a/app/src/main/java/tech/salroid/filmy/theme/LimeGreen.kt +++ b/app/src/main/java/tech/salroid/filmy/theme/LimeGreen.kt @@ -0,0 +1,51 @@ +package tech.salroid.filmy.theme + +import androidx.compose.ui.graphics.Color + +val LimeGreen = Color(0xFFC6FF00) +val Primary80 = Color(0xFF6DDD81) +val Primary90 = Color(0xFF89FA9B) +val Blue = Color(0xFF4285F4) + +// Primary colors +val Primary = Color(0xFF34A853) +val OnPrimary = Color(0xFFFFFFFF) +val PrimaryContainer = Color(0xFF34A853) +val OnPrimaryContainer = Color(0xFF202124) + +// Secondary colors +val Secondary = Color(0xFFE6F4EA) +val OnSecondary = Color(0xFF202124) +val SecondaryContainer = Color(0xFFE6F4EA) +val OnSecondaryContainer = Color(0xFF202124) + +// Tertiary colors +val Tertiary = Color(0xFF202124) +val OnTertiary = Color(0xFFFFFFFF) +val TertiaryContainer = Color(0xFF202124) +val OnTertiaryContainer = Color(0xFFFFFFFF) + +// Error colors +val Error = Color(0xFFBA1A1A) // Red +val OnError = Color(0xFFFFFFFF) // White +val ErrorContainer = Color(0xFFFFDAD6) // Light Red +val OnErrorContainer = Color(0xFF93000A) // Dark Red + +// Surface colors +val Surface = Color(0xFFF1F3F4) // White +val SurfaceBright = Color(0xFFE6F4EA) // Light Green +val InverseSurface = Color(0xFF313030) // Dark Gray +val InverseOnSurface = Color(0xFFF3F0EF) // Light gray +val SurfaceContainerLowest = Color(0xFFFFFFFF) // Off White +val SurfaceContainerLow = Color(0xFFEEF0F2) // Light gray +val SurfaceContainer = Color(0xFFE8EBED) // Gray +val SurfaceContainerHigh = Color(0xFFE5E9EB) // Dark Gray +val SurfaceContainerHighest = Color(0xFFE5E9EB) // Very dark Gray + +// Others colors +val OnSurface = Color(0xFF202124) // Black +val OnSurfaceVariant = Color(0xFF434846) // Dark Gray +val Outline = Color(0xFF202124) // Dark Gray +val OutlineVariant = Color(0xFF313030) // Light Dark Gray +val Scrim = Color(0xFF000000) // Black +val Shadow = Color(0xFF000000) // Dark gray \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/theme/Theme.kt b/app/src/main/java/tech/salroid/filmy/theme/Theme.kt index 12e707cd..13747b00 100644 --- a/app/src/main/java/tech/salroid/filmy/theme/Theme.kt +++ b/app/src/main/java/tech/salroid/filmy/theme/Theme.kt @@ -1,4 +1,51 @@ package tech.salroid.filmy.theme -class Theme { +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +private val LightColorScheme = lightColorScheme( + primary = Primary, + onPrimary = OnPrimary, + primaryContainer = PrimaryContainer, + onPrimaryContainer = OnPrimaryContainer, + + secondary = Secondary, + secondaryContainer = SecondaryContainer, + onSecondaryContainer = OnSecondaryContainer, + onSecondary = OnSecondary, + + tertiary = Tertiary, + tertiaryContainer = TertiaryContainer, + onTertiaryContainer = OnTertiaryContainer, + onTertiary = OnTertiary, + + error = Error, + onError = OnError, + onErrorContainer = OnErrorContainer, + errorContainer = ErrorContainer, + inverseSurface = InverseSurface, + inverseOnSurface = InverseOnSurface, + scrim = Scrim, + surfaceContainerLowest = SurfaceContainerLowest, + surfaceContainerLow = SurfaceContainerLow, + surfaceContainer = SurfaceContainer, + surfaceContainerHigh = SurfaceContainerHigh, + surfaceContainerHighest = SurfaceContainerHighest, + onSurface = OnSurface, + onSurfaceVariant = OnSurfaceVariant, + surface = Surface, + surfaceBright = SurfaceBright, + outline = Outline, + outlineVariant = OutlineVariant, +) + +@Composable +fun FilmyTheme(content: @Composable () -> Unit) { + val colorScheme = LightColorScheme + MaterialTheme( + colorScheme = colorScheme + ) { + content() + } } \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/composables/Error.kt b/app/src/main/java/tech/salroid/filmy/ui/composables/Error.kt index 93d337d0..58831f8a 100644 --- a/app/src/main/java/tech/salroid/filmy/ui/composables/Error.kt +++ b/app/src/main/java/tech/salroid/filmy/ui/composables/Error.kt @@ -2,28 +2,38 @@ package tech.salroid.filmy.ui.composables import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.LoadingIndicator +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun LoadingWidget(modifier: Modifier) { +fun ErrorWidget( + modifier: Modifier, + message: String +) { Box( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .padding(32.dp), contentAlignment = Alignment.Center ) { - LoadingIndicator() + Text( + message, + textAlign = TextAlign.Center + ) } } - @Composable @Preview(showBackground = true) -fun LoadingWidgetPreview() { - LoadingWidget(modifier = Modifier) +fun ErrorWidgetPreview() { + ErrorWidget( + modifier = Modifier, + "Something went wrong! Check your internet connection also." + ) } \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/composables/Loading.kt b/app/src/main/java/tech/salroid/filmy/ui/composables/Loading.kt index d98161a5..93d337d0 100644 --- a/app/src/main/java/tech/salroid/filmy/ui/composables/Loading.kt +++ b/app/src/main/java/tech/salroid/filmy/ui/composables/Loading.kt @@ -1,4 +1,29 @@ package tech.salroid.filmy.ui.composables -class Loading { +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.LoadingIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview + + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun LoadingWidget(modifier: Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + LoadingIndicator() + } +} + + +@Composable +@Preview(showBackground = true) +fun LoadingWidgetPreview() { + LoadingWidget(modifier = Modifier) } \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/composables/MovieItem.kt b/app/src/main/java/tech/salroid/filmy/ui/composables/MovieItem.kt index 39a9c404..0842d1cb 100644 --- a/app/src/main/java/tech/salroid/filmy/ui/composables/MovieItem.kt +++ b/app/src/main/java/tech/salroid/filmy/ui/composables/MovieItem.kt @@ -1,4 +1,4 @@ -package tech.salroid.filmy.ui.components +package tech.salroid.filmy.ui.composables import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -8,16 +8,19 @@ import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +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.draw.clip import androidx.compose.ui.graphics.Color.Companion.Transparent import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import tech.salroid.filmy.R import tech.salroid.filmy.data.local.db.entity.Movie @@ -34,27 +37,29 @@ fun MovieItem( horizontalAlignment = Alignment.CenterHorizontally ) { Card( - shape = RoundedCornerShape(corner = CornerSize(16.dp)), - colors = CardDefaults.cardColors(containerColor = Transparent) + shape = RoundedCornerShape(corner = CornerSize(4.dp)), + colors = CardDefaults.cardColors(containerColor = Transparent), ) { AsyncImage( modifier = Modifier .fillMaxWidth() - .height(170.dp), - //.clip(RoundedCornerShape(corner = CornerSize(16.dp))), - contentScale = ContentScale.Fit, + .height(170.dp) + .clip(RoundedCornerShape(corner = CornerSize(4.dp))), + contentScale = ContentScale.FillBounds, model = stringResource(R.string.movie_poster_url, movie.posterPath.orEmpty()), - contentDescription = "${movie.title} movie poster}" + contentDescription = "${movie.title} movie poster" ) } Text( modifier = Modifier.padding(top = 8.dp), textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium.copy(letterSpacing = 0.0.sp), text = movie.title.orEmpty() ) Text( - modifier = Modifier.padding(top = 4.dp), + modifier = Modifier.padding(top = 2.dp), textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodySmall, text = movie.releaseDate?.toReadableDate().orEmpty() ) } diff --git a/app/src/main/java/tech/salroid/filmy/ui/home/MainActivityCompose.kt b/app/src/main/java/tech/salroid/filmy/ui/home/MainActivityCompose.kt index 87e3e04c..6d34269c 100644 --- a/app/src/main/java/tech/salroid/filmy/ui/home/MainActivityCompose.kt +++ b/app/src/main/java/tech/salroid/filmy/ui/home/MainActivityCompose.kt @@ -1,106 +1,34 @@ package tech.salroid.filmy.ui.home -import android.content.Intent import android.os.Bundle -import androidx.activity.OnBackPressedCallback -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.lifecycleScope -import androidx.navigation.findNavController -import androidx.navigation.ui.NavigationUI +import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import tech.salroid.filmy.R -import tech.salroid.filmy.databinding.ActivityMainBinding -import tech.salroid.filmy.ui.intro.FilmyIntroActivity -import tech.salroid.filmy.ui.search.SearchViewModel -import tech.salroid.filmy.utility.PreferenceHelper.isColdStart -import tech.salroid.filmy.utility.PreferenceHelper.setColdStartDone -import tech.salroid.filmy.utility.themeSystemBars +import tech.salroid.filmy.theme.FilmyTheme +import tech.salroid.filmy.ui.screens.HomeScreen @AndroidEntryPoint -class MainActivity : AppCompatActivity() { - - private val viewModel: MainViewModel by viewModels() - private val viewModelSearch: SearchViewModel by viewModels() - private var throughShortcut: Boolean? = null - private lateinit var binding: ActivityMainBinding +class MainActivityCompose : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { - setTheme(R.style.AppTheme) super.onCreate(savedInstanceState) - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - - throughShortcut = intent.getBooleanExtra("throughShortcut", false) - themeSystemBars(lightStatusBar = true, navigationColorAsStatus = false) - - onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - if (viewModelSearch.isSearchOpen.value) { - viewModelSearch.closeSearch() - } else { - if (isEnabled) { - isEnabled = false - onBackPressedDispatcher.onBackPressed() - } - } + setContent { + FilmyTheme { + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.dark( + Color.White.toArgb(), + ), + navigationBarStyle = SystemBarStyle.dark( + Color.Transparent.toArgb(), + ), + ) + HomeScreen() } - }) - introLogic() - setupNavigation() - observerUiStates() - } - - private fun observerUiStates() { - lifecycleScope.launch { - /*viewModelSearch.uiStateSearchView.collect { - it?.let { - val margin = when (it) { - SearchViewUiState.Hidden -> 0 - SearchViewUiState.Visible -> binding.navigationBarView.height - } - animateBottomNavigation(margin) - } - }*/ - } - - lifecycleScope.launch { - viewModel.uiStateNavigationVisibility.collect { - // binding.mainAppBarContent.root.isVisible = it - // animateTopNavigation(it) - } - } - } - - private fun animateBottomNavigation(margin: Int) { - /*binding.navigationBarView.clearAnimation() - binding.navigationBarView - .animate() - .translationY(margin.toFloat()).apply { - duration = 175 - startDelay = if (margin == 0) 100 else 0 - interpolator = - if (margin == 0) LinearOutSlowInInterpolator() else FastOutLinearInInterpolator() - }*/ - } - - private fun setupNavigation() { - val navController = findNavController(R.id.navHostFragment) - NavigationUI.setupWithNavController(binding.navigationBarView, navController) - - if (throughShortcut == true) { - navController.navigate(R.id.collections) - binding.navigationBarView.selectedItemId = R.id.collections - } - } - - private fun introLogic() { - if (isColdStart(this)) { - val intent = Intent(this@MainActivity, FilmyIntroActivity::class.java) - startActivity(intent) - setColdStartDone(this) } } } \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/home/MoviesRepository.kt b/app/src/main/java/tech/salroid/filmy/ui/home/MoviesRepository.kt index a498b862..f1d1c7f1 100644 --- a/app/src/main/java/tech/salroid/filmy/ui/home/MoviesRepository.kt +++ b/app/src/main/java/tech/salroid/filmy/ui/home/MoviesRepository.kt @@ -18,6 +18,9 @@ class MoviesRepository @Inject constructor( fun getMovies(type: String, isTrending: Boolean): Flow> = moviesApiHelper.getMovies(type, isTrending) + fun getMoviesFlow(type: String, isTrending: Boolean): Flow = + moviesApiHelper.getMoviesFlow(type, isTrending) + fun getTvShows(type: String, isTrending: Boolean): Flow> = moviesApiHelper.getTvShows(type, isTrending) diff --git a/app/src/main/java/tech/salroid/filmy/ui/intro/FilmyIntroActivity.kt b/app/src/main/java/tech/salroid/filmy/ui/intro/FilmyIntroActivity.kt index abf16e9c..2c39f7a6 100644 --- a/app/src/main/java/tech/salroid/filmy/ui/intro/FilmyIntroActivity.kt +++ b/app/src/main/java/tech/salroid/filmy/ui/intro/FilmyIntroActivity.kt @@ -1,12 +1,16 @@ package tech.salroid.filmy.ui.intro -import com.github.paolorotolo.appintro.AppIntro2 import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment +import com.github.appintro.AppIntro -class FilmyIntroActivity : AppIntro2() { +class FilmyIntroActivity : AppIntro() { override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) addSlide(IntroFragmentA()) @@ -15,16 +19,22 @@ class FilmyIntroActivity : AppIntro2() { addSlide(IntroFragmentD()) showStatusBar(true) - isProgressButtonEnabled = true - setDepthAnimation() + //isProgressButtonEnabled = true + //setDepthAnimation() + + ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, 0, systemBars.right, systemBars.bottom) + insets + } } - override fun onSkipPressed(currentFragment: Fragment) { + override fun onSkipPressed(currentFragment: Fragment?) { super.onSkipPressed(currentFragment) finish() } - override fun onDonePressed(currentFragment: Fragment) { + override fun onDonePressed(currentFragment: Fragment?) { super.onDonePressed(currentFragment) finish() } diff --git a/app/src/main/java/tech/salroid/filmy/ui/intro/IntroFragmentA.kt b/app/src/main/java/tech/salroid/filmy/ui/intro/IntroFragmentA.kt index 227b4d8e..e353e005 100644 --- a/app/src/main/java/tech/salroid/filmy/ui/intro/IntroFragmentA.kt +++ b/app/src/main/java/tech/salroid/filmy/ui/intro/IntroFragmentA.kt @@ -1,6 +1,20 @@ package tech.salroid.filmy.ui.intro +import android.os.Bundle +import android.view.View +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment import tech.salroid.filmy.R -class IntroFragmentA : Fragment(R.layout.fragment_intro) \ No newline at end of file +class IntroFragmentA : Fragment(R.layout.fragment_intro) { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, 0) + insets + } + } +} \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/intro/IntroFragmentB.kt b/app/src/main/java/tech/salroid/filmy/ui/intro/IntroFragmentB.kt index e792a057..f4e0cd0b 100644 --- a/app/src/main/java/tech/salroid/filmy/ui/intro/IntroFragmentB.kt +++ b/app/src/main/java/tech/salroid/filmy/ui/intro/IntroFragmentB.kt @@ -1,6 +1,19 @@ package tech.salroid.filmy.ui.intro +import android.os.Bundle +import android.view.View +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment import tech.salroid.filmy.R -class IntroFragmentB : Fragment(R.layout.fragment_intro_fragment_b) \ No newline at end of file +class IntroFragmentB : Fragment(R.layout.fragment_intro_fragment_b) { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, 0) + insets + } + } +} \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/intro/IntroFragmentC.kt b/app/src/main/java/tech/salroid/filmy/ui/intro/IntroFragmentC.kt index 8f62f6c2..2b975d5b 100644 --- a/app/src/main/java/tech/salroid/filmy/ui/intro/IntroFragmentC.kt +++ b/app/src/main/java/tech/salroid/filmy/ui/intro/IntroFragmentC.kt @@ -1,6 +1,19 @@ package tech.salroid.filmy.ui.intro +import android.os.Bundle +import android.view.View +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment import tech.salroid.filmy.R -class IntroFragmentC : Fragment(R.layout.fragment_intro_fragment_c) \ No newline at end of file +class IntroFragmentC : Fragment(R.layout.fragment_intro_fragment_c) { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, 0) + insets + } + } +} \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/intro/IntroFragmentD.kt b/app/src/main/java/tech/salroid/filmy/ui/intro/IntroFragmentD.kt index 5e53ec71..e28b88d6 100644 --- a/app/src/main/java/tech/salroid/filmy/ui/intro/IntroFragmentD.kt +++ b/app/src/main/java/tech/salroid/filmy/ui/intro/IntroFragmentD.kt @@ -1,6 +1,19 @@ package tech.salroid.filmy.ui.intro +import android.os.Bundle +import android.view.View +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment import tech.salroid.filmy.R -class IntroFragmentD : Fragment(R.layout.fragment_intro_fragment_d) \ No newline at end of file +class IntroFragmentD : Fragment(R.layout.fragment_intro_fragment_d) { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, 0) + insets + } + } +} \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/screens/HomeScreen.kt b/app/src/main/java/tech/salroid/filmy/ui/screens/HomeScreen.kt index ddd457e1..f2166293 100644 --- a/app/src/main/java/tech/salroid/filmy/ui/screens/HomeScreen.kt +++ b/app/src/main/java/tech/salroid/filmy/ui/screens/HomeScreen.kt @@ -1,4 +1,15 @@ package tech.salroid.filmy.ui.screens -class HomeScreen { +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun HomeScreen() { + Scaffold { paddingValues -> + MoviesScreen( + modifier = Modifier.padding(paddingValues) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesScreen.kt b/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesScreen.kt index 208cb7b4..44822a4f 100644 --- a/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesScreen.kt +++ b/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesScreen.kt @@ -1,29 +1,52 @@ package tech.salroid.filmy.ui.screens +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import tech.salroid.filmy.data.local.db.entity.Movie -import tech.salroid.filmy.ui.components.MovieItem +import tech.salroid.filmy.ui.composables.ErrorWidget +import tech.salroid.filmy.ui.composables.LoadingWidget +import tech.salroid.filmy.ui.composables.MovieItem @Composable fun MoviesScreen( modifier: Modifier, - viewModel: MoviesViewModel = viewModel() + viewModel: MoviesViewModel = hiltViewModel() ) { + val moviesUiState by viewModel.movieScreenState.collectAsStateWithLifecycle() - val moviesUiState by viewModel. + when (moviesUiState) { + is MoviesScreenState.Loading -> LoadingWidget(modifier = Modifier) + is MoviesScreenState.Success -> MoviesList( + modifier = modifier, + movies = (moviesUiState as MoviesScreenState.Success).moviesList + ) + is MoviesScreenState.Error -> ErrorWidget( + modifier = Modifier, + message = (moviesUiState as MoviesScreenState.Error).errorMessage + ) + } } - @Composable -fun MoviesList(movies: List) { - LazyVerticalGrid(columns = GridCells.Fixed(3)) { +fun MoviesList(modifier: Modifier, movies: List) { + LazyVerticalGrid( + modifier = modifier, + columns = GridCells.Fixed(3), + contentPadding = PaddingValues(12.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { items(movies) { MovieItem(modifier = Modifier, movie = it) } @@ -34,6 +57,7 @@ fun MoviesList(movies: List) { @Composable fun MoviesListPreview() { MoviesList( + modifier = Modifier, movies = listOf( dummyMovie, dummyMovie, @@ -47,10 +71,8 @@ fun MoviesListPreview() { ) } - val dummyMovie = Movie( id = 12141, title = "People We Meet On Vacation", - releaseDate = "2026-01-06", - + releaseDate = "2026-01-06" ) \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesViewModel.kt b/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesViewModel.kt index d8bf7c5b..f8f29cc3 100644 --- a/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesViewModel.kt +++ b/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesViewModel.kt @@ -1,4 +1,39 @@ package tech.salroid.filmy.ui.screens -class MoviesViewModel { -} \ No newline at end of file +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import tech.salroid.filmy.data.local.db.entity.Movie +import tech.salroid.filmy.ui.home.MoviesRepository +import javax.inject.Inject + +sealed class MoviesScreenState { + object Loading : MoviesScreenState() + data class Success(val moviesList: List) : MoviesScreenState() + data class Error(val errorMessage: String) : MoviesScreenState() +} + +@HiltViewModel +class MoviesViewModel @Inject constructor( + moviesRepository: MoviesRepository +) : ViewModel() { + + val movieScreenState = moviesRepository.getMoviesFlow( + type = "trending", + isTrending = true + ).catch { + MoviesScreenState.Error(it.message ?: "Something went wrong.") + }.map { + MoviesScreenState.Success(it.results) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = MoviesScreenState.Loading + ) +} + + diff --git a/build.gradle b/build.gradle index 3000b20e..7dd2e436 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '2.2.20' + ext.kotlin_version = '2.2.21' repositories { google() @@ -9,9 +9,12 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:9.3.1' - classpath 'com.google.gms:google-services:4.4.3' + classpath 'com.google.gms:google-services:4.4.4' classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.6' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:$kotlin_version" + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files } } From 4fc15cf48e3bcff3bbc7aabc8a0925637dbcc481 Mon Sep 17 00:00:00 2001 From: webianks Date: Sun, 11 Jan 2026 17:33:29 +0530 Subject: [PATCH 03/25] Compose: MovieScreen + BottomNavigation --- app/build.gradle | 12 ++ .../salroid/filmy/data/model/MoviePreview.kt | 11 ++ .../java/tech/salroid/filmy/theme/Theme.kt | 7 +- .../java/tech/salroid/filmy/ui/FilmyApp.kt | 73 ++++++++++++ .../salroid/filmy/ui/composables/MovieItem.kt | 65 ++++++----- .../filmy/ui/home/MainActivityCompose.kt | 14 ++- .../filmy/ui/movies/MovieDetailsScreen.kt | 51 +++++++++ .../filmy/ui/movies/MoviePreviewMapper.kt | 29 +++++ .../salroid/filmy/ui/movies/MoviesScreen.kt | 108 ++++++++++++++++++ .../filmy/ui/movies/MoviesScreenState.kt | 10 ++ .../ui/{screens => movies}/MoviesViewModel.kt | 25 ++-- .../salroid/filmy/ui/navigation/AppNavHost.kt | 80 +++++++++++++ .../filmy/ui/navigation/AppNavigationBar.kt | 55 +++++++++ .../salroid/filmy/ui/navigation/AppRoute.kt | 16 +++ .../ui/navigation/TopLevelDestination.kt | 42 +++++++ .../salroid/filmy/ui/screens/HomeScreen.kt | 15 --- .../salroid/filmy/ui/screens/MoviesScreen.kt | 78 ------------- .../tech/salroid/filmy/utility/ImageConfig.kt | 5 + app/src/main/res/values-night-v27/themes.xml | 12 -- app/src/main/res/values-night-v31/themes.xml | 14 --- app/src/main/res/values-v27/themes.xml | 12 -- app/src/main/res/values-v31/themes.xml | 12 -- app/src/main/res/values/strings.xml | 6 +- app/src/main/res/values/themes.xml | 6 +- 24 files changed, 561 insertions(+), 197 deletions(-) create mode 100644 app/src/main/java/tech/salroid/filmy/data/model/MoviePreview.kt create mode 100644 app/src/main/java/tech/salroid/filmy/ui/FilmyApp.kt create mode 100644 app/src/main/java/tech/salroid/filmy/ui/movies/MovieDetailsScreen.kt create mode 100644 app/src/main/java/tech/salroid/filmy/ui/movies/MoviePreviewMapper.kt create mode 100644 app/src/main/java/tech/salroid/filmy/ui/movies/MoviesScreen.kt create mode 100644 app/src/main/java/tech/salroid/filmy/ui/movies/MoviesScreenState.kt rename app/src/main/java/tech/salroid/filmy/ui/{screens => movies}/MoviesViewModel.kt (65%) create mode 100644 app/src/main/java/tech/salroid/filmy/ui/navigation/AppNavHost.kt create mode 100644 app/src/main/java/tech/salroid/filmy/ui/navigation/AppNavigationBar.kt create mode 100644 app/src/main/java/tech/salroid/filmy/ui/navigation/AppRoute.kt create mode 100644 app/src/main/java/tech/salroid/filmy/ui/navigation/TopLevelDestination.kt delete mode 100644 app/src/main/java/tech/salroid/filmy/ui/screens/HomeScreen.kt delete mode 100644 app/src/main/java/tech/salroid/filmy/ui/screens/MoviesScreen.kt create mode 100644 app/src/main/java/tech/salroid/filmy/utility/ImageConfig.kt delete mode 100644 app/src/main/res/values-night-v27/themes.xml delete mode 100644 app/src/main/res/values-night-v31/themes.xml delete mode 100644 app/src/main/res/values-v27/themes.xml diff --git a/app/build.gradle b/app/build.gradle index a7ede3a5..65d51d12 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,6 +35,15 @@ android { buildConfigField("String", "OMDB_API_KEY", "\"" + getOMDBApiKey() + "\"") buildConfigField("String", "YOUTUBE_API_KEY", "\"" + getYoutubeApiKey() + "\"") } + + releaseUnminified { + initWith release + minifyEnabled false + shrinkResources false + signingConfig signingConfigs.debug + matchingFallbacks = ['release'] + } + debug { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' buildConfigField("String", "TMDB_API_KEY", "\"" + getTMDBApiKey() + "\"") @@ -149,12 +158,15 @@ dependencies { implementation("androidx.compose.material3:material3:1.5.0-alpha04") implementation 'androidx.lifecycle:lifecycle-viewmodel-compose' implementation 'androidx.activity:activity-compose' + implementation 'androidx.compose.material3.adaptive:adaptive-android' implementation("androidx.hilt:hilt-navigation-compose:1.3.0") implementation("io.coil-kt.coil3:coil-compose:3.3.0") implementation("io.coil-kt.coil3:coil-network-okhttp:3.3.0") implementation 'androidx.compose.ui:ui-tooling-preview' debugImplementation 'androidx.compose.ui:ui-tooling' + implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7") + //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' testImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/java/tech/salroid/filmy/data/model/MoviePreview.kt b/app/src/main/java/tech/salroid/filmy/data/model/MoviePreview.kt new file mode 100644 index 00000000..4cdcac23 --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/data/model/MoviePreview.kt @@ -0,0 +1,11 @@ +package tech.salroid.filmy.data.model + +import androidx.compose.runtime.Immutable + +@Immutable +data class MoviePreview( + val id: Int, + val title: String, + val posterUrl: String, + val readableReleaseDate: String +) \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/theme/Theme.kt b/app/src/main/java/tech/salroid/filmy/theme/Theme.kt index 13747b00..5a3a7b3b 100644 --- a/app/src/main/java/tech/salroid/filmy/theme/Theme.kt +++ b/app/src/main/java/tech/salroid/filmy/theme/Theme.kt @@ -1,6 +1,7 @@ package tech.salroid.filmy.theme -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable @@ -40,10 +41,12 @@ private val LightColorScheme = lightColorScheme( outlineVariant = OutlineVariant, ) +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun FilmyTheme(content: @Composable () -> Unit) { val colorScheme = LightColorScheme - MaterialTheme( + + MaterialExpressiveTheme( colorScheme = colorScheme ) { content() diff --git a/app/src/main/java/tech/salroid/filmy/ui/FilmyApp.kt b/app/src/main/java/tech/salroid/filmy/ui/FilmyApp.kt new file mode 100644 index 00000000..d19dcb18 --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/ui/FilmyApp.kt @@ -0,0 +1,73 @@ +package tech.salroid.filmy.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import androidx.window.core.layout.WindowSizeClass +import tech.salroid.filmy.ui.navigation.AppNavHost +import tech.salroid.filmy.ui.navigation.AppNavigationBar +import tech.salroid.filmy.ui.navigation.TopLevelDestinations + +@Composable +fun FilmyApp( + windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo( + supportLargeAndXLargeWidth = true + ).windowSizeClass +) { + val navController = rememberNavController() + + Scaffold(bottomBar = { + Box(modifier = Modifier.height(90.dp)) { // proper fix needed + AnimatedVisibility( + visible = shouldShowNavigationBar(navController), + enter = fadeIn(animationSpec = ChromeMotionSpec), + exit = fadeOut(animationSpec = ChromeMotionSpec) + ) { + AppNavigationBar(navController) + } + } + }) { paddingValues -> + CompositionLocalProvider( + LocalWindowSizeClass provides windowSizeClass + ) { + AppNavHost( + navController = navController, + modifier = Modifier.padding(paddingValues) + ) + } + } +} + +@Composable +private fun shouldShowNavigationBar(navController: NavController): Boolean { + val backStackEntry by navController.currentBackStackEntryAsState() + val destinationRoute = backStackEntry?.destination?.route + + return TopLevelDestinations + .any { it.startDestinationRoute == destinationRoute } +} + +val LocalWindowSizeClass = staticCompositionLocalOf { + error("WindowSizeClass not provided") +} + +private val ChromeMotionSpec = tween( + durationMillis = 500, + easing = FastOutSlowInEasing +) \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/composables/MovieItem.kt b/app/src/main/java/tech/salroid/filmy/ui/composables/MovieItem.kt index 0842d1cb..ec85d84d 100644 --- a/app/src/main/java/tech/salroid/filmy/ui/composables/MovieItem.kt +++ b/app/src/main/java/tech/salroid/filmy/ui/composables/MovieItem.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column 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.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card @@ -16,52 +17,57 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color.Companion.Transparent import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage -import tech.salroid.filmy.R -import tech.salroid.filmy.data.local.db.entity.Movie -import tech.salroid.filmy.ui.screens.dummyMovie -import tech.salroid.filmy.utility.toReadableDate +import tech.salroid.filmy.data.model.MoviePreview +import tech.salroid.filmy.ui.movies.dummyMoviePreview @Composable fun MovieItem( modifier: Modifier, - movie: Movie + movie: MoviePreview, + onMovieClick: (Int) -> Unit ) { - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally + Card( + shape = RoundedCornerShape(corner = CornerSize(4.dp)), + colors = CardDefaults.cardColors(containerColor = Transparent), + onClick = { onMovieClick(movie.id) } ) { - Card( - shape = RoundedCornerShape(corner = CornerSize(4.dp)), - colors = CardDefaults.cardColors(containerColor = Transparent), + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally ) { AsyncImage( modifier = Modifier .fillMaxWidth() .height(170.dp) .clip(RoundedCornerShape(corner = CornerSize(4.dp))), - contentScale = ContentScale.FillBounds, - model = stringResource(R.string.movie_poster_url, movie.posterPath.orEmpty()), + contentScale = ContentScale.Crop, + model = movie.posterUrl, contentDescription = "${movie.title} movie poster" ) + + Text( + modifier = Modifier.padding(top = 8.dp), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleSmall.copy( + letterSpacing = 0.0.sp, + fontWeight = FontWeight.Medium, + lineHeight = 18.sp + ), + text = movie.title + ) + Text( + modifier = Modifier.padding(top = 4.dp), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodySmall, + text = movie.readableReleaseDate + ) } - Text( - modifier = Modifier.padding(top = 8.dp), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleMedium.copy(letterSpacing = 0.0.sp), - text = movie.title.orEmpty() - ) - Text( - modifier = Modifier.padding(top = 2.dp), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodySmall, - text = movie.releaseDate?.toReadableDate().orEmpty() - ) } } @@ -69,7 +75,10 @@ fun MovieItem( @Composable fun MovieItemPreview() { MovieItem( - modifier = Modifier, - movie = dummyMovie + modifier = Modifier + .width(140.dp) + .padding(16.dp), + movie = dummyMoviePreview, + onMovieClick = { } ) } \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/home/MainActivityCompose.kt b/app/src/main/java/tech/salroid/filmy/ui/home/MainActivityCompose.kt index 6d34269c..0898bd92 100644 --- a/app/src/main/java/tech/salroid/filmy/ui/home/MainActivityCompose.kt +++ b/app/src/main/java/tech/salroid/filmy/ui/home/MainActivityCompose.kt @@ -7,27 +7,31 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import dagger.hilt.android.AndroidEntryPoint import tech.salroid.filmy.theme.FilmyTheme -import tech.salroid.filmy.ui.screens.HomeScreen +import tech.salroid.filmy.ui.FilmyApp @AndroidEntryPoint class MainActivityCompose : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen().setKeepOnScreenCondition { false } super.onCreate(savedInstanceState) setContent { FilmyTheme { enableEdgeToEdge( - statusBarStyle = SystemBarStyle.dark( - Color.White.toArgb(), + statusBarStyle = SystemBarStyle.light( + Color.Transparent.toArgb(), + Color.Transparent.toArgb(), ), - navigationBarStyle = SystemBarStyle.dark( + navigationBarStyle = SystemBarStyle.light( + Color.Transparent.toArgb(), Color.Transparent.toArgb(), ), ) - HomeScreen() + FilmyApp() } } } diff --git a/app/src/main/java/tech/salroid/filmy/ui/movies/MovieDetailsScreen.kt b/app/src/main/java/tech/salroid/filmy/ui/movies/MovieDetailsScreen.kt new file mode 100644 index 00000000..f4eb082b --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/ui/movies/MovieDetailsScreen.kt @@ -0,0 +1,51 @@ +package tech.salroid.filmy.ui.movies + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +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.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun MovieDetails( + movieId: Int, + modifier: Modifier = Modifier, + onBackNavigation: () -> Unit +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text("$movieId", style = MaterialTheme.typography.headlineMedium) + Text( + modifier = Modifier.padding(top = 16.dp), + text = "Will load the movie details for this movie...", + textAlign = TextAlign.Center + ) + Button(onClick = onBackNavigation) { + Text("Go Back") + } + } +} + + +@Composable +@Preview(showBackground = true) +fun MovieDetailsPreview() { + MovieDetails( + 2414, + modifier = Modifier, + onBackNavigation = { } + ) +} diff --git a/app/src/main/java/tech/salroid/filmy/ui/movies/MoviePreviewMapper.kt b/app/src/main/java/tech/salroid/filmy/ui/movies/MoviePreviewMapper.kt new file mode 100644 index 00000000..b5aec6e8 --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/ui/movies/MoviePreviewMapper.kt @@ -0,0 +1,29 @@ +package tech.salroid.filmy.ui.movies + +import tech.salroid.filmy.data.local.db.entity.Movie +import tech.salroid.filmy.data.model.MoviePreview +import tech.salroid.filmy.utility.ImageConfig +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import javax.inject.Inject + +class MoviePreviewMapper @Inject constructor() { + + // Make it more testable by taking from constructor + private val posterBaseUrl = ImageConfig.BASE_URL + + fun map(movie: Movie): MoviePreview = + MoviePreview( + id = movie.id, + title = movie.title.orEmpty(), + posterUrl = movie.posterPath?.let { posterBaseUrl + it }.orEmpty(), + readableReleaseDate = movie.releaseDate?.let(::formateReleaseDate).orEmpty() + ) + + private fun formateReleaseDate(raw: String): String { + return runCatching { + LocalDate.parse(raw) + .format(DateTimeFormatter.ofPattern("dd MMM yyyy")) + }.getOrElse { "" } + } +} \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/movies/MoviesScreen.kt b/app/src/main/java/tech/salroid/filmy/ui/movies/MoviesScreen.kt new file mode 100644 index 00000000..d5ae7a98 --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/ui/movies/MoviesScreen.kt @@ -0,0 +1,108 @@ +package tech.salroid.filmy.ui.movies + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.window.core.layout.WindowSizeClass +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import tech.salroid.filmy.data.model.MoviePreview +import tech.salroid.filmy.ui.LocalWindowSizeClass +import tech.salroid.filmy.ui.composables.ErrorWidget +import tech.salroid.filmy.ui.composables.LoadingWidget +import tech.salroid.filmy.ui.composables.MovieItem +import kotlin.math.floor + +@Composable +fun MoviesScreen( + modifier: Modifier = Modifier, + viewModel: MoviesViewModel = hiltViewModel(), + onMovieClick: (Int) -> Unit +) { + val moviesUiState by viewModel.movieScreenState.collectAsStateWithLifecycle() + val onMovieClickState by rememberUpdatedState(onMovieClick) + + when (val state = moviesUiState) { + is MoviesScreenState.Loading -> LoadingWidget(modifier = Modifier) + is MoviesScreenState.Success -> MoviesList( + modifier = modifier, + movies = state.moviesList, + onMovieClick = onMovieClickState + ) + + is MoviesScreenState.Error -> ErrorWidget( + modifier = Modifier, + message = state.errorMessage + ) + } +} + +@Composable +fun MoviesList( + modifier: Modifier, + movies: ImmutableList, + onMovieClick: (Int) -> Unit +) { + + val windowSizeClass = LocalWindowSizeClass.current + val cells = when { + windowSizeClass.isWidthAtLeastBreakpoint( + WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND + ) -> 6 + + windowSizeClass.isWidthAtLeastBreakpoint( + WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND + ) -> 4 + + else -> 3 + } + + LazyVerticalGrid( + modifier = modifier, + columns = GridCells.Fixed(cells), + contentPadding = PaddingValues(12.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(movies, key = { it.id }) { + MovieItem(modifier = Modifier, movie = it, onMovieClick) + } + } +} + +@Preview(showBackground = true) +@Composable +fun MoviesListPreview() { + val windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo( + supportLargeAndXLargeWidth = true + ).windowSizeClass + + CompositionLocalProvider(LocalWindowSizeClass provides windowSizeClass) { + MoviesList( + modifier = Modifier, + movies = (1..10).map { id -> + dummyMoviePreview.copy(id = id) + }.toImmutableList(), + onMovieClick = { } + ) + } +} + +val dummyMoviePreview = MoviePreview( + id = (floor(Math.random() * 9000) + 1000).toInt(), + title = "People We Meet On Vacation", + posterUrl = "", + readableReleaseDate = "06 Jan 2026" +) diff --git a/app/src/main/java/tech/salroid/filmy/ui/movies/MoviesScreenState.kt b/app/src/main/java/tech/salroid/filmy/ui/movies/MoviesScreenState.kt new file mode 100644 index 00000000..6b1d3908 --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/ui/movies/MoviesScreenState.kt @@ -0,0 +1,10 @@ +package tech.salroid.filmy.ui.movies + +import kotlinx.collections.immutable.ImmutableList +import tech.salroid.filmy.data.model.MoviePreview + +sealed class MoviesScreenState { + object Loading : MoviesScreenState() + data class Success(val moviesList: ImmutableList) : MoviesScreenState() + data class Error(val errorMessage: String) : MoviesScreenState() +} \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesViewModel.kt b/app/src/main/java/tech/salroid/filmy/ui/movies/MoviesViewModel.kt similarity index 65% rename from app/src/main/java/tech/salroid/filmy/ui/screens/MoviesViewModel.kt rename to app/src/main/java/tech/salroid/filmy/ui/movies/MoviesViewModel.kt index f8f29cc3..0ad3cf42 100644 --- a/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesViewModel.kt +++ b/app/src/main/java/tech/salroid/filmy/ui/movies/MoviesViewModel.kt @@ -1,34 +1,34 @@ -package tech.salroid.filmy.ui.screens +package tech.salroid.filmy.ui.movies import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import tech.salroid.filmy.data.local.db.entity.Movie import tech.salroid.filmy.ui.home.MoviesRepository import javax.inject.Inject -sealed class MoviesScreenState { - object Loading : MoviesScreenState() - data class Success(val moviesList: List) : MoviesScreenState() - data class Error(val errorMessage: String) : MoviesScreenState() -} - @HiltViewModel class MoviesViewModel @Inject constructor( - moviesRepository: MoviesRepository + moviesRepository: MoviesRepository, + moviePreviewMapper: MoviePreviewMapper ) : ViewModel() { val movieScreenState = moviesRepository.getMoviesFlow( type = "trending", isTrending = true - ).catch { + ).map { response -> + + val previews = response.results + .map(moviePreviewMapper::map) + .toImmutableList() + + MoviesScreenState.Success(previews) + }.catch { MoviesScreenState.Error(it.message ?: "Something went wrong.") - }.map { - MoviesScreenState.Success(it.results) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), @@ -36,4 +36,3 @@ class MoviesViewModel @Inject constructor( ) } - diff --git a/app/src/main/java/tech/salroid/filmy/ui/navigation/AppNavHost.kt b/app/src/main/java/tech/salroid/filmy/ui/navigation/AppNavHost.kt new file mode 100644 index 00000000..5c5fe7a4 --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/ui/navigation/AppNavHost.kt @@ -0,0 +1,80 @@ +package tech.salroid.filmy.ui.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.navigation +import androidx.navigation.navArgument +import tech.salroid.filmy.ui.movies.MovieDetails +import tech.salroid.filmy.ui.movies.MoviesScreen + +@Composable +fun AppNavHost( + navController: NavHostController, + modifier: Modifier = Modifier +) { + NavHost( + modifier = modifier, + navController = navController, + startDestination = AppRoute.MoviesGraph.route, + ) { + moviesGraph(navController) + showsGraph() + collectionGraph() + accountGraph() + } +} + +private fun NavGraphBuilder.moviesGraph(navController: NavHostController) { + navigation( + startDestination = AppRoute.Movies.route, + route = AppRoute.MoviesGraph.route + ) { + composable(AppRoute.Movies.route) { + MoviesScreen { id -> + navController.navigate(AppRoute.MovieDetails.create(id)) + } + } + composable( + route = AppRoute.MovieDetails.route, + arguments = listOf(navArgument("movieId") { + type = NavType.IntType + }) + ) { + MovieDetails(movieId = it.arguments?.getInt("movieId") ?: 1234) { + navController.popBackStack() + } + } + } +} + +private fun NavGraphBuilder.showsGraph() { + navigation( + route = AppRoute.ShowsGraph.route, + startDestination = "shows" + ) { + composable("shows") { MoviesScreen { } } + } +} + +private fun NavGraphBuilder.collectionGraph() { + navigation( + route = AppRoute.CollectionGraph.route, + startDestination = "collection" + ) { + composable("collection") { MoviesScreen { } } + } +} + +private fun NavGraphBuilder.accountGraph() { + navigation( + route = AppRoute.AccountGraph.route, + startDestination = "account" + ) { + composable("account") { MoviesScreen { } } + } +} \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/navigation/AppNavigationBar.kt b/app/src/main/java/tech/salroid/filmy/ui/navigation/AppNavigationBar.kt new file mode 100644 index 00000000..c75297ff --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/ui/navigation/AppNavigationBar.kt @@ -0,0 +1,55 @@ +package tech.salroid.filmy.ui.navigation + +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.res.painterResource +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState + +@Composable +fun AppNavigationBar(navController: NavHostController) { + val backStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = backStackEntry?.destination + + NavigationBar { + TopLevelDestinations.forEach { destination -> + NavigationBarItem( + selected = currentDestination + ?.hierarchy + ?.any { + it.route == destination.graphRoute + } == true, + onClick = { + navController.navigate(route = destination.graphRoute) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + icon = { + Icon( + painterResource(destination.iconRes), + contentDescription = destination.contentDescription + ) + }, + label = { Text(destination.label) }, + colors = NavigationBarItemDefaults.colors( + selectedTextColor = MaterialTheme.colorScheme.primary, + selectedIconColor = MaterialTheme.colorScheme.primary, + unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, + unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/navigation/AppRoute.kt b/app/src/main/java/tech/salroid/filmy/ui/navigation/AppRoute.kt new file mode 100644 index 00000000..31d5c2dd --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/ui/navigation/AppRoute.kt @@ -0,0 +1,16 @@ +package tech.salroid.filmy.ui.navigation + +sealed class AppRoute(val route: String) { + // Bottom Tabs (graphs) + object MoviesGraph : AppRoute("movies_graph") + object ShowsGraph : AppRoute("shows_graph") + object CollectionGraph : AppRoute("collection_graph") + object AccountGraph : AppRoute("account_graph") + + // Screens + object Movies : AppRoute("movies") + + object MovieDetails : AppRoute("movie/{movieId}") { + fun create(id: Int) = "movie/$id" + } +} \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/navigation/TopLevelDestination.kt b/app/src/main/java/tech/salroid/filmy/ui/navigation/TopLevelDestination.kt new file mode 100644 index 00000000..b29721e0 --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/ui/navigation/TopLevelDestination.kt @@ -0,0 +1,42 @@ +package tech.salroid.filmy.ui.navigation + +import tech.salroid.filmy.R + +data class TopLevelDestination( + val graphRoute: String, + val startDestinationRoute: String, + val label: String, + val iconRes: Int, + val contentDescription: String +) + +val TopLevelDestinations = listOf( + TopLevelDestination( + graphRoute = AppRoute.MoviesGraph.route, + startDestinationRoute = AppRoute.Movies.route, + label = "Movies", + iconRes = R.drawable.outline_movie_24, + contentDescription = "Movies" + ), + TopLevelDestination( + graphRoute = AppRoute.ShowsGraph.route, + startDestinationRoute = "shows", + label = "Shows", + iconRes = R.drawable.ic_tv, + "Shows" + ), + TopLevelDestination( + graphRoute = AppRoute.CollectionGraph.route, + startDestinationRoute = "collection", + label = "Collection", + iconRes = R.drawable.ic_collections_bookmark_24dp, + contentDescription = "Collection" + ), + TopLevelDestination( + graphRoute = AppRoute.AccountGraph.route, + startDestinationRoute = "account", + label = "Account", + iconRes = R.drawable.ic_face, + contentDescription = "Account" + ) +) \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/screens/HomeScreen.kt b/app/src/main/java/tech/salroid/filmy/ui/screens/HomeScreen.kt deleted file mode 100644 index f2166293..00000000 --- a/app/src/main/java/tech/salroid/filmy/ui/screens/HomeScreen.kt +++ /dev/null @@ -1,15 +0,0 @@ -package tech.salroid.filmy.ui.screens - -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -fun HomeScreen() { - Scaffold { paddingValues -> - MoviesScreen( - modifier = Modifier.padding(paddingValues) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesScreen.kt b/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesScreen.kt deleted file mode 100644 index 44822a4f..00000000 --- a/app/src/main/java/tech/salroid/filmy/ui/screens/MoviesScreen.kt +++ /dev/null @@ -1,78 +0,0 @@ -package tech.salroid.filmy.ui.screens - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import tech.salroid.filmy.data.local.db.entity.Movie -import tech.salroid.filmy.ui.composables.ErrorWidget -import tech.salroid.filmy.ui.composables.LoadingWidget -import tech.salroid.filmy.ui.composables.MovieItem - -@Composable -fun MoviesScreen( - modifier: Modifier, - viewModel: MoviesViewModel = hiltViewModel() -) { - val moviesUiState by viewModel.movieScreenState.collectAsStateWithLifecycle() - - when (moviesUiState) { - is MoviesScreenState.Loading -> LoadingWidget(modifier = Modifier) - is MoviesScreenState.Success -> MoviesList( - modifier = modifier, - movies = (moviesUiState as MoviesScreenState.Success).moviesList - ) - - is MoviesScreenState.Error -> ErrorWidget( - modifier = Modifier, - message = (moviesUiState as MoviesScreenState.Error).errorMessage - ) - } -} - -@Composable -fun MoviesList(modifier: Modifier, movies: List) { - LazyVerticalGrid( - modifier = modifier, - columns = GridCells.Fixed(3), - contentPadding = PaddingValues(12.dp), - horizontalArrangement = Arrangement.spacedBy(14.dp), - verticalArrangement = Arrangement.spacedBy(14.dp) - ) { - items(movies) { - MovieItem(modifier = Modifier, movie = it) - } - } -} - -@Preview(showBackground = true) -@Composable -fun MoviesListPreview() { - MoviesList( - modifier = Modifier, - movies = listOf( - dummyMovie, - dummyMovie, - dummyMovie, - dummyMovie, - dummyMovie, - dummyMovie, - dummyMovie, - dummyMovie - ) - ) -} - -val dummyMovie = Movie( - id = 12141, - title = "People We Meet On Vacation", - releaseDate = "2026-01-06" -) \ No newline at end of file diff --git a/app/src/main/java/tech/salroid/filmy/utility/ImageConfig.kt b/app/src/main/java/tech/salroid/filmy/utility/ImageConfig.kt new file mode 100644 index 00000000..1553cc1f --- /dev/null +++ b/app/src/main/java/tech/salroid/filmy/utility/ImageConfig.kt @@ -0,0 +1,5 @@ +package tech.salroid.filmy.utility + +object ImageConfig { + const val BASE_URL = "https://image.tmdb.org/t/p/w500" +} \ No newline at end of file diff --git a/app/src/main/res/values-night-v27/themes.xml b/app/src/main/res/values-night-v27/themes.xml deleted file mode 100644 index fa3f5bea..00000000 --- a/app/src/main/res/values-night-v27/themes.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-night-v31/themes.xml b/app/src/main/res/values-night-v31/themes.xml deleted file mode 100644 index d12627bb..00000000 --- a/app/src/main/res/values-night-v31/themes.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v27/themes.xml b/app/src/main/res/values-v27/themes.xml deleted file mode 100644 index 1cfeb527..00000000 --- a/app/src/main/res/values-v27/themes.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v31/themes.xml b/app/src/main/res/values-v31/themes.xml index f20ad56b..dd6b86b9 100644 --- a/app/src/main/res/values-v31/themes.xml +++ b/app/src/main/res/values-v31/themes.xml @@ -4,16 +4,4 @@ @android:dimen/system_app_widget_background_radius @android:dimen/system_app_widget_inner_radius - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0c2c8ef4..f87577c1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,6 +33,9 @@ Settings Offline Movies medium + low + high + Image Quality Something went wrong ! Retrying Retry @@ -69,9 +72,6 @@ IN THEATERS UPCOMING Details - Low - High - Image Quality Made for Cinephiles diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 3bfd55f3..c2ac5f99 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -34,9 +34,9 @@