├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── drawable │ │ │ │ ├── empty.png │ │ │ │ ├── blank_profile_image.png │ │ │ │ ├── no_image_available.jpg │ │ │ │ ├── auth_background_image.jpeg │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── values │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── themes.xml │ │ │ │ ├── colors.xml │ │ │ │ └── splash.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values-night │ │ │ │ └── splash.xml │ │ │ ├── xml │ │ │ │ ├── backup_rules.xml │ │ │ │ └── data_extraction_rules.xml │ │ │ └── layout │ │ │ │ └── youtube_trailer_item.xml │ │ ├── ic_launcher-playstore.png │ │ ├── java │ │ │ └── com │ │ │ │ └── ahmetocak │ │ │ │ └── movieapp │ │ │ │ ├── MovieApplication.kt │ │ │ │ └── di │ │ │ │ └── AppModule.kt │ │ └── AndroidManifest.xml │ └── androidTest │ │ └── java │ │ └── com │ │ └── ahmetocak │ │ └── movieapp │ │ └── ExampleInstrumentedTest.kt └── proguard-rules.pro ├── core ├── common │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── drawable │ │ │ │ │ └── empty.png │ │ │ │ ├── values-tr-rTR │ │ │ │ │ └── strings.xml │ │ │ │ └── values │ │ │ │ │ └── strings.xml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── ahmetocak │ │ │ │ │ └── common │ │ │ │ │ ├── constants │ │ │ │ │ ├── Constants.kt │ │ │ │ │ └── SeeAllType.kt │ │ │ │ │ ├── helpers │ │ │ │ │ ├── DialogUiEvent.kt │ │ │ │ │ ├── ScreenOrientation.kt │ │ │ │ │ ├── ConditionalModifier.kt │ │ │ │ │ ├── Response.kt │ │ │ │ │ ├── UiState.kt │ │ │ │ │ ├── ShowUserMessage.kt │ │ │ │ │ ├── HandleTaskError.kt │ │ │ │ │ └── UiText.kt │ │ │ │ │ ├── connectivity │ │ │ │ │ └── ConnectivityObserver.kt │ │ │ │ │ ├── utils │ │ │ │ │ └── ResponseMapper.kt │ │ │ │ │ └── Extensions.kt │ │ │ └── AndroidManifest.xml │ │ ├── test │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── ahmetocak │ │ │ │ └── common │ │ │ │ └── ExampleUnitTest.kt │ │ └── androidTest │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── common │ │ │ └── ExampleInstrumentedTest.kt │ ├── proguard-rules.pro │ └── build.gradle.kts ├── data │ ├── .gitignore │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── data │ │ │ ├── repository │ │ │ ├── datastore │ │ │ │ ├── DataStoreRepository.kt │ │ │ │ └── DataStoreRepositoryImpl.kt │ │ │ └── firebase │ │ │ │ ├── FirebaseRepository.kt │ │ │ │ └── FirebaseRepositoryImpl.kt │ │ │ └── mapper │ │ │ └── FirebaseMapper.kt │ └── build.gradle.kts ├── domain │ ├── .gitignore │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── domain │ │ │ ├── movie │ │ │ ├── SearchMovieUseCase.kt │ │ │ ├── GetAllTopRatedMoviesUseCase.kt │ │ │ ├── GetAllTrendingMoviesUseCase.kt │ │ │ ├── GetAllUpcomingMoviesUseCase.kt │ │ │ ├── GetActorMoviesUseCase.kt │ │ │ ├── ObserveWatchListUseCase.kt │ │ │ ├── GetActorDetailsUseCase.kt │ │ │ ├── GetMovieCreditsUseCase.kt │ │ │ ├── GetMovieDetailsUseCase.kt │ │ │ ├── GetMovieTrailersUseCase.kt │ │ │ ├── GetTrendingMoviesFirstPageUseCase.kt │ │ │ ├── GetUserMovieReviewsUseCase.kt │ │ │ ├── GetMovieRecommendationsUseCase.kt │ │ │ ├── GetTopRatedMoviesFirstPageUseCase.kt │ │ │ ├── GetUpcomingMoviesFirstPageUseCase.kt │ │ │ ├── RemoveMovieFromWatchListUseCase.kt │ │ │ └── AddMovieToDbWatchListUseCase.kt │ │ │ ├── firebase │ │ │ ├── auth │ │ │ │ ├── GetUserEmailUseCase.kt │ │ │ │ ├── SendResetPasswordEmailUseCase.kt │ │ │ │ ├── LoginUseCase.kt │ │ │ │ ├── SignUpUseCase.kt │ │ │ │ ├── SignOutUseCase.kt │ │ │ │ ├── SignInWithGoogleUseCase.kt │ │ │ │ └── ReAuthenticateUseCase.kt │ │ │ ├── firestore │ │ │ │ ├── GetMovieDataUseCase.kt │ │ │ │ ├── UpdateMovieDataUseCase.kt │ │ │ │ └── AddMovieToWatchListUseCase.kt │ │ │ ├── storage │ │ │ │ ├── GetUserProfileImageUseCase.kt │ │ │ │ └── UploadProfileImageUseCase.kt │ │ │ └── CheckMovieInWatchListUseCase.kt │ │ │ ├── preferences │ │ │ ├── ObserveAppThemeUseCase.kt │ │ │ ├── ObserveDynamicColorUseCase.kt │ │ │ ├── UpdateAppThemeUseCase.kt │ │ │ └── UpdateDynamicColorUseCase.kt │ │ │ └── GetGeminiResponseUseCase.kt │ └── build.gradle.kts ├── model │ ├── .gitignore │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── model │ │ │ ├── movie_detail │ │ │ ├── MovieTrailer.kt │ │ │ ├── MovieCredit.kt │ │ │ └── MovieDetail.kt │ │ │ ├── watch_list │ │ │ └── WatchList.kt │ │ │ ├── movie │ │ │ ├── MovieRecommendations.kt │ │ │ ├── ActorDetails.kt │ │ │ ├── ActorMovies.kt │ │ │ ├── Movie.kt │ │ │ └── UserMovieReviews.kt │ │ │ └── firebase │ │ │ └── WatchListMovie.kt │ └── build.gradle.kts ├── ui │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── ahmetocak │ │ │ │ └── ui │ │ │ │ └── TmdbLogo.kt │ │ ├── test │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── ahmetocak │ │ │ │ └── ui │ │ │ │ └── ExampleUnitTest.kt │ │ └── androidTest │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── ui │ │ │ └── ExampleInstrumentedTest.kt │ ├── proguard-rules.pro │ └── build.gradle.kts ├── database │ ├── .gitignore │ ├── src │ │ └── main │ │ │ ├── res │ │ │ ├── values-tr │ │ │ │ └── strings.xml │ │ │ └── values │ │ │ │ └── strings.xml │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── database │ │ │ ├── db │ │ │ └── WatchListDatabase.kt │ │ │ ├── di │ │ │ ├── DaoModule.kt │ │ │ ├── DatasourceModule.kt │ │ │ └── DatabaseModule.kt │ │ │ ├── datasource │ │ │ ├── WatchListLocalDataSource.kt │ │ │ └── WatchListLocalDataSourceImpl.kt │ │ │ ├── entity │ │ │ └── WatchListEntity.kt │ │ │ ├── dao │ │ │ └── WatchListDao.kt │ │ │ └── utils │ │ │ └── DbCall.kt │ └── build.gradle.kts ├── datastore │ ├── .gitignore │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── datastore │ │ │ ├── DatastoreConstants.kt │ │ │ ├── datasource │ │ │ ├── MovieAppPreferenceDataSource.kt │ │ │ └── MovieAppPreferenceDataSourceImpl.kt │ │ │ └── di │ │ │ └── DatastoreModule.kt │ └── build.gradle.kts ├── navigation │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ └── strings.xml │ │ │ │ └── values-tr-rTR │ │ │ │ │ └── strings.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── ahmetocak │ │ │ │ └── navigation │ │ │ │ └── Routes.kt │ │ ├── test │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── ahmetocak │ │ │ │ └── navigation │ │ │ │ └── ExampleUnitTest.kt │ │ └── androidTest │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── navigation │ │ │ └── ExampleInstrumentedTest.kt │ ├── proguard-rules.pro │ └── build.gradle.kts ├── network │ ├── .gitignore │ ├── src │ │ └── main │ │ │ ├── res │ │ │ ├── values │ │ │ │ └── strings.xml │ │ │ └── values-tr-rTR │ │ │ │ └── strings.xml │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── network │ │ │ ├── model │ │ │ ├── movie_detail │ │ │ │ ├── NetworkMovieTrailer.kt │ │ │ │ ├── NetworkMovieCredit.kt │ │ │ │ └── NetworkMovieDetail.kt │ │ │ ├── firebase │ │ │ │ └── firestore │ │ │ │ │ └── NetworkWatchListMovie.kt │ │ │ └── movie │ │ │ │ ├── NetworkActorDetails.kt │ │ │ │ ├── NetworkActorMovies.kt │ │ │ │ ├── NetworkMovie.kt │ │ │ │ ├── NetworkMovieRecommendations.kt │ │ │ │ └── NetworkUserMovieReview.kt │ │ │ ├── datasource │ │ │ ├── firebase │ │ │ │ ├── storage │ │ │ │ │ ├── FirebaseStorageDataSource.kt │ │ │ │ │ └── FirebaseStorageDataSourceImpl.kt │ │ │ │ └── firestore │ │ │ │ │ ├── FirebaseFirestoreDataSource.kt │ │ │ │ │ └── FirebaseFirestoreDataSourceImpl.kt │ │ │ └── movie │ │ │ │ ├── MovieRemoteDataSource.kt │ │ │ │ ├── paging_source │ │ │ │ ├── MoviesPagingSource.kt │ │ │ │ ├── SearchMoviesPagingSource.kt │ │ │ │ ├── UserMovieReviewsPagingSource.kt │ │ │ │ └── RecommendedMoviesPagingSource.kt │ │ │ │ └── MovieRemoteDataSourceImpl.kt │ │ │ ├── di │ │ │ ├── ApiModule.kt │ │ │ ├── FirebaseModule.kt │ │ │ └── DatasourceModule.kt │ │ │ └── helpers │ │ │ ├── ApiCall.kt │ │ │ └── NetworkConstants.kt │ └── build.gradle.kts ├── authentication │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── res │ │ │ └── values │ │ │ │ └── strings.xml │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── authentication │ │ │ ├── di │ │ │ └── AuthModule.kt │ │ │ └── firebase │ │ │ └── FirebaseAuthClient.kt │ ├── proguard-rules.pro │ └── build.gradle.kts └── designsystem │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ ├── main │ │ ├── res │ │ │ └── drawable │ │ │ │ ├── no_image_available.jpg │ │ │ │ └── auth_background_image.jpeg │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── designsystem │ │ │ ├── AdaptiveSizeSetter.kt │ │ │ ├── dimens │ │ │ ├── Dimens.kt │ │ │ └── AdaptiveModifiers.kt │ │ │ ├── components │ │ │ ├── auth │ │ │ │ └── AuthWelcomeMessage.kt │ │ │ ├── Dialog.kt │ │ │ ├── CircularProgressIndicator.kt │ │ │ ├── Scaffold.kt │ │ │ ├── navigation │ │ │ │ ├── NavigationBar.kt │ │ │ │ └── NavigationRail.kt │ │ │ ├── ErrorView.kt │ │ │ ├── TextButton.kt │ │ │ ├── Button.kt │ │ │ └── DynamicLayout.kt │ │ │ ├── theme │ │ │ └── Type.kt │ │ │ └── WindowSize.kt │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── designsystem │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── ahmetocak │ │ └── designsystem │ │ └── ExampleInstrumentedTest.kt │ └── proguard-rules.pro ├── feature ├── login │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── res │ │ │ │ ├── values-tr │ │ │ │ └── strings.xml │ │ │ │ ├── drawable │ │ │ │ └── ic_google.xml │ │ │ │ └── values │ │ │ │ └── strings.xml │ │ ├── test │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── ahmetocak │ │ │ │ └── login │ │ │ │ └── ExampleUnitTest.kt │ │ └── androidTest │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── login │ │ │ └── ExampleInstrumentedTest.kt │ └── proguard-rules.pro ├── movies │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── res │ │ │ │ ├── values │ │ │ │ └── strings.xml │ │ │ │ └── values-tr-rTR │ │ │ │ └── strings.xml │ │ ├── test │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── ahmetocak │ │ │ │ └── movies │ │ │ │ └── ExampleUnitTest.kt │ │ └── androidTest │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── movies │ │ │ └── ExampleInstrumentedTest.kt │ └── proguard-rules.pro ├── profile │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ └── blank_profile_image.png │ │ │ │ ├── values-tr-rTR │ │ │ │ └── strings.xml │ │ │ │ └── values │ │ │ │ └── strings.xml │ │ ├── test │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── ahmetocak │ │ │ │ └── profile │ │ │ │ └── ExampleUnitTest.kt │ │ └── androidTest │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── profile │ │ │ └── ExampleInstrumentedTest.kt │ └── proguard-rules.pro ├── search │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── res │ │ │ │ ├── values-tr-rTR │ │ │ │ │ └── strings.xml │ │ │ │ └── values │ │ │ │ │ └── strings.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── ahmetocak │ │ │ │ └── search │ │ │ │ └── SearchViewModel.kt │ │ ├── test │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── ahmetocak │ │ │ │ └── search │ │ │ │ └── ExampleUnitTest.kt │ │ └── androidTest │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── search │ │ │ └── ExampleInstrumentedTest.kt │ └── proguard-rules.pro ├── see_all │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ └── AndroidManifest.xml │ │ ├── test │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── ahmetocak │ │ │ │ └── see_all │ │ │ │ └── ExampleUnitTest.kt │ │ └── androidTest │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── see_all │ │ │ └── ExampleInstrumentedTest.kt │ └── proguard-rules.pro ├── signup │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── res │ │ │ │ ├── values-tr │ │ │ │ └── strings.xml │ │ │ │ └── values │ │ │ │ └── strings.xml │ │ ├── test │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── ahmetocak │ │ │ │ └── signup │ │ │ │ └── ExampleUnitTest.kt │ │ └── androidTest │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── signup │ │ │ └── ExampleInstrumentedTest.kt │ └── proguard-rules.pro ├── actor_details │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── res │ │ │ │ ├── values-tr-rTR │ │ │ │ └── strings.xml │ │ │ │ └── values │ │ │ │ └── strings.xml │ │ ├── test │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── ahmetocak │ │ │ │ └── actor_details │ │ │ │ └── ExampleUnitTest.kt │ │ └── androidTest │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── actor_details │ │ │ └── ExampleInstrumentedTest.kt │ └── proguard-rules.pro ├── movie_details │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── res │ │ │ │ ├── values │ │ │ │ └── strings.xml │ │ │ │ ├── values-tr-rTR │ │ │ │ └── strings.xml │ │ │ │ ├── drawable │ │ │ │ └── ic_google_gemini.xml │ │ │ │ └── layout │ │ │ │ └── youtube_trailer_item.xml │ │ ├── test │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── ahmetocak │ │ │ │ └── movie_details │ │ │ │ └── ExampleUnitTest.kt │ │ └── androidTest │ │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── movie_details │ │ │ └── ExampleInstrumentedTest.kt │ └── proguard-rules.pro └── watch_list │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ ├── main │ │ ├── res │ │ │ ├── drawable │ │ │ │ └── empty.png │ │ │ ├── values │ │ │ │ └── strings.xml │ │ │ └── values-tr-rTR │ │ │ │ └── strings.xml │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── ahmetocak │ │ │ └── watch_list │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── ahmetocak │ │ └── watch_list │ │ └── ExampleInstrumentedTest.kt │ └── proguard-rules.pro ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle.kts └── gradle.properties /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/common/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/model/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/ui/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/ui/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/common/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/database/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/datastore/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/navigation/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/network/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/login/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/login/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /feature/movies/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/movies/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /feature/profile/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/search/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/search/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /feature/see_all/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/signup/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/signup/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/authentication/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/authentication/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/designsystem/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/designsystem/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/navigation/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /feature/actor_details/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/movie_details/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/profile/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /feature/see_all/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /feature/watch_list/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/watch_list/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /feature/actor_details/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /feature/movie_details/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/authentication/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/drawable/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/drawable/empty.png -------------------------------------------------------------------------------- /feature/actor_details/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /core/common/src/main/res/drawable/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/core/common/src/main/res/drawable/empty.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/blank_profile_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/drawable/blank_profile_image.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/no_image_available.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/drawable/no_image_available.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/auth_background_image.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/drawable/auth_background_image.jpeg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /feature/watch_list/src/main/res/drawable/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/feature/watch_list/src/main/res/drawable/empty.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /core/ui/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/designsystem/src/main/res/drawable/no_image_available.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/core/designsystem/src/main/res/drawable/no_image_available.jpg -------------------------------------------------------------------------------- /core/navigation/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feature/login/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feature/movies/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feature/profile/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feature/profile/src/main/res/drawable/blank_profile_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/feature/profile/src/main/res/drawable/blank_profile_image.png -------------------------------------------------------------------------------- /feature/search/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feature/see_all/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feature/signup/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /core/designsystem/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feature/movie_details/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feature/watch_list/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/designsystem/src/main/res/drawable/auth_background_image.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmetOcak/MovieApp/HEAD/core/designsystem/src/main/res/drawable/auth_background_image.jpeg -------------------------------------------------------------------------------- /core/database/src/main/res/values-tr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Lütfen daha sonra tekrar deneyin 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /core/common/src/main/java/com/ahmetocak/common/utils/ResponseMapper.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.common.utils 2 | 3 | import com.ahmetocak.common.helpers.Response 4 | 5 | fun Response.mapResponse(mapper: (I) -> O): Response { 6 | return when (this) { 7 | is Response.Success -> { 8 | Response.Success(mapper.invoke(this.data)) 9 | } 10 | is Response.Error -> { 11 | Response.Error(errorMessage) 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /core/domain/src/main/java/com/ahmetocak/domain/firebase/storage/UploadProfileImageUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.domain.firebase.storage 2 | 3 | import android.net.Uri 4 | import com.ahmetocak.data.repository.firebase.FirebaseRepository 5 | import javax.inject.Inject 6 | 7 | class UploadProfileImageUseCase @Inject constructor(private val firebaseRepository: FirebaseRepository) { 8 | 9 | operator fun invoke(imageUri: Uri) = firebaseRepository.uploadProfileImage(imageUri = imageUri) 10 | } -------------------------------------------------------------------------------- /app/src/main/res/values-night/splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /core/model/src/main/java/com/ahmetocak/model/movie/ActorMovies.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.model.movie 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class ActorMovies( 7 | val movies: List 8 | ) 9 | 10 | @Immutable 11 | data class ActorMoviesContent( 12 | val id: Int, 13 | val movieName: String, 14 | val posterImagePath: String?, 15 | val releaseDate: String, 16 | val voteAverage: Double, 17 | val voteCount: Int 18 | ) 19 | -------------------------------------------------------------------------------- /core/model/src/main/java/com/ahmetocak/model/movie/Movie.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.model.movie 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class Movie( 7 | val movies: List, 8 | val totalPages: Int 9 | ) 10 | 11 | @Immutable 12 | data class MovieContent( 13 | val id: Int, 14 | val posterImagePath: String?, 15 | val releaseDate: String, 16 | val movieName: String, 17 | val voteAverage: Double, 18 | val voteCount: Int? 19 | ) -------------------------------------------------------------------------------- /feature/actor_details/src/test/java/com/ahmetocak/actor_details/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.actor_details 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /feature/movie_details/src/test/java/com/ahmetocak/movie_details/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.movie_details 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /core/domain/src/main/java/com/ahmetocak/domain/preferences/UpdateDynamicColorUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.domain.preferences 2 | 3 | import com.ahmetocak.data.repository.datastore.DataStoreRepository 4 | import javax.inject.Inject 5 | 6 | class UpdateDynamicColorUseCase @Inject constructor(private val preferencesRepository: DataStoreRepository) { 7 | 8 | suspend operator fun invoke(isDynamicColor: Boolean) = 9 | preferencesRepository.updateDynamicColor(dynamicColor = isDynamicColor) 10 | } -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/datasource/firebase/storage/FirebaseStorageDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.datasource.firebase.storage 2 | 3 | import android.net.Uri 4 | import com.google.android.gms.tasks.Task 5 | import com.google.firebase.storage.UploadTask 6 | 7 | interface FirebaseStorageDataSource { 8 | 9 | fun uploadProfileImage(imageUri: Uri): UploadTask 10 | 11 | fun getUserProfileImage(): Task 12 | 13 | fun deleteUserProfileImage(): Task 14 | } -------------------------------------------------------------------------------- /core/domain/src/main/java/com/ahmetocak/domain/movie/AddMovieToDbWatchListUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.domain.movie 2 | 3 | import com.ahmetocak.data.repository.movie.MovieRepository 4 | import com.ahmetocak.model.firebase.WatchListMovie 5 | import javax.inject.Inject 6 | 7 | class AddMovieToDbWatchListUseCase @Inject constructor(private val repository: MovieRepository) { 8 | 9 | suspend operator fun invoke(watchListMovie: WatchListMovie) = 10 | repository.addMovieToWatchList(watchListMovie = watchListMovie) 11 | } -------------------------------------------------------------------------------- /core/model/src/main/java/com/ahmetocak/model/firebase/WatchListMovie.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.model.firebase 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | data class WatchList( 6 | val watchList: List = emptyList() 7 | ) 8 | 9 | @Immutable 10 | data class WatchListMovie( 11 | val id: Int? = null, 12 | val name: String? = null, 13 | val releaseYear: String? = null, 14 | val voteAverage: Float? = null, 15 | val voteCount: Int? = null, 16 | val imageUrlPath: String? = null 17 | ) -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/model/firebase/firestore/NetworkWatchListMovie.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.model.firebase.firestore 2 | 3 | data class NetworkWatchList( 4 | val watchList: List = emptyList() 5 | ) 6 | 7 | data class NetworkWatchListMovie( 8 | val id: Int? = null, 9 | val name: String? = null, 10 | val releaseYear: String? = null, 11 | val voteAverage: Float? = null, 12 | val voteCount: Int? = null, 13 | val imageUrlPath: String? = null 14 | ) -------------------------------------------------------------------------------- /core/common/src/main/java/com/ahmetocak/common/helpers/UiState.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.common.helpers 2 | 3 | import androidx.compose.runtime.Stable 4 | 5 | /** 6 | * Sealed interface representing different UI states with associated data. 7 | * 8 | * @param T The type of data associated with the UI state. 9 | */ 10 | @Stable 11 | sealed interface UiState { 12 | data object Loading: UiState 13 | data class OnDataLoaded(val data: T): UiState 14 | data class OnError(val errorMessage: UiText): UiState 15 | } -------------------------------------------------------------------------------- /core/model/src/main/java/com/ahmetocak/model/movie/UserMovieReviews.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.model.movie 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class UserReviewResults( 7 | val id: String, 8 | val author: String, 9 | val content: String, 10 | val createdAt: String, 11 | val updatedAt: String, 12 | val authorDetails: UserReviewAuthorDetails 13 | ) 14 | 15 | @Immutable 16 | data class UserReviewAuthorDetails( 17 | val rating: Int? = null, 18 | val avatarPath: String? = null 19 | ) -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /core/domain/src/main/java/com/ahmetocak/domain/firebase/firestore/UpdateMovieDataUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.domain.firebase.firestore 2 | 3 | import com.ahmetocak.data.repository.firebase.FirebaseRepository 4 | import com.ahmetocak.model.firebase.WatchListMovie 5 | import javax.inject.Inject 6 | 7 | class UpdateMovieDataUseCase @Inject constructor(private val firebaseRepository: FirebaseRepository) { 8 | 9 | operator fun invoke(watchListMovie: List) = 10 | firebaseRepository.updateMovieData(watchListMovie = watchListMovie) 11 | } -------------------------------------------------------------------------------- /core/domain/src/main/java/com/ahmetocak/domain/firebase/firestore/AddMovieToWatchListUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.domain.firebase.firestore 2 | 3 | import com.ahmetocak.data.repository.firebase.FirebaseRepository 4 | import com.ahmetocak.model.firebase.WatchListMovie 5 | import javax.inject.Inject 6 | 7 | class AddMovieToWatchListUseCase @Inject constructor(private val firebaseRepository: FirebaseRepository) { 8 | 9 | operator fun invoke(watchListMovie: WatchListMovie) = 10 | firebaseRepository.addMovieToFirestore(watchListMovie = watchListMovie) 11 | } -------------------------------------------------------------------------------- /core/model/src/main/java/com/ahmetocak/model/movie_detail/MovieDetail.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.model.movie_detail 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class MovieDetail( 7 | val id: Int, 8 | val genres: String, 9 | val overview: String, 10 | val posterImageUrlPath: String, 11 | val backdropImageUrlPath: String, 12 | val movieName: String, 13 | val voteAverage: Float, 14 | val voteCount: Int, 15 | val releaseDate: String, 16 | val duration: Int, 17 | val originalMovieName: String 18 | ) -------------------------------------------------------------------------------- /core/common/src/main/java/com/ahmetocak/common/helpers/ShowUserMessage.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.common.helpers 2 | 3 | import android.widget.Toast 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.platform.LocalContext 6 | 7 | @Composable 8 | inline fun ShowUserMessage( 9 | message: String, 10 | toastLength: Int = Toast.LENGTH_LONG, 11 | consumedMessage: () -> Unit = {} 12 | ) { 13 | Toast.makeText( 14 | LocalContext.current, 15 | message, 16 | toastLength 17 | ).show() 18 | consumedMessage() 19 | } -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/model/movie/NetworkActorDetails.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.model.movie 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class NetworkActorDetails( 6 | val id: Int, 7 | val biography: String?, 8 | val birthday: String?, 9 | val homepage: String?, 10 | val name: String?, 11 | 12 | @SerializedName("deathday") 13 | val deathDay: String?, 14 | 15 | @SerializedName("place_of_birth") 16 | val placeOfBirth: String?, 17 | 18 | @SerializedName("profile_path") 19 | val profileImagePath: String? 20 | ) -------------------------------------------------------------------------------- /core/database/src/main/java/com/ahmetocak/database/di/DaoModule.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.database.di 2 | 3 | import com.ahmetocak.database.dao.WatchListDao 4 | import com.ahmetocak.database.db.WatchListDatabase 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | object DaoModule { 14 | 15 | @Provides 16 | @Singleton 17 | fun provideWatchListDao(db: WatchListDatabase): WatchListDao { 18 | return db.watchListDao() 19 | } 20 | } -------------------------------------------------------------------------------- /core/database/src/main/java/com/ahmetocak/database/datasource/WatchListLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.database.datasource 2 | 3 | import com.ahmetocak.common.helpers.Response 4 | import com.ahmetocak.database.entity.WatchListEntity 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface WatchListLocalDataSource { 8 | 9 | suspend fun addMovieToWatchList(watchListEntity: WatchListEntity): Response 10 | 11 | suspend fun observeWatchList(): Response>> 12 | 13 | suspend fun removeMovieFromWatchList(movieId: Int): Response 14 | 15 | suspend fun deleteWatchList(): Response 16 | } -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/datasource/firebase/firestore/FirebaseFirestoreDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.datasource.firebase.firestore 2 | 3 | import com.ahmetocak.network.model.firebase.firestore.NetworkWatchListMovie 4 | import com.google.android.gms.tasks.Task 5 | import com.google.firebase.firestore.DocumentSnapshot 6 | 7 | interface FirebaseFirestoreDataSource { 8 | 9 | fun addMovieData(watchListMovie: NetworkWatchListMovie): Task 10 | 11 | fun updateMovieData(watchListMovie: List): Task 12 | 13 | fun getMovieData(): Task 14 | 15 | fun deleteMovieDocument(): Task 16 | } -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/model/movie_detail/NetworkMovieCredit.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.model.movie_detail 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class NetworkMovieCredit( 6 | val cast: List, 7 | 8 | @SerializedName("crew") 9 | val crew: List 10 | ) 11 | 12 | data class NetworkCast( 13 | val id: Int, 14 | val name: String?, 15 | 16 | @SerializedName("character") 17 | val characterName: String?, 18 | 19 | @SerializedName("profile_path") 20 | val imageUrlPath: String? 21 | ) 22 | 23 | data class NetworkCrew( 24 | val name: String?, 25 | val job: String? 26 | ) -------------------------------------------------------------------------------- /core/database/src/main/java/com/ahmetocak/database/entity/WatchListEntity.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.database.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity 8 | data class WatchListEntity( 9 | 10 | @PrimaryKey 11 | @ColumnInfo("id") 12 | val movieId: Int, 13 | 14 | @ColumnInfo("movie_name") 15 | val movieName: String, 16 | 17 | @ColumnInfo("release_year") 18 | val releaseYear: String, 19 | 20 | @ColumnInfo("vote_average") 21 | val voteAverage: Float, 22 | 23 | @ColumnInfo("vote_count") 24 | val voteCount: Int, 25 | 26 | @ColumnInfo("image_url_path") 27 | val imageUrlPath: String? 28 | ) 29 | -------------------------------------------------------------------------------- /core/data/src/main/java/com/ahmetocak/data/mapper/FirebaseMapper.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.data.mapper 2 | 3 | import com.ahmetocak.model.firebase.WatchListMovie 4 | import com.ahmetocak.network.model.firebase.firestore.NetworkWatchListMovie 5 | 6 | internal fun WatchListMovie.toNetworkWatchListMovie(): NetworkWatchListMovie { 7 | return NetworkWatchListMovie( 8 | id = id, 9 | name = name, 10 | releaseYear = releaseYear, 11 | voteAverage = voteAverage, 12 | voteCount = voteCount, 13 | imageUrlPath = imageUrlPath 14 | ) 15 | } 16 | 17 | internal fun List.toNetworkWatchListMovie(): List { 18 | return this.map { it.toNetworkWatchListMovie() } 19 | } -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/ahmetocak/designsystem/AdaptiveSizeSetter.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.designsystem 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.unit.Dp 5 | 6 | @Composable 7 | fun setSize( 8 | windowSizeClasses: WindowSizeClasses = computeWindowHeightSize() ?: WindowSizeClasses.MEDIUM, 9 | onCompact: Dp, 10 | onMedium: Dp, 11 | onExpanded: Dp 12 | ): Dp { 13 | return when (windowSizeClasses) { 14 | is WindowSizeClasses.COMPACT -> { 15 | onCompact 16 | } 17 | 18 | is WindowSizeClasses.MEDIUM -> { 19 | onMedium 20 | } 21 | 22 | is WindowSizeClasses.EXPANDED -> { 23 | onExpanded 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /core/database/src/main/java/com/ahmetocak/database/di/DatasourceModule.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.database.di 2 | 3 | import com.ahmetocak.database.dao.WatchListDao 4 | import com.ahmetocak.database.datasource.WatchListLocalDataSource 5 | import com.ahmetocak.database.datasource.WatchListLocalDataSourceImpl 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.components.SingletonComponent 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object DatasourceModule { 15 | 16 | @Singleton 17 | @Provides 18 | fun provideWatchListLocalDataSource(dao: WatchListDao): WatchListLocalDataSource { 19 | return WatchListLocalDataSourceImpl(dao) 20 | } 21 | } -------------------------------------------------------------------------------- /core/common/src/main/java/com/ahmetocak/common/helpers/HandleTaskError.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.common.helpers 2 | 3 | import com.ahmetocak.common.R 4 | 5 | /** 6 | * Handles an exception from a task operation and returns a [UiText] representation of the error message. 7 | * 8 | * @param e The exception to be handled. 9 | * @param defaultErrorMessage The default [UiText] to be used when the exception does not provide a message. 10 | * @return A UiText representation of the error message. 11 | */ 12 | fun handleTaskError( 13 | e: Exception?, 14 | defaultErrorMessage: UiText = UiText.StringResource(R.string.unknown_error) 15 | ): UiText { 16 | return e?.message?.let { errorMessage -> 17 | UiText.DynamicString(errorMessage) 18 | } ?: defaultErrorMessage 19 | } -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/ahmetocak/designsystem/dimens/Dimens.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.designsystem.dimens 2 | 3 | import androidx.compose.ui.unit.dp 4 | 5 | object Dimens { 6 | val oneLevelPadding = 8.dp 7 | val twoLevelPadding = 16.dp 8 | val threeLevelPadding = 24.dp 9 | val fourLevelPadding = 32.dp 10 | val fiveLevelPadding = 40.dp 11 | val sixLevelPadding = 48.dp 12 | val sevenLevelPadding = 56.dp 13 | val eightLevelPadding = 64.dp 14 | } 15 | 16 | object ComponentDimens { 17 | val buttonHeight = 56.dp 18 | val emptyWarningImageSize = 112.dp 19 | val buttonCircularProgressIndicatorSize = 24.dp 20 | val errorViewIconSize = 64.dp 21 | val appIconSize = 96.dp 22 | val profileImageSize = 144.dp 23 | val searchSomethingViewSize = 112.dp 24 | } -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/model/movie/NetworkActorMovies.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.model.movie 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class NetworkActorMovies( 6 | @SerializedName("cast") 7 | val movies: List 8 | ) 9 | 10 | data class NetworkActorMoviesContent( 11 | val id: Int, 12 | 13 | @SerializedName("title") 14 | val movieName: String? = null, 15 | 16 | @SerializedName("poster_path") 17 | val posterImagePath: String? = null, 18 | 19 | @SerializedName("release_date") 20 | val releaseDate: String? = null, 21 | 22 | @SerializedName("vote_average") 23 | val voteAverage: Double? = null, 24 | 25 | @SerializedName("vote_count") 26 | val voteCount: Int? = null 27 | ) 28 | -------------------------------------------------------------------------------- /core/ui/src/androidTest/java/com/ahmetocak/ui/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.ui 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ahmetocak.ui.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /core/authentication/src/main/java/com/ahmetocak/authentication/di/AuthModule.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.authentication.di 2 | 3 | import android.content.Context 4 | import com.ahmetocak.authentication.firebase.GoogleAuthClient 5 | import com.google.android.gms.auth.api.identity.Identity 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.android.qualifiers.ApplicationContext 10 | import dagger.hilt.components.SingletonComponent 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | object AuthModule { 16 | 17 | @Singleton 18 | @Provides 19 | fun provideGoogleAuthClient(@ApplicationContext context: Context): GoogleAuthClient { 20 | return GoogleAuthClient(context, Identity.getSignInClient(context)) 21 | } 22 | } -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/model/movie/NetworkMovie.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.model.movie 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class NetworkMovie( 6 | @SerializedName("results") 7 | val movies: List, 8 | 9 | @SerializedName("total_pages") 10 | val totalPages: Int 11 | ) 12 | 13 | data class NetworkMovieContent( 14 | val id: Int, 15 | 16 | @SerializedName("poster_path") 17 | val posterImagePath: String?, 18 | 19 | @SerializedName("release_date") 20 | val releaseDate: String?, 21 | 22 | @SerializedName("original_title") 23 | val movieName: String?, 24 | 25 | @SerializedName("vote_average") 26 | val voteAverage: Double?, 27 | 28 | @SerializedName("vote_count") 29 | val voteCount: Int? 30 | ) -------------------------------------------------------------------------------- /app/src/androidTest/java/com/ahmetocak/movieapp/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.movieapp 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ahmetocak.movieapp", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /core/common/src/androidTest/java/com/ahmetocak/common/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.common 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ahmetocak.common.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /feature/login/src/androidTest/java/com/ahmetocak/login/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.login 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ahmetocak.login.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /feature/movies/src/androidTest/java/com/ahmetocak/movies/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.movies 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ahmetocak.movies.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /feature/search/src/androidTest/java/com/ahmetocak/search/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.search 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ahmetocak.search.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /feature/signup/src/androidTest/java/com/ahmetocak/signup/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.signup 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ahmetocak.signup.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /core/data/src/main/java/com/ahmetocak/data/repository/firebase/FirebaseRepository.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.data.repository.firebase 2 | 3 | import android.net.Uri 4 | import com.ahmetocak.model.firebase.WatchListMovie 5 | import com.google.android.gms.tasks.Task 6 | import com.google.firebase.firestore.DocumentSnapshot 7 | import com.google.firebase.storage.UploadTask 8 | 9 | interface FirebaseRepository { 10 | 11 | fun addMovieToFirestore(watchListMovie: WatchListMovie): Task 12 | 13 | fun updateMovieData(watchListMovie: List): Task 14 | 15 | fun getMovieData(): Task 16 | 17 | fun deleteMovieDocument(): Task 18 | 19 | fun uploadProfileImage(imageUri: Uri): UploadTask 20 | 21 | fun getUserProfileImage(): Task 22 | 23 | fun deleteUserProfileImage(): Task 24 | } -------------------------------------------------------------------------------- /core/ui/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /feature/profile/src/androidTest/java/com/ahmetocak/profile/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.profile 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ahmetocak.profile.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /feature/see_all/src/androidTest/java/com/ahmetocak/see_all/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.see_all 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ahmetocak.see_all.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /core/common/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/ahmetocak/designsystem/components/auth/AuthWelcomeMessage.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.designsystem.components.auth 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.material3.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.text.font.FontWeight 8 | import androidx.compose.ui.text.style.TextAlign 9 | import com.ahmetocak.designsystem.dimens.setAdaptiveWidth 10 | 11 | @Composable 12 | fun AuthWelcomeMessage( 13 | modifier: Modifier = Modifier, 14 | text: String 15 | ) { 16 | Text( 17 | modifier = modifier.setAdaptiveWidth(), 18 | text = text, 19 | textAlign = TextAlign.Center, 20 | style = MaterialTheme.typography.displayLarge.copy(fontWeight = FontWeight.W500), 21 | ) 22 | } -------------------------------------------------------------------------------- /feature/login/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /core/database/src/main/java/com/ahmetocak/database/dao/WatchListDao.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.database.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.ahmetocak.database.entity.WatchListEntity 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | @Dao 11 | interface WatchListDao { 12 | 13 | @Insert(onConflict = OnConflictStrategy.REPLACE) 14 | suspend fun addMovieToWatchList(watchListEntity: WatchListEntity) 15 | 16 | @Query("SELECT * FROM WatchListEntity") 17 | fun observeWatchList(): Flow> 18 | 19 | @Query("DELETE FROM WatchListEntity WHERE id == :movieId") 20 | suspend fun removeMovieFromWatchList(movieId: Int) 21 | 22 | @Query("DELETE FROM WatchListEntity") 23 | suspend fun deleteWatchList() 24 | } -------------------------------------------------------------------------------- /core/designsystem/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /core/navigation/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /core/navigation/src/androidTest/java/com/ahmetocak/navigation/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.navigation 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ahmetocak.navigation.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /feature/movies/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /feature/profile/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /feature/search/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /feature/see_all/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /feature/signup/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /feature/watch_list/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /feature/watch_list/src/androidTest/java/com/ahmetocak/watch_list/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.watch_list 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ahmetocak.watch_list.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /core/authentication/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /core/designsystem/src/androidTest/java/com/ahmetocak/designsystem/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.designsystem 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ahmetocak.designsystem.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /feature/actor_details/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /feature/movie_details/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /core/common/src/main/java/com/ahmetocak/common/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.common 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.ContextWrapper 6 | import android.util.Patterns 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.res.stringResource 9 | 10 | @Composable 11 | fun Int.convertToDurationTime(): String = 12 | "${this / 60}${stringResource(id = R.string.hour_text)} ${this % 60}${stringResource(id = R.string.min_tex)}" 13 | 14 | @Composable 15 | fun Float.roundToDecimal(): String = String.format("%.1f", this) 16 | 17 | fun Context.findActivity(): Activity? = when (this) { 18 | is Activity -> this 19 | is ContextWrapper -> baseContext.findActivity() 20 | else -> null 21 | } 22 | 23 | fun String.isValidEmail(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches() -------------------------------------------------------------------------------- /core/domain/src/main/java/com/ahmetocak/domain/firebase/auth/SendResetPasswordEmailUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.domain.firebase.auth 2 | 3 | import com.ahmetocak.authentication.firebase.FirebaseAuthClient 4 | import com.ahmetocak.common.helpers.UiText 5 | import com.ahmetocak.common.helpers.handleTaskError 6 | import javax.inject.Inject 7 | 8 | class SendResetPasswordEmailUseCase @Inject constructor(private val firebaseAuthClient: FirebaseAuthClient) { 9 | 10 | operator fun invoke(email: String, onTaskSuccess: () -> Unit, onTaskFailed: (UiText) -> Unit) { 11 | firebaseAuthClient.sendResetPasswordEmail(email).addOnCompleteListener { task -> 12 | if (task.isSuccessful) { 13 | onTaskSuccess() 14 | } else { 15 | onTaskFailed(handleTaskError(task.exception)) 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/di/ApiModule.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.di 2 | 3 | import com.ahmetocak.network.helpers.NetworkConstants 4 | import com.ahmetocak.network.api.MovieApi 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import retrofit2.Retrofit 10 | import retrofit2.converter.gson.GsonConverterFactory 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | object ApiModule { 16 | 17 | @Singleton 18 | @Provides 19 | fun provideMovieApi(): MovieApi { 20 | return Retrofit.Builder() 21 | .baseUrl(NetworkConstants.BASE_URL) 22 | .addConverterFactory(GsonConverterFactory.create()) 23 | .build() 24 | .create(MovieApi::class.java) 25 | } 26 | } -------------------------------------------------------------------------------- /feature/actor_details/src/androidTest/java/com/ahmetocak/actor_details/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.actor_details 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ahmetocak.actor_details.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /feature/movie_details/src/androidTest/java/com/ahmetocak/movie_details/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.movie_details 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ahmetocak.movie_details.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /feature/movie_details/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Movie added to watch list. 4 | The movie removed from the watch list. 5 | Director 6 | Actors 7 | Trailers 8 | reviews 9 | Updated at 10 | User Reviews 11 | Recommendations 12 | Can you tell me about %s movie ? 13 | We are currently unable to display movie details due to some issues. Please try again later. 14 | -------------------------------------------------------------------------------- /core/database/src/main/java/com/ahmetocak/database/di/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.database.di 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import com.ahmetocak.database.db.WatchListDatabase 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.android.qualifiers.ApplicationContext 11 | import dagger.hilt.components.SingletonComponent 12 | import javax.inject.Singleton 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | object DatabaseModule { 17 | 18 | @Provides 19 | @Singleton 20 | fun provideWatchListDatabase(@ApplicationContext context: Context): WatchListDatabase { 21 | return Room.databaseBuilder( 22 | context, 23 | WatchListDatabase::class.java, 24 | "watch_list_db" 25 | ).build() 26 | } 27 | } -------------------------------------------------------------------------------- /core/domain/src/main/java/com/ahmetocak/domain/firebase/auth/LoginUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.domain.firebase.auth 2 | 3 | import com.ahmetocak.authentication.firebase.FirebaseAuthClient 4 | import com.ahmetocak.common.helpers.UiText 5 | import com.ahmetocak.common.helpers.handleTaskError 6 | import javax.inject.Inject 7 | 8 | class LoginUseCase @Inject constructor(private val firebaseAuthClient: FirebaseAuthClient) { 9 | 10 | operator fun invoke( 11 | email: String, 12 | password: String, 13 | onTaskSuccess: () -> Unit, 14 | onTaskFailed: (UiText) -> Unit 15 | ) { 16 | firebaseAuthClient.login(email, password).addOnCompleteListener { task -> 17 | if (task.isSuccessful) { 18 | onTaskSuccess() 19 | } else { 20 | onTaskFailed(handleTaskError(task.exception)) 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /core/ui/src/main/java/com/ahmetocak/ui/TmdbLogo.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.ui 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.res.painterResource 10 | import androidx.compose.ui.unit.Dp 11 | import androidx.compose.ui.unit.dp 12 | import com.ahmetocak.designsystem.theme.TmdbBlue 13 | 14 | @Composable 15 | fun TmdbLogo(modifier: Modifier = Modifier, logoSize: Dp = 96.dp) { 16 | Image( 17 | modifier = modifier 18 | .size(logoSize) 19 | .background(TmdbBlue) 20 | .padding(4.dp), 21 | painter = painterResource(id = R.drawable.tmdb_logo), 22 | contentDescription = null 23 | ) 24 | } -------------------------------------------------------------------------------- /core/domain/src/main/java/com/ahmetocak/domain/firebase/auth/SignUpUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.domain.firebase.auth 2 | 3 | import com.ahmetocak.authentication.firebase.FirebaseAuthClient 4 | import com.ahmetocak.common.helpers.UiText 5 | import com.ahmetocak.common.helpers.handleTaskError 6 | import javax.inject.Inject 7 | 8 | class SignUpUseCase @Inject constructor(private val firebaseAuthClient: FirebaseAuthClient) { 9 | 10 | operator fun invoke( 11 | email: String, 12 | password: String, 13 | onTaskSuccess: () -> Unit, 14 | onTaskFailed: (UiText) -> Unit 15 | ) { 16 | firebaseAuthClient.signUp(email, password).addOnCompleteListener { task -> 17 | if (task.isSuccessful) { 18 | onTaskSuccess() 19 | } else { 20 | onTaskFailed(handleTaskError(task.exception)) 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/model/movie/NetworkMovieRecommendations.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.model.movie 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class NetworkMovieRecommendations( 6 | val page: Int, 7 | 8 | @SerializedName("results") 9 | val recommendations: List, 10 | 11 | @SerializedName("total_pages") 12 | val totalPages: Int 13 | ) 14 | 15 | data class NetworkRecommendedMovieContent( 16 | val id: Int, 17 | 18 | @SerializedName("original_title") 19 | val movieName: String?, 20 | 21 | @SerializedName("poster_path") 22 | val image: String?, 23 | 24 | @SerializedName("release_date") 25 | val releaseDate: String?, 26 | 27 | @SerializedName("vote_average") 28 | val voteAverage: Double?, 29 | 30 | @SerializedName("vote_count") 31 | val voteCount: Int? 32 | ) -------------------------------------------------------------------------------- /feature/profile/src/main/res/values-tr-rTR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ayarlar 4 | Karanlık Tema 5 | Diller 6 | İngilizce 7 | Türkçe 8 | Dinamik Renk 9 | Gönder 10 | İptal 11 | Hesabı Sil 12 | Hesabınızı silmek istediğinizden emin misiniz ? Hesabınızı sildiğinizde tüm kullanıcı bilgileriniz de silinir. Bu süreç geri döndürülemez. 13 | Sil 14 | Şifre 15 | -------------------------------------------------------------------------------- /core/domain/src/main/java/com/ahmetocak/domain/firebase/auth/SignOutUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.domain.firebase.auth 2 | 3 | import com.ahmetocak.authentication.firebase.FirebaseAuthClient 4 | import com.ahmetocak.common.helpers.Response 5 | import com.ahmetocak.common.helpers.UiText 6 | import com.ahmetocak.data.repository.movie.MovieRepository 7 | import javax.inject.Inject 8 | 9 | class SignOutUseCase @Inject constructor( 10 | private val firebaseAuthClient: FirebaseAuthClient, 11 | private val movieRepository: MovieRepository 12 | ) { 13 | suspend operator fun invoke(onFailed: (UiText) -> Unit) { 14 | when(val response = movieRepository.deleteWatchList()) { 15 | is Response.Success -> { 16 | firebaseAuthClient.signOut() 17 | } 18 | is Response.Error -> { 19 | onFailed(response.errorMessage) 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /feature/profile/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Settings 4 | Dark Theme 5 | Languages 6 | English 7 | Turkish 8 | Dynamic Color 9 | Send 10 | Cancel 11 | Delete Account 12 | Are you sure you want to delete your account ? When you delete your account, all your user information is also deleted. This process is irreversible. 13 | Delete 14 | Password 15 | -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/di/FirebaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.di 2 | 3 | import com.google.firebase.firestore.FirebaseFirestore 4 | import com.google.firebase.firestore.ktx.firestore 5 | import com.google.firebase.ktx.Firebase 6 | import com.google.firebase.storage.FirebaseStorage 7 | import com.google.firebase.storage.ktx.storage 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.components.SingletonComponent 12 | import javax.inject.Singleton 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | object FirebaseModule { 17 | 18 | @Singleton 19 | @Provides 20 | fun provideFirebaseFirestore(): FirebaseFirestore { 21 | return Firebase.firestore 22 | } 23 | 24 | @Singleton 25 | @Provides 26 | fun provideFirebaseStorageReference(): FirebaseStorage { 27 | return Firebase.storage 28 | } 29 | } -------------------------------------------------------------------------------- /core/data/src/main/java/com/ahmetocak/data/repository/datastore/DataStoreRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.data.repository.datastore 2 | 3 | import com.ahmetocak.datastore.datasource.MovieAppPreferenceDataSource 4 | import kotlinx.coroutines.flow.Flow 5 | import javax.inject.Inject 6 | 7 | class DataStoreRepositoryImpl @Inject constructor( 8 | private val preferenceDataSource: MovieAppPreferenceDataSource 9 | ) : DataStoreRepository { 10 | 11 | override suspend fun observeAppTheme(): Flow = preferenceDataSource.observeAppTheme() 12 | 13 | override suspend fun updateAppTheme(isDarkMode: Boolean) = 14 | preferenceDataSource.updateAppTheme(isDarkMode) 15 | 16 | override suspend fun observeDynamicColor(): Flow = preferenceDataSource.observeDynamicColor() 17 | 18 | override suspend fun updateDynamicColor(dynamicColor: Boolean) = 19 | preferenceDataSource.updateDynamicColor(dynamicColor) 20 | } -------------------------------------------------------------------------------- /feature/movie_details/src/main/res/values-tr-rTR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Film izleme listesine eklendi. 4 | Film izleme listesinden çıkartıldı. 5 | Yönetmen 6 | Oyuncu Kadrosu 7 | Fragmanlar 8 | incelemeler 9 | Güncellendi 10 | Kullanıcı Değerlendirmeleri 11 | Önerilenler 12 | Bana %s filminden bahseder misin ? 13 | Şu an için bazı sorunlardan ötürü film detaylarını gösteremiyoruz. Lütfen daha sonra tekrar deneyin. 14 | -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/model/movie/NetworkUserMovieReview.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.model.movie 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class NetworkUserMovieReviews( 6 | val page: Int, 7 | val results: List, 8 | 9 | @SerializedName("total_pages") 10 | val totalPages: Int, 11 | ) 12 | 13 | data class NetworkUserReviewResults( 14 | val id: String, 15 | val author: String? = null, 16 | val content: String? = null, 17 | 18 | @SerializedName("created_at") 19 | val createdAt: String, 20 | 21 | @SerializedName("updated_at") 22 | val updatedAt: String? = null, 23 | 24 | @SerializedName("author_details") 25 | val authorDetails: NetworkUserReviewAuthorDetails 26 | ) 27 | 28 | data class NetworkUserReviewAuthorDetails( 29 | val rating: Int? = null, 30 | 31 | @SerializedName("avatar_path") 32 | val avatarPath: String? = null 33 | ) -------------------------------------------------------------------------------- /feature/movie_details/src/main/res/drawable/ic_google_gemini.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /core/domain/src/main/java/com/ahmetocak/domain/GetGeminiResponseUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.domain 2 | 3 | import com.ahmetocak.common.helpers.Response 4 | import com.ahmetocak.common.helpers.UiText 5 | import com.ahmetocak.common.R as common 6 | import com.google.ai.client.generativeai.GenerativeModel 7 | import javax.inject.Inject 8 | 9 | class GetGeminiResponseUseCase @Inject constructor() { 10 | 11 | suspend operator fun invoke(prompt: String): Response { 12 | val generativeModel = GenerativeModel( 13 | modelName = "gemini-pro", 14 | apiKey = BuildConfig.GEMINI_API_KEY 15 | ) 16 | 17 | return try { 18 | Response.Success(data = generativeModel.generateContent(prompt).text) 19 | } catch (e: Exception) { 20 | Response.Error(errorMessage = e.message?.let { message -> 21 | UiText.DynamicString(message) 22 | } ?: UiText.StringResource(common.string.unknown_error)) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/model/movie_detail/NetworkMovieDetail.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.model.movie_detail 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class NetworkMovieDetail( 6 | val id: Int, 7 | val genres: List, 8 | val overview: String?, 9 | 10 | @SerializedName("poster_path") 11 | val posterImageUrlPath: String?, 12 | 13 | @SerializedName("backdrop_path") 14 | val backdropImageUrlPath: String?, 15 | 16 | @SerializedName("title") 17 | val movieName: String?, 18 | 19 | @SerializedName("vote_average") 20 | val voteAverage: Double?, 21 | 22 | @SerializedName("vote_count") 23 | val voteCount: Int?, 24 | 25 | @SerializedName("release_date") 26 | val releaseDate: String?, 27 | 28 | @SerializedName("runtime") 29 | val duration: Int?, 30 | 31 | @SerializedName("original_title") 32 | val originalMovieName: String? 33 | ) 34 | 35 | data class NetworkMovieGenre( 36 | val name: String 37 | ) -------------------------------------------------------------------------------- /core/common/src/main/res/values-tr-rTR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bir şeyler ters gitti. Lütfen daha sonra tekrar deneyiniz! 4 | s 5 | d 6 | Filmler 7 | Ara 8 | İzleme Listesi 9 | Profil 10 | Lütfen bu alanı doldurunuz 11 | Şifreler uyuşmuyor 12 | Şifre minimum 6 karakterden oluşmalı 13 | Lütfen geçerli bir email adresi giriniz 14 | Aradığınız film bulunamadı. 15 | Trendler 16 | Top Rated 17 | Upcoming 18 | -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/ahmetocak/designsystem/components/Dialog.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.designsystem.components 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.foundation.layout.width 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material3.Card 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.platform.LocalConfiguration 10 | import androidx.compose.ui.unit.dp 11 | import androidx.compose.ui.window.Dialog 12 | import com.ahmetocak.designsystem.dimens.Dimens 13 | 14 | @Composable 15 | fun MovieDialog( 16 | onDismissRequest: () -> Unit, 17 | content: @Composable (ColumnScope.() -> Unit) 18 | ) { 19 | Dialog(onDismissRequest = onDismissRequest) { 20 | Card( 21 | modifier = Modifier.width(LocalConfiguration.current.screenWidthDp.dp - Dimens.fourLevelPadding), 22 | shape = RoundedCornerShape(28.dp), 23 | content = content 24 | ) 25 | } 26 | } -------------------------------------------------------------------------------- /core/common/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Something went wrong. Please try again later! 4 | h 5 | m 6 | Movies 7 | Search 8 | Watch List 9 | Profile 10 | Please fill in this field 11 | Please enter a valid email address 12 | Passwords do not match 13 | Password must be at least 6 characters in length 14 | The movie you are looking for could not be found. 15 | Top Rated 16 | Upcoming 17 | Trending 18 | -------------------------------------------------------------------------------- /core/database/src/main/java/com/ahmetocak/database/datasource/WatchListLocalDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.database.datasource 2 | 3 | import com.ahmetocak.common.helpers.Response 4 | import com.ahmetocak.database.dao.WatchListDao 5 | import com.ahmetocak.database.entity.WatchListEntity 6 | import com.ahmetocak.database.utils.dbCall 7 | import kotlinx.coroutines.flow.Flow 8 | import javax.inject.Inject 9 | 10 | class WatchListLocalDataSourceImpl @Inject constructor( 11 | private val dao: WatchListDao 12 | ) : WatchListLocalDataSource { 13 | override suspend fun addMovieToWatchList(watchListEntity: WatchListEntity): Response = 14 | dbCall { dao.addMovieToWatchList(watchListEntity) } 15 | 16 | override suspend fun observeWatchList(): Response>> = 17 | dbCall { dao.observeWatchList() } 18 | 19 | override suspend fun removeMovieFromWatchList(movieId: Int) = 20 | dbCall { dao.removeMovieFromWatchList(movieId) } 21 | 22 | override suspend fun deleteWatchList(): Response = dbCall { dao.deleteWatchList() } 23 | } -------------------------------------------------------------------------------- /core/database/src/main/java/com/ahmetocak/database/utils/DbCall.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.database.utils 2 | 3 | import com.ahmetocak.common.helpers.Response 4 | import com.ahmetocak.common.helpers.UiText 5 | import com.ahmetocak.database.R 6 | 7 | /** 8 | * A generic function for making database-related calls with error handling. 9 | * 10 | * This function is designed to be used with suspending database operations. It wraps the database 11 | * operation with a try-catch block to handle exceptions and provides a uniform way of dealing with responses. 12 | * 13 | * @param call A suspending lambda function representing the database operation. 14 | * @return A sealed class [Response] containing either the success data or an error message. 15 | */ 16 | suspend inline fun dbCall(crossinline call: suspend () -> T): Response { 17 | return try { 18 | Response.Success(data = call()) 19 | } catch (e: Exception) { 20 | Response.Error(errorMessage = e.message?.let { message -> 21 | UiText.DynamicString(message) 22 | } ?: UiText.StringResource(R.string.unknown_error)) 23 | } 24 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven("https://jitpack.io") 14 | } 15 | } 16 | 17 | rootProject.name = "MovieApp" 18 | include(":app") 19 | include(":core") 20 | include(":core:ui") 21 | include(":core:designsystem") 22 | include(":core:data") 23 | include(":core:domain") 24 | include(":core:model") 25 | include(":core:network") 26 | include(":core:database") 27 | include(":core:datastore") 28 | include(":core:common") 29 | include(":feature") 30 | include(":feature:login") 31 | include(":feature:signup") 32 | include(":feature:movie_details") 33 | include(":feature:see_all") 34 | include(":feature:movies") 35 | include(":feature:profile") 36 | include(":feature:search") 37 | include(":feature:watch_list") 38 | include(":core:navigation") 39 | include(":feature:actor_details") 40 | include(":core:authentication") 41 | -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/ahmetocak/designsystem/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.movieapp.presentation.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /core/model/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed 2 | plugins { 3 | alias(libs.plugins.androidLibrary) 4 | alias(libs.plugins.kotlin.android) 5 | kotlin("kapt") 6 | } 7 | 8 | android { 9 | namespace = "com.ahmetocak.model" 10 | compileSdk = ConfigData.compileSdk 11 | 12 | defaultConfig { 13 | minSdk = ConfigData.minSdk 14 | 15 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles("consumer-rules.pro") 17 | } 18 | 19 | buildTypes { 20 | release { 21 | isMinifyEnabled = BuildTypes.isMinifyEnabled 22 | proguardFiles( 23 | getDefaultProguardFile("proguard-android-optimize.txt"), 24 | "proguard-rules.pro" 25 | ) 26 | } 27 | } 28 | compileOptions { 29 | sourceCompatibility = JavaVersion.VERSION_17 30 | targetCompatibility = JavaVersion.VERSION_17 31 | } 32 | kotlinOptions { 33 | jvmTarget = "17" 34 | } 35 | } 36 | 37 | dependencies { 38 | 39 | implementation(libs.androidx.core.ktx) 40 | implementation(libs.androidx.activity.compose) 41 | } -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/helpers/ApiCall.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.helpers 2 | 3 | import com.ahmetocak.common.helpers.Response 4 | import com.ahmetocak.common.helpers.UiText 5 | import com.ahmetocak.network.R 6 | import java.io.IOException 7 | 8 | /** 9 | * A generic function for making API calls with error handling. 10 | * 11 | * This function is designed to be used with suspending API calls. It wraps the API call 12 | * with try-catch blocks to handle exceptions and provides a uniform way of dealing with responses. 13 | * 14 | * @param call A suspending lambda function representing the API call. 15 | * @return A sealed class [Response] containing either the success data or an error message. 16 | */ 17 | suspend inline fun apiCall(crossinline call: suspend () -> T): Response { 18 | return try { 19 | Response.Success(data = call()) 20 | } catch (e: IOException) { 21 | Response.Error(errorMessage = UiText.StringResource(R.string.internet_error)) 22 | } catch (e: Exception) { 23 | Response.Error(errorMessage = e.message?.let { message -> 24 | UiText.DynamicString(message) 25 | } ?: UiText.StringResource(R.string.unknown_error)) 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ahmetocak/movieapp/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.movieapp.di 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | import com.google.firebase.auth.FirebaseAuth 6 | import com.google.firebase.auth.ktx.auth 7 | import com.google.firebase.ktx.Firebase 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.android.qualifiers.ApplicationContext 12 | import dagger.hilt.components.SingletonComponent 13 | import kotlinx.coroutines.CoroutineDispatcher 14 | import kotlinx.coroutines.Dispatchers 15 | import javax.inject.Singleton 16 | 17 | @Module 18 | @InstallIn(SingletonComponent::class) 19 | object AppModule { 20 | 21 | @Singleton 22 | @Provides 23 | fun provideNetworkConnectivityManager(@ApplicationContext context: Context): ConnectivityManager { 24 | return context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 25 | } 26 | 27 | @Singleton 28 | @Provides 29 | fun provideFirebaseAuth(): FirebaseAuth { 30 | return Firebase.auth 31 | } 32 | 33 | @Singleton 34 | @Provides 35 | fun provideIoDispatcher(): CoroutineDispatcher { 36 | return Dispatchers.IO 37 | } 38 | } -------------------------------------------------------------------------------- /feature/login/src/main/res/values-tr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Merhaba, Film Dünyasına Hoş Geldiniz! 4 | Şifre 5 | Giriş 6 | Şifremi Unuttum 7 | Şifre Sıfırlama 8 | Şifrenizi sıfırlamak için uygulamada oturum açarken kullandığınız e-posta adresini girin. 9 | E-mail Adresi 10 | Gönder 11 | İptal 12 | Hesabın yok mu ? 13 | Hesap Oluştur 14 | Or Continue With 15 | Parola sıfırlama maili gönderildi. Lütfen email kutunu kontrol et. 16 | İzleme listeniz yüklenemedi. Lütfen daha sonra tekrar deneyiniz. 17 | Şununla oturum açın 18 | -------------------------------------------------------------------------------- /core/datastore/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidLibrary) 3 | alias(libs.plugins.kotlin.android) 4 | kotlin("kapt") 5 | } 6 | 7 | android { 8 | namespace = "com.ahmetocak.datastore" 9 | compileSdk = ConfigData.compileSdk 10 | 11 | defaultConfig { 12 | minSdk = ConfigData.minSdk 13 | 14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles("consumer-rules.pro") 16 | } 17 | 18 | buildTypes { 19 | release { 20 | isMinifyEnabled = BuildTypes.isMinifyEnabled 21 | proguardFiles( 22 | getDefaultProguardFile("proguard-android-optimize.txt"), 23 | "proguard-rules.pro" 24 | ) 25 | } 26 | } 27 | compileOptions { 28 | sourceCompatibility = JavaVersion.VERSION_17 29 | targetCompatibility = JavaVersion.VERSION_17 30 | } 31 | kotlinOptions { 32 | jvmTarget = "17" 33 | } 34 | } 35 | 36 | dependencies { 37 | 38 | implementation(libs.androidx.core.ktx) 39 | 40 | // Datastore 41 | implementation(libs.androidx.datastore.preferences) 42 | 43 | // Hilt 44 | implementation(libs.hilt.android) 45 | kapt(libs.hilt.android.compiler) 46 | } -------------------------------------------------------------------------------- /core/authentication/src/main/java/com/ahmetocak/authentication/firebase/FirebaseAuthClient.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.authentication.firebase 2 | 3 | import com.google.android.gms.tasks.Task 4 | import com.google.firebase.auth.AuthResult 5 | import com.google.firebase.auth.EmailAuthProvider 6 | import com.google.firebase.auth.FirebaseAuth 7 | import javax.inject.Inject 8 | 9 | class FirebaseAuthClient @Inject constructor() { 10 | 11 | private val auth = FirebaseAuth.getInstance() 12 | 13 | fun signUp(email: String, password: String): Task = 14 | auth.createUserWithEmailAndPassword(email, password) 15 | 16 | fun signOut() = auth.signOut() 17 | 18 | fun login(email: String, password: String): Task = 19 | auth.signInWithEmailAndPassword(email, password) 20 | 21 | fun sendResetPasswordEmail(email: String): Task = 22 | auth.sendPasswordResetEmail(email) 23 | 24 | fun reAuthenticate(email: String, password: String): Task? { 25 | return auth.currentUser?.reauthenticate( 26 | EmailAuthProvider.getCredential(email, password) 27 | ) 28 | } 29 | 30 | fun deleteAccount(): Task? = auth.currentUser?.delete() 31 | 32 | fun getUserEmail(): String? = auth.currentUser?.email 33 | } -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/datasource/movie/MovieRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.datasource.movie 2 | 3 | import com.ahmetocak.common.helpers.Response 4 | import com.ahmetocak.network.model.movie.NetworkActorDetails 5 | import com.ahmetocak.network.model.movie.NetworkActorMovies 6 | import com.ahmetocak.network.model.movie.NetworkMovie 7 | import com.ahmetocak.network.model.movie_detail.NetworkMovieCredit 8 | import com.ahmetocak.network.model.movie_detail.NetworkMovieDetail 9 | import com.ahmetocak.network.model.movie_detail.NetworkMovieTrailer 10 | 11 | interface MovieRemoteDataSource { 12 | 13 | suspend fun getTrendingMoviesFirstPage(): Response 14 | 15 | suspend fun getTopRatedMoviesFirstPage(): Response 16 | 17 | suspend fun getUpcomingMoviesFirstPage(): Response 18 | 19 | suspend fun getMovieDetails(movieId: Int): Response 20 | 21 | suspend fun getMovieCredits(movieId: Int): Response 22 | 23 | suspend fun getMovieTrailers(movieId: Int): Response 24 | 25 | suspend fun getActorDetails(actorId: Int): Response 26 | 27 | suspend fun getActorMovies(actorId: Int): Response 28 | } -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/ahmetocak/designsystem/components/CircularProgressIndicator.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.designsystem.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.material3.CircularProgressIndicator 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | import com.ahmetocak.designsystem.dimens.ComponentDimens 14 | 15 | @Composable 16 | fun ButtonCircularProgressIndicator() { 17 | CircularProgressIndicator( 18 | modifier = Modifier.size(ComponentDimens.buttonCircularProgressIndicatorSize), 19 | strokeWidth = 2.dp 20 | ) 21 | } 22 | 23 | @Composable 24 | fun FullScreenCircularProgressIndicator(paddingValues: PaddingValues = PaddingValues(0.dp)) { 25 | Box( 26 | modifier = Modifier 27 | .fillMaxSize() 28 | .padding(paddingValues), 29 | contentAlignment = Alignment.Center 30 | ) { 31 | CircularProgressIndicator() 32 | } 33 | } -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/ahmetocak/designsystem/dimens/AdaptiveModifiers.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.designsystem.dimens 2 | 3 | import androidx.compose.foundation.layout.width 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.composed 6 | import androidx.compose.ui.platform.LocalConfiguration 7 | import androidx.compose.ui.unit.dp 8 | import com.ahmetocak.designsystem.WindowSizeClasses 9 | import com.ahmetocak.designsystem.computeWindowWidthSize 10 | 11 | fun Modifier.setAdaptiveWidth( 12 | compactWidthRatio: Int = 1, 13 | mediumWidthRatio: Int = 2, 14 | expandedWidthRatio: Int = 2 15 | ): Modifier = this.composed { 16 | computeWindowWidthSize()?.let { windowWidthSizeClasses -> 17 | when (windowWidthSizeClasses) { 18 | WindowSizeClasses.COMPACT -> { 19 | then(Modifier.width(LocalConfiguration.current.screenWidthDp.dp / compactWidthRatio)) 20 | } 21 | 22 | WindowSizeClasses.MEDIUM -> { 23 | then(Modifier.width(LocalConfiguration.current.screenWidthDp.dp / mediumWidthRatio)) 24 | } 25 | 26 | WindowSizeClasses.EXPANDED -> { 27 | then(Modifier.width(LocalConfiguration.current.screenWidthDp.dp / expandedWidthRatio)) 28 | } 29 | } 30 | } ?: this 31 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/ahmetocak/designsystem/components/Scaffold.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.designsystem.components 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.material3.FabPosition 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.Scaffold 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | 11 | @Composable 12 | fun MovieScaffold( 13 | modifier: Modifier = Modifier, 14 | topBar: @Composable (() -> Unit) = {}, 15 | bottomBar: @Composable (() -> Unit) = {}, 16 | floatingActionButton: @Composable (() -> Unit) = {}, 17 | floatingActionButtonPosition: FabPosition = FabPosition.End, 18 | backgroundColor: Color = MaterialTheme.colorScheme.background, 19 | contentColor: Color = MaterialTheme.colorScheme.onBackground, 20 | content: @Composable (PaddingValues) -> Unit 21 | ) { 22 | Scaffold( 23 | modifier = modifier, 24 | topBar = topBar, 25 | bottomBar = bottomBar, 26 | floatingActionButton = floatingActionButton, 27 | floatingActionButtonPosition = floatingActionButtonPosition, 28 | containerColor = backgroundColor, 29 | contentColor = contentColor, 30 | content = content 31 | ) 32 | } -------------------------------------------------------------------------------- /feature/login/src/main/res/drawable/ic_google.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /core/authentication/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidLibrary) 3 | alias(libs.plugins.kotlin.android) 4 | kotlin("kapt") 5 | } 6 | 7 | android { 8 | namespace = "com.ahmetocak.authentication" 9 | compileSdk = ConfigData.compileSdk 10 | 11 | defaultConfig { 12 | minSdk = ConfigData.minSdk 13 | 14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles("consumer-rules.pro") 16 | } 17 | 18 | buildTypes { 19 | release { 20 | isMinifyEnabled = BuildTypes.isMinifyEnabled 21 | proguardFiles( 22 | getDefaultProguardFile("proguard-android-optimize.txt"), 23 | "proguard-rules.pro" 24 | ) 25 | } 26 | } 27 | compileOptions { 28 | sourceCompatibility = JavaVersion.VERSION_17 29 | targetCompatibility = JavaVersion.VERSION_17 30 | } 31 | kotlinOptions { 32 | jvmTarget = "17" 33 | } 34 | } 35 | 36 | dependencies { 37 | 38 | implementation(libs.androidx.core.ktx) 39 | 40 | // Hilt 41 | implementation(libs.hilt.android) 42 | kapt(libs.hilt.android.compiler) 43 | 44 | // Firebase 45 | implementation(platform(libs.firebase.bom)) 46 | implementation(libs.firebase.auth) 47 | implementation(libs.google.android.gms) 48 | } -------------------------------------------------------------------------------- /feature/login/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Email 4 | Password 5 | E-mail Address 6 | Hello, Welcome to the Movie World! 7 | Login 8 | Forgot Password ? 9 | Don\'t have an account ? 10 | Sign Up 11 | Password Reset 12 | To reset your password, enter the email address you use to sign in to app. 13 | Send 14 | Cancel 15 | Password reset mail has been sent. Please check your email box. 16 | Please enter a valid email address 17 | Your watchlist could not be loaded. please try again later. 18 | Or Continue With 19 | Sign in With 20 | -------------------------------------------------------------------------------- /core/common/src/main/java/com/ahmetocak/common/helpers/UiText.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.common.helpers 2 | 3 | import android.content.Context 4 | import androidx.annotation.StringRes 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.res.stringResource 7 | 8 | /** 9 | * Sealed class representing different types of UI text resources. 10 | */ 11 | sealed class UiText { 12 | /** 13 | * Represents a dynamic string value. 14 | * 15 | * @param value The dynamic string value. 16 | */ 17 | data class DynamicString(val value: String) : UiText() 18 | 19 | /** 20 | * Represents a string resource with optional format arguments. 21 | * 22 | * @param resId The resource ID of the string. 23 | * @param args Optional format arguments for the string resource. 24 | */ 25 | class StringResource( 26 | @StringRes val resId: Int, 27 | vararg val args: Any 28 | ): UiText() 29 | 30 | @Composable 31 | fun asString(): String { 32 | return when(this) { 33 | is DynamicString -> value 34 | is StringResource -> stringResource(resId, *args) 35 | } 36 | } 37 | 38 | fun asString(context: Context): String { 39 | return when(this) { 40 | is DynamicString -> value 41 | is StringResource -> context.getString(resId, args) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /core/domain/src/main/java/com/ahmetocak/domain/firebase/CheckMovieInWatchListUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.domain.firebase 2 | 3 | import com.ahmetocak.common.helpers.UiText 4 | import com.ahmetocak.common.helpers.handleTaskError 5 | import com.ahmetocak.domain.firebase.firestore.GetMovieDataUseCase 6 | import com.ahmetocak.model.firebase.WatchList 7 | import javax.inject.Inject 8 | 9 | class CheckMovieInWatchListUseCase @Inject constructor( 10 | private val getMovieDataUseCase: GetMovieDataUseCase 11 | ) { 12 | 13 | operator fun invoke( 14 | movieId: Int, 15 | onError: (UiText) -> Unit, 16 | onSuccess: (Boolean) -> Unit 17 | ) { 18 | getMovieDataUseCase().addOnCompleteListener { task -> 19 | if (task.isSuccessful) { 20 | if (task.result != null && task.result.exists()) { 21 | val watchList = task.result.toObject(WatchList::class.java)?.watchList ?: emptyList() 22 | 23 | val movieInWatchList = watchList.firstOrNull { movie -> 24 | movie.id == movieId 25 | } 26 | 27 | onSuccess(movieInWatchList != null) 28 | } else { 29 | onSuccess(false) 30 | } 31 | } else { 32 | onError(handleTaskError(task.exception)) 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/youtube_trailer_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 17 | 27 | -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/datasource/movie/paging_source/MoviesPagingSource.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.datasource.movie.paging_source 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import com.ahmetocak.network.model.movie.NetworkMovie 6 | import com.ahmetocak.network.model.movie.NetworkMovieContent 7 | 8 | class MoviesPagingSource( 9 | private val apiCall: suspend (Int) -> NetworkMovie 10 | ) : PagingSource() { 11 | 12 | override suspend fun load(params: LoadParams): LoadResult { 13 | return try { 14 | val currentPageNumber = params.key ?: 1 15 | val response = apiCall(currentPageNumber) 16 | LoadResult.Page( 17 | data = response.movies, 18 | prevKey = null, 19 | nextKey = if (currentPageNumber < response.totalPages) currentPageNumber + 1 else null 20 | ) 21 | } catch (e: Exception) { 22 | LoadResult.Error(e) 23 | } 24 | } 25 | 26 | override fun getRefreshKey(state: PagingState): Int? { 27 | return state.anchorPosition?.let { anchorPosition -> 28 | val anchorPage = state.closestPageToPosition(anchorPosition) 29 | anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /core/datastore/src/main/java/com/ahmetocak/datastore/di/DatastoreModule.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.datastore.di 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.PreferenceDataStoreFactory 6 | import androidx.datastore.preferences.core.Preferences 7 | import androidx.datastore.preferences.preferencesDataStoreFile 8 | import com.ahmetocak.datastore.datasource.MovieAppPreferenceDataSource 9 | import com.ahmetocak.datastore.datasource.MovieAppPreferenceDataSourceImpl 10 | import dagger.Module 11 | import dagger.Provides 12 | import dagger.hilt.InstallIn 13 | import dagger.hilt.android.qualifiers.ApplicationContext 14 | import dagger.hilt.components.SingletonComponent 15 | import javax.inject.Singleton 16 | 17 | @Module 18 | @InstallIn(SingletonComponent::class) 19 | object DatastoreModule { 20 | 21 | @Singleton 22 | @Provides 23 | fun provideDataStoreDataSource(datastore: DataStore): MovieAppPreferenceDataSource { 24 | return MovieAppPreferenceDataSourceImpl(datastore) 25 | } 26 | 27 | @Singleton 28 | @Provides 29 | fun provideDatastore(@ApplicationContext context: Context): DataStore { 30 | return PreferenceDataStoreFactory.create( 31 | produceFile = { 32 | context.preferencesDataStoreFile("app_preferences_datastore") 33 | } 34 | ) 35 | } 36 | } -------------------------------------------------------------------------------- /feature/movie_details/src/main/res/layout/youtube_trailer_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 17 | 27 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /core/database/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidLibrary) 3 | alias(libs.plugins.kotlin.android) 4 | kotlin("kapt") 5 | } 6 | 7 | android { 8 | namespace = "com.ahmetocak.database" 9 | compileSdk = ConfigData.compileSdk 10 | 11 | defaultConfig { 12 | minSdk = ConfigData.minSdk 13 | 14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles("consumer-rules.pro") 16 | } 17 | 18 | buildTypes { 19 | release { 20 | isMinifyEnabled = BuildTypes.isMinifyEnabled 21 | proguardFiles( 22 | getDefaultProguardFile("proguard-android-optimize.txt"), 23 | "proguard-rules.pro" 24 | ) 25 | } 26 | } 27 | compileOptions { 28 | sourceCompatibility = JavaVersion.VERSION_17 29 | targetCompatibility = JavaVersion.VERSION_17 30 | } 31 | kotlinOptions { 32 | jvmTarget = "17" 33 | } 34 | } 35 | 36 | dependencies { 37 | 38 | implementation(libs.androidx.core.ktx) 39 | 40 | testImplementation(libs.junit) 41 | 42 | // Hilt 43 | implementation(libs.hilt.android) 44 | kapt(libs.hilt.android.compiler) 45 | 46 | // Room 47 | implementation(libs.androidx.room.runtime) 48 | annotationProcessor(libs.androidx.room.compiler) 49 | kapt(libs.room.compiler) 50 | implementation(libs.androidx.room.ktx) 51 | 52 | implementation(project(":core:common")) 53 | } -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/datasource/movie/paging_source/SearchMoviesPagingSource.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.datasource.movie.paging_source 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import com.ahmetocak.network.api.MovieApi 6 | import com.ahmetocak.network.model.movie.NetworkMovieContent 7 | 8 | class SearchMoviesPagingSource( 9 | private val query: String, 10 | private val api: MovieApi 11 | ): PagingSource() { 12 | 13 | override suspend fun load(params: LoadParams): LoadResult { 14 | return try { 15 | val currentPageNumber = params.key ?: 1 16 | val response = api.searchMovie(query = query, page = currentPageNumber) 17 | LoadResult.Page( 18 | data = response.movies.filter { it.posterImagePath != null }, 19 | prevKey = null, 20 | nextKey = if (currentPageNumber < response.totalPages) currentPageNumber + 1 else null 21 | ) 22 | } catch (e: Exception) { 23 | LoadResult.Error(e) 24 | } 25 | } 26 | 27 | override fun getRefreshKey(state: PagingState): Int? { 28 | return state.anchorPosition?.let { anchorPosition -> 29 | val anchorPage = state.closestPageToPosition(anchorPosition) 30 | anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/ahmetocak/designsystem/components/navigation/NavigationBar.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.designsystem.components.navigation 2 | 3 | import androidx.compose.material3.Icon 4 | import androidx.compose.material3.NavigationBar 5 | import androidx.compose.material3.NavigationBarItem 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.res.stringResource 9 | import com.ahmetocak.navigation.HomeSections 10 | 11 | @Composable 12 | fun MovieNavigationBar( 13 | tabs: Array, 14 | currentRoute: String, 15 | navigateToRoute: (String) -> Unit 16 | ) { 17 | val currentSection = tabs.first { it.route == currentRoute } 18 | 19 | NavigationBar { 20 | tabs.forEach { section -> 21 | NavigationBarItem( 22 | selected = currentSection.route == section.route, 23 | onClick = { 24 | navigateToRoute(section.route) 25 | }, 26 | icon = { 27 | Icon( 28 | imageVector = if (currentSection.route == section.route) 29 | section.selectedIcon else section.unSelectedIcon, 30 | contentDescription = null 31 | ) 32 | }, 33 | label = { 34 | Text(text = stringResource(id = section.title)) 35 | } 36 | ) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/datasource/movie/paging_source/UserMovieReviewsPagingSource.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.datasource.movie.paging_source 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import com.ahmetocak.network.api.MovieApi 6 | import com.ahmetocak.network.model.movie.NetworkUserReviewResults 7 | 8 | class UserMovieReviewsPagingSource( 9 | private val movieId: Int, 10 | private val api: MovieApi 11 | ): PagingSource() { 12 | 13 | override suspend fun load(params: LoadParams): LoadResult { 14 | return try { 15 | val currentPageNumber = params.key ?: 1 16 | val response = api.getUserMovieReviews(movieId = movieId, page = currentPageNumber) 17 | LoadResult.Page( 18 | data = response.results, 19 | prevKey = null, 20 | nextKey = if (currentPageNumber < response.totalPages) currentPageNumber + 1 else null 21 | ) 22 | } catch (e: Exception) { 23 | LoadResult.Error(e) 24 | } 25 | } 26 | 27 | override fun getRefreshKey(state: PagingState): Int? { 28 | return state.anchorPosition?.let { anchorPosition -> 29 | val anchorPage = state.closestPageToPosition(anchorPosition) 30 | anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/datasource/movie/paging_source/RecommendedMoviesPagingSource.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.datasource.movie.paging_source 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import com.ahmetocak.network.api.MovieApi 6 | import com.ahmetocak.network.model.movie.NetworkRecommendedMovieContent 7 | 8 | class RecommendedMoviesPagingSource( 9 | private val movieId: Int, 10 | private val api: MovieApi 11 | ): PagingSource() { 12 | 13 | override suspend fun load(params: LoadParams): LoadResult { 14 | return try { 15 | val currentPageNumber = params.key ?: 1 16 | val response = api.getMovieRecommendations(movieId = movieId, page = currentPageNumber) 17 | LoadResult.Page( 18 | data = response.recommendations.filter { it.image != null }, 19 | prevKey = null, 20 | nextKey = if (currentPageNumber < response.totalPages) currentPageNumber + 1 else null 21 | ) 22 | } catch (e: Exception) { 23 | LoadResult.Error(e) 24 | } 25 | } 26 | 27 | override fun getRefreshKey(state: PagingState): Int? { 28 | return state.anchorPosition?.let { anchorPosition -> 29 | val anchorPage = state.closestPageToPosition(anchorPosition) 30 | anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /core/domain/src/main/java/com/ahmetocak/domain/firebase/auth/SignInWithGoogleUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.domain.firebase.auth 2 | 3 | import android.content.Intent 4 | import androidx.activity.result.IntentSenderRequest 5 | import com.ahmetocak.authentication.firebase.GoogleAuthClient 6 | import com.ahmetocak.common.helpers.UiText 7 | import com.ahmetocak.common.helpers.handleTaskError 8 | import javax.inject.Inject 9 | 10 | class SignInWithGoogleUseCase @Inject constructor(private val googleAuthClient: GoogleAuthClient) { 11 | 12 | fun startSignInIntent( 13 | onTaskFailed: (UiText) -> Unit, 14 | onTaskSuccess: (IntentSenderRequest) -> Unit 15 | ) { 16 | googleAuthClient.startSignInIntent().addOnCompleteListener { task -> 17 | if (task.isSuccessful) { 18 | onTaskSuccess( 19 | IntentSenderRequest.Builder( 20 | task.result.pendingIntent.intentSender 21 | ).build() 22 | ) 23 | } else { 24 | onTaskFailed(handleTaskError(task.exception)) 25 | } 26 | } 27 | } 28 | 29 | fun signInWithGoogle( 30 | intent: Intent, 31 | onTaskSuccess: () -> Unit, 32 | onTaskFailed: (UiText) -> Unit 33 | ) { 34 | googleAuthClient.signInWithIntent(intent).addOnCompleteListener { task -> 35 | if (task.isSuccessful) { 36 | onTaskSuccess() 37 | } else { 38 | onTaskFailed(handleTaskError(task.exception)) 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/datasource/firebase/storage/FirebaseStorageDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.datasource.firebase.storage 2 | 3 | import android.net.Uri 4 | import com.ahmetocak.network.helpers.FirebaseConstants 5 | import com.google.android.gms.tasks.Task 6 | import com.google.firebase.auth.FirebaseAuth 7 | import com.google.firebase.storage.FirebaseStorage 8 | import com.google.firebase.storage.UploadTask 9 | import javax.inject.Inject 10 | 11 | class FirebaseStorageDataSourceImpl @Inject constructor( 12 | firebaseStorage: FirebaseStorage, 13 | private val firebaseAuth: FirebaseAuth 14 | ) : FirebaseStorageDataSource { 15 | 16 | private val storageRef = firebaseStorage.reference 17 | 18 | override fun uploadProfileImage(imageUri: Uri): UploadTask { 19 | val profileImageRef = 20 | storageRef.child("${FirebaseConstants.Firestorage.USER_PROFILE_IMG_DIRECTORY_NAME}/${firebaseAuth.currentUser?.uid}") 21 | return profileImageRef.putFile(imageUri) 22 | } 23 | 24 | override fun getUserProfileImage(): Task { 25 | val profileImageRef = 26 | storageRef.child("${FirebaseConstants.Firestorage.USER_PROFILE_IMG_DIRECTORY_NAME}/${firebaseAuth.currentUser?.uid}") 27 | return profileImageRef.downloadUrl 28 | } 29 | 30 | override fun deleteUserProfileImage(): Task { 31 | val profileImageRef = 32 | storageRef.child("${FirebaseConstants.Firestorage.USER_PROFILE_IMG_DIRECTORY_NAME}/${firebaseAuth.currentUser?.uid}") 33 | return profileImageRef.delete() 34 | } 35 | } -------------------------------------------------------------------------------- /core/data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidLibrary) 3 | alias(libs.plugins.kotlin.android) 4 | kotlin("kapt") 5 | } 6 | 7 | android { 8 | namespace = "com.ahmetocak.data" 9 | compileSdk = ConfigData.compileSdk 10 | 11 | defaultConfig { 12 | minSdk = ConfigData.minSdk 13 | 14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles("consumer-rules.pro") 16 | } 17 | 18 | buildTypes { 19 | release { 20 | isMinifyEnabled = BuildTypes.isMinifyEnabled 21 | proguardFiles( 22 | getDefaultProguardFile("proguard-android-optimize.txt"), 23 | "proguard-rules.pro" 24 | ) 25 | } 26 | } 27 | compileOptions { 28 | sourceCompatibility = JavaVersion.VERSION_17 29 | targetCompatibility = JavaVersion.VERSION_17 30 | } 31 | kotlinOptions { 32 | jvmTarget = "17" 33 | } 34 | } 35 | 36 | dependencies { 37 | 38 | implementation(libs.androidx.core.ktx) 39 | 40 | // Hilt 41 | implementation(libs.hilt.android) 42 | kapt(libs.hilt.android.compiler) 43 | 44 | // Paging 3 45 | implementation(libs.androidx.paging.runtime.ktx) 46 | 47 | // Firebase 48 | implementation(platform(libs.firebase.bom)) 49 | implementation(libs.firebase.firestore) 50 | implementation(libs.firebase.storage) 51 | 52 | implementation(project(":core:network")) 53 | implementation(project(":core:database")) 54 | implementation(project(":core:datastore")) 55 | implementation(project(":core:model")) 56 | implementation(project(":core:common")) 57 | } -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/ahmetocak/designsystem/components/ErrorView.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.designsystem.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.Error 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.text.style.TextAlign 16 | import androidx.compose.ui.unit.Dp 17 | import com.ahmetocak.designsystem.dimens.ComponentDimens 18 | import com.ahmetocak.designsystem.dimens.Dimens 19 | 20 | @Composable 21 | fun ErrorView( 22 | modifier: Modifier = Modifier, 23 | iconSize: Dp = ComponentDimens.errorViewIconSize, 24 | errorMessage: String 25 | ) { 26 | Column( 27 | modifier = modifier, 28 | verticalArrangement = Arrangement.Center, 29 | horizontalAlignment = Alignment.CenterHorizontally 30 | ) { 31 | Icon( 32 | modifier = Modifier.size(iconSize), 33 | imageVector = Icons.Filled.Error, 34 | contentDescription = null, 35 | tint = MaterialTheme.colorScheme.error 36 | ) 37 | Text( 38 | modifier = Modifier.padding(top = Dimens.twoLevelPadding), 39 | text = errorMessage, 40 | textAlign = TextAlign.Center 41 | ) 42 | } 43 | } -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/helpers/NetworkConstants.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.helpers 2 | 3 | object NetworkConstants { 4 | const val BASE_URL = "https://api.themoviedb.org/" 5 | object EndPoints { 6 | const val TRENDING = "/3/trending/movie/{${Paths.TIME}}" 7 | const val TOP_RATED = "/3/movie/top_rated" 8 | const val UPCOMING = "/3/movie/upcoming" 9 | const val MOVIE_DETAILS = "/3/movie/{${Paths.MOVIE_ID}}" 10 | const val MOVIE_CREDITS = "/3/movie/{${Paths.MOVIE_ID}}/credits" 11 | const val MOVIE_TRAILERS = "/3/movie/{${Paths.MOVIE_ID}}/videos" 12 | const val SEARCH_MOVIE = "/3/search/movie" 13 | const val ACTOR_DETAILS = "/3/person/{${Paths.ACTOR_ID}}" 14 | const val ACTOR_MOVIES = "/3/person/{${Paths.ACTOR_ID}}/movie_credits" 15 | const val USER_REVIEWS = "/3/movie/{${Paths.MOVIE_ID}}/reviews" 16 | const val RECOMMENDATIONS = "/3/movie/{${Paths.MOVIE_ID}}/recommendations" 17 | } 18 | 19 | object Queries { 20 | const val PAGE = "page" 21 | const val API_KEY = "api_key" 22 | const val SEARCH_QUERY = "query" 23 | const val LANGUAGE = "language" 24 | } 25 | 26 | object Paths { 27 | const val MOVIE_ID = "movie_id" 28 | const val ACTOR_ID = "actor_id" 29 | const val TIME = "time_window" 30 | } 31 | } 32 | 33 | object FirebaseConstants { 34 | object Firestore { 35 | const val WATCH_LIST_COLLECTION_NAME = "movie_watch_lists" 36 | const val WATCH_LIST_ARRAY_NAME = "watchList" 37 | } 38 | 39 | object Firestorage { 40 | const val USER_PROFILE_IMG_DIRECTORY_NAME = "user_profile_images" 41 | } 42 | } -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/ahmetocak/designsystem/components/navigation/NavigationRail.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.designsystem.components.navigation 2 | 3 | import androidx.compose.foundation.layout.Spacer 4 | import androidx.compose.foundation.layout.offset 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.NavigationRail 7 | import androidx.compose.material3.NavigationRailItem 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.res.stringResource 12 | import androidx.compose.ui.unit.dp 13 | import com.ahmetocak.navigation.HomeSections 14 | 15 | @Composable 16 | fun MovieNavigationRail( 17 | tabs: Array, 18 | navigateToRoute: (String) -> Unit, 19 | currentRoute: String 20 | ) { 21 | NavigationRail(modifier = Modifier.offset(x = (-1).dp)) { 22 | Spacer(Modifier.weight(1f)) 23 | tabs.forEachIndexed { _, section -> 24 | NavigationRailItem( 25 | selected = currentRoute == section.route, 26 | onClick = { 27 | navigateToRoute(section.route) 28 | }, 29 | icon = { 30 | Icon( 31 | imageVector = if (currentRoute == section.route) 32 | section.selectedIcon else section.unSelectedIcon, 33 | contentDescription = null 34 | ) 35 | }, 36 | label = { 37 | Text(text = stringResource(id = section.title)) 38 | } 39 | ) 40 | } 41 | Spacer(Modifier.weight(1f)) 42 | } 43 | } -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/ahmetocak/designsystem/components/TextButton.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.designsystem.components 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.material3.ButtonColors 7 | import androidx.compose.material3.ButtonDefaults 8 | import androidx.compose.material3.ButtonElevation 9 | import androidx.compose.material3.Text 10 | import androidx.compose.material3.TextButton 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Shape 15 | import androidx.compose.ui.text.font.FontWeight 16 | 17 | @Composable 18 | fun MovieTextButton( 19 | modifier: Modifier = Modifier, 20 | text: String, 21 | onClick: () -> Unit, 22 | fontWeight: FontWeight = FontWeight.W700, 23 | enabled: Boolean = true, 24 | shape: Shape = ButtonDefaults.textShape, 25 | colors: ButtonColors = ButtonDefaults.textButtonColors(), 26 | elevation: ButtonElevation? = null, 27 | border: BorderStroke? = null, 28 | contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding, 29 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 30 | ) { 31 | TextButton( 32 | modifier = modifier, 33 | onClick = onClick, 34 | contentPadding = contentPadding, 35 | enabled = enabled, 36 | shape = shape, 37 | colors = colors, 38 | elevation = elevation, 39 | border = border, 40 | interactionSource = interactionSource 41 | ) { 42 | Text(text = text, fontWeight = fontWeight) 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/di/DatasourceModule.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.di 2 | 3 | import com.ahmetocak.network.api.MovieApi 4 | import com.ahmetocak.network.datasource.firebase.firestore.FirebaseFirestoreDataSource 5 | import com.ahmetocak.network.datasource.firebase.firestore.FirebaseFirestoreDataSourceImpl 6 | import com.ahmetocak.network.datasource.firebase.storage.FirebaseStorageDataSource 7 | import com.ahmetocak.network.datasource.firebase.storage.FirebaseStorageDataSourceImpl 8 | import com.ahmetocak.network.datasource.movie.MovieRemoteDataSource 9 | import com.ahmetocak.network.datasource.movie.MovieRemoteDataSourceImpl 10 | import com.google.firebase.auth.FirebaseAuth 11 | import com.google.firebase.firestore.FirebaseFirestore 12 | import com.google.firebase.storage.FirebaseStorage 13 | import dagger.Module 14 | import dagger.Provides 15 | import dagger.hilt.InstallIn 16 | import dagger.hilt.components.SingletonComponent 17 | import javax.inject.Singleton 18 | 19 | @Module 20 | @InstallIn(SingletonComponent::class) 21 | object DataSourceModule { 22 | 23 | @Singleton 24 | @Provides 25 | fun provideMovieRemoteDataSource(api: MovieApi): MovieRemoteDataSource { 26 | return MovieRemoteDataSourceImpl(api) 27 | } 28 | 29 | @Singleton 30 | @Provides 31 | fun provideFirebaseFirestoreDataSource( 32 | firestore: FirebaseFirestore, 33 | auth: FirebaseAuth 34 | ): FirebaseFirestoreDataSource { 35 | return FirebaseFirestoreDataSourceImpl(firestore, auth) 36 | } 37 | 38 | @Singleton 39 | @Provides 40 | fun provideFirebaseStorageDataSource( 41 | storage: FirebaseStorage, 42 | auth: FirebaseAuth 43 | ): FirebaseStorageDataSource { 44 | return FirebaseStorageDataSourceImpl(storage, auth) 45 | } 46 | } -------------------------------------------------------------------------------- /core/data/src/main/java/com/ahmetocak/data/repository/firebase/FirebaseRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.data.repository.firebase 2 | 3 | import android.net.Uri 4 | import com.ahmetocak.data.mapper.toNetworkWatchListMovie 5 | import com.ahmetocak.model.firebase.WatchListMovie 6 | import com.ahmetocak.network.datasource.firebase.firestore.FirebaseFirestoreDataSource 7 | import com.ahmetocak.network.datasource.firebase.storage.FirebaseStorageDataSource 8 | import com.google.android.gms.tasks.Task 9 | import com.google.firebase.firestore.DocumentSnapshot 10 | import com.google.firebase.storage.UploadTask 11 | import javax.inject.Inject 12 | 13 | class FirebaseRepositoryImpl @Inject constructor( 14 | private val firebaseFirestoreDataSource: FirebaseFirestoreDataSource, 15 | private val firebaseStorageDataSource: FirebaseStorageDataSource 16 | ) : FirebaseRepository { 17 | 18 | override fun addMovieToFirestore(watchListMovie: WatchListMovie): Task = 19 | firebaseFirestoreDataSource.addMovieData(watchListMovie.toNetworkWatchListMovie()) 20 | 21 | override fun updateMovieData(watchListMovie: List): Task = 22 | firebaseFirestoreDataSource.updateMovieData(watchListMovie.toNetworkWatchListMovie()) 23 | 24 | override fun getMovieData(): Task = firebaseFirestoreDataSource.getMovieData() 25 | 26 | override fun deleteMovieDocument(): Task = 27 | firebaseFirestoreDataSource.deleteMovieDocument() 28 | 29 | override fun uploadProfileImage(imageUri: Uri): UploadTask = 30 | firebaseStorageDataSource.uploadProfileImage(imageUri) 31 | 32 | override fun getUserProfileImage(): Task = firebaseStorageDataSource.getUserProfileImage() 33 | 34 | override fun deleteUserProfileImage(): Task = 35 | firebaseStorageDataSource.deleteUserProfileImage() 36 | } -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/ahmetocak/designsystem/WindowSize.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.designsystem 2 | 3 | import android.app.Activity 4 | import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi 5 | import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass 6 | import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass 7 | import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.platform.LocalContext 10 | import com.ahmetocak.common.findActivity 11 | 12 | @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) 13 | @Composable 14 | fun computeWindowWidthSize(activity: Activity? = LocalContext.current.findActivity()): WindowSizeClasses? { 15 | return when (activity?.let { calculateWindowSizeClass(activity = it).widthSizeClass }) { 16 | WindowWidthSizeClass.Compact -> WindowSizeClasses.COMPACT 17 | 18 | WindowWidthSizeClass.Medium -> WindowSizeClasses.MEDIUM 19 | 20 | WindowWidthSizeClass.Expanded -> WindowSizeClasses.EXPANDED 21 | 22 | else -> null 23 | } 24 | } 25 | 26 | @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) 27 | @Composable 28 | fun computeWindowHeightSize(activity: Activity? = LocalContext.current.findActivity()): WindowSizeClasses? { 29 | return when (activity?.let { calculateWindowSizeClass(activity = it).heightSizeClass }) { 30 | WindowHeightSizeClass.Compact -> WindowSizeClasses.COMPACT 31 | 32 | WindowHeightSizeClass.Medium -> WindowSizeClasses.MEDIUM 33 | 34 | WindowHeightSizeClass.Expanded -> WindowSizeClasses.EXPANDED 35 | 36 | else -> null 37 | } 38 | } 39 | 40 | sealed interface WindowSizeClasses { 41 | data object COMPACT : WindowSizeClasses 42 | data object MEDIUM : WindowSizeClasses 43 | data object EXPANDED : WindowSizeClasses 44 | } -------------------------------------------------------------------------------- /core/navigation/src/main/java/com/ahmetocak/navigation/Routes.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.navigation 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.AccountCircle 6 | import androidx.compose.material.icons.filled.Bookmark 7 | import androidx.compose.material.icons.filled.Movie 8 | import androidx.compose.material.icons.filled.Search 9 | import androidx.compose.material.icons.outlined.AccountCircle 10 | import androidx.compose.material.icons.outlined.BookmarkBorder 11 | import androidx.compose.material.icons.outlined.Movie 12 | import androidx.compose.material.icons.outlined.Search 13 | import androidx.compose.ui.graphics.vector.ImageVector 14 | 15 | object MainDestinations { 16 | const val LOGIN_ROUTE = "login" 17 | const val SIGN_UP_ROUTE = "signUp" 18 | const val MOVIE_DETAILS_ROUTE = "movieDetails" 19 | const val MOVIE_DETAILS_ID_KEY = "movieId" 20 | const val SEE_ALL_ROUTE = "seeAll" 21 | const val SEE_ALL_TYPE_KEY = "seeAllType" 22 | const val HOME_ROUTE = "home" 23 | const val ACTOR_DETAILS_ROUTE = "actorDetails" 24 | const val ACTOR_DETAILS_ID_KEY = "actorId" 25 | } 26 | 27 | enum class HomeSections( 28 | @StringRes val title: Int, 29 | val selectedIcon: ImageVector, 30 | val unSelectedIcon: ImageVector, 31 | val route: String 32 | ) { 33 | MOVIES(R.string.movies_text, Icons.Filled.Movie, Icons.Outlined.Movie, "home/movies"), 34 | SEARCH(R.string.search_text, Icons.Filled.Search, Icons.Outlined.Search, "home/search"), 35 | WATCH_LIST( 36 | R.string.watch_list_text, 37 | Icons.Filled.Bookmark, 38 | Icons.Outlined.BookmarkBorder, 39 | "home/watch_list" 40 | ), 41 | PROFILE( 42 | R.string.profile_text, 43 | Icons.Filled.AccountCircle, 44 | Icons.Outlined.AccountCircle, 45 | "home/profile" 46 | ) 47 | } -------------------------------------------------------------------------------- /core/datastore/src/main/java/com/ahmetocak/datastore/datasource/MovieAppPreferenceDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.datastore.datasource 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import androidx.datastore.preferences.core.booleanPreferencesKey 6 | import androidx.datastore.preferences.core.edit 7 | import com.ahmetocak.datastore.DataStoreConstants 8 | import com.ahmetocak.datastore.datasource.MovieAppPreferenceDataSourceImpl.PreferencesKeys.APP_THEME 9 | import com.ahmetocak.datastore.datasource.MovieAppPreferenceDataSourceImpl.PreferencesKeys.DYNAMIC_COLOR 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.map 12 | import javax.inject.Inject 13 | 14 | class MovieAppPreferenceDataSourceImpl @Inject constructor( 15 | private val datastore: DataStore 16 | ): MovieAppPreferenceDataSource { 17 | 18 | private object PreferencesKeys { 19 | val APP_THEME = booleanPreferencesKey(DataStoreConstants.APP_THEME_KEY) 20 | val DYNAMIC_COLOR = booleanPreferencesKey(DataStoreConstants.DYNAMIC_COLOR_KEY) 21 | } 22 | 23 | override suspend fun observeAppTheme(): Flow { 24 | return datastore.data.map { preferences -> 25 | preferences[APP_THEME] ?: true 26 | } 27 | } 28 | 29 | override suspend fun updateAppTheme(darkMode: Boolean) { 30 | datastore.edit { preferences -> 31 | preferences[APP_THEME] = darkMode 32 | } 33 | } 34 | 35 | override suspend fun observeDynamicColor(): Flow { 36 | return datastore.data.map { preferences -> 37 | preferences[DYNAMIC_COLOR] ?: false 38 | } 39 | } 40 | 41 | override suspend fun updateDynamicColor(dynamicColor: Boolean) { 42 | datastore.edit { preferences -> 43 | preferences[DYNAMIC_COLOR] = dynamicColor 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /core/navigation/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidLibrary) 3 | alias(libs.plugins.kotlin.android) 4 | } 5 | 6 | android { 7 | namespace = "com.ahmetocak.navigation" 8 | compileSdk = ConfigData.compileSdk 9 | 10 | defaultConfig { 11 | minSdk = ConfigData.minSdk 12 | 13 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles("consumer-rules.pro") 15 | } 16 | 17 | buildTypes { 18 | release { 19 | isMinifyEnabled = BuildTypes.isMinifyEnabled 20 | proguardFiles( 21 | getDefaultProguardFile("proguard-android-optimize.txt"), 22 | "proguard-rules.pro" 23 | ) 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility = JavaVersion.VERSION_1_8 28 | targetCompatibility = JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = "1.8" 32 | } 33 | } 34 | 35 | dependencies { 36 | 37 | implementation(libs.androidx.core.ktx) 38 | implementation(libs.androidx.lifecycle.runtime.ktx) 39 | implementation(libs.androidx.activity.compose) 40 | implementation(platform(libs.androidx.compose.bom)) 41 | implementation(libs.androidx.compose.ui) 42 | implementation(libs.androidx.compose.ui.graphics) 43 | implementation(libs.androidx.compose.ui.preview) 44 | implementation(libs.androidx.compose.material3) 45 | 46 | testImplementation(libs.junit) 47 | androidTestImplementation(libs.androidx.junit) 48 | androidTestImplementation(libs.androidx.espresso.core) 49 | androidTestImplementation(platform(libs.androidx.compose.bom)) 50 | androidTestImplementation(libs.androidx.compose.ui.test.junit4) 51 | debugImplementation(libs.androidx.compose.ui.tooling) 52 | debugImplementation(libs.androidx.compose.ui.test.manifest) 53 | 54 | // Material Icons Extended 55 | implementation(libs.androidx.compose.icons.extended) 56 | } -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/datasource/movie/MovieRemoteDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.datasource.movie 2 | 3 | import com.ahmetocak.common.helpers.Response 4 | import com.ahmetocak.network.api.MovieApi 5 | import com.ahmetocak.network.model.movie.NetworkActorDetails 6 | import com.ahmetocak.network.model.movie.NetworkActorMovies 7 | import com.ahmetocak.network.helpers.apiCall 8 | import com.ahmetocak.network.model.movie.NetworkMovie 9 | import com.ahmetocak.network.model.movie_detail.NetworkMovieCredit 10 | import com.ahmetocak.network.model.movie_detail.NetworkMovieDetail 11 | import com.ahmetocak.network.model.movie_detail.NetworkMovieTrailer 12 | import javax.inject.Inject 13 | 14 | class MovieRemoteDataSourceImpl @Inject constructor( 15 | private val api: MovieApi 16 | ) : MovieRemoteDataSource { 17 | 18 | override suspend fun getTrendingMoviesFirstPage(): Response = 19 | apiCall { api.getTrendingMovies() } 20 | 21 | override suspend fun getTopRatedMoviesFirstPage(): Response = 22 | apiCall { api.getTopRatedMovies() } 23 | 24 | override suspend fun getUpcomingMoviesFirstPage(): Response = 25 | apiCall { api.getUpcomingMovies() } 26 | 27 | override suspend fun getMovieDetails(movieId: Int): Response = 28 | apiCall { api.getMovieDetails(movieId) } 29 | 30 | override suspend fun getMovieCredits(movieId: Int): Response = 31 | apiCall { api.getMovieCredits(movieId) } 32 | 33 | override suspend fun getMovieTrailers(movieId: Int): Response = 34 | apiCall { api.getMovieTrailers(movieId) } 35 | 36 | override suspend fun getActorDetails(actorId: Int): Response = 37 | apiCall { api.getActorDetails(actorId) } 38 | 39 | override suspend fun getActorMovies(actorId: Int): Response = 40 | apiCall { api.getActorMovies(actorId) } 41 | } -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/ahmetocak/designsystem/components/Button.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.designsystem.components 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.material3.Button 7 | import androidx.compose.material3.ButtonColors 8 | import androidx.compose.material3.ButtonDefaults 9 | import androidx.compose.material3.ButtonElevation 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Shape 15 | import androidx.compose.ui.text.font.FontWeight 16 | import androidx.compose.ui.unit.TextUnit 17 | import androidx.compose.ui.unit.sp 18 | 19 | @Composable 20 | fun MovieButton( 21 | modifier: Modifier = Modifier, 22 | text: String, 23 | onClick: () -> Unit, 24 | fontWeight: FontWeight = FontWeight.W700, 25 | fontSize: TextUnit = 18.sp, 26 | enabled: Boolean = true, 27 | contentPadding: PaddingValues = ButtonDefaults.ContentPadding, 28 | shape: Shape = ButtonDefaults.shape, 29 | colors: ButtonColors = ButtonDefaults.buttonColors(), 30 | elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), 31 | border: BorderStroke? = null, 32 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 33 | ) { 34 | Button( 35 | modifier = modifier, 36 | onClick = onClick, 37 | enabled = enabled, 38 | contentPadding = contentPadding, 39 | shape = shape, 40 | colors = colors, 41 | elevation = elevation, 42 | border = border, 43 | interactionSource = interactionSource 44 | ) { 45 | Text( 46 | text = text, 47 | fontWeight = fontWeight, 48 | fontSize = fontSize 49 | ) 50 | } 51 | } -------------------------------------------------------------------------------- /core/domain/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.util.Properties 2 | 3 | plugins { 4 | alias(libs.plugins.androidLibrary) 5 | alias(libs.plugins.kotlin.android) 6 | kotlin("kapt") 7 | } 8 | 9 | android { 10 | namespace = "com.ahmetocak.domain" 11 | compileSdk = ConfigData.compileSdk 12 | 13 | val p = Properties() 14 | p.load(project.rootProject.file("local.properties").inputStream()) 15 | 16 | defaultConfig { 17 | minSdk = ConfigData.minSdk 18 | 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | consumerProguardFiles("consumer-rules.pro") 21 | 22 | buildConfigField("String","GEMINI_API_KEY", p.getProperty("GEMINI_API_KEY")) 23 | } 24 | 25 | buildTypes { 26 | release { 27 | isMinifyEnabled = BuildTypes.isMinifyEnabled 28 | proguardFiles( 29 | getDefaultProguardFile("proguard-android-optimize.txt"), 30 | "proguard-rules.pro" 31 | ) 32 | } 33 | } 34 | compileOptions { 35 | sourceCompatibility = JavaVersion.VERSION_17 36 | targetCompatibility = JavaVersion.VERSION_17 37 | } 38 | kotlinOptions { 39 | jvmTarget = "17" 40 | } 41 | buildFeatures { 42 | buildConfig = true 43 | } 44 | } 45 | 46 | dependencies { 47 | 48 | implementation(libs.androidx.core.ktx) 49 | implementation(libs.javax.inject) 50 | 51 | // Firebase 52 | implementation(platform(libs.firebase.bom)) 53 | implementation(libs.firebase.auth) 54 | implementation(libs.firebase.firestore) 55 | implementation(libs.firebase.storage) 56 | implementation(libs.google.android.gms) 57 | 58 | //Google AI client SDK for Android 59 | implementation(libs.google.ai.client.generativeai) 60 | 61 | implementation(project(":core:model")) 62 | implementation(project(":core:data")) 63 | implementation(project(":core:common")) 64 | implementation(project(":core:authentication")) 65 | } -------------------------------------------------------------------------------- /core/network/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.util.Properties 2 | 3 | plugins { 4 | alias(libs.plugins.androidLibrary) 5 | alias(libs.plugins.kotlin.android) 6 | kotlin("kapt") 7 | } 8 | 9 | android { 10 | namespace = "com.ahmetocak.network" 11 | compileSdk = ConfigData.compileSdk 12 | 13 | val p = Properties() 14 | p.load(project.rootProject.file("local.properties").inputStream()) 15 | 16 | defaultConfig { 17 | minSdk = ConfigData.minSdk 18 | 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | consumerProguardFiles("consumer-rules.pro") 21 | 22 | buildConfigField("String", "API_KEY", p.getProperty("API_KEY")) 23 | } 24 | 25 | buildTypes { 26 | release { 27 | isMinifyEnabled = BuildTypes.isMinifyEnabled 28 | proguardFiles( 29 | getDefaultProguardFile("proguard-android-optimize.txt"), 30 | "proguard-rules.pro" 31 | ) 32 | } 33 | } 34 | compileOptions { 35 | sourceCompatibility = JavaVersion.VERSION_1_8 36 | targetCompatibility = JavaVersion.VERSION_1_8 37 | } 38 | kotlinOptions { 39 | jvmTarget = "1.8" 40 | } 41 | buildFeatures { 42 | buildConfig = true 43 | } 44 | } 45 | 46 | dependencies { 47 | 48 | implementation(libs.androidx.core.ktx) 49 | 50 | // Retrofit 51 | implementation(libs.retrofit) 52 | implementation(libs.converter.gson) 53 | 54 | // Hilt 55 | implementation(libs.hilt.android) 56 | kapt(libs.hilt.android.compiler) 57 | 58 | // Okhttp 59 | implementation(libs.okhttp) 60 | 61 | // Paging 3 62 | implementation(libs.androidx.paging.runtime.ktx) 63 | 64 | // Firebase 65 | implementation(platform(libs.firebase.bom)) 66 | implementation(libs.firebase.auth) 67 | implementation(libs.firebase.firestore) 68 | implementation(libs.firebase.storage) 69 | 70 | implementation(project(":core:common")) 71 | } -------------------------------------------------------------------------------- /core/common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidLibrary) 3 | alias(libs.plugins.kotlin.android) 4 | } 5 | 6 | android { 7 | namespace = "com.ahmetocak.common" 8 | compileSdk = ConfigData.compileSdk 9 | 10 | defaultConfig { 11 | minSdk = ConfigData.minSdk 12 | 13 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles("consumer-rules.pro") 15 | } 16 | 17 | buildTypes { 18 | release { 19 | isMinifyEnabled = BuildTypes.isMinifyEnabled 20 | proguardFiles( 21 | getDefaultProguardFile("proguard-android-optimize.txt"), 22 | "proguard-rules.pro" 23 | ) 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility = JavaVersion.VERSION_1_8 28 | targetCompatibility = JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = "1.8" 32 | } 33 | buildFeatures { 34 | compose = true 35 | } 36 | composeOptions { 37 | kotlinCompilerExtensionVersion = ComposeOptions.kotlinCompilerExtensionVersion 38 | } 39 | } 40 | 41 | dependencies { 42 | 43 | implementation(libs.androidx.core.ktx) 44 | implementation(libs.androidx.lifecycle.runtime.ktx) 45 | implementation(libs.androidx.activity.compose) 46 | implementation(platform(libs.androidx.compose.bom)) 47 | implementation(libs.androidx.compose.ui) 48 | implementation(libs.androidx.compose.ui.graphics) 49 | implementation(libs.androidx.compose.ui.preview) 50 | implementation(libs.androidx.compose.material3) 51 | 52 | testImplementation(libs.junit) 53 | androidTestImplementation(libs.androidx.junit) 54 | androidTestImplementation(libs.androidx.espresso.core) 55 | androidTestImplementation(platform(libs.androidx.compose.bom)) 56 | androidTestImplementation(libs.androidx.compose.ui.test.junit4) 57 | debugImplementation(libs.androidx.compose.ui.tooling) 58 | debugImplementation(libs.androidx.compose.ui.test.manifest) 59 | 60 | implementation(libs.javax.inject) 61 | } -------------------------------------------------------------------------------- /feature/search/src/main/java/com/ahmetocak/search/SearchViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.search 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import androidx.paging.PagingData 9 | import androidx.paging.cachedIn 10 | import com.ahmetocak.common.helpers.UiText 11 | import com.ahmetocak.domain.movie.SearchMovieUseCase 12 | import com.ahmetocak.model.movie.MovieContent 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import kotlinx.coroutines.flow.Flow 15 | import kotlinx.coroutines.flow.MutableStateFlow 16 | import kotlinx.coroutines.flow.StateFlow 17 | import kotlinx.coroutines.flow.asStateFlow 18 | import kotlinx.coroutines.flow.emptyFlow 19 | import kotlinx.coroutines.flow.update 20 | import javax.inject.Inject 21 | 22 | @HiltViewModel 23 | class SearchViewModel @Inject constructor( 24 | private val searchMovieUseCase: SearchMovieUseCase 25 | ) : ViewModel() { 26 | 27 | private val _uiState = MutableStateFlow(SearchUiState()) 28 | val uiState: StateFlow = _uiState.asStateFlow() 29 | 30 | var query by mutableStateOf("") 31 | private set 32 | 33 | fun updateQueryValue(value: String) { 34 | query = value 35 | } 36 | 37 | fun searchMovie() { 38 | if (query.isNotBlank()) { 39 | _uiState.update { 40 | it.copy( 41 | queryFieldErrorMessage = null, 42 | isSearchDone = true, 43 | searchResult = searchMovieUseCase(query).cachedIn(viewModelScope) 44 | ) 45 | } 46 | } else { 47 | _uiState.update { 48 | it.copy(queryFieldErrorMessage = UiText.StringResource(R.string.blank_field)) 49 | } 50 | } 51 | } 52 | } 53 | 54 | data class SearchUiState( 55 | val isSearchDone: Boolean = false, 56 | val searchResult: Flow> = emptyFlow(), 57 | val queryFieldErrorMessage: UiText? = null 58 | ) -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/ahmetocak/designsystem/components/DynamicLayout.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.designsystem.components 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.platform.LocalConfiguration 11 | 12 | /** 13 | * A composable function to dynamically adjust the layout based on screen orientation. 14 | * In portrait mode, it arranges the content in a column, while in landscape mode, it arranges 15 | * the content in a row. 16 | * 17 | * @param modifier the modifier for the layout 18 | * @param horizontalAlignment the horizontal alignment of the content 19 | * @param horizontalArrangement the horizontal arrangement of the content 20 | * @param verticalAlignment the vertical alignment of the content 21 | * @param verticalArrangement the vertical arrangement of the content 22 | * @param content the composable content to be displayed within the layout 23 | */ 24 | @Composable 25 | inline fun DynamicLayout( 26 | modifier: Modifier = Modifier, 27 | horizontalAlignment: Alignment.Horizontal = Alignment.Start, 28 | horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, 29 | verticalAlignment: Alignment.Vertical = Alignment.Top, 30 | verticalArrangement: Arrangement.Vertical = Arrangement.Top, 31 | content: @Composable () -> Unit 32 | ) { 33 | if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) { 34 | Column( 35 | modifier = modifier, 36 | horizontalAlignment = horizontalAlignment, 37 | verticalArrangement = verticalArrangement 38 | ) { 39 | content() 40 | } 41 | } else { 42 | Row( 43 | modifier = modifier, 44 | horizontalArrangement = horizontalArrangement, 45 | verticalAlignment = verticalAlignment 46 | ) { 47 | content() 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /core/ui/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidLibrary) 3 | alias(libs.plugins.kotlin.android) 4 | } 5 | 6 | android { 7 | namespace = "com.ahmetocak.ui" 8 | compileSdk = ConfigData.compileSdk 9 | 10 | defaultConfig { 11 | minSdk = ConfigData.minSdk 12 | 13 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles("consumer-rules.pro") 15 | } 16 | 17 | buildTypes { 18 | release { 19 | isMinifyEnabled = BuildTypes.isMinifyEnabled 20 | proguardFiles( 21 | getDefaultProguardFile("proguard-android-optimize.txt"), 22 | "proguard-rules.pro" 23 | ) 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility = JavaVersion.VERSION_1_8 28 | targetCompatibility = JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = "1.8" 32 | } 33 | buildFeatures { 34 | compose = true 35 | } 36 | composeOptions { 37 | kotlinCompilerExtensionVersion = ComposeOptions.kotlinCompilerExtensionVersion 38 | } 39 | } 40 | 41 | dependencies { 42 | 43 | implementation(libs.androidx.core.ktx) 44 | implementation(libs.androidx.lifecycle.runtime.ktx) 45 | implementation(libs.androidx.activity.compose) 46 | implementation(platform(libs.androidx.compose.bom)) 47 | implementation(libs.androidx.compose.ui) 48 | implementation(libs.androidx.compose.ui.graphics) 49 | implementation(libs.androidx.compose.ui.preview) 50 | implementation(libs.androidx.compose.material3) 51 | implementation(libs.androidx.constraintlayout) 52 | 53 | testImplementation(libs.junit) 54 | androidTestImplementation(libs.androidx.junit) 55 | androidTestImplementation(libs.androidx.espresso.core) 56 | androidTestImplementation(platform(libs.androidx.compose.bom)) 57 | androidTestImplementation(libs.androidx.compose.ui.test.junit4) 58 | debugImplementation(libs.androidx.compose.ui.tooling) 59 | debugImplementation(libs.androidx.compose.ui.test.manifest) 60 | 61 | implementation(project(":core:designsystem")) 62 | } -------------------------------------------------------------------------------- /core/network/src/main/java/com/ahmetocak/network/datasource/firebase/firestore/FirebaseFirestoreDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.network.datasource.firebase.firestore 2 | 3 | import com.ahmetocak.network.model.firebase.firestore.NetworkWatchListMovie 4 | import com.ahmetocak.network.helpers.FirebaseConstants 5 | import com.google.android.gms.tasks.Task 6 | import com.google.firebase.auth.FirebaseAuth 7 | import com.google.firebase.firestore.DocumentSnapshot 8 | import com.google.firebase.firestore.FieldValue 9 | import com.google.firebase.firestore.FirebaseFirestore 10 | import com.google.firebase.firestore.SetOptions 11 | import javax.inject.Inject 12 | 13 | class FirebaseFirestoreDataSourceImpl @Inject constructor( 14 | private val firestoreDb: FirebaseFirestore, 15 | private val firebaseAuth: FirebaseAuth 16 | ) : FirebaseFirestoreDataSource { 17 | 18 | override fun addMovieData(watchListMovie: NetworkWatchListMovie): Task { 19 | return firestoreDb.collection(FirebaseConstants.Firestore.WATCH_LIST_COLLECTION_NAME) 20 | .document(firebaseAuth.currentUser?.uid ?: "") 21 | .set( 22 | hashMapOf(FirebaseConstants.Firestore.WATCH_LIST_ARRAY_NAME to FieldValue.arrayUnion(watchListMovie)), 23 | SetOptions.merge() 24 | ) 25 | } 26 | 27 | override fun updateMovieData(watchListMovie: List): Task { 28 | return firestoreDb.collection(FirebaseConstants.Firestore.WATCH_LIST_COLLECTION_NAME) 29 | .document(firebaseAuth.currentUser?.uid ?: "") 30 | .update(mapOf(FirebaseConstants.Firestore.WATCH_LIST_ARRAY_NAME to watchListMovie)) 31 | } 32 | 33 | override fun getMovieData(): Task { 34 | return firestoreDb.collection(FirebaseConstants.Firestore.WATCH_LIST_COLLECTION_NAME) 35 | .document(firebaseAuth.currentUser?.uid ?: "") 36 | .get() 37 | } 38 | 39 | override fun deleteMovieDocument(): Task { 40 | return firestoreDb.collection(FirebaseConstants.Firestore.WATCH_LIST_COLLECTION_NAME) 41 | .document(firebaseAuth.currentUser?.uid ?: "") 42 | .delete() 43 | } 44 | } -------------------------------------------------------------------------------- /core/domain/src/main/java/com/ahmetocak/domain/firebase/auth/ReAuthenticateUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ahmetocak.domain.firebase.auth 2 | 3 | import com.ahmetocak.authentication.firebase.FirebaseAuthClient 4 | import com.ahmetocak.authentication.firebase.GoogleAuthClient 5 | import com.ahmetocak.common.helpers.UiText 6 | import com.ahmetocak.common.helpers.handleTaskError 7 | import com.google.firebase.auth.FirebaseAuth 8 | import com.google.firebase.auth.GoogleAuthProvider 9 | import javax.inject.Inject 10 | 11 | class ReAuthenticateUseCase @Inject constructor( 12 | private val firebaseAuthClient: FirebaseAuthClient, 13 | private val googleAuthClient: GoogleAuthClient 14 | ) { 15 | operator fun invoke(email: String, password: String, onTaskSuccess: () -> Unit, onTaskFailed: (UiText) -> Unit) { 16 | val signInProvider = FirebaseAuth.getInstance().getAccessToken(false).result.signInProvider 17 | 18 | if (signInProvider == GoogleAuthProvider.PROVIDER_ID) { 19 | googleAuthClient.reAuthenticate().addOnCompleteListener { task -> 20 | if (task.isSuccessful) { 21 | val account = task.result 22 | val credential = GoogleAuthProvider.getCredential(account.idToken, null) 23 | 24 | FirebaseAuth.getInstance().currentUser?.reauthenticate(credential) 25 | ?.addOnCompleteListener { reAuthTask -> 26 | if (reAuthTask.isSuccessful) { 27 | onTaskSuccess() 28 | } else { 29 | onTaskFailed(handleTaskError(reAuthTask.exception)) 30 | } 31 | } 32 | } else { 33 | onTaskFailed(handleTaskError(task.exception)) 34 | } 35 | } 36 | } else { 37 | firebaseAuthClient.reAuthenticate(email, password)?.addOnCompleteListener { task -> 38 | if (task.isSuccessful) { 39 | onTaskSuccess() 40 | } else { 41 | onTaskFailed(handleTaskError(task.exception)) 42 | } 43 | } 44 | } 45 | } 46 | } --------------------------------------------------------------------------------