diff --git a/.gitignore b/.gitignore index 8a6b3320..4472a6c5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ !.idea/codeStyles .DS_Store /build +/app/build/ /captures .externalNativeBuild .cxx - diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 00000000..4e9ba356 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Finema \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 4d30507b..118a5c1c 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,5 +1,27 @@ \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..61a9130c --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 00000000..b617266a --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 00000000..a5f05cd8 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..d5d35ec4 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/.idea/codeStyles/Project.xml b/app/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..4d30507b --- /dev/null +++ b/app/.idea/codeStyles/Project.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/.idea/codeStyles/codeStyleConfig.xml b/app/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..79ee123c --- /dev/null +++ b/app/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/CI.md b/app/CI.md new file mode 100644 index 00000000..f334a698 --- /dev/null +++ b/app/CI.md @@ -0,0 +1,20 @@ +# Continius Integration + +Это CI для студентческих проектов Технопарка. + +## Запуск локально + +Для запуска проверки стиля кода необходимо запустить три скрипта: +1. Запустить `./detekt` или `.\detekt.bat` +2. Запустить `./ktlint` или `.\ktlint.bat` +3. Запустить `./checkstyle` или `.\checkstyle.bat` + + +## Исправление ошибок + +В некоторых утилитах есть автоматическое исправление ошибок. +Например, большинство ошибок можно поправить автоформаттером ktlint: + +``` +./ktlint -F +``` diff --git a/app/README.md b/app/README.md new file mode 100644 index 00000000..99e9da4e --- /dev/null +++ b/app/README.md @@ -0,0 +1,11 @@ +# Movie App + +Приложение для выбора лучшего фильма. Предлагается какое-то кол-во фильмов из одного жанра и пользователь выбирает лучшее посредством турнира. + +## Команда авторов + +- [Иван Абрамов](https://github.com/Alberto195) +- [Дмитрий Костык](https://github.com/kodzzzima) +- [Иван Цыганов](https://github.com/fatalem0) +- [Иван Демидов](https://github.com/GypsyJR777) + diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..8398f292 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,171 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'kotlin-kapt' + id 'androidx.navigation.safeargs' + id 'com.google.gms.google-services' +} +apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-parcelize' + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + defaultConfig { + applicationId "com.example.finema" + minSdkVersion 21 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures { + viewBinding true + dataBinding true + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +ext { + dagger_version = '2.30.1' + appcompat_version = '1.2.0' + lifecycle_version = '2.3.1' + activity_version = '1.3.0-alpha07' + fragment_version = '1.3.3' + nav_version = '2.3.5' + koin_version= "3.0.1" + pagingVersion = "3.0.0" +} + +dependencies { + + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.3.2' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'com.google.android.material:material:1.3.0' + implementation 'com.google.firebase:firebase-auth:20.0.4' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'com.google.android.gms:play-services-ads:20.1.0' + implementation 'com.google.android.gms:play-services-maps:17.0.1' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'com.google.firebase:firebase-database:20.0.0' + testImplementation 'junit:junit:4.+' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + + //firebase + implementation 'com.google.firebase:firebase-auth-ktx' + implementation 'com.google.firebase:firebase-firestore-ktx' + implementation 'com.google.firebase:firebase-analytics-ktx' + implementation platform('com.google.firebase:firebase-bom:27.1.0') + + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + + //Dagger 2 + api "com.google.dagger:dagger:$dagger_version" + kapt "com.google.dagger:dagger-compiler:$dagger_version" + api "com.google.dagger:dagger-android-support:$dagger_version" // if you use the support libraries + kapt "com.google.dagger:dagger-android-processor:$dagger_version" + + //Rxjava2 + implementation "io.reactivex.rxjava2:rxjava:2.2.10" + implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + + //Retrofit + OkHttp3 + implementation "com.squareup.okhttp3:okhttp:4.7.2" + implementation "com.squareup.okhttp3:logging-interceptor:4.6.0" + implementation "com.squareup.retrofit2:retrofit:2.9.0" + implementation "com.squareup.retrofit2:converter-gson:2.9.0" + implementation 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0' + implementation "com.squareup.retrofit2:adapter-rxjava2:2.5.0" + + //appcompat + implementation "androidx.appcompat:appcompat:$appcompat_version" + implementation "androidx.appcompat:appcompat-resources:$appcompat_version" + + //lifecycle + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + // LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + // Lifecycles only (without ViewModel or LiveData) + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" + + //Activity + implementation "androidx.activity:activity-ktx:$activity_version" + //Fragment + implementation "androidx.fragment:fragment-ktx:$fragment_version" + + //navigation + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" + + implementation 'com.google.gms:google-services:4.3.5' + + //retrofit + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + + //signIN + implementation 'com.google.android.gms:play-services-drive:17.0.0' + implementation 'com.google.android.gms:play-services-auth:19.0.0' + + //viewmodel + implementation "android.arch.lifecycle:extensions:1.1.1" + + //glide + implementation 'com.github.bumptech.glide:glide:4.12.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' + + //roomDatabase + implementation 'androidx.room:room-ktx:2.3.0' + kapt "androidx.room:room-compiler:2.3.0" + + implementation 'com.google.android.material:material:1.3.0' + + implementation 'androidx.preference:preference-ktx:1.1.1' + + // Koin main features for Android (Scope,ViewModel ...) + implementation "io.insert-koin:koin-android:$koin_version" + // Koin Android - experimental builder extensions + implementation "io.insert-koin:koin-android-ext:$koin_version" + // Koin for Jetpack WorkManager + implementation "io.insert-koin:koin-androidx-workmanager:$koin_version" + // Koin for Jetpack Compose (unstable version) + implementation "io.insert-koin:koin-androidx-compose:$koin_version" + // Koin for Kotlin Multiplatform + implementation "io.insert-koin:koin-core:$koin_version" + // Koin Test for Kotlin Multiplatform + testImplementation "io.insert-koin:koin-test:$koin_version" + // Koin for JUnit 4 + testImplementation "io.insert-koin:koin-test-junit4:$koin_version" + // Koin for JUnit 5 + testImplementation "io.insert-koin:koin-test-junit5:$koin_version" + // Koin Extended & experimental features (JVM) + implementation "io.insert-koin:koin-core-ext:$koin_version" + + implementation "androidx.paging:paging-runtime-ktx:$pagingVersion" + + // WorkManager + implementation "android.arch.work:work-runtime:1.0.1" + + //FlexBox + implementation 'com.google.android.flexbox:flexbox:3.0.0' +} \ No newline at end of file diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 00000000..ccd77ac6 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,93 @@ +{ + "project_info": { + "project_number": "677958878281", + "firebase_url": "https://finema-b7bde-default-rtdb.europe-west1.firebasedatabase.app", + "project_id": "finema-b7bde", + "storage_bucket": "finema-b7bde.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:677958878281:android:abcc3c987d1a658a9deb98", + "android_client_info": { + "package_name": "Finema.app" + } + }, + "oauth_client": [ + { + "client_id": "677958878281-h8s3da3dpf41u4cbc0csu5t9iq6b250g.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "Finema.app", + "certificate_hash": "549d918079a35c978f5226f4a7ecd7d5bf250ae9" + } + }, + { + "client_id": "677958878281-pmcs285i4cdvd5tob7c6ovd2j0msknvs.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDO1vX5IwzD0tWU002zPXJuda5gseEPA38" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "677958878281-pmcs285i4cdvd5tob7c6ovd2j0msknvs.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:677958878281:android:59346e44ac0064a59deb98", + "android_client_info": { + "package_name": "com.example.finema" + } + }, + "oauth_client": [ + { + "client_id": "677958878281-bc2fulmg7en9sgv9ingij4sav2u4g35r.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.example.finema", + "certificate_hash": "08fa697c85845235d17c47709bceeae4bfe3692c" + } + }, + { + "client_id": "677958878281-o32p6sa8b711am4ikvr2inhjql9en9us.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.example.finema", + "certificate_hash": "549d918079a35c978f5226f4a7ecd7d5bf250ae9" + } + }, + { + "client_id": "677958878281-pmcs285i4cdvd5tob7c6ovd2j0msknvs.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDO1vX5IwzD0tWU002zPXJuda5gseEPA38" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "677958878281-pmcs285i4cdvd5tob7c6ovd2j0msknvs.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/finema/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/finema/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..a64699d3 --- /dev/null +++ b/app/src/androidTest/java/com/example/finema/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.example.finema + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.finema", appContext.packageName) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..df380acc --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher1-playstore.png b/app/src/main/ic_launcher1-playstore.png new file mode 100644 index 00000000..87931d13 Binary files /dev/null and b/app/src/main/ic_launcher1-playstore.png differ diff --git a/app/src/main/ic_launcher2-playstore.png b/app/src/main/ic_launcher2-playstore.png new file mode 100644 index 00000000..40b442bd Binary files /dev/null and b/app/src/main/ic_launcher2-playstore.png differ diff --git a/app/src/main/java/com/example/finema/MainActivity.kt b/app/src/main/java/com/example/finema/MainActivity.kt new file mode 100644 index 00000000..b131d0a9 --- /dev/null +++ b/app/src/main/java/com/example/finema/MainActivity.kt @@ -0,0 +1,203 @@ +package com.example.finema + +import android.net.Uri +import android.os.Bundle +import android.preference.PreferenceManager +import android.util.Log +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.drawerlayout.widget.DrawerLayout +import androidx.navigation.findNavController +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import com.example.finema.databinding.ActivityMainBinding +import com.example.finema.repositories.IAppPreference +import com.example.finema.ui.settings.NotificationService +import com.example.finema.util.downloadAndSetImageUri +import com.google.firebase.auth.FirebaseAuth +import java.util.concurrent.TimeUnit +import org.koin.android.ext.android.inject + +class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding + private val appPreference: IAppPreference by inject() + private var simpleNotification = + PeriodicWorkRequestBuilder( + NOTIFICATION_REPEAT, + TimeUnit.HOURS, + NOTIFICATION_FLEX, + TimeUnit.HOURS + ) + .addTag("finema") + .build() + + override fun onCreate(savedInstanceState: Bundle?) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + super.onCreate(savedInstanceState) + appPreference.getPreference() + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.topAppBar.setNavigationOnClickListener { + binding.drawerLayout.open() + } + + binding.navView.getHeaderView(HEADER_VIEW_INDEX).setOnClickListener { + binding.drawerLayout.close() + findNavController(R.id.fragment) + .navigate(R.id.action_global_fragmentProfile) + } + + binding.navView.setNavigationItemSelectedListener { + binding.drawerLayout.close() + + when (it.itemId) { + R.id.lovely -> + findNavController(R.id.fragment) + .navigate(R.id.action_global_fragmentFavourite) + + R.id.budget -> + findNavController(R.id.fragment) + .navigate(R.id.action_global_fragmentHigherLower) + + R.id.rating -> + findNavController(R.id.fragment) + .navigate(R.id.action_global_higherLowerRatingFragment) + + R.id.tournament -> + findNavController(R.id.fragment) + .navigate(R.id.action_global_fragmentTmp) + + R.id.settings -> + findNavController(R.id.fragment) + .navigate(R.id.action_global_fragmentSettings) + + R.id.addFav -> + findNavController(R.id.fragment) + .navigate(R.id.action_global_chooseFavouriteFragment) + } + true + } + } + + override fun onStart() { + super.onStart() + addDestinationChangedListener() + checkIfUserInited() + } + + override fun onStop() { + super.onStop() + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) + val notifications = sharedPreferences.getBoolean("notifications", true) + + if (notifications) { + WorkManager.getInstance(this) + .enqueueUniquePeriodicWork( + "notification", + ExistingPeriodicWorkPolicy.REPLACE, + simpleNotification + ) + } else { + WorkManager.getInstance(this).cancelAllWorkByTag("finema") + } + } + + private fun checkIfUserInited() { + if (!appPreference.getInitUser()) { + toSignInFragment() + } else { + if (FirebaseAuth.getInstance().currentUser?.displayName == null) { + setPhotoAndNameGuest() + } else { + setPhotoAndNameUser() + } + } + } + + private fun addDestinationChangedListener() { + findNavController(R.id.fragment) + .addOnDestinationChangedListener { _, destination, _ -> + if (SCREENS_WITHOUT_DRAWER.contains(destination.id)) { + binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + binding.topAppBar.visibility = View.GONE + binding.appBarCollapse.visibility = View.GONE + } else { + binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED) + binding.topAppBar.visibility = View.VISIBLE + binding.appBarCollapse.visibility = View.VISIBLE + } + + if (SCREENS_WITH_IMAGE.contains(destination.id)) { + binding.imageAppBar.visibility = View.VISIBLE + } else { + binding.imageAppBar.visibility = View.GONE + binding.imageAppBar.setImageDrawable(null) + binding.topAppBar.title = resources.getString(R.string.app_name) + } + + when (destination.label) { + "Tournament fragment" -> { + appPreference.setFragment(destination.label as String) + } + "Film fragment" -> { + } + + "SigInFragment" -> { + appPreference.setFragment(destination.label as String) + } + + else -> { + appPreference.setFragment("") + Log.d("Destination", destination.label as String) + } + } + } + } + + private fun setPhotoAndNameUser() { + binding.navView + .getHeaderView(HEADER_VIEW_INDEX) + .findViewById(R.id.nickProfile) + .text = FirebaseAuth.getInstance().currentUser?.displayName.orEmpty() + binding.navView + .getHeaderView(HEADER_VIEW_INDEX) + .findViewById(R.id.userAvatar) + .downloadAndSetImageUri(FirebaseAuth.getInstance().currentUser?.photoUrl) + } + + private fun setPhotoAndNameGuest() { + binding.navView + .getHeaderView(HEADER_VIEW_INDEX) + .findViewById(R.id.nickProfile) + .text = "Гость" + binding.navView + .getHeaderView(HEADER_VIEW_INDEX) + .findViewById(R.id.userAvatar) + .downloadAndSetImageUri(Uri.parse(DEFAULT_URI)) + } + + private fun toSignInFragment() { + findNavController(R.id.fragment) + .navigate(R.id.action_global_signIn) + } + + companion object { + private val SCREENS_WITHOUT_DRAWER = listOf( + R.id.sigInFragment + ) + private val SCREENS_WITH_IMAGE = listOf( + R.id.fragmentFilm + ) + private const val DEFAULT_URI = + "android.resource://com.example.finema/drawable/default_profile_avatar" + private const val NOTIFICATION_REPEAT = 12L + private const val NOTIFICATION_FLEX = 3L + private const val HEADER_VIEW_INDEX = 0 + } +} diff --git a/app/src/main/java/com/example/finema/MovieApplication.kt b/app/src/main/java/com/example/finema/MovieApplication.kt new file mode 100644 index 00000000..5e239960 --- /dev/null +++ b/app/src/main/java/com/example/finema/MovieApplication.kt @@ -0,0 +1,31 @@ +package com.example.finema + +import android.app.Application +import com.example.finema.util.apiModule +import com.example.finema.util.databaseModule +import com.example.finema.util.repositoryModule +import com.example.finema.util.viewModelModule +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidFileProperties +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin + +class MovieApplication : Application() { + + override fun onCreate() { + super.onCreate() + startKoin { + androidLogger() + androidContext(this@MovieApplication) + androidFileProperties() + modules( + listOf( + apiModule, + databaseModule, + repositoryModule, + viewModelModule + ) + ) + } + } +} diff --git a/app/src/main/java/com/example/finema/api/IMoviesRepository.kt b/app/src/main/java/com/example/finema/api/IMoviesRepository.kt new file mode 100644 index 00000000..2dbffb0e --- /dev/null +++ b/app/src/main/java/com/example/finema/api/IMoviesRepository.kt @@ -0,0 +1,23 @@ +package com.example.finema.api + +import com.example.finema.models.genreRequest.GenreList +import com.example.finema.models.movieResponse.MovieDetails +import com.example.finema.models.movieResponse.MovieResponse +import com.example.finema.models.movieResponse.MovieResponseFromList +import com.example.finema.models.movieResponse.TrailersList + +interface IMoviesRepository { + val api: MoviesApi + + suspend fun getMovies(page: Int): MovieResponse + + suspend fun getGenres(): GenreList + + suspend fun getMoviesWithGenre(page: Int, withGenres: String): MovieResponse + + suspend fun getTrailers(id: Long): TrailersList + + suspend fun getMovieDetails(id: Long): MovieDetails + + suspend fun getMovieFromList(listId: Int): MovieResponseFromList +} diff --git a/app/src/main/java/com/example/finema/api/MoviesApi.kt b/app/src/main/java/com/example/finema/api/MoviesApi.kt new file mode 100644 index 00000000..21d9fb82 --- /dev/null +++ b/app/src/main/java/com/example/finema/api/MoviesApi.kt @@ -0,0 +1,75 @@ +package com.example.finema.api + +import com.example.finema.models.genreRequest.GenreList +import com.example.finema.models.infinite.MovieDiscover +import com.example.finema.models.movieResponse.MovieDetails +import com.example.finema.models.movieResponse.MovieResponse +import com.example.finema.models.movieResponse.MovieResponseFromList +import com.example.finema.models.movieResponse.TrailersList +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface MoviesApi { + + @GET("movie/popular?api_key=bbf5a3000e95f1dddf266b5e187d4b21&language=ru-Ru") + suspend fun getMovies( + @Query("page") page: Int + ): Response + + @GET("movie/{movie_id}/videos?api_key=bbf5a3000e95f1dddf266b5e187d4b21") + suspend fun getTrailers( + @Path("movie_id") id: Long, + @Query("language") language: String + ): Response + + + @GET(GENRE_LIST) + suspend fun getGenreList(): Response + + @GET("movie/{movie_id}?api_key=bbf5a3000e95f1dddf266b5e187d4b21") + suspend fun getMovieDetails( + @Path("movie_id") id: Long, + @Query("language") language: String + ): Response + + @GET(TOP_RATED_LIST) + suspend fun getMoviesWithGenre( + @Query("page") page: Int, + @Query("withGenres") withGenres: String + ): Response + + @GET("list/{listId}?$API_AND_LANGUAGE") + suspend fun getMovieFromList( + @Path("listId") listId: Int + ): Response + + @GET(DISCOVER) + suspend fun everything( + @Query("page") page: Int, + @Query("query") query: String + ): Response + + companion object { + operator fun invoke(): MoviesApi { + return Retrofit.Builder() + .addConverterFactory(GsonConverterFactory.create()) + .baseUrl(BASE_URL) + .build() + .create(MoviesApi::class.java) + } + private const val API_AND_LANGUAGE = + "api_key=bbf5a3000e95f1dddf266b5e187d4b21&language=ru-ru" + + private const val BASE_URL = "https://api.themoviedb.org/3/" + private const val GENRE_LIST = + "genre/movie/list?api_key=bbf5a3000e95f1dddf266b5e187d4b21&language=ru-ru" + private const val TOP_RATED_LIST = + "movie/top_rated?api_key=bbf5a3000e95f1dddf266b5e187d4b21&language=ru-Ru" + private const val DISCOVER = + "search/movie?api_key=bbf5a3000e95f1dddf266b5e187d4b21&language=ru-Ru" + } +} diff --git a/app/src/main/java/com/example/finema/api/MoviesRepository.kt b/app/src/main/java/com/example/finema/api/MoviesRepository.kt new file mode 100644 index 00000000..5a759fc6 --- /dev/null +++ b/app/src/main/java/com/example/finema/api/MoviesRepository.kt @@ -0,0 +1,33 @@ +package com.example.finema.api + +import com.example.finema.repositories.SafeApiRequest +import java.util.Locale + +class MoviesRepository( + override val api: MoviesApi +) : IMoviesRepository, SafeApiRequest { + + override suspend fun getMovies(page: Int) = apiRequest { + api.getMovies(page) + } + + override suspend fun getGenres() = apiRequest { + api.getGenreList() + } + + override suspend fun getMoviesWithGenre(page: Int, withGenres: String) = apiRequest { + api.getMoviesWithGenre(page, withGenres) + } + + override suspend fun getTrailers(id: Long) = apiRequest { + api.getTrailers(id, Locale.getDefault().toString().replace('_', '-')) + } + + override suspend fun getMovieDetails(id: Long) = apiRequest { + api.getMovieDetails(id, Locale.getDefault().toString().replace('_', '-')) + } + + override suspend fun getMovieFromList(listId: Int) = apiRequest { + api.getMovieFromList(listId) + } +} diff --git a/app/src/main/java/com/example/finema/database/DatabaseRepository.kt b/app/src/main/java/com/example/finema/database/DatabaseRepository.kt new file mode 100644 index 00000000..6e2e0897 --- /dev/null +++ b/app/src/main/java/com/example/finema/database/DatabaseRepository.kt @@ -0,0 +1,35 @@ +package com.example.finema.database + +import androidx.lifecycle.LiveData +import com.example.finema.database.room.RoomDao +import com.example.finema.models.databaseModels.GenreModel +import com.example.finema.models.databaseModels.MovieModel +import com.example.finema.models.databaseModels.TopModel + +interface DatabaseRepository { + val roomDao: RoomDao + + val allGenres: LiveData> + + val allFavourites: LiveData> + + val allTop: LiveData> + + fun checkFavourite(movieId: List): LiveData> + + suspend fun insert(genre: GenreModel, onSuccess: () -> Unit) + + suspend fun insertTop(movie: TopModel, onSuccess: () -> Unit) + + suspend fun insertFavourite(movie: MovieModel, onSuccess: () -> Unit) + + suspend fun deleteAllFavourite(onSuccess: () -> Unit) + + suspend fun deleteFavouriteMovie(movieId: Long, onSuccess: () -> Unit) + + suspend fun deleteFavourite(movie: MovieModel, onSuccess: () -> Unit) + + suspend fun deleteAllTop(onSuccess: () -> Unit) + + fun signOut() {} +} diff --git a/app/src/main/java/com/example/finema/database/firebase/CategoriesLiveData.kt b/app/src/main/java/com/example/finema/database/firebase/CategoriesLiveData.kt new file mode 100644 index 00000000..dbb5301b --- /dev/null +++ b/app/src/main/java/com/example/finema/database/firebase/CategoriesLiveData.kt @@ -0,0 +1,39 @@ +package com.example.finema.database.firebase + +import android.util.Log +import androidx.lifecycle.LiveData +import com.example.finema.models.databaseModels.CategoryModel +import com.google.firebase.database.DataSnapshot +import com.google.firebase.database.DatabaseError +import com.google.firebase.database.FirebaseDatabase +import com.google.firebase.database.ValueEventListener + +class CategoriesLiveData : LiveData>() { + private val refDatabaseCategory = FirebaseDatabase + .getInstance() + .reference + .child("category") + + private val listener = object : ValueEventListener { + + override fun onDataChange(p0: DataSnapshot) { + value = p0.children.map { + it.getValue(CategoryModel::class.java) ?: CategoryModel() + } + } + + override fun onCancelled(error: DatabaseError) { + Log.d("CategoriesLiveData", error.message) + } + } + + override fun onInactive() { + refDatabaseCategory.removeEventListener(listener) + super.onInactive() + } + + override fun onActive() { + refDatabaseCategory.addValueEventListener(listener) + super.onActive() + } +} diff --git a/app/src/main/java/com/example/finema/database/firebase/FirebaseRepository.kt b/app/src/main/java/com/example/finema/database/firebase/FirebaseRepository.kt new file mode 100644 index 00000000..c5157689 --- /dev/null +++ b/app/src/main/java/com/example/finema/database/firebase/FirebaseRepository.kt @@ -0,0 +1,63 @@ +package com.example.finema.database.firebase + +import androidx.lifecycle.LiveData +import com.example.finema.models.databaseModels.CategoryModel +import com.example.finema.models.databaseModels.MovieModel +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.database.DatabaseReference +import com.google.firebase.database.FirebaseDatabase + +class FirebaseRepository( + override val firebaseDatabase: FirebaseDatabase +) : IFirebaseRepository { + override val allCategories: LiveData> = CategoriesLiveData() + override val allMovies: LiveData> = MoviesFromFirebaseLiveData() + + private val auth = FirebaseAuth.getInstance() + private val databaseReference = firebaseDatabase.reference + private lateinit var reDatabase: DatabaseReference + private lateinit var reDatabaseUser: DatabaseReference + private lateinit var reDatabaseUserSaved: DatabaseReference + + override fun initRefCategory() { + reDatabase = databaseReference + .child("category") + } + + override fun initRefs() { + + val currentId = auth.currentUser?.uid.toString() + reDatabaseUser = databaseReference + .child("user_list") + .child(currentId) + + reDatabaseUserSaved = reDatabaseUser.child("saved") + } + + override fun insertFirebaseFavouriteFilm(movieModel: MovieModel) { + + val fId = "idFirebase" + val fTitle = "title" + val fImageUrl = "imageUrl" + val fAbout = "about" + val fRating = "rating" + + val mapMovie = hashMapOf() + mapMovie[fId] = movieModel.id.toString() + mapMovie[fTitle] = movieModel.title + mapMovie[fImageUrl] = movieModel.imageUrl.toString() + mapMovie[fAbout] = movieModel.about + mapMovie[fRating] = movieModel.rating + + reDatabaseUserSaved.child(movieModel.id.toString()) + .updateChildren(mapMovie) + } + + override fun deleteFirebaseFavouriteFilm(movieModel: MovieModel) { + reDatabaseUserSaved.child(movieModel.id.toString()).removeValue() + } + + override fun clearFirebaseFavourite() { + reDatabaseUserSaved.removeValue() + } +} diff --git a/app/src/main/java/com/example/finema/database/firebase/IFirebaseRepository.kt b/app/src/main/java/com/example/finema/database/firebase/IFirebaseRepository.kt new file mode 100644 index 00000000..39921563 --- /dev/null +++ b/app/src/main/java/com/example/finema/database/firebase/IFirebaseRepository.kt @@ -0,0 +1,22 @@ +package com.example.finema.database.firebase + +import androidx.lifecycle.LiveData +import com.example.finema.models.databaseModels.CategoryModel +import com.example.finema.models.databaseModels.MovieModel +import com.google.firebase.database.FirebaseDatabase + +interface IFirebaseRepository { + val firebaseDatabase: FirebaseDatabase + val allCategories: LiveData> + val allMovies: LiveData> + + fun initRefCategory() + + fun initRefs() + + fun insertFirebaseFavouriteFilm(movieModel: MovieModel) + + fun deleteFirebaseFavouriteFilm(movieModel: MovieModel) + + fun clearFirebaseFavourite() +} diff --git a/app/src/main/java/com/example/finema/database/firebase/MoviesFromFirebaseLiveData.kt b/app/src/main/java/com/example/finema/database/firebase/MoviesFromFirebaseLiveData.kt new file mode 100644 index 00000000..78e496dd --- /dev/null +++ b/app/src/main/java/com/example/finema/database/firebase/MoviesFromFirebaseLiveData.kt @@ -0,0 +1,44 @@ +package com.example.finema.database.firebase + +import android.util.Log +import androidx.lifecycle.LiveData +import com.example.finema.models.databaseModels.MovieModel +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.database.DataSnapshot +import com.google.firebase.database.DatabaseError +import com.google.firebase.database.FirebaseDatabase +import com.google.firebase.database.ValueEventListener + +class MoviesFromFirebaseLiveData : LiveData>() { + + private val auth = FirebaseAuth.getInstance() + private val currentId = auth.currentUser?.uid.toString() + private val reDatabaseUserSaved = FirebaseDatabase + .getInstance() + .reference + .child("user_list") + .child(currentId) + .child("saved") + + private val listener = object : ValueEventListener { + override fun onCancelled(p0: DatabaseError) { + Log.d("CategoriesLiveData", p0.message) + } + + override fun onDataChange(p0: DataSnapshot) { + value = p0.children.map { + it.getValue(MovieModel::class.java) ?: MovieModel() + } + } + } + + override fun onInactive() { + reDatabaseUserSaved.removeEventListener(listener) + super.onInactive() + } + + override fun onActive() { + reDatabaseUserSaved.addValueEventListener(listener) + super.onActive() + } +} diff --git a/app/src/main/java/com/example/finema/database/room/RoomDao.kt b/app/src/main/java/com/example/finema/database/room/RoomDao.kt new file mode 100644 index 00000000..4b3a0999 --- /dev/null +++ b/app/src/main/java/com/example/finema/database/room/RoomDao.kt @@ -0,0 +1,48 @@ +package com.example.finema.database.room + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.example.finema.models.databaseModels.GenreModel +import com.example.finema.models.databaseModels.MovieModel +import com.example.finema.models.databaseModels.TopModel + +// Data Access Object +@Dao +interface RoomDao { + @Query("SELECT * FROM genre_list") + fun getAllGenres(): LiveData> + + @Query("SELECT * FROM favourite_list") + fun getAllFavourites(): LiveData> + + @Query("SELECT * FROM top_list") + fun getAllTop(): LiveData> + + @Query("SELECT id FROM favourite_list WHERE id IN (:movieId)") + fun checkFavourite(movieId: List): LiveData> + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insertFavourite(note: MovieModel) + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(note: GenreModel) + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insertTop(movie: TopModel) + + @Query("DELETE FROM favourite_list WHERE id = :movieId;") + suspend fun deleteFavouriteMovie(movieId: Long) + + @Query("DELETE FROM favourite_list;") + suspend fun deleteAllFavourite() + + @Delete + suspend fun deleteFavourite(note: MovieModel) + + @Query("DELETE FROM top_list;") + suspend fun deleteAllTop() +} diff --git a/app/src/main/java/com/example/finema/database/room/RoomDataBase.kt b/app/src/main/java/com/example/finema/database/room/RoomDataBase.kt new file mode 100644 index 00000000..a4feef2b --- /dev/null +++ b/app/src/main/java/com/example/finema/database/room/RoomDataBase.kt @@ -0,0 +1,36 @@ +package com.example.finema.database.room + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import com.example.finema.models.databaseModels.GenreModel +import com.example.finema.models.databaseModels.MovieModel +import com.example.finema.models.databaseModels.TopModel + +@Database(entities = [GenreModel::class, MovieModel::class, TopModel::class], version = 9) +abstract class RoomDataBase : RoomDatabase() { + + abstract fun getRoomDao(): RoomDao + + companion object { + + @Volatile + private var database: RoomDataBase? = null + + @Synchronized + fun getInstance(context: Context): RoomDataBase { + return if (database == null) { + database = Room + .databaseBuilder( + context, + RoomDataBase::class.java, + "database" + ) + .fallbackToDestructiveMigration().build() + + database as RoomDataBase + } else database as RoomDataBase + } + } +} diff --git a/app/src/main/java/com/example/finema/database/room/RoomRepository.kt b/app/src/main/java/com/example/finema/database/room/RoomRepository.kt new file mode 100644 index 00000000..031c2380 --- /dev/null +++ b/app/src/main/java/com/example/finema/database/room/RoomRepository.kt @@ -0,0 +1,53 @@ +package com.example.finema.database.room + +import androidx.lifecycle.LiveData +import com.example.finema.database.DatabaseRepository +import com.example.finema.models.databaseModels.GenreModel +import com.example.finema.models.databaseModels.MovieModel +import com.example.finema.models.databaseModels.TopModel + +class RoomRepository( + override val roomDao: RoomDao +) : DatabaseRepository { + + override val allGenres: LiveData> = roomDao.getAllGenres() + + override val allFavourites: LiveData> = roomDao.getAllFavourites() + + override val allTop: LiveData> = roomDao.getAllTop() + + override fun checkFavourite(movieId: List) = roomDao.checkFavourite(movieId) + + override suspend fun insert(genre: GenreModel, onSuccess: () -> Unit) { + roomDao.insert(genre) + onSuccess() + } + + override suspend fun insertTop(movie: TopModel, onSuccess: () -> Unit) { + roomDao.insertTop(movie) + } + + override suspend fun insertFavourite(movie: MovieModel, onSuccess: () -> Unit) { + roomDao.insertFavourite(movie) + onSuccess() + } + + override suspend fun deleteAllFavourite(onSuccess: () -> Unit) { + roomDao.deleteAllFavourite() + onSuccess() + } + + override suspend fun deleteFavouriteMovie(movieId: Long, onSuccess: () -> Unit) { + roomDao.deleteFavouriteMovie(movieId) + onSuccess() + } + + override suspend fun deleteFavourite(movie: MovieModel, onSuccess: () -> Unit) { + roomDao.deleteFavourite(movie) + onSuccess() + } + + override suspend fun deleteAllTop(onSuccess: () -> Unit) { + roomDao.deleteAllTop() + } +} diff --git a/app/src/main/java/com/example/finema/models/databaseModels/CategoryModel.kt b/app/src/main/java/com/example/finema/models/databaseModels/CategoryModel.kt new file mode 100644 index 00000000..a560c9cb --- /dev/null +++ b/app/src/main/java/com/example/finema/models/databaseModels/CategoryModel.kt @@ -0,0 +1,15 @@ +package com.example.finema.models.databaseModels + +import androidx.room.ColumnInfo +import androidx.room.PrimaryKey + +data class CategoryModel( + @PrimaryKey + val id: String = "0", + @ColumnInfo + val name: String = "", + @ColumnInfo + val description: String = "", + @ColumnInfo + val link: String = "", +) diff --git a/app/src/main/java/com/example/finema/models/databaseModels/GenreModel.kt b/app/src/main/java/com/example/finema/models/databaseModels/GenreModel.kt new file mode 100644 index 00000000..f4d76788 --- /dev/null +++ b/app/src/main/java/com/example/finema/models/databaseModels/GenreModel.kt @@ -0,0 +1,19 @@ +package com.example.finema.models.databaseModels + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.io.Serializable + +@Entity(tableName = "genre_list") +data class GenreModel( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + @ColumnInfo + val name: String = "", + @ColumnInfo(typeAffinity = ColumnInfo.BLOB) + var image: ByteArray? = null, + + val idFirebase: String = "" + +) : Serializable diff --git a/app/src/main/java/com/example/finema/models/databaseModels/MovieModel.kt b/app/src/main/java/com/example/finema/models/databaseModels/MovieModel.kt new file mode 100644 index 00000000..a8cef70b --- /dev/null +++ b/app/src/main/java/com/example/finema/models/databaseModels/MovieModel.kt @@ -0,0 +1,32 @@ +package com.example.finema.models.databaseModels + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import java.io.Serializable + +@Entity(tableName = "favourite_list", indices = [Index("id")]) +data class MovieModel( + @PrimaryKey(autoGenerate = true) + var id: Long = 0, + @ColumnInfo + val title: String = "", +// @ColumnInfo(typeAffinity = ColumnInfo.BLOB) +// var image: ByteArray? = null, + @ColumnInfo + val originalTitle: String = "", + @ColumnInfo + val imageUrl: String? = "", + @ColumnInfo + val about: String = "", + @ColumnInfo + val genres: String? = "", + @ColumnInfo + val rating: String = "", + @ColumnInfo + val companies: String? = "", + + val idFirebase: String = "", + +) : Serializable diff --git a/app/src/main/java/com/example/finema/models/databaseModels/TopModel.kt b/app/src/main/java/com/example/finema/models/databaseModels/TopModel.kt new file mode 100644 index 00000000..eda22261 --- /dev/null +++ b/app/src/main/java/com/example/finema/models/databaseModels/TopModel.kt @@ -0,0 +1,25 @@ +package com.example.finema.models.databaseModels + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import java.io.Serializable + +@Entity(tableName = "top_list", indices = [Index("id")]) +data class TopModel( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + @ColumnInfo + val title: String = "", + @ColumnInfo + val imageUrl: String? = "", + @ColumnInfo + val about: String = "", + @ColumnInfo + val genres: String? = "", + @ColumnInfo + val rating: String = "", + @ColumnInfo + val companies: String? = "", +) : Serializable diff --git a/app/src/main/java/com/example/finema/models/genreRequest/Genre.kt b/app/src/main/java/com/example/finema/models/genreRequest/Genre.kt new file mode 100644 index 00000000..ee213e3b --- /dev/null +++ b/app/src/main/java/com/example/finema/models/genreRequest/Genre.kt @@ -0,0 +1,6 @@ +package com.example.finema.models.genreRequest + +data class Genre( + val id: Int, + val name: String +) diff --git a/app/src/main/java/com/example/finema/models/genreRequest/GenreList.kt b/app/src/main/java/com/example/finema/models/genreRequest/GenreList.kt new file mode 100644 index 00000000..38a0d4de --- /dev/null +++ b/app/src/main/java/com/example/finema/models/genreRequest/GenreList.kt @@ -0,0 +1,5 @@ +package com.example.finema.models.genreRequest + +data class GenreList( + val genres: List +) diff --git a/app/src/main/java/com/example/finema/models/infinite/MovieDiscover.kt b/app/src/main/java/com/example/finema/models/infinite/MovieDiscover.kt new file mode 100644 index 00000000..cf453639 --- /dev/null +++ b/app/src/main/java/com/example/finema/models/infinite/MovieDiscover.kt @@ -0,0 +1,14 @@ +package com.example.finema.models.infinite + +import com.google.gson.annotations.SerializedName + +data class MovieDiscover( + @SerializedName("page") + val page: Int, + @SerializedName("results") + val results: List, + @SerializedName("total_pages") + val totalPages: Int, + @SerializedName("total_results") + val totalResults: Int +) diff --git a/app/src/main/java/com/example/finema/models/infinite/MovieDiscoverResult.kt b/app/src/main/java/com/example/finema/models/infinite/MovieDiscoverResult.kt new file mode 100644 index 00000000..847b08ab --- /dev/null +++ b/app/src/main/java/com/example/finema/models/infinite/MovieDiscoverResult.kt @@ -0,0 +1,34 @@ +package com.example.finema.models.infinite + +import com.google.gson.annotations.SerializedName + +data class MovieDiscoverResult( + @SerializedName("adult") + val adult: Boolean, + @SerializedName("backdrop_path") + val backdropPath: String, + @SerializedName("genre_ids") + val genreIds: List, + @SerializedName("id") + val id: Int, + @SerializedName("original_language") + val originalLanguage: String, + @SerializedName("original_title") + val originalTitle: String, + @SerializedName("overview") + val overview: String, + @SerializedName("popularity") + val popularity: Double, + @SerializedName("poster_path") + val posterPath: String, + @SerializedName("release_date") + val releaseDate: String, + @SerializedName("title") + val title: String, + @SerializedName("video") + val video: Boolean, + @SerializedName("vote_average") + val voteAverage: Double, + @SerializedName("vote_count") + val voteCount: Int +) diff --git a/app/src/main/java/com/example/finema/models/movieResponse/Movie.kt b/app/src/main/java/com/example/finema/models/movieResponse/Movie.kt new file mode 100644 index 00000000..ed0e273e --- /dev/null +++ b/app/src/main/java/com/example/finema/models/movieResponse/Movie.kt @@ -0,0 +1,37 @@ +package com.example.finema.models.movieResponse + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class Movie( + @SerializedName("adult") + val adult: Boolean, + @SerializedName("backdrop_path") + val backdropPath: String, + @SerializedName("genre_ids") + val genreIds: List, + @SerializedName("id") + val id: Int, + @SerializedName("original_language") + val originalLanguage: String, + @SerializedName("original_title") + val originalTitle: String, + @SerializedName("overview") + val overview: String, + @SerializedName("popularity") + val popularity: Double, + @SerializedName("poster_path") + val posterPath: String, + @SerializedName("release_date") + val releaseDate: String, + @SerializedName("title") + val title: String, + @SerializedName("video") + val video: Boolean, + @SerializedName("vote_average") + val voteAverage: Double, + @SerializedName("vote_count") + val voteCount: Int, +) : Parcelable diff --git a/app/src/main/java/com/example/finema/models/movieResponse/MovieDetails.kt b/app/src/main/java/com/example/finema/models/movieResponse/MovieDetails.kt new file mode 100644 index 00000000..f066aa71 --- /dev/null +++ b/app/src/main/java/com/example/finema/models/movieResponse/MovieDetails.kt @@ -0,0 +1,60 @@ +package com.example.finema.models.movieResponse + +import com.example.finema.models.genreRequest.Genre +import com.google.gson.annotations.SerializedName + +data class MovieDetails( + @SerializedName("adult") + val adult: Boolean, + @SerializedName("backdrop_path") + val backdropPath: String, +// @SerializedName("belongs_to_collection") +// val belongsToCollection : String, + @SerializedName("budget") + val budget: Int, + @SerializedName("genres") + val genres: List, + @SerializedName("homepage") + val homepage: String, + @SerializedName("id") + val id: Int, + @SerializedName("imdb_id") + val imdbId: String, + @SerializedName("original_language") + val originalLanguage: String, + @SerializedName("original_title") + val originalTitle: String, + @SerializedName("overview") + val overview: String, + @SerializedName("popularity") + val popularity: Double, + @SerializedName("poster_path") + var posterPath: String, + @SerializedName("production_companies") + val productionCompanies: List, +// @SerializedName("production_countries") +// val productionCountries : List, + @SerializedName("release_date") + val releaseDate: String, + @SerializedName("revenue") + val revenue: Int, + @SerializedName("runtime") + val runtime: Int, +// @SerializedName("spoken_languages") +// val spokenLanguages : List, + @SerializedName("status") + val status: String, + @SerializedName("tagline") + val tagline: String, + @SerializedName("title") + val title: String, + @SerializedName("video") + val video: Boolean, + @SerializedName("vote_average") + val voteAverage: Double, + @SerializedName("vote_count") + val voteCount: Int, + + var stringGenres: String?, + var stringCompanies: String? +) diff --git a/app/src/main/java/com/example/finema/models/movieResponse/MovieResponse.kt b/app/src/main/java/com/example/finema/models/movieResponse/MovieResponse.kt new file mode 100644 index 00000000..291a721a --- /dev/null +++ b/app/src/main/java/com/example/finema/models/movieResponse/MovieResponse.kt @@ -0,0 +1,11 @@ +package com.example.finema.models.movieResponse + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Parcelize +data class MovieResponse( + @SerializedName("results") + var movies: List +) : Parcelable diff --git a/app/src/main/java/com/example/finema/models/movieResponse/MovieResponseFromList.kt b/app/src/main/java/com/example/finema/models/movieResponse/MovieResponseFromList.kt new file mode 100644 index 00000000..b7995537 --- /dev/null +++ b/app/src/main/java/com/example/finema/models/movieResponse/MovieResponseFromList.kt @@ -0,0 +1,11 @@ +package com.example.finema.models.movieResponse + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Parcelize +data class MovieResponseFromList( + @SerializedName("items") + var movies: List +) : Parcelable diff --git a/app/src/main/java/com/example/finema/models/movieResponse/MovieTrailer.kt b/app/src/main/java/com/example/finema/models/movieResponse/MovieTrailer.kt new file mode 100644 index 00000000..db4571e7 --- /dev/null +++ b/app/src/main/java/com/example/finema/models/movieResponse/MovieTrailer.kt @@ -0,0 +1,26 @@ +package com.example.finema.models.movieResponse + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Parcelize +data class MovieTrailer( + @SerializedName("id") + val id: String, + + @SerializedName("key") + val key: String, + + @SerializedName("name") + val name: String, + + @SerializedName("site") + val site: String, + + @SerializedName("size") + val size: Int, + + @SerializedName("type") + val type: String +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/example/finema/models/movieResponse/ProductionCompanies.kt b/app/src/main/java/com/example/finema/models/movieResponse/ProductionCompanies.kt new file mode 100644 index 00000000..9e88e95f --- /dev/null +++ b/app/src/main/java/com/example/finema/models/movieResponse/ProductionCompanies.kt @@ -0,0 +1,14 @@ +package com.example.finema.models.movieResponse + +import com.google.gson.annotations.SerializedName + +data class ProductionCompanies( + @SerializedName("id") + val id: Int, + @SerializedName("logo_path") + val logoPath: String, + @SerializedName("name") + val name: String, + @SerializedName("origin_country") + val originCountry: String +) diff --git a/app/src/main/java/com/example/finema/models/movieResponse/TrailersList.kt b/app/src/main/java/com/example/finema/models/movieResponse/TrailersList.kt new file mode 100644 index 00000000..78717a40 --- /dev/null +++ b/app/src/main/java/com/example/finema/models/movieResponse/TrailersList.kt @@ -0,0 +1,8 @@ +package com.example.finema.models.movieResponse + +import com.google.gson.annotations.SerializedName + +data class TrailersList( + @SerializedName("results") + val trailers: List +) diff --git a/app/src/main/java/com/example/finema/repositories/AppPreference.kt b/app/src/main/java/com/example/finema/repositories/AppPreference.kt new file mode 100644 index 00000000..266926f1 --- /dev/null +++ b/app/src/main/java/com/example/finema/repositories/AppPreference.kt @@ -0,0 +1,142 @@ +package com.example.finema.repositories + +import android.content.Context +import android.content.SharedPreferences +import android.util.Log +import com.google.firebase.auth.FirebaseAuth + +// TODO Сделать репозиторием с интерфейсом +class AppPreference( + override val context: Context +) : IAppPreference { + + companion object { + private const val INIT_USER = "initUser" + private const val NAME_PREF = "preference" + private const val NUM_FILMS = "numFilms" + private const val TOURNAMENT_TYPE = "tournamentType" + private const val GENRE_ID = "genre_id" + private const val GENRE_NAME = "genre_name" + private const val CATEGORY_LINK = "category_link" + private const val CATEGORY_NAME = "category_name" + private const val FRAGMENT = "fragment" + private const val FIRST_SIGN_IN = "firstSignIn" + private const val GUEST_OR_AUTH = "guestOrAuth" + private const val DEFAULT_NULL_VAL = "" + + private const val DEFAULT_NUM_VAL = 8 + private const val DEFAULT_NUM__CAT_VAL = -1 + } + + override val googleUser: FirebaseAuth = FirebaseAuth.getInstance() + override lateinit var mPreferences: SharedPreferences + + override fun getPreference() { + mPreferences = context.getSharedPreferences(NAME_PREF, Context.MODE_PRIVATE) + } + + override fun setInitUser(init: Boolean) { + mPreferences.edit() + .putBoolean(INIT_USER, init) + .apply() + } + + override fun getInitUser(): Boolean { + return mPreferences.getBoolean(INIT_USER, false) + } + + override fun googleUserSignOut() { + googleUser.signOut() + Log.d("OJOF", googleUser.currentUser?.displayName.orEmpty()) + } + + override fun getNumOfFilms(): Int { + return mPreferences.getInt(NUM_FILMS, DEFAULT_NUM_VAL) + } + + override fun setNumOfFilms(num: Int) { + mPreferences.edit() + .putInt(NUM_FILMS, num) + .apply() + } + + override fun getTournamentType(): String? { + return mPreferences.getString(TOURNAMENT_TYPE, DEFAULT_NULL_VAL) + } + + override fun setTournamentType(type: String) { + mPreferences.edit() + .putString(TOURNAMENT_TYPE, type) + .apply() + } + + override fun getGenreId(): String? { + return mPreferences.getString(GENRE_ID, DEFAULT_NULL_VAL) + } + + override fun setGenre(genre: String) { + mPreferences.edit() + .putString(GENRE_ID, genre) + .apply() + } + + override fun getGenreName(): String? { + return mPreferences.getString(GENRE_NAME, DEFAULT_NULL_VAL) + } + + override fun setGenreName(genre: String) { + mPreferences.edit() + .putString(GENRE_NAME, genre) + .apply() + } + + override fun getCategoryLink(): Int { + return mPreferences.getInt(CATEGORY_LINK, DEFAULT_NUM__CAT_VAL) + } + + override fun setCategoryLink(categoryLink: Int) { + mPreferences.edit() + .putInt(CATEGORY_LINK, categoryLink) + .apply() + } + + override fun getCategoryName(): String? { + return mPreferences.getString(CATEGORY_NAME, DEFAULT_NULL_VAL) + } + + override fun setCategoryName(categoryName: String) { + mPreferences.edit() + .putString(CATEGORY_NAME, categoryName) + .apply() + } + + override fun setFragment(name: String) { + mPreferences.edit() + .putString(FRAGMENT, name) + .apply() + } + + override fun getFragment(): String? { + return mPreferences.getString(FRAGMENT, DEFAULT_NULL_VAL) + } + + override fun setFirstSignIn(init: Boolean) { + mPreferences.edit() + .putBoolean(FIRST_SIGN_IN, init) + .apply() + } + + override fun getFirstSignIn(): Boolean { + return mPreferences.getBoolean(FIRST_SIGN_IN, false) + } + + override fun getGuestOrAuth(): String? { + return mPreferences.getString(GUEST_OR_AUTH, DEFAULT_NULL_VAL) + } + + override fun setGuestOrAuth(name: String) { + mPreferences.edit() + .putString(GUEST_OR_AUTH, name) + .apply() + } +} diff --git a/app/src/main/java/com/example/finema/repositories/Contract.kt b/app/src/main/java/com/example/finema/repositories/Contract.kt new file mode 100644 index 00000000..d5be022f --- /dev/null +++ b/app/src/main/java/com/example/finema/repositories/Contract.kt @@ -0,0 +1,96 @@ +package com.example.finema.repositories + +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.activity.result.contract.ActivityResultContract +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.auth.api.signin.GoogleSignInClient +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.tasks.Task +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.GoogleAuthProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class Contract : ActivityResultContract() { + + private val _name = MutableStateFlow("") + val name: StateFlow = _name.asStateFlow() + + private val mAuth = FirebaseAuth.getInstance() + private lateinit var googleSignInClient: GoogleSignInClient + + private val gso: GoogleSignInOptions = GoogleSignInOptions + .Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken(defaultWebClientId) + .requestEmail() + .build() + + fun setName(name: String) { + _name.value = name + } + + fun signOut() { + if (this::googleSignInClient.isInitialized) { + googleSignInClient.signOut() + } + } + + override fun createIntent(context: Context, input: Unit): Intent { + googleSignInClient = GoogleSignIn.getClient(context, gso) + + return googleSignInClient.signInIntent + } + + override fun parseResult(resultCode: Int, intent: Intent?) { + activityResult(intent) + } + + private fun activityResult(data: Intent?) { + val task: Task = GoogleSignIn.getSignedInAccountFromIntent(data) + val exception = task.exception + if (task.isSuccessful) { + try { + // Google Sign In was successful, authenticate with Firebase + val account: GoogleSignInAccount? = task.getResult(ApiException::class.java) + Log.d("Sign In Fragment", "firebaseAuthWithGoogle:" + account?.id) + firebaseAuthWithGoogle(account?.idToken!!) + } catch (e: ApiException) { + // Google Sign In failed, update UI appropriately + Log.w("Sign In Fragment", "Google sign in failed", e) + } + } else { + Log.w("Sign In Fragment", exception.toString()) + } + } + + private fun firebaseAuthWithGoogle(idToken: String) { + val credential = GoogleAuthProvider.getCredential(idToken, null) + mAuth.signInWithCredential(credential) + .addOnCompleteListener { task -> + if (task.isSuccessful) { + // Sign in success, update UI with the signed-in user's information + Log.d("firebaseAuthWithGoogle", "signInWithCredential:success") + val user = mAuth.currentUser +// _name.tryEmit(user.displayName!!) + _name.value = user.displayName!! + Log.d("WOW", user.displayName!!) + } else { + // If sign in fails, display a message to the user. + Log.w( + "firebaseAuthWithGoogle", + "signInWithCredential:failure", + task.exception + ) + } + } + } + companion object { + const val defaultWebClientId = + "677958878281-pmcs285i4cdvd5tob7c6ovd2j0msknvs.apps.googleusercontent.com" + } +} diff --git a/app/src/main/java/com/example/finema/repositories/IAppPreference.kt b/app/src/main/java/com/example/finema/repositories/IAppPreference.kt new file mode 100644 index 00000000..592af81d --- /dev/null +++ b/app/src/main/java/com/example/finema/repositories/IAppPreference.kt @@ -0,0 +1,55 @@ +package com.example.finema.repositories + +import android.content.Context +import android.content.SharedPreferences +import com.google.firebase.auth.FirebaseAuth + +interface IAppPreference { + val context: Context + val googleUser: FirebaseAuth + var mPreferences: SharedPreferences + + fun getPreference() + + fun setInitUser(init: Boolean) + + fun getInitUser(): Boolean + + fun googleUserSignOut() + + fun getNumOfFilms(): Int + + fun setNumOfFilms(num: Int) + + fun getTournamentType(): String? + + fun setTournamentType(type: String) + + fun getGenreId(): String? + + fun setGenre(genre: String) + + fun getGenreName(): String? + + fun setGenreName(genre: String) + + fun getCategoryLink(): Int + + fun setCategoryLink(categoryLink: Int) + + fun getCategoryName(): String? + + fun setCategoryName(categoryName: String) + + fun setFragment(name: String) + + fun getFragment(): String? + + fun setFirstSignIn(init: Boolean) + + fun getFirstSignIn(): Boolean + + fun getGuestOrAuth(): String? + + fun setGuestOrAuth(name: String) +} diff --git a/app/src/main/java/com/example/finema/repositories/SafeApiRequest.kt b/app/src/main/java/com/example/finema/repositories/SafeApiRequest.kt new file mode 100644 index 00000000..b506855e --- /dev/null +++ b/app/src/main/java/com/example/finema/repositories/SafeApiRequest.kt @@ -0,0 +1,19 @@ +package com.example.finema.repositories + +import java.io.IOException +import retrofit2.Response + +interface SafeApiRequest { + + suspend fun apiRequest(call: suspend () -> Response): T { + val response = call.invoke() + if (response.isSuccessful) { + return response.body()!! + } else { + // @todo handle api exception + throw ApiException(response.code().toString()) + } + } +} + +class ApiException(message: String) : IOException(message) diff --git a/app/src/main/java/com/example/finema/ui/base/BaseFragment.kt b/app/src/main/java/com/example/finema/ui/base/BaseFragment.kt new file mode 100644 index 00000000..038da631 --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/base/BaseFragment.kt @@ -0,0 +1,33 @@ +package com.example.finema.ui.base + +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.lifecycle.observe +import androidx.viewbinding.ViewBinding +import java.lang.reflect.ParameterizedType + +abstract class BaseFragment : Fragment() { + + protected open lateinit var viewModel: VModel + + protected lateinit var binding: Binding + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + showError() + } + + protected fun showError() { + viewModel.publicErrorMessage.observe(viewLifecycleOwner) { + Toast.makeText(context, it, Toast.LENGTH_LONG).show() + } + } + + @Suppress("UNCHECKED_CAST") + private fun getViewModelClass(): Class { + val type = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] + return type as Class + } +} diff --git a/app/src/main/java/com/example/finema/ui/base/BaseViewModel.kt b/app/src/main/java/com/example/finema/ui/base/BaseViewModel.kt new file mode 100644 index 00000000..51c11a13 --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/base/BaseViewModel.kt @@ -0,0 +1,18 @@ +package com.example.finema.ui.base + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.Job + +open class BaseViewModel : ViewModel() { + + protected lateinit var job: Job + + val publicErrorMessage: LiveData + get() = errorMessage + + private val errorMessage = MutableLiveData() + + fun showError(throwable: Throwable) = errorMessage.postValue("ERROR") +} diff --git a/app/src/main/java/com/example/finema/ui/chooseFavourite/ChooseFavouriteFragment.kt b/app/src/main/java/com/example/finema/ui/chooseFavourite/ChooseFavouriteFragment.kt new file mode 100644 index 00000000..ec3925d3 --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/chooseFavourite/ChooseFavouriteFragment.kt @@ -0,0 +1,68 @@ +package com.example.finema.ui.chooseFavourite + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.widget.doAfterTextChanged +import androidx.lifecycle.lifecycleScope +import androidx.navigation.Navigation +import androidx.recyclerview.widget.LinearLayoutManager +import com.example.finema.R +import com.example.finema.databinding.ChooseFavouriteFragmentBinding +import com.example.finema.ui.base.BaseFragment +import com.example.finema.util.hideKeyboard +import kotlinx.coroutines.flow.collectLatest +import org.koin.androidx.viewmodel.ext.android.getViewModel + +class ChooseFavouriteFragment : + BaseFragment(), + MovieAdapter.CharacterViewHolder.Listener { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ChooseFavouriteFragmentBinding.inflate(inflater, container, false) + + return binding.root + } + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle? + ) { + viewModel = getViewModel() + super.onViewCreated(view, savedInstanceState) + + val adapterMovs = MovieAdapter(this) + + binding.movs.apply { + layoutManager = LinearLayoutManager(context) + adapter = adapterMovs + } + + binding.query.doAfterTextChanged { text -> + viewModel.setQuery(text?.toString() ?: "") + } + + viewLifecycleOwner.lifecycleScope.launchWhenCreated { + viewModel.movies.collectLatest { pagingData -> + adapterMovs.submitData(pagingData) + } + } + } + + override fun onMovieClicked(index: Int) { + goDetailsFragment(index.toLong()) + binding.query.hideKeyboard() + } + + private fun goDetailsFragment(filmIdInfo: Long) { + val bundle = Bundle() + bundle.putSerializable("filmId", filmIdInfo) + Navigation.findNavController(requireActivity(), R.id.fragment) + .navigate(R.id.action_chooseFavouriteFragment_to_fragmentFilm, bundle) + } +} diff --git a/app/src/main/java/com/example/finema/ui/chooseFavourite/ChooseFavouriteViewModel.kt b/app/src/main/java/com/example/finema/ui/chooseFavourite/ChooseFavouriteViewModel.kt new file mode 100644 index 00000000..8c3bc3d2 --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/chooseFavourite/ChooseFavouriteViewModel.kt @@ -0,0 +1,48 @@ +package com.example.finema.ui.chooseFavourite + +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.example.finema.api.IMoviesRepository +import com.example.finema.models.infinite.MovieDiscoverResult +import com.example.finema.ui.base.BaseViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +class ChooseFavouriteViewModel( + private val repository: IMoviesRepository +) : BaseViewModel() { + + private val _query = MutableStateFlow("") + val query: StateFlow = _query.asStateFlow() + + val movies: Flow> = query + .map(::newPager) + // TODO Попробовать с map + .flatMapLatest { pager -> pager.flow.cachedIn(viewModelScope) } + .stateIn(viewModelScope, SharingStarted.Lazily, PagingData.empty()) // TODO ?????? + + private fun newPager(query: String): Pager { + return Pager( + PagingConfig( + pageSize = 20, + maxSize = 100, + enablePlaceholders = false + ) + ) { + MoviePagingSource(repository.api, query) + } + } + + fun setQuery(query: String) { + _query.tryEmit(query) + } +} diff --git a/app/src/main/java/com/example/finema/ui/chooseFavourite/MovieAdapter.kt b/app/src/main/java/com/example/finema/ui/chooseFavourite/MovieAdapter.kt new file mode 100644 index 00000000..b5eb652a --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/chooseFavourite/MovieAdapter.kt @@ -0,0 +1,72 @@ +package com.example.finema.ui.chooseFavourite + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.example.finema.R +import com.example.finema.models.infinite.MovieDiscoverResult +import com.example.finema.util.downloadAndSetImageUrl +import com.google.android.material.card.MaterialCardView + +class MovieAdapter( + private val listener: CharacterViewHolder.Listener, +) : + PagingDataAdapter(CharacterComparator) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): CharacterViewHolder { + return CharacterViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.nice, parent, false), + listener + ) + } + + override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) { + getItem(position)?.let { holder.bind(it) } + } + + class CharacterViewHolder( + view: View, + private val listener: Listener + ) : + RecyclerView.ViewHolder(view) { + private val filmTitle: TextView = view.findViewById(R.id.tv_name) + private val movieCard: MaterialCardView = view.findViewById(R.id.movie_title) + private val moviePoster: ImageView = view.findViewById(R.id.imageViewNice) + + interface Listener { + fun onMovieClicked(index: Int) + } + + fun bind(item: MovieDiscoverResult) { + filmTitle.text = item.title + moviePoster.downloadAndSetImageUrl( + POSTER_BASE_URL + item.posterPath + ) + movieCard.setOnClickListener { + listener.onMovieClicked(item.id) + } + } + } + + object CharacterComparator : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: MovieDiscoverResult, newItem: MovieDiscoverResult) = + oldItem.id == newItem.id + + override fun areContentsTheSame( + oldItem: MovieDiscoverResult, + newItem: MovieDiscoverResult + ) = + oldItem == newItem + } + + companion object { + const val POSTER_BASE_URL = "https://image.tmdb.org/t/p/w342" + } +} diff --git a/app/src/main/java/com/example/finema/ui/chooseFavourite/MoviePagingSource.kt b/app/src/main/java/com/example/finema/ui/chooseFavourite/MoviePagingSource.kt new file mode 100644 index 00000000..21f771db --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/chooseFavourite/MoviePagingSource.kt @@ -0,0 +1,46 @@ +package com.example.finema.ui.chooseFavourite + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.example.finema.api.MoviesApi +import com.example.finema.models.infinite.MovieDiscoverResult +import java.lang.NullPointerException +import retrofit2.HttpException + +class MoviePagingSource( + private val api: MoviesApi, + private val query: String +) : PagingSource() { + + override suspend fun load(params: LoadParams): + LoadResult { + return try { + val pageNumber = params.key ?: INITIAL_PAGE_NUMBER + val response = api.everything(pageNumber, query) + + if (response.isSuccessful) { + val movies = response.body()!!.results + val nextPageNumber = if (movies.isEmpty()) null else pageNumber + 1 + val prevPageNumber = if (pageNumber > 1) pageNumber - 1 else null + LoadResult.Page(movies, prevPageNumber, nextPageNumber) + } else { + LoadResult.Error(HttpException(response)) + } + } catch (e: HttpException) { + LoadResult.Error(e) + } catch (e: NullPointerException) { + LoadResult.Error(e) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + val anchorPage = state.closestPageToPosition(anchorPosition) + anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) + } + } + + companion object { + const val INITIAL_PAGE_NUMBER = 1 + } +} diff --git a/app/src/main/java/com/example/finema/ui/favourite/FavouriteAdapter.kt b/app/src/main/java/com/example/finema/ui/favourite/FavouriteAdapter.kt new file mode 100644 index 00000000..1a85388a --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/favourite/FavouriteAdapter.kt @@ -0,0 +1,57 @@ +package com.example.finema.ui.favourite + +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.example.finema.R +import com.example.finema.models.databaseModels.MovieModel +import com.example.finema.util.downloadAndSetImageUrl + +class FavouriteAdapter( + private val navigateToMovie: (Long) -> Unit +) : + RecyclerView.Adapter() { + + private var movies: List = emptyList() + + override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): MovieViewHolder { + val view = LayoutInflater.from(viewGroup.context) + .inflate(R.layout.favourite_movie_item, viewGroup, false) + + return MovieViewHolder(view) + } + + override fun onBindViewHolder(viewholder: MovieViewHolder, position: Int) { + Log.d("gypsy", position.toString()) + viewholder.bind(movies[position], navigateToMovie) + } + + override fun getItemCount() = movies.size + + fun update(movies: List) { + this.movies = movies + notifyDataSetChanged() + } + + class MovieViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val imageMovie: ImageView = view.findViewById(R.id.imageMovie) as ImageView + val filmTitle: TextView = view.findViewById(R.id.filmTitle) + val rating: TextView = view.findViewById(R.id.rating) + val genre: TextView = view.findViewById(R.id.genre) + + fun bind(movie: MovieModel, navigateToMovie: (Long) -> Unit) = itemView.apply { + imageMovie.downloadAndSetImageUrl( + movie.imageUrl + ) + filmTitle.text = movie.title + rating.text = movie.rating + genre.text = movie.genres + + setOnClickListener { navigateToMovie(movie.id) } + } + } +} diff --git a/app/src/main/java/com/example/finema/ui/favourite/FavouriteFragment.kt b/app/src/main/java/com/example/finema/ui/favourite/FavouriteFragment.kt new file mode 100644 index 00000000..90cc1ddb --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/favourite/FavouriteFragment.kt @@ -0,0 +1,74 @@ +package com.example.finema.ui.favourite + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import com.example.finema.R +import com.example.finema.databinding.FavouriteFragmentBinding +import com.example.finema.ui.base.BaseFragment +import com.google.android.flexbox.AlignItems +import com.google.android.flexbox.FlexDirection +import com.google.android.flexbox.FlexboxLayoutManager +import com.google.android.flexbox.JustifyContent +import org.koin.androidx.viewmodel.ext.android.getViewModel + +class FavouriteFragment : BaseFragment() { + + private val favouriteAdapter = FavouriteAdapter { + val action = FavouriteFragmentDirections.actionFragmentFavouriteToFragmentFilm(it) + findNavController().navigate(action) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FavouriteFragmentBinding.inflate(inflater, container, false) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + viewModel = getViewModel() + super.onViewCreated(view, savedInstanceState) + + content() + + binding.choose.setOnClickListener { + findNavController().navigate(R.id.action_fragmentFavourite_to_chooseFavouriteFragment) + } + } + + private fun content() { + viewModel.favouriteMovies.observe( + viewLifecycleOwner, + { + it?.let { + binding.searchLoader.visibility = View.INVISIBLE + binding.moviesList.visibility = View.VISIBLE + favouriteAdapter.update(it) + + if (it.isEmpty()) { + binding.choose.visibility = View.VISIBLE + } else { + binding.choose.visibility = View.INVISIBLE + } + + binding.moviesList.layoutManager = flexBox() + binding.moviesList.adapter = favouriteAdapter + } + } + ) + } + + private fun flexBox(): FlexboxLayoutManager { + val flex = FlexboxLayoutManager(context) + flex.flexDirection = FlexDirection.ROW + flex.alignItems = AlignItems.CENTER + flex.justifyContent = JustifyContent.CENTER + return flex + } +} diff --git a/app/src/main/java/com/example/finema/ui/favourite/FavouriteViewModel.kt b/app/src/main/java/com/example/finema/ui/favourite/FavouriteViewModel.kt new file mode 100644 index 00000000..2ae5966b --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/favourite/FavouriteViewModel.kt @@ -0,0 +1,20 @@ +package com.example.finema.ui.favourite + +import android.util.Log +import androidx.lifecycle.LiveData +import com.example.finema.database.DatabaseRepository +import com.example.finema.models.databaseModels.MovieModel +import com.example.finema.ui.base.BaseViewModel + +class FavouriteViewModel( + private val dbRepository: DatabaseRepository +) : BaseViewModel() { + + val favouriteMovies: LiveData> = dbRepository.allFavourites + + init { + Log.d("gypsy", "DB") + + Log.d("gypsy", favouriteMovies.value.toString()) + } +} diff --git a/app/src/main/java/com/example/finema/ui/higherlower/HigherLowerFragment.kt b/app/src/main/java/com/example/finema/ui/higherlower/HigherLowerFragment.kt new file mode 100644 index 00000000..0f12cfde --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/higherlower/HigherLowerFragment.kt @@ -0,0 +1,134 @@ +package com.example.finema.ui.higherlower + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import com.example.finema.R +import com.example.finema.databinding.HigherLowerFragmentBinding +import com.example.finema.models.movieResponse.MovieResponse +import com.example.finema.ui.base.BaseFragment +import com.example.finema.util.downloadAndSetImageUrl +import org.koin.androidx.viewmodel.ext.android.getViewModel + +class HigherLowerFragment : BaseFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = HigherLowerFragmentBinding.inflate(inflater, container, false) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + viewModel = getViewModel() + super.onViewCreated(view, savedInstanceState) + + viewModel.movies.observe( + viewLifecycleOwner, + { movieList -> + if (viewModel.score == 0) { + viewModel.shuffle() + } + binding.txtFilm1.text = movieList.movies[viewModel.img1].title + binding.txtFilm2.text = movieList.movies[viewModel.img2].title + binding.points.text = getString(R.string.n_points, viewModel.score) + + setImage(binding.img1, movieList, viewModel.img1) + setImage(binding.img2, movieList, viewModel.img2) + + fillInBookmarks(binding.txtFilm1, binding.bookmark1) + fillInBookmarks(binding.txtFilm2, binding.bookmark2) + } + ) + setBookmarkClickListeners(binding.bookmark1, binding.txtFilm1, 0) + setBookmarkClickListeners(binding.bookmark2, binding.txtFilm2, 1) + } + + private fun setImage(image: ImageView, movieList: MovieResponse, imgInd: Int) { + image.downloadAndSetImageUrl( + getString( + R.string.poster_base_url, + movieList.movies[imgInd].posterPath + ) + ) + image.setOnClickListener { + viewModel.onMovieClicked(imgInd) + binding.points.text = getString( + R.string.higher_lower_score, + viewModel.score + ) + } + } + + private fun setBookmarkClickListeners(bookmark: ImageButton, title: TextView, position: Int) { + bookmark.setOnClickListener { + animateBookmark(bookmark) + addODelFav(title, position) + } + } + + private fun fillInBookmarks(title: TextView, bookmark: ImageButton) { + viewModel.favouriteMovies.observe( + viewLifecycleOwner, + { + var counter = 0 + for (i in it) { + counter += 1 + if (title.text == i.title || title.text == i.originalTitle) { + bookmark.setImageResource( + R.drawable.bookmark_24 + ) + break + } + if (counter == it.size) { + bookmark.setImageResource( + R.drawable.bookmark_border_24 + ) + } + } + } + ) + } + + private fun addODelFav(title: TextView, position: Int) { + viewModel.favouriteMovies.value?.let { + var counter = 0 + for (i in it) { + counter += 1 + if (title.text == i.title) { + viewModel.removeFromFav(position) + break + } + if (it.size == counter) { + viewModel.addToFav(position) + } + } + } + } + + private fun animateBookmark(bookmark: ImageButton) { + bookmark.animate().apply { + duration = ANIMATION_DURATION + scaleXBy(ANIMATION_ROTATION) + scaleYBy(ANIMATION_ROTATION) + }.withEndAction { + bookmark.animate().apply { + duration = ANIMATION_DURATION + scaleXBy(-ANIMATION_ROTATION) + scaleYBy(-ANIMATION_ROTATION) + } + } + } + + companion object { + private const val ANIMATION_DURATION = 250L + private const val ANIMATION_ROTATION = 1f + } +} diff --git a/app/src/main/java/com/example/finema/ui/higherlower/HigherLowerViewModel.kt b/app/src/main/java/com/example/finema/ui/higherlower/HigherLowerViewModel.kt new file mode 100644 index 00000000..ccb85e19 --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/higherlower/HigherLowerViewModel.kt @@ -0,0 +1,161 @@ +package com.example.finema.ui.higherlower + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.example.finema.api.IMoviesRepository +import com.example.finema.database.DatabaseRepository +import com.example.finema.database.firebase.IFirebaseRepository +import com.example.finema.models.databaseModels.MovieModel +import com.example.finema.models.movieResponse.Movie +import com.example.finema.models.movieResponse.MovieResponse +import com.example.finema.repositories.IAppPreference +import com.example.finema.ui.base.BaseViewModel +import com.example.finema.ui.tournaments.tournament.TournamentVM +import com.example.finema.util.Coroutines +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class HigherLowerViewModel( + private val repository: IMoviesRepository, + private val dbRepository: DatabaseRepository, + private val fbRepository: IFirebaseRepository, + private val appPreference: IAppPreference +) : BaseViewModel() { + + val favouriteMovies: LiveData> = dbRepository.allFavourites + + private var _movies = MutableLiveData() + val movies: LiveData + get() = _movies + + var score = RESET_SCORE_INDEX + var img1 = IMG1_INDEX + var img2 = IMG2_INDEX + private var page = DEFAULT_PAGE_INDEX + + init { + getMovies() + } + + fun shuffle() { + _movies.value?.movies.let { + _movies.value?.movies = it?.shuffled()!! + } + } + + private fun getMovies() { + job = Coroutines.ioThenMan( + { repository.getMovies(page) }, + { _movies.value = it } + ) + } + + private fun clickedRight() { + score += ADD_SCORE_POINT + changeMovRes() + } + + private fun clickedWrong() { + score = RESET_SCORE_INDEX + page = DEFAULT_PAGE_INDEX + getMovies() + } + + private fun changeMovRes() { + if (score % MOVIE_SIZE_RESET == 0) { + page += NEXT_PAGE + getMovies() + } else { + _movies.value?.movies = _movies.value?.movies?.drop(1)!! + _movies.value = _movies.value + } + } + + fun onMovieClicked(position: Int) { + when (position) { + img1 -> + if (movies.value?.movies?.get(img1)?.popularity!! + >= movies.value?.movies?.get(img2)?.popularity!! + ) { + clickedRight() + } else { + clickedWrong() + } + img2 -> + if (movies.value?.movies?.get(img2)?.popularity!! + >= movies.value?.movies?.get(img1)?.popularity!! + ) { + clickedRight() + } else { + clickedWrong() + } + } + } + + fun addToFav(position: Int) { + when (position) { + 0 -> { + insert(_movies.value?.movies?.get(0)!!) + } + 1 -> { + insert(_movies.value?.movies?.get(1)!!) + } + } + } + + fun removeFromFav(position: Int) { + when (position) { + 0 -> { + delete(_movies.value?.movies?.get(0)!!) + } + 1 -> { + delete(_movies.value?.movies?.get(1)!!) + } + } + } + + private fun insert(movie: Movie) { + viewModelScope.launch(Dispatchers.Main) { + dbRepository.insertFavourite( + makeMovieModel(movie) + ) {} + if (appPreference.getGuestOrAuth() == "AUTH") { + fbRepository.insertFirebaseFavouriteFilm(makeMovieModel(movie)) + } + } + } + + private fun delete(movie: Movie) { + viewModelScope.launch(Dispatchers.Main) { + dbRepository.deleteFavourite( + makeMovieModel(movie) + ) {} + if (appPreference.getGuestOrAuth() == "AUTH") { + fbRepository.deleteFirebaseFavouriteFilm(makeMovieModel(movie)) + } + } + } + + private fun makeMovieModel(movie: Movie) = + MovieModel( + movie.id.toLong(), + movie.title, + movie.originalTitle, + TournamentVM.POSTER_BASE_URL + movie.posterPath, + movie.overview, + null, + movie.voteAverage.toString(), + null + ) + + companion object { + const val DEFAULT_PAGE_INDEX = 1 + const val RESET_SCORE_INDEX = 0 + const val ADD_SCORE_POINT = 1 + const val NEXT_PAGE = 1 + const val MOVIE_SIZE_RESET = 19 + const val IMG1_INDEX = 0 + const val IMG2_INDEX = 1 + } +} diff --git a/app/src/main/java/com/example/finema/ui/higherlowerrating/HigherLowerRatingFragment.kt b/app/src/main/java/com/example/finema/ui/higherlowerrating/HigherLowerRatingFragment.kt new file mode 100644 index 00000000..00ae64e1 --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/higherlowerrating/HigherLowerRatingFragment.kt @@ -0,0 +1,137 @@ +package com.example.finema.ui.higherlowerrating + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import com.example.finema.R +import com.example.finema.databinding.HigherLowerRatingFragmentBinding +import com.example.finema.models.movieResponse.MovieResponse +import com.example.finema.ui.base.BaseFragment +import com.example.finema.util.downloadAndSetImageUrl +import org.koin.androidx.viewmodel.ext.android.getViewModel + +class HigherLowerRatingFragment : + BaseFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = HigherLowerRatingFragmentBinding.inflate(inflater, container, false) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + viewModel = getViewModel() + super.onViewCreated(view, savedInstanceState) + + viewModel.movies.observe( + viewLifecycleOwner, + { movieList -> + if (viewModel.score == 0) { + viewModel.shuffle() + } + binding.txtFilm1.text = movieList.movies[viewModel.img1].title + binding.txtFilm2.text = movieList.movies[viewModel.img2].title + binding.points.text = getString(R.string.n_points, viewModel.score) + binding.txtRating.text = movieList.movies[viewModel.img1].voteAverage.toString() + + setImage(binding.img1, movieList, viewModel.img1) + setImage(binding.img2, movieList, viewModel.img2) + + fillInBookmarks(binding.txtFilm1, binding.bookmark1) + fillInBookmarks(binding.txtFilm2, binding.bookmark2) + } + ) + + setBookmarkClickListeners(binding.bookmark1, binding.txtFilm1, 0) + setBookmarkClickListeners(binding.bookmark2, binding.txtFilm2, 1) + } + + private fun setImage(image: ImageView, movieList: MovieResponse, imgInd: Int) { + image.downloadAndSetImageUrl( + getString( + R.string.poster_base_url, + movieList.movies[imgInd].posterPath + ) + ) + image.setOnClickListener { + viewModel.onMovieClicked(imgInd) + binding.points.text = getString( + R.string.higher_lower_score, + viewModel.score + ) + } + } + + private fun setBookmarkClickListeners(bookmark: ImageButton, title: TextView, position: Int) { + bookmark.setOnClickListener { + animateBookmark(bookmark) + addODelFav(title, position) + } + } + + private fun fillInBookmarks(txtview: TextView, bookmark: ImageButton) { + viewModel.favouriteMovies.observe( + viewLifecycleOwner, + { + var counter = 0 + for (i in it) { + counter += 1 + if (txtview.text == i.title) { + bookmark.setImageResource( + R.drawable.bookmark_24 + ) + break + } + if (counter == it.size) { + bookmark.setImageResource( + R.drawable.bookmark_border_24 + ) + } + } + } + ) + } + + private fun addODelFav(title: TextView, position: Int) { + viewModel.favouriteMovies.value?.let { + var counter = 0 + for (i in it) { + counter += 1 + if (title.text == i.title || title.text == i.originalTitle) { + viewModel.removeFromFav(position) + break + } + if (it.size == counter) { + viewModel.addToFav(position) + } + } + } + } + + private fun animateBookmark(bookmark: ImageButton) { + bookmark.animate().apply { + duration = ANIMATION_DURATION + scaleXBy(ANIMATION_ROTATION) + scaleYBy(ANIMATION_ROTATION) + }.withEndAction { + bookmark.animate().apply { + duration = ANIMATION_DURATION + scaleXBy(-ANIMATION_ROTATION) + scaleYBy(-ANIMATION_ROTATION) + } + } + } + + companion object { + private const val ANIMATION_DURATION = 250L + private const val ANIMATION_ROTATION = 1f + } +} diff --git a/app/src/main/java/com/example/finema/ui/higherlowerrating/HigherLowerRatingViewModel.kt b/app/src/main/java/com/example/finema/ui/higherlowerrating/HigherLowerRatingViewModel.kt new file mode 100644 index 00000000..5ce250c2 --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/higherlowerrating/HigherLowerRatingViewModel.kt @@ -0,0 +1,173 @@ +package com.example.finema.ui.higherlowerrating + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.example.finema.api.IMoviesRepository +import com.example.finema.database.DatabaseRepository +import com.example.finema.database.firebase.IFirebaseRepository +import com.example.finema.models.databaseModels.MovieModel +import com.example.finema.models.movieResponse.Movie +import com.example.finema.models.movieResponse.MovieResponse +import com.example.finema.repositories.IAppPreference +import com.example.finema.ui.base.BaseViewModel +import com.example.finema.ui.tournaments.tournament.TournamentVM +import com.example.finema.util.Coroutines +import kotlin.random.Random +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class HigherLowerRatingViewModel( + private val repository: IMoviesRepository, + private val dbRepository: DatabaseRepository, + private val fbRepository: IFirebaseRepository, + private val appPreference: IAppPreference +) : BaseViewModel() { + + val favouriteMovies: LiveData> = dbRepository.allFavourites + + private var _movies = MutableLiveData() + val movies: LiveData + get() = _movies + + var score = RESET_SCORE_INDEX + var img1 = IMG1_INDEX + var img2 = IMG2_INDEX + private var page = DEFAULT_PAGE_INDEX + + init { + getMovies() + } + + fun shuffle() { + _movies.value?.movies.let { + _movies.value?.movies = it?.shuffled()!! + } + } + + private fun getMovies() { + job = Coroutines.ioThenMan( + { repository.getMovies(page) }, + { _movies.value = it } + ) + } + + private fun clickedRight() { + score += ADD_SCORE_POINT + changeMovRes() + } + + private fun clickedWrong() { + score = RESET_SCORE_INDEX + page = Random.nextInt(DEFAULT_PAGE_INDEX, LAST_PAGE) + getMovies() + } + + private fun changeMovRes() { + if (score % MOVIE_SIZE_RESET == 0) { + page += NEXT_PAGE + getMovies() + } else { + _movies.value?.movies = _movies.value?.movies?.drop(1)!! + _movies.value = _movies.value + } + } + + fun onMovieClicked(position: Int) { + when (position) { + img1 -> + if (movies.value?.movies?.get(img1)?.voteAverage!! + >= movies.value?.movies?.get(img2)?.voteAverage!! + ) { + clickedRight() + } else { + clickedWrong() + } + img2 -> + if (movies.value?.movies?.get(img2)?.voteAverage!! + >= movies.value?.movies?.get(img1)?.voteAverage!! + ) { + clickedRight() + } else { + clickedWrong() + } + } + } + + fun addToFav(position: Int) { + when (position) { + 0 -> { + insert(_movies.value?.movies?.get(0)!!) + } + 1 -> { + insert(_movies.value?.movies?.get(1)!!) + } + } + } + + fun removeFromFav(position: Int) { + when (position) { + 0 -> { + delete(_movies.value?.movies?.get(0)!!) + } + 1 -> { + delete(_movies.value?.movies?.get(1)!!) + } + } + } + + private fun insert(movie: Movie) { + viewModelScope.launch(Dispatchers.Main) { + dbRepository.insertFavourite( + makeMovieModel(movie) + ) { + } + if (appPreference.getGuestOrAuth() == "AUTH") { + viewModelScope.launch(Dispatchers.Main) { + fbRepository.insertFirebaseFavouriteFilm(makeMovieModel(movie)) + } + } + } + + viewModelScope.launch(Dispatchers.Main) { + fbRepository.insertFirebaseFavouriteFilm(makeMovieModel(movie)) + } + } + + private fun delete(movie: Movie) { + viewModelScope.launch(Dispatchers.Main) { + dbRepository.deleteFavourite( + makeMovieModel(movie) + ) { + } + } + if (appPreference.getGuestOrAuth() == "AUTH") { + viewModelScope.launch(Dispatchers.Main) { + fbRepository.deleteFirebaseFavouriteFilm(makeMovieModel(movie)) + } + } + } + + private fun makeMovieModel(movie: Movie) = + MovieModel( + movie.id.toLong(), + movie.title, + movie.originalTitle, + TournamentVM.POSTER_BASE_URL + movie.posterPath, + movie.overview, + null, + movie.voteAverage.toString(), + null + ) + + companion object { + const val DEFAULT_PAGE_INDEX = 1 + const val RESET_SCORE_INDEX = 0 + const val ADD_SCORE_POINT = 1 + const val NEXT_PAGE = 1 + const val MOVIE_SIZE_RESET = 19 + const val IMG1_INDEX = 0 + const val IMG2_INDEX = 1 + const val LAST_PAGE = 500 + } +} diff --git a/app/src/main/java/com/example/finema/ui/movieDetail/MovieDetailsFragment.kt b/app/src/main/java/com/example/finema/ui/movieDetail/MovieDetailsFragment.kt new file mode 100644 index 00000000..dcc13d21 --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/movieDetail/MovieDetailsFragment.kt @@ -0,0 +1,184 @@ +package com.example.finema.ui.movieDetail + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.VISIBLE +import android.view.View.INVISIBLE +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.Toast +import androidx.lifecycle.Observer +import com.example.finema.R +import com.example.finema.databinding.MovieDetailsFragmentBinding +import com.example.finema.models.databaseModels.MovieModel +import com.example.finema.models.movieResponse.MovieDetails +import com.example.finema.ui.base.BaseFragment +import com.example.finema.util.downloadAndSetImageUrl +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.koin.androidx.viewmodel.ext.android.getViewModel + +class MovieDetailsFragment : BaseFragment() { + + var movie: MovieModel = MovieModel() + var trailer: String? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = MovieDetailsFragmentBinding + .inflate(inflater, container, false) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + initViewModel() + super.onViewCreated(view, savedInstanceState) + + viewModel.trailer.observe(viewLifecycleOwner, { item -> + trailer = item.key + if (item.key.isNotEmpty()) { + binding.trailer.visibility = VISIBLE + } else { + binding.trailer.visibility = INVISIBLE + } + }) + viewModel.film.observe(viewLifecycleOwner, observerList) + viewModel.favouriteMovies.observe(viewLifecycleOwner, {}) + + binding.favourite.setOnClickListener { + addRemoveFavourite(movie.id) + } + + binding.trailer.setOnClickListener { + if (trailer != null) + startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("http://www.youtube.com/watch?v=$trailer") + ) + ) + } + } + + private val observerList: Observer = Observer { + binding.filmId = it + + viewModel.film.value?.let { film -> + binding.genres.text = film.stringGenres + + binding.companies.text = film.stringCompanies + } + + val image = requireActivity().findViewById(R.id.imageAppBar) + + image.downloadAndSetImageUrl( + POSTER_BASE_URL + it.posterPath + ) + + binding.rating.text = it.voteAverage.toString() + + if (viewModel.favouriteMovies.value!!.contains(it.id.toLong())) { + binding.favourite.setImageResource(R.drawable.bookmark_24) + } + + movie = MovieModel( + it.id.toLong(), + it.title, + it.originalTitle, + POSTER_BASE_URL + it.posterPath, + it.overview, + null, + it.voteAverage.toString(), + null + ) + + if (viewModel.getFragment() == "Tournament fragment") { + CoroutineScope(Dispatchers.IO).launch { + viewModel.addToTopMovies( + viewModel.toTopModel(movie) + ) + } + } + + afterLoading() + } + + private fun initViewModel() { + viewModel = getViewModel() + viewModel.arg = requireArguments().getLong(KEY) + viewModel.getTrailer() + viewModel.getMovieDetails() + viewModel.checkFavourite() + } + + private fun addRemoveFavourite(id: Long) { + viewModel.favouriteMovies.value?.let { + if (it.contains(id)) { + deleteFilm() + } else { + addFilm() + } + } + } + + private fun deleteFilm() { + Toast.makeText( + context, + resources.getString(R.string.delete_from_favourite), + Toast.LENGTH_SHORT + ).show() + animateBookmark(binding.favourite) + binding.favourite.setImageResource(R.drawable.bookmark_border_24) + viewModel.deleteMovie(movie.id, movie) + } + + private fun addFilm() { + Toast.makeText( + context, + resources.getString(R.string.add_to_favourite), + Toast.LENGTH_SHORT + ).show() + animateBookmark(binding.favourite) + binding.favourite.setImageResource(R.drawable.bookmark_24) + viewModel.insert(movie) + } + + private fun animateBookmark(bookmark: ImageButton) { + bookmark.animate().apply { + duration = ANIMATION_DURATION + scaleXBy(ANIMATION_ROTATION) + scaleYBy(ANIMATION_ROTATION) + }.withEndAction { + bookmark.animate().apply { + duration = ANIMATION_DURATION + scaleXBy(-ANIMATION_ROTATION) + scaleYBy(-ANIMATION_ROTATION) + } + } + } + + private fun afterLoading() { + binding.filmLoader.visibility = INVISIBLE + binding.aboutTitle.visibility = VISIBLE + binding.genreTitle.visibility = VISIBLE + binding.ratingTitle.visibility = VISIBLE + binding.companiesTitle.visibility = VISIBLE + binding.favourite.visibility = VISIBLE + } + + companion object { + const val POSTER_BASE_URL = "https://image.tmdb.org/t/p/w342" + const val KEY = "filmId" + private const val ANIMATION_DURATION = 250L + private const val ANIMATION_ROTATION = 1f + } +} diff --git a/app/src/main/java/com/example/finema/ui/movieDetail/MovieDetailsViewModel.kt b/app/src/main/java/com/example/finema/ui/movieDetail/MovieDetailsViewModel.kt new file mode 100644 index 00000000..1445c4fb --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/movieDetail/MovieDetailsViewModel.kt @@ -0,0 +1,121 @@ +package com.example.finema.ui.movieDetail + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.example.finema.api.IMoviesRepository +import com.example.finema.database.DatabaseRepository +import com.example.finema.database.firebase.IFirebaseRepository +import com.example.finema.models.databaseModels.MovieModel +import com.example.finema.models.databaseModels.TopModel +import com.example.finema.models.movieResponse.MovieDetails +import com.example.finema.models.movieResponse.MovieTrailer +import com.example.finema.repositories.IAppPreference +import com.example.finema.ui.base.BaseViewModel +import com.example.finema.util.Coroutines +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class MovieDetailsViewModel( + private val repository: IMoviesRepository, + private val dbRepository: DatabaseRepository, + private val fbRepository: IFirebaseRepository, + private val appPreference: IAppPreference +) : BaseViewModel() { + + var film = MutableLiveData() + var trailer = MutableLiveData() + var arg: Long = 0 + var favouriteMovies: LiveData> = dbRepository.checkFavourite(listOf(arg)) + + fun getMovieDetails() { + job = Coroutines.ioThenMan( + { repository.getMovieDetails(arg) }, + { + film.value = it + film.value!!.stringGenres = "" + film.value!!.stringCompanies = "" + + for (item in it!!.genres) { + film.value!!.stringGenres += item.name + NEW_LINE + } + + for (item in it.productionCompanies) { + film.value!!.stringCompanies += item.name + TAB + item.originCountry + NEW_LINE + } + + Log.d("gypsy", "Details") + } + ) + } + + fun getTrailer() { + job = Coroutines.ioThenMan( + { repository.getTrailers(arg) }, + { + val smt = it?.trailers + Log.d("gypsy", "SMT " + smt?.size.toString()) + smt?.forEach { item -> + if (item.site == "YouTube" && item.type == "Trailer") { + trailer.value = item + Log.d("gypsy", "TrailerYES") + } + } + } + ) + job.start() + } + + fun insert(movieModel: MovieModel) { + viewModelScope.launch(Dispatchers.Main) { + dbRepository.insertFavourite(movieModel) { + } + } + if (appPreference.getGuestOrAuth() == "AUTH") { + viewModelScope.launch(Dispatchers.Main) { + fbRepository.insertFirebaseFavouriteFilm(movieModel) + } + } + } + + fun checkFavourite() { + favouriteMovies = dbRepository.checkFavourite(listOf(arg)) + } + + fun toTopModel(movieModel: MovieModel): TopModel { + return TopModel( + movieModel.id, + movieModel.title, + movieModel.imageUrl, + movieModel.about, + movieModel.genres, + movieModel.rating, + movieModel.companies + ) + } + + fun deleteMovie(id: Long, movie: MovieModel) { + viewModelScope.launch(Dispatchers.Main) { + dbRepository.deleteFavouriteMovie(id) {} + } + if (appPreference.getGuestOrAuth() == "AUTH") { + viewModelScope.launch(Dispatchers.Main) { + fbRepository.deleteFirebaseFavouriteFilm(movie) + } + } + } + + suspend fun addToTopMovies(movieModel: TopModel) { + dbRepository.insertTop(movieModel) {} + } + + fun getFragment(): String? { + return appPreference.getFragment() + } + + companion object { + const val NEW_LINE = "\n" + const val TAB = "\t" + } +} diff --git a/app/src/main/java/com/example/finema/ui/settings/NotificationService.kt b/app/src/main/java/com/example/finema/ui/settings/NotificationService.kt new file mode 100644 index 00000000..1c2a2822 --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/settings/NotificationService.kt @@ -0,0 +1,49 @@ +package com.example.finema.ui.settings + +import android.R +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.TaskStackBuilder +import android.content.Context +import android.content.Context.NOTIFICATION_SERVICE +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.example.finema.MainActivity + +class NotificationService(context: Context, workerParams: WorkerParameters) : + Worker(context, workerParams) { + + override fun doWork(): Result { + sendNotification() + return Result.success() + } + + private fun sendNotification() { + val notificationManager = + applicationContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager + val resultIntent = Intent(applicationContext, MainActivity::class.java) + val resultPendingIntent = TaskStackBuilder.create(applicationContext) + .addParentStack(MainActivity::class.java) + .addNextIntent(resultIntent) + .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) + + val notification = NotificationCompat.Builder(applicationContext, "default") + .setContentTitle("Стало скучно?") + .setContentText("Выбери себе фильм на вечер") + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setSmallIcon(R.mipmap.sym_def_app_icon) + .setAutoCancel(true) + .setContentIntent(resultPendingIntent) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = + NotificationChannel("default", "Default", NotificationManager.IMPORTANCE_DEFAULT) + notificationManager.createNotificationChannel(channel) + } + notificationManager.notify(0, notification.build()) + } +} diff --git a/app/src/main/java/com/example/finema/ui/settings/SettingsFragment.kt b/app/src/main/java/com/example/finema/ui/settings/SettingsFragment.kt new file mode 100644 index 00000000..da31c65b --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/settings/SettingsFragment.kt @@ -0,0 +1,53 @@ +package com.example.finema.ui.settings + +import android.os.Bundle +import android.widget.Toast +import androidx.navigation.fragment.findNavController +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.example.finema.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.koin.androidx.viewmodel.ext.android.getViewModel + +class SettingsFragment : PreferenceFragmentCompat() { + private lateinit var viewModel: SettingsViewModel + + private lateinit var notification: Preference + private lateinit var quit: Preference + private lateinit var clear: Preference + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + viewModel = getViewModel() + setPreferencesFromResource(R.xml.settings, rootKey) + + quit = findPreference("quit")!! + clear = findPreference("clear_statistics")!! + notification = findPreference("notifications")!! + + quit.onPreferenceClickListener = + Preference.OnPreferenceClickListener { // code for what you want it to do + viewModel.googleSignOut() + CoroutineScope(Dispatchers.IO).launch { + viewModel.clearAllStatistics() + } + + findNavController().navigate(R.id.action_fragmentSettings_to_sigInFragment) + true + } + + clear.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + CoroutineScope(Dispatchers.IO).launch { + viewModel.clearAllStatistics() + if (viewModel.getGuestOrAuth() == "AUTH") { + viewModel.clearFireBase() + } + } + + Toast.makeText(context, R.string.clear, Toast.LENGTH_SHORT).show() + true + } + } +} diff --git a/app/src/main/java/com/example/finema/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/example/finema/ui/settings/SettingsViewModel.kt new file mode 100644 index 00000000..be2d691a --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/settings/SettingsViewModel.kt @@ -0,0 +1,34 @@ +package com.example.finema.ui.settings + +import com.example.finema.database.DatabaseRepository +import com.example.finema.database.firebase.IFirebaseRepository +import com.example.finema.repositories.Contract +import com.example.finema.repositories.IAppPreference +import com.example.finema.ui.base.BaseViewModel + +class SettingsViewModel( + private val dbRepository: DatabaseRepository, + private val contract: Contract, + private val fbRepository: IFirebaseRepository, + private val appPreference: IAppPreference + +) : BaseViewModel() { + suspend fun clearAllStatistics() { + dbRepository.deleteAllFavourite {} + dbRepository.deleteAllTop {} + } + + fun clearFireBase() { + fbRepository.clearFirebaseFavourite() + } + + fun googleSignOut() { + appPreference.setInitUser(false) + appPreference.googleUserSignOut() + contract.signOut() + } + + fun getGuestOrAuth(): String? { + return appPreference.getGuestOrAuth() + } +} diff --git a/app/src/main/java/com/example/finema/ui/signIn/SigInFragment.kt b/app/src/main/java/com/example/finema/ui/signIn/SigInFragment.kt new file mode 100644 index 00000000..2b3b3d17 --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/signIn/SigInFragment.kt @@ -0,0 +1,107 @@ +package com.example.finema.ui.signIn + +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.activity.result.ActivityResultLauncher +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.example.finema.R +import com.example.finema.databinding.SignInFragmentBinding +import com.example.finema.ui.base.BaseFragment +import com.example.finema.util.downloadAndSetImageUri +import com.google.android.material.navigation.NavigationView +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.flow.collect +import org.koin.androidx.viewmodel.ext.android.getViewModel + +class SigInFragment : BaseFragment() { + + private lateinit var header: TextView + private lateinit var avatar: ImageView + + private lateinit var customContract: ActivityResultLauncher + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SignInFragmentBinding.inflate(inflater, container, false) + + return binding.root + } + + @InternalCoroutinesApi + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + viewModel = getViewModel() + finishActivityOnClickBack() + initCustomContract() + + super.onViewCreated(view, savedInstanceState) + + header = requireActivity().findViewById(R.id.nav_view) + .getHeaderView(0).findViewById(R.id.nickProfile) + avatar = requireActivity().findViewById(R.id.nav_view) + .getHeaderView(0).findViewById(R.id.userAvatar) + + binding.signInWithGoogle.setOnClickListener { + binding.loader.visibility = View.VISIBLE + signIn() + viewModel.signInAuth() + } + + binding.signInAsGuest.setOnClickListener { + viewModel.signInGuest() + header.text = resources.getText(R.string.guest) + avatar.downloadAndSetImageUri(Uri.parse(DEFAULT_URI)) + findNavController().navigate(R.id.action_sigInFragment_to_tmpFragment) + } + } + + @InternalCoroutinesApi + private fun signIn() { + customContract.launch(Unit) + } + + @InternalCoroutinesApi + private fun initCustomContract() { + customContract = registerForActivityResult(viewModel.contract) { + lifecycleScope.launchWhenStarted { + viewModel.contract.name.collect { name -> + when (name) { + "" -> Unit + else -> letUserIn(name) + } + } + } + } + } + + private fun letUserIn(name: String) { + Log.d("ID", viewModel.mAuth.currentUser?.uid.toString()) + binding.loader.visibility = View.INVISIBLE + header.text = name + avatar.downloadAndSetImageUri(viewModel.mAuth.currentUser?.photoUrl) + findNavController().navigate(R.id.action_sigInFragment_to_tmpFragment) + } + + private fun finishActivityOnClickBack() { + binding.root.isFocusableInTouchMode = true + binding.root.requestFocus() + binding.root.setOnKeyListener { _, _, _ -> + activity?.finish() + false + } + } + + companion object { + private const val DEFAULT_URI = + "android.resource://com.example.finema/drawable/default_profile_avatar" + } +} diff --git a/app/src/main/java/com/example/finema/ui/signIn/SignInViewModel.kt b/app/src/main/java/com/example/finema/ui/signIn/SignInViewModel.kt new file mode 100644 index 00000000..dbb1f3c0 --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/signIn/SignInViewModel.kt @@ -0,0 +1,29 @@ +package com.example.finema.ui.signIn + +import com.example.finema.repositories.Contract +import com.example.finema.repositories.IAppPreference +import com.example.finema.ui.base.BaseViewModel +import com.google.firebase.auth.FirebaseAuth + +class SignInViewModel( + val contract: Contract, + private val appPreference: IAppPreference +) : BaseViewModel() { + + val mAuth = FirebaseAuth.getInstance() + + init { + contract.setName("") + } + + fun signInAuth() { + appPreference.setFirstSignIn(true) + appPreference.setInitUser(true) + appPreference.setGuestOrAuth("AUTH") + } + + fun signInGuest() { + appPreference.setInitUser(true) + appPreference.setGuestOrAuth("GUEST") + } +} diff --git a/app/src/main/java/com/example/finema/ui/tmp/TmpFragment.kt b/app/src/main/java/com/example/finema/ui/tmp/TmpFragment.kt new file mode 100644 index 00000000..71f7c0db --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/tmp/TmpFragment.kt @@ -0,0 +1,54 @@ +package com.example.finema.ui.tmp + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import com.example.finema.R +import com.example.finema.databinding.TmpFragmentBinding +import com.example.finema.ui.base.BaseFragment +import org.koin.androidx.viewmodel.ext.android.getViewModel + +class TmpFragment : BaseFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = TmpFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + viewModel = getViewModel() + if (viewModel.getFirstSignIn() && viewModel.getGuestOrAuth() == "AUTH") { + viewModel.initRoomFromFirebaseToRoom() + viewModel.setFirstSignIn(false) + } + + super.onViewCreated(view, savedInstanceState) + binding.genre.setOnClickListener { + findNavController().navigate(R.id.action_fragment_tmp_to_fragment_genre) + viewModel.setTournamentType("GENRE") + } + + binding.category.setOnClickListener { + findNavController().navigate(R.id.action_fragment_tmp_to_fragment_others) + viewModel.setTournamentType("CATEGORY") + } + + binding.searchMovie.setOnClickListener { + findNavController().navigate(R.id.action_fragmentTmp_to_chooseFavouriteFragment) + } + + binding.popularity.setOnClickListener { + findNavController().navigate(R.id.action_fragmentTmp_to_fragmentHigherLower) + } + + binding.rating.setOnClickListener { + findNavController().navigate(R.id.action_fragmentTmp_to_higherLowerRatingFragment) + } + } +} diff --git a/app/src/main/java/com/example/finema/ui/tmp/TmpViewModel.kt b/app/src/main/java/com/example/finema/ui/tmp/TmpViewModel.kt new file mode 100644 index 00000000..20225f7a --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/tmp/TmpViewModel.kt @@ -0,0 +1,67 @@ +package com.example.finema.ui.tmp + +import androidx.lifecycle.Observer +import androidx.lifecycle.viewModelScope +import com.example.finema.database.DatabaseRepository +import com.example.finema.database.firebase.IFirebaseRepository +import com.example.finema.models.databaseModels.MovieModel +import com.example.finema.repositories.IAppPreference +import com.example.finema.ui.base.BaseViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class TmpViewModel( + private val dbRepository: DatabaseRepository, + private val fbRepository: IFirebaseRepository, + private val appPreference: IAppPreference +) : BaseViewModel() { + + private val allMovies = fbRepository.allMovies + + private lateinit var observerList: Observer> + + init { + fbRepository.initRefs() + } + + fun initRoomFromFirebaseToRoom() { + observerList = Observer { listMovie -> + listMovie.map { it.id = it.idFirebase.toLong() } + for (item in listMovie) { + viewModelScope.launch(Dispatchers.Main) { + dbRepository.insertFavourite(item) {} + } + } + } + allMovies.observeForever { + observerList + } + } + + fun getFirstSignIn(): Boolean { + return appPreference.getFirstSignIn() + } + + fun getGuestOrAuth(): String? { + return appPreference.getGuestOrAuth() + } + + fun setFirstSignIn(boolean: Boolean) { + appPreference.setFirstSignIn(boolean) + } + + fun setTournamentType(string: String) { + appPreference.setTournamentType(string) + } + + fun insertFavourite(item: MovieModel) { + viewModelScope.launch(Dispatchers.Main) { + dbRepository.insertFavourite(item) {} + } + } + + override fun onCleared() { + allMovies.removeObserver(observerList) + super.onCleared() + } +} diff --git a/app/src/main/java/com/example/finema/ui/tournaments/categories/CategoryTournamentAdapter.kt b/app/src/main/java/com/example/finema/ui/tournaments/categories/CategoryTournamentAdapter.kt new file mode 100644 index 00000000..a4aff7f9 --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/tournaments/categories/CategoryTournamentAdapter.kt @@ -0,0 +1,64 @@ +package com.example.finema.ui.tournaments.categories + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.example.finema.R +import com.example.finema.models.databaseModels.CategoryModel + +class CategoryTournamentAdapter( + private val listener: TournamentHolder.Listener, +) : RecyclerView.Adapter() { + + private var mListCategories = emptyList() + + class TournamentHolder( + view: View, + private val listener: Listener + ) : RecyclerView.ViewHolder(view) { + + private val categoryName: TextView = view.findViewById(R.id.item_category_name) + private val categoryDescription: TextView = view.findViewById( + R.id.item_category_description + ) + + interface Listener { + fun onMovieClicked(view: View, categoryModel: CategoryModel) + } + + fun bind(name: String, description: String, mListCategories: List) { + categoryName.text = name + categoryDescription.text = description + itemView.setOnClickListener { + listener.onMovieClicked(itemView, mListCategories[adapterPosition]) + } + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): TournamentHolder { + + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.category_item, parent, false) + return TournamentHolder(view, listener) + } + + override fun getItemCount(): Int = mListCategories.size + + fun setList(list: List) { + mListCategories = list + notifyDataSetChanged() + } + + override fun onBindViewHolder(holder: TournamentHolder, position: Int) { + holder.bind( + mListCategories[position].name, + mListCategories[position].description, + mListCategories + ) + } +} diff --git a/app/src/main/java/com/example/finema/ui/tournaments/categories/CategoryTournamentFragment.kt b/app/src/main/java/com/example/finema/ui/tournaments/categories/CategoryTournamentFragment.kt new file mode 100644 index 00000000..a8bab018 --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/tournaments/categories/CategoryTournamentFragment.kt @@ -0,0 +1,125 @@ +package com.example.finema.ui.tournaments.categories + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.Observer +import androidx.navigation.Navigation +import androidx.recyclerview.widget.RecyclerView +import com.example.finema.R +import com.example.finema.databinding.FragmentCategoryTournamentBinding +import com.example.finema.models.databaseModels.CategoryModel +import com.example.finema.ui.base.BaseFragment +import org.koin.androidx.viewmodel.ext.android.getViewModel + +class CategoryTournamentFragment : + BaseFragment(), + CategoryTournamentAdapter.TournamentHolder.Listener { + + private lateinit var recyclerView: RecyclerView + private lateinit var adapter: CategoryTournamentAdapter + private lateinit var observerList: Observer> + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentCategoryTournamentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + viewModel = getViewModel() + super.onViewCreated(view, savedInstanceState) + initialization() + } + + private fun initialization() { + adapter = CategoryTournamentAdapter(this) + recyclerView = binding.categoryRecycler + recyclerView.adapter = adapter + observerList = Observer { + val list = it + adapter.setList(list) + } + viewModel.allCategories.observe(requireActivity(), observerList) + } + + override fun onMovieClicked(view: View, categoryModel: CategoryModel) { + if (categoryModel.link == CATEGORY_WARNING) { + warningDialog(categoryModel.link, categoryModel.name) + } else dialogBinding(categoryModel.link, categoryModel.name) + } + + private fun warningDialog(link: String, categoryName: String) { + val builder = AlertDialog.Builder(requireContext()) + builder.apply { + setTitle("Эта категория Содержит контент 18+") + builder.setMessage("Продолжить?") + setPositiveButton("Да") { _, _ -> + dialogBinding(link, categoryName) + } + setNegativeButton("Нет") { dialog, _ -> + dialog.cancel() + } + } + builder.create() + builder.show() + } + + private fun dialogBinding(link: String, categoryName: String) { + // TODO Изменить на фрагмент + val dialog = Dialog(requireContext()) + dialog.window?.setBackgroundDrawableResource(android.R.color.transparent) + dialog.window?.attributes?.windowAnimations = R.style.DialogAnimation + dialog.let { + it.setContentView(R.layout.number_fragment) + it.findViewById(R.id.btn8).setOnClickListener { + goNextFragment(PRESSED_EIGHT_MOVIES, link, categoryName) + dialog.onBackPressed() + } + + it.findViewById(R.id.btn16).setOnClickListener { + goNextFragment(PRESSED_SIXTEEN_MOVIES, link, categoryName) + dialog.onBackPressed() + } + it.findViewById(R.id.btn32).setOnClickListener { + goNextFragment(PRESSED_THIRTY_TWO_MOVIES, link, categoryName) + dialog.onBackPressed() + } + it.findViewById(R.id.btn64).setOnClickListener { + goNextFragment(PRESSED_SIXTY_FOUR_MOVIES, link, categoryName) + dialog.onBackPressed() + } + it.findViewById(R.id.btn128).setOnClickListener { + goNextFragment(PRESSED_ONE_HUNDRED_AND_TWENTY_EIGHT_MOVIES, link, categoryName) + dialog.onBackPressed() + } + it.findViewById(R.id.btn256).setOnClickListener { + goNextFragment(PRESSED_TWO_HUNDRED_AND_FIFTY_SIX_MOVIES, link, categoryName) + dialog.onBackPressed() + } + } + dialog.show() + } + + private fun goNextFragment(num: Int, link: String, categoryName: String) { + viewModel.setParameters(num, categoryName, link) + Navigation.findNavController(requireActivity(), R.id.fragment) + .navigate(R.id.action_fragmentOthers_to_fragmentTournament) + } + + companion object { + const val PRESSED_EIGHT_MOVIES = 8 + const val PRESSED_SIXTEEN_MOVIES = 16 + const val PRESSED_THIRTY_TWO_MOVIES = 32 + const val PRESSED_SIXTY_FOUR_MOVIES = 64 + const val PRESSED_ONE_HUNDRED_AND_TWENTY_EIGHT_MOVIES = 128 + const val PRESSED_TWO_HUNDRED_AND_FIFTY_SIX_MOVIES = 256 + const val CATEGORY_WARNING = "133" + } +} diff --git a/app/src/main/java/com/example/finema/ui/tournaments/categories/CategoryTournamentVM.kt b/app/src/main/java/com/example/finema/ui/tournaments/categories/CategoryTournamentVM.kt new file mode 100644 index 00000000..67e6bb22 --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/tournaments/categories/CategoryTournamentVM.kt @@ -0,0 +1,22 @@ +package com.example.finema.ui.tournaments.categories + +import com.example.finema.database.firebase.IFirebaseRepository +import com.example.finema.repositories.IAppPreference +import com.example.finema.ui.base.BaseViewModel + +class CategoryTournamentVM( + fbRepository: IFirebaseRepository, + private val appPreference: IAppPreference +) : BaseViewModel() { + val allCategories = fbRepository.allCategories + + init { + fbRepository.initRefCategory() + } + + fun setParameters(num: Int, categoryName: String, link: String) { + appPreference.setNumOfFilms(num) + appPreference.setCategoryName(categoryName) + appPreference.setCategoryLink(link.toInt()) + } +} diff --git a/app/src/main/java/com/example/finema/ui/tournaments/genres/GenresTournamentAdapter.kt b/app/src/main/java/com/example/finema/ui/tournaments/genres/GenresTournamentAdapter.kt new file mode 100644 index 00000000..e2c3c42c --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/tournaments/genres/GenresTournamentAdapter.kt @@ -0,0 +1,52 @@ +package com.example.finema.ui.tournaments.genres + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.example.finema.R +import com.example.finema.models.databaseModels.GenreModel + +class GenresTournamentAdapter( + private val listener: TournamentHolder.Listener, +) : RecyclerView.Adapter() { + + private var listGenres = emptyList() + + fun setList(list: List) { + listGenres = list + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TournamentHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.genre_item, parent, false) + return TournamentHolder(view, listener) + } + + override fun onBindViewHolder(holder: TournamentHolder, position: Int) { + holder.bind(listGenres[position].name, listGenres) + } + + override fun getItemCount(): Int = listGenres.size + + class TournamentHolder( + view: View, + private val listener: Listener + ) : RecyclerView.ViewHolder(view) { + + private val genreName: TextView = view.findViewById(R.id.item_genre_name) + + interface Listener { + fun onMovieClicked(view: View, genreModelId: GenreModel) + } + + fun bind(name: String, mListGenres: List) { + genreName.text = name + itemView.setOnClickListener { + listener.onMovieClicked(itemView, mListGenres[adapterPosition]) + } + } + } +} diff --git a/app/src/main/java/com/example/finema/ui/tournaments/genres/GenresTournamentFragment.kt b/app/src/main/java/com/example/finema/ui/tournaments/genres/GenresTournamentFragment.kt new file mode 100644 index 00000000..d45c272e --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/tournaments/genres/GenresTournamentFragment.kt @@ -0,0 +1,128 @@ +package com.example.finema.ui.tournaments.genres + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.lifecycle.Observer +import androidx.navigation.Navigation +import androidx.recyclerview.widget.RecyclerView +import com.example.finema.R +import com.example.finema.databinding.FragmentTournamentGenresBinding +import com.example.finema.models.databaseModels.GenreModel +import com.example.finema.models.genreRequest.GenreList +import com.example.finema.ui.base.BaseFragment +import org.koin.androidx.viewmodel.ext.android.getViewModel + +class GenresTournamentFragment : + BaseFragment(), + GenresTournamentAdapter.TournamentHolder.Listener { + + private lateinit var recyclerView: RecyclerView + private lateinit var adapter: GenresTournamentAdapter + private lateinit var observerList: Observer> + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentTournamentGenresBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + viewModel = getViewModel() + super.onViewCreated(view, savedInstanceState) + initialization() + } + + private fun initialization() { + if (!viewModel.checkDatabaseNotEmpty()) loadGenresList() + + adapter = GenresTournamentAdapter(this) + recyclerView = binding.tournamentsRecycler + recyclerView.adapter = adapter + + observerList = Observer { + adapter.setList(it) + } + // TODO Убрать получение и обращаться к VM базового класса + viewModel.allGenres.observe(requireActivity(), observerList) + } + + // TODO genreModel -> {} : GenreModel -> Unit + // Заменить на лямбду, хз так ли написал выше + override fun onMovieClicked(view: View, genreModelId: GenreModel) { + if (genreModelId.id == GENRE_NOT_FOUND_TEMPORARILY) { + Toast.makeText(context, "Этот жанр временно отсутсвует", Toast.LENGTH_SHORT).show() + } else { + dialogBinding(genreModelId.id.toString(), genreModelId.name) + } + } + + private fun dialogBinding(genreId: String, genreName: String) { + val dialog = Dialog(requireContext()) + dialog.window?.setBackgroundDrawableResource(android.R.color.transparent) + dialog.window?.attributes?.windowAnimations = R.style.DialogAnimation + dialog.let { + it.setContentView(R.layout.number_fragment) + it.findViewById(R.id.btn8).setOnClickListener { + goNextFragment(PRESSED_EIGHT_MOVIES, genreId, genreName) + dialog.onBackPressed() + } + it.findViewById(R.id.btn16).setOnClickListener { + goNextFragment(PRESSED_SIXTEEN_MOVIES, genreId, genreName) + dialog.onBackPressed() + } + it.findViewById(R.id.btn32).setOnClickListener { + goNextFragment(PRESSED_THIRTY_TWO_MOVIES, genreId, genreName) + dialog.onBackPressed() + } + it.findViewById(R.id.btn64).setOnClickListener { + goNextFragment(PRESSED_SIXTY_FOUR_MOVIES, genreId, genreName) + dialog.onBackPressed() + } + it.findViewById(R.id.btn128).setOnClickListener { + goNextFragment(PRESSED_ONE_HUNDRED_AND_TWENTY_EIGHT_MOVIES, genreId, genreName) + dialog.onBackPressed() + } + it.findViewById(R.id.btn256).setOnClickListener { + goNextFragment(PRESSED_TWO_HUNDRED_AND_FIFTY_SIX_MOVIES, genreId, genreName) + dialog.onBackPressed() + } + } + dialog.show() + } + + private fun goNextFragment(num: Int, genreId: String, genreName: String) { + viewModel.setParameters(num, genreName, genreId) + Navigation.findNavController(requireActivity(), R.id.fragment) + .navigate(R.id.action_fragmentGenre_to_fragmentTournament) + } + + private fun loadGenresList() { + val observerList: Observer = Observer { + val list = it.genres + for (item in list) { + viewModel.insert(GenreModel(name = item.name, id = item.id)) + } + } + + viewModel.getGenres() + + viewModel.genreListVM.observe(viewLifecycleOwner, observerList) + } + + companion object { + const val GENRE_NOT_FOUND_TEMPORARILY = 99 + const val PRESSED_EIGHT_MOVIES = 8 + const val PRESSED_SIXTEEN_MOVIES = 16 + const val PRESSED_THIRTY_TWO_MOVIES = 32 + const val PRESSED_SIXTY_FOUR_MOVIES = 64 + const val PRESSED_ONE_HUNDRED_AND_TWENTY_EIGHT_MOVIES = 128 + const val PRESSED_TWO_HUNDRED_AND_FIFTY_SIX_MOVIES = 256 + } +} diff --git a/app/src/main/java/com/example/finema/ui/tournaments/genres/GenresTournamentVM.kt b/app/src/main/java/com/example/finema/ui/tournaments/genres/GenresTournamentVM.kt new file mode 100644 index 00000000..4b0c769f --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/tournaments/genres/GenresTournamentVM.kt @@ -0,0 +1,56 @@ +package com.example.finema.ui.tournaments.genres + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.example.finema.api.IMoviesRepository +import com.example.finema.database.DatabaseRepository +import com.example.finema.models.databaseModels.GenreModel +import com.example.finema.models.genreRequest.GenreList +import com.example.finema.repositories.IAppPreference +import com.example.finema.ui.base.BaseViewModel +import com.example.finema.util.Coroutines +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class GenresTournamentVM( + private val apiRepository: IMoviesRepository, + private val dbRepository: DatabaseRepository, + private val appPreference: IAppPreference +) : BaseViewModel() { + + val allGenres: LiveData> + get() { + return dbRepository.allGenres + } + + var genreListVM = MutableLiveData() + + fun checkDatabaseNotEmpty(): Boolean { + // TODO не работает, так как вызов асинхронный + return allGenres.value != null + } + + fun signOut() { + dbRepository.signOut() + } + + fun getGenres() { + job = Coroutines.ioThenMan( + { apiRepository.getGenres() }, + { genreListVM.value = it } + ) + } + + fun insert(genreModel: GenreModel) = + viewModelScope.launch(Dispatchers.Main) { + dbRepository.insert(genreModel) { + } + } + + fun setParameters(num: Int, genreName: String, genreId: String) { + appPreference.setNumOfFilms(num) + appPreference.setGenreName(genreName) + appPreference.setGenre(genreId) + } +} diff --git a/app/src/main/java/com/example/finema/ui/tournaments/tournament/TournamentFragment.kt b/app/src/main/java/com/example/finema/ui/tournaments/tournament/TournamentFragment.kt new file mode 100644 index 00000000..fc3d8af2 --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/tournaments/tournament/TournamentFragment.kt @@ -0,0 +1,197 @@ +package com.example.finema.ui.tournaments.tournament + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import androidx.cardview.widget.CardView +import androidx.navigation.fragment.findNavController +import com.example.finema.R +import com.example.finema.databinding.FragmentTournamentBinding +import com.example.finema.models.movieResponse.Movie +import com.example.finema.ui.base.BaseFragment +import com.example.finema.util.downloadAndSetImageUrl +import org.koin.androidx.viewmodel.ext.android.getViewModel + +class TournamentFragment : BaseFragment() { + + private lateinit var desc: TextView + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentTournamentBinding + .inflate(inflater, container, false) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + viewModel = getViewModel() + + super.onViewCreated(view, savedInstanceState) + viewModel.twoFilms.observe( + requireActivity(), + { movieList -> + + binding.txtFilm1.text = movieList[0].title + binding.txtFilm2.text = movieList[1].title + + binding.progressBar.visibility = View.GONE + binding.cardview1.visibility = View.VISIBLE + binding.cardview2.visibility = View.VISIBLE + + setImage(binding.img1, movieList, 0) + setImage(binding.img2, movieList, 1) + + binding.txtNumCategory.text = viewModel.title + binding.roundCount.text = getString(R.string.n_round, viewModel.roundCount) + + cardClickListener(binding.cardview1, 0) + cardClickListener(binding.cardview2, 1) + + infoClicked(binding.more1, 0) + infoClicked(binding.more2, 1) + + fillInBookmarks(binding.txtFilm1, binding.bookmark1) + fillInBookmarks(binding.txtFilm2, binding.bookmark2) + } + ) + setBookmarkClickListeners(binding.bookmark1, binding.txtFilm1, 0) + setBookmarkClickListeners(binding.bookmark2, binding.txtFilm2, 1) + } + + private fun fillInBookmarks(txtview: TextView, bookmark: ImageButton) { + viewModel.favouriteMovies.observe( + viewLifecycleOwner, + { + var counter = 0 + for (i in it) { + counter += 1 + if (txtview.text == i.title) { + bookmark.setImageResource( + R.drawable.bookmark_24 + ) + break + } + if (counter == it.size) { + bookmark.setImageResource( + R.drawable.bookmark_border_24 + ) + } + } + } + ) + } + + private fun infoClicked(button: ImageButton, position: Int) { + button.setOnClickListener { + dialogBinding(position) + } + } + + private fun cardClickListener(cardView: CardView, position: Int) { + cardView.setOnClickListener { + itemClick(position) + } + } + + private fun setImage(image: ImageView, movieList: List, imgInd: Int) { + image.downloadAndSetImageUrl( + getString( + R.string.poster_base_url, + movieList[imgInd].posterPath + ) + ) + } + + private fun setBookmarkClickListeners(bookmark: ImageButton, title: TextView, position: Int) { + bookmark.setOnClickListener { + animateBookmark(bookmark) + addODelFav(title, position) + } + } + + private fun dialogBinding(index: Int) { + val dialog = Dialog(requireContext()) + dialog.window?.setBackgroundDrawableResource(android.R.color.transparent) + dialog.window?.attributes?.windowAnimations = R.style.DialogAnimation + dialog.let { + it.setContentView(R.layout.movie_description) + desc = it.findViewById(R.id.desc) + if (viewModel.returnDesc(index) == "") { + desc.text = "Пусто" + } else { + desc.text = viewModel.returnDesc(index) + } + } + dialog.show() + } + + private fun addODelFav(title: TextView, position: Int) { + viewModel.favouriteMovies.value?.let { + var counter = 0 + for (i in it) { + counter += 1 + if (title.text == i.title || title.text == i.originalTitle) { + viewModel.removeFromFav(position) + break + } + if (it.size == counter) { + viewModel.addToFav(position) + } + } + } + } + + private fun animateBookmark(bookmark: ImageButton) { + bookmark.animate().apply { + duration = ANIMATION_DURATION + scaleXBy(ANIMATION_ROTATION) + scaleYBy(ANIMATION_ROTATION) + }.withEndAction { + bookmark.animate().apply { + duration = ANIMATION_DURATION + scaleXBy(-ANIMATION_ROTATION) + scaleYBy(-ANIMATION_ROTATION) + } + } + } + + private fun itemClick(position: Int) { + (if (position == 0) viewModel.el1 else viewModel.el2) + .let { + if (viewModel.mainList.isEmpty()) { + if (viewModel.secondList.isEmpty()) { + val filmIdInfo = it.id.toLong() + goNextFragment(filmIdInfo) + } else { + viewModel.secondList.add(it) + viewModel.secondListToMainList() + viewModel.updateCards() + } + } else { + viewModel.secondList.add(it) + viewModel.updateCards() + } + } + } + + private fun goNextFragment(filmIdInfo: Long) { + val bundle = Bundle() + bundle.putSerializable("filmId", filmIdInfo) + findNavController() + .navigate(R.id.action_fragment_tournament_to_fragment_film, bundle) + } + + companion object { + private const val ANIMATION_DURATION = 250L + private const val ANIMATION_ROTATION = 1f + } +} diff --git a/app/src/main/java/com/example/finema/ui/tournaments/tournament/TournamentVM.kt b/app/src/main/java/com/example/finema/ui/tournaments/tournament/TournamentVM.kt new file mode 100644 index 00000000..5e47011d --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/tournaments/tournament/TournamentVM.kt @@ -0,0 +1,258 @@ +package com.example.finema.ui.tournaments.tournament + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.viewModelScope +import com.example.finema.api.IMoviesRepository +import com.example.finema.database.DatabaseRepository +import com.example.finema.database.firebase.IFirebaseRepository +import com.example.finema.models.databaseModels.MovieModel +import com.example.finema.models.movieResponse.Movie +import com.example.finema.repositories.IAppPreference +import com.example.finema.ui.base.BaseViewModel +import com.example.finema.util.Coroutines +import kotlin.math.floor +import kotlin.math.log +import kotlin.math.pow +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class TournamentVM( + private val apiRepository: IMoviesRepository, + private val dbRepository: DatabaseRepository, + private val fbRepository: IFirebaseRepository, + private val appPreference: IAppPreference +) : BaseViewModel() { + + val favouriteMovies: LiveData> = dbRepository.allFavourites + + var twoFilms = MutableLiveData>() + var loopNum: Int = ONE_LOOP + var flag: Int = FLAG_DEFAULT + var gotList = MutableLiveData>() + var mainList: ArrayList = ArrayList() + var secondList: ArrayList = ArrayList() + + private lateinit var observerList: Observer> + + lateinit var el1: Movie + lateinit var el2: Movie + + var roundCount = ROUND_COUNT_START + + var numFilms = PRESSED_EIGHT_MOVIES + + var title = TITLE_DEFAULT + + init { + start() + } + + private fun start() { + numFilms = appPreference.getNumOfFilms() + genreOrCategory() + setLoopNum() + observerList = Observer { + numFilms = checkLessNum(it, numFilms) + mainList.addAll(it) + flag += 1 + + if (flag == loopNum) { + mainList = mainList.take(numFilms) as ArrayList + updateCards() + } + } + getMovies { + gotList.observeForever(observerList) + } + } + + private fun getMovies(onSuccess: () -> Unit) { + when (appPreference.getTournamentType()) { + "GENRE" -> { + val genre = appPreference.getGenreId() ?: GENRE_ID_DEFAULT + for (page in 1..loopNum) { + job = Coroutines.ioThenMan( + { apiRepository.getMoviesWithGenre(page, genre) }, + { gotList.value = it?.movies } + ) + } + } + "CATEGORY" -> { + val categoryLink = appPreference.getCategoryLink() + for (page in 1..loopNum) { + job = Coroutines.ioThenMan( + { apiRepository.getMovieFromList(categoryLink) }, + { gotList.value = it?.movies } + ) + } + } + } + onSuccess() + } + + fun secondListToMainList() { + mainList.addAll(secondList) + secondList.clear() + roundCount += 1 + } + + fun updateCards() { + el1 = mainList.random() + mainList.remove(el1) + el2 = mainList.random() + mainList.remove(el2) + twoFilms.value = listOf(el1, el2) + } + + fun addToFav(position: Int) { + when (position) { + 0 -> { + insert(el1) + } + 1 -> { + insert(el2) + } + } + } + + private fun endOfWordGrammar(): String { + var string = FILM_DECLENSION + when (numFilms) { + PRESSED_EIGHT_MOVIES -> string = "фильмов" + PRESSED_SIXTEEN_MOVIES -> string = "фильмов" + PRESSED_THIRTY_TWO_MOVIES -> string = "фильма" + PRESSED_SIXTY_FOUR_MOVIES -> string = "фильма" + PRESSED_ONE_HUNDRED_AND_TWENTY_EIGHT_MOVIES -> string = "фильмов" + PRESSED_TWO_HUNDRED_AND_FIFTY_SIX_MOVIES -> string = "фильма" + } + return string + } + + private fun genreOrCategory() { + when (appPreference.getTournamentType()) { + "GENRE" -> { + val film = endOfWordGrammar() + val name = appPreference.getGenreName() + title = "$numFilms Лучших $film в жанре $name" + } + "CATEGORY" -> { + title = appPreference.getCategoryName() ?: " " + } + } + } + + fun removeFromFav(position: Int) { + when (position) { + 0 -> { + delete(el1) + } + 1 -> { + delete(el2) + } + } + } + + private fun insert(movie: Movie) { + viewModelScope.launch(Dispatchers.Main) { + dbRepository.insertFavourite( + makeMovieModel(movie) + ) { + } + } + if (appPreference.getGuestOrAuth() == "AUTH") { + fbRepository.insertFirebaseFavouriteFilm(makeMovieModel(movie)) + } + } + + private fun delete(movie: Movie) { + viewModelScope.launch(Dispatchers.Main) { + dbRepository.deleteFavourite( + makeMovieModel(movie) + ) { + } + if (appPreference.getGuestOrAuth() == "AUTH") { + fbRepository.deleteFirebaseFavouriteFilm(makeMovieModel(movie)) + } + } + } + + private fun makeMovieModel(movie: Movie) = + MovieModel( + movie.id.toLong(), + movie.title, + movie.originalTitle, + POSTER_BASE_URL + movie.posterPath, + movie.overview, + null, + movie.voteAverage.toString(), + null + ) + + private fun setLoopNum() { + when (numFilms) { + PRESSED_EIGHT_MOVIES or PRESSED_SIXTEEN_MOVIES -> { + loopNum = ONE_LOOP + } + PRESSED_THIRTY_TWO_MOVIES -> { + loopNum = TWO_LOOP + } + PRESSED_SIXTY_FOUR_MOVIES -> { + loopNum = FOUR_LOOP + } + PRESSED_ONE_HUNDRED_AND_TWENTY_EIGHT_MOVIES -> { + loopNum = SEVEN_LOOP + } + PRESSED_TWO_HUNDRED_AND_FIFTY_SIX_MOVIES -> { + loopNum = THIRTEEN_LOOP + } + } + } + + fun returnDesc(index: Int): String { + return when (index) { + 0 -> { + el1.overview + } + 1 -> { + el2.overview + } + else -> "" + } + } + + private fun checkLessNum(list: List, num: Int): Int { + /* Если нажали на 32 фильма, а их всего 20, то мы округляем вниз число 20 до 16. */ + var variable = num + if (list.size < num) { + variable = 2.0.pow(floor(log(list.size.toDouble(), 2.0))).toInt() + } + return variable + } + + companion object { + const val POSTER_BASE_URL = "https://image.tmdb.org/t/p/w342" + const val PRESSED_EIGHT_MOVIES = 8 + const val PRESSED_SIXTEEN_MOVIES = 16 + const val PRESSED_THIRTY_TWO_MOVIES = 32 + const val PRESSED_SIXTY_FOUR_MOVIES = 64 + const val PRESSED_ONE_HUNDRED_AND_TWENTY_EIGHT_MOVIES = 128 + const val PRESSED_TWO_HUNDRED_AND_FIFTY_SIX_MOVIES = 256 + const val ROUND_COUNT_START = 1 + const val ONE_LOOP = 1 + const val TWO_LOOP = 2 + const val FOUR_LOOP = 4 + const val SEVEN_LOOP = 7 + const val THIRTEEN_LOOP = 13 + const val FLAG_DEFAULT = 0 + const val TITLE_DEFAULT = "" + const val GENRE_ID_DEFAULT = "12" + const val FILM_DECLENSION = "фильмов" + } + + override fun onCleared() { + gotList.removeObserver(observerList) + super.onCleared() + } +} diff --git a/app/src/main/java/com/example/finema/ui/userProfile/ProfileAdapter.kt b/app/src/main/java/com/example/finema/ui/userProfile/ProfileAdapter.kt new file mode 100644 index 00000000..88bf62e2 --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/userProfile/ProfileAdapter.kt @@ -0,0 +1,59 @@ +package com.example.finema.ui.userProfile + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.example.finema.R +import com.example.finema.models.databaseModels.TopModel +import com.example.finema.util.downloadAndSetImageUrl + +class ProfileAdapter( + private val listener: ProfileHolder.Listener +) : RecyclerView.Adapter() { + var movies: List = emptyList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.top_movie_item, parent, false) + + return ProfileHolder(view, listener) + } + + override fun onBindViewHolder(holder: ProfileHolder, position: Int) { + holder.bind(position, movies) + } + + override fun getItemCount(): Int { + return movies.size + } + + class ProfileHolder( + view: View, + private val listener: Listener + ) : RecyclerView.ViewHolder(view) { + + private val movieName: TextView = view.findViewById(R.id.top_title) + private val moviePoster: ImageView = view.findViewById(R.id.posterTop) + + interface Listener { + fun onMovieClicked(movie: TopModel) + } + + fun bind(position: Int, listMovie: List) { + movieName.text = listMovie[position].title + moviePoster.downloadAndSetImageUrl( + POSTER_BASE_URL + listMovie[adapterPosition].imageUrl + ) + itemView.setOnClickListener { + listener.onMovieClicked(listMovie[adapterPosition]) + } + } + } + + companion object { + const val POSTER_BASE_URL = "https://image.tmdb.org/t/p/w342" + } +} diff --git a/app/src/main/java/com/example/finema/ui/userProfile/ProfileFragment.kt b/app/src/main/java/com/example/finema/ui/userProfile/ProfileFragment.kt new file mode 100644 index 00000000..bb6f176a --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/userProfile/ProfileFragment.kt @@ -0,0 +1,65 @@ +package com.example.finema.ui.userProfile + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.Navigation +import androidx.recyclerview.widget.LinearLayoutManager +import com.example.finema.R +import com.example.finema.databinding.ProfileFragmentBinding +import com.example.finema.models.databaseModels.TopModel +import com.example.finema.ui.base.BaseFragment +import com.example.finema.util.downloadAndSetImageUri +import org.koin.androidx.viewmodel.ext.android.getViewModel + +class ProfileFragment : + BaseFragment(), + ProfileAdapter.ProfileHolder.Listener { + + private lateinit var profileAdapter: ProfileAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ProfileFragmentBinding + .inflate(inflater, container, false) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + viewModel = getViewModel() + super.onViewCreated(view, savedInstanceState) + + binding.userName.text = viewModel.getName() + binding.userPhone.text = viewModel.getNumber() + binding.Avatar.downloadAndSetImageUri(viewModel.getImage()) + + profileAdapter = ProfileAdapter(this) + viewModel.topMovies.observe( + viewLifecycleOwner, + { + it?.let { + binding.topRecycler.visibility = View.VISIBLE + profileAdapter.movies = it + binding.topRecycler.layoutManager = LinearLayoutManager(context) + binding.topRecycler.adapter = profileAdapter + } + } + ) + } + + override fun onMovieClicked(movie: TopModel) { + goDetailsFragment(movie.id) + } + + private fun goDetailsFragment(filmIdInfo: Long) { + val bundle = Bundle() + bundle.putSerializable("filmId", filmIdInfo) + Navigation.findNavController(requireActivity(), R.id.fragment) + .navigate(R.id.action_fragment_profile_to_fragment_film, bundle) + } +} diff --git a/app/src/main/java/com/example/finema/ui/userProfile/ProfileViewModel.kt b/app/src/main/java/com/example/finema/ui/userProfile/ProfileViewModel.kt new file mode 100644 index 00000000..9196796e --- /dev/null +++ b/app/src/main/java/com/example/finema/ui/userProfile/ProfileViewModel.kt @@ -0,0 +1,41 @@ +package com.example.finema.ui.userProfile + +import android.net.Uri +import androidx.lifecycle.LiveData +import com.example.finema.database.DatabaseRepository +import com.example.finema.models.databaseModels.TopModel +import com.example.finema.ui.base.BaseViewModel +import com.google.firebase.auth.FirebaseAuth + +class ProfileViewModel( + private val dbRepository: DatabaseRepository +) : BaseViewModel() { + + private val mAuth = FirebaseAuth.getInstance() + val topMovies: LiveData> = dbRepository.allTop + + fun getName(): String? { + return if (mAuth.currentUser?.displayName == null) { + "Гость" + } else { + mAuth.currentUser?.displayName + } + } + + fun getNumber(): String? { + return if (mAuth.currentUser?.phoneNumber == null) { + " " + } else { + mAuth.currentUser?.phoneNumber + } + } + + fun getImage(): Uri? { + + return if (mAuth.currentUser?.photoUrl == null) { + Uri.parse("android.resource://com.example.finema/drawable/default_profile_avatar") + } else { + mAuth.currentUser?.photoUrl + } + } +} diff --git a/app/src/main/java/com/example/finema/util/AppModule.kt b/app/src/main/java/com/example/finema/util/AppModule.kt new file mode 100644 index 00000000..2fc86b89 --- /dev/null +++ b/app/src/main/java/com/example/finema/util/AppModule.kt @@ -0,0 +1,84 @@ +package com.example.finema.util + +import com.example.finema.api.IMoviesRepository +import com.example.finema.api.MoviesApi +import com.example.finema.api.MoviesRepository +import com.example.finema.database.DatabaseRepository +import com.example.finema.database.firebase.FirebaseRepository +import com.example.finema.database.firebase.IFirebaseRepository +import com.example.finema.database.room.RoomDataBase +import com.example.finema.database.room.RoomRepository +import com.example.finema.repositories.AppPreference +import com.example.finema.repositories.Contract +import com.example.finema.repositories.IAppPreference +import com.example.finema.ui.chooseFavourite.ChooseFavouriteViewModel +import com.example.finema.ui.favourite.FavouriteViewModel +import com.example.finema.ui.higherlower.HigherLowerViewModel +import com.example.finema.ui.higherlowerrating.HigherLowerRatingViewModel +import com.example.finema.ui.movieDetail.MovieDetailsViewModel +import com.example.finema.ui.settings.SettingsViewModel +import com.example.finema.ui.signIn.SignInViewModel +import com.example.finema.ui.tmp.TmpViewModel +import com.example.finema.ui.tournaments.categories.CategoryTournamentVM +import com.example.finema.ui.tournaments.genres.GenresTournamentVM +import com.example.finema.ui.tournaments.tournament.TournamentVM +import com.example.finema.ui.userProfile.ProfileViewModel +import com.google.firebase.database.FirebaseDatabase +import org.koin.android.ext.koin.androidContext +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.module.Module +import org.koin.dsl.module + +val viewModelModule: Module = module { + + viewModel { HigherLowerViewModel(get(), get(), get(), get()) } + + viewModel { SignInViewModel(get(), get()) } + + viewModel { TmpViewModel(get(), get(), get()) } + + viewModel { GenresTournamentVM(get(), get(), get()) } + + viewModel { MovieDetailsViewModel(get(), get(), get(), get()) } + + viewModel { TournamentVM(get(), get(), get(), get()) } + + viewModel { CategoryTournamentVM(get(), get()) } + + viewModel { HigherLowerRatingViewModel(get(), get(), get(), get()) } + + viewModel { FavouriteViewModel(get()) } + + viewModel { SettingsViewModel(get(), get(), get(), get()) } + + viewModel { ChooseFavouriteViewModel(get()) } + + viewModel { ProfileViewModel(get()) } +} + +val apiModule: Module = module { + + single { MoviesApi() } +} + +val databaseModule: Module = module { + + single { RoomDataBase.getInstance(androidContext()) } + + single { RoomDataBase.getInstance(androidContext()).getRoomDao() } + + single { FirebaseDatabase.getInstance() } +} + +val repositoryModule: Module = module { + + single { Contract() } + + single { AppPreference(androidContext()) } + + single { RoomRepository(get()) } + + single { MoviesRepository(get()) } + + single { FirebaseRepository(get()) } +} diff --git a/app/src/main/java/com/example/finema/util/Coroutines.kt b/app/src/main/java/com/example/finema/util/Coroutines.kt new file mode 100644 index 00000000..a9deb19f --- /dev/null +++ b/app/src/main/java/com/example/finema/util/Coroutines.kt @@ -0,0 +1,17 @@ +package com.example.finema.util + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.launch + +object Coroutines { + + fun ioThenMan(work: suspend (() -> T?), callback: ((T?) -> Unit)) = + CoroutineScope(Dispatchers.Main).launch { + val data = CoroutineScope(Dispatchers.IO).async rt@{ + return@rt work() + }.await() + callback(data) + } +} diff --git a/app/src/main/java/com/example/finema/util/funs.kt b/app/src/main/java/com/example/finema/util/funs.kt new file mode 100644 index 00000000..2d914133 --- /dev/null +++ b/app/src/main/java/com/example/finema/util/funs.kt @@ -0,0 +1,35 @@ +package com.example.finema.util +import android.content.Context +import android.net.Uri +import android.view.View +import android.view.inputmethod.InputMethodManager +import android.widget.ImageView +import com.bumptech.glide.Glide +import com.example.finema.R + +private const val PICTURE_MEASURES = 500 +private const val FLAG_ZERO = 0 + +fun ImageView.downloadAndSetImageUrl(url: String?) { + Glide + .with(this) + .load(url) + .centerInside() + .override(PICTURE_MEASURES, PICTURE_MEASURES) + .placeholder(R.drawable.movies_24) + .into(this) +} + +fun ImageView.downloadAndSetImageUri(uri: Uri?) { + Glide + .with(this) + .load(uri) + .centerInside() + .placeholder(R.drawable.movies_24) + .into(this) +} + +fun View.hideKeyboard() { + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(windowToken, FLAG_ZERO) +} diff --git a/app/src/main/res/anim/slide_down.xml b/app/src/main/res/anim/slide_down.xml new file mode 100644 index 00000000..2f7ef940 --- /dev/null +++ b/app/src/main/res/anim/slide_down.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_up.xml b/app/src/main/res/anim/slide_up.xml new file mode 100644 index 00000000..b5caae9c --- /dev/null +++ b/app/src/main/res/anim/slide_up.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/parasite1.jpg b/app/src/main/res/drawable-v24/parasite1.jpg new file mode 100644 index 00000000..28f129c8 Binary files /dev/null and b/app/src/main/res/drawable-v24/parasite1.jpg differ diff --git a/app/src/main/res/drawable/background_selector.xml b/app/src/main/res/drawable/background_selector.xml new file mode 100644 index 00000000..6aa2e9f5 --- /dev/null +++ b/app/src/main/res/drawable/background_selector.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bookmark_24.xml b/app/src/main/res/drawable/bookmark_24.xml new file mode 100644 index 00000000..7e480e24 --- /dev/null +++ b/app/src/main/res/drawable/bookmark_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/bookmark_border_24.xml b/app/src/main/res/drawable/bookmark_border_24.xml new file mode 100644 index 00000000..61dc4cf3 --- /dev/null +++ b/app/src/main/res/drawable/bookmark_border_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/corner_button.xml b/app/src/main/res/drawable/corner_button.xml new file mode 100644 index 00000000..e44af957 --- /dev/null +++ b/app/src/main/res/drawable/corner_button.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/custom_button.xml b/app/src/main/res/drawable/custom_button.xml new file mode 100644 index 00000000..75cd53c3 --- /dev/null +++ b/app/src/main/res/drawable/custom_button.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/default_profile_avatar.png b/app/src/main/res/drawable/default_profile_avatar.png new file mode 100644 index 00000000..269eab83 Binary files /dev/null and b/app/src/main/res/drawable/default_profile_avatar.png differ diff --git a/app/src/main/res/drawable/exit_to_app_24.xml b/app/src/main/res/drawable/exit_to_app_24.xml new file mode 100644 index 00000000..83cdf05a --- /dev/null +++ b/app/src/main/res/drawable/exit_to_app_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher2_background.xml b/app/src/main/res/drawable/ic_launcher2_background.xml new file mode 100644 index 00000000..ca3826a4 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher2_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/info_24.xml b/app/src/main/res/drawable/info_24.xml new file mode 100644 index 00000000..17255b7a --- /dev/null +++ b/app/src/main/res/drawable/info_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/movies_24.xml b/app/src/main/res/drawable/movies_24.xml new file mode 100644 index 00000000..afcfb9c8 --- /dev/null +++ b/app/src/main/res/drawable/movies_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/notifications_24.xml b/app/src/main/res/drawable/notifications_24.xml new file mode 100644 index 00000000..21cb88d1 --- /dev/null +++ b/app/src/main/res/drawable/notifications_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/outline_menu_24.xml b/app/src/main/res/drawable/outline_menu_24.xml new file mode 100644 index 00000000..4350ba96 --- /dev/null +++ b/app/src/main/res/drawable/outline_menu_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/outline_star_24.xml b/app/src/main/res/drawable/outline_star_24.xml new file mode 100644 index 00000000..5412e798 --- /dev/null +++ b/app/src/main/res/drawable/outline_star_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/restore_from_trash_24.xml b/app/src/main/res/drawable/restore_from_trash_24.xml new file mode 100644 index 00000000..c83d9698 --- /dev/null +++ b/app/src/main/res/drawable/restore_from_trash_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/search_24.xml b/app/src/main/res/drawable/search_24.xml new file mode 100644 index 00000000..c0d516d4 --- /dev/null +++ b/app/src/main/res/drawable/search_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/settings_24.xml b/app/src/main/res/drawable/settings_24.xml new file mode 100644 index 00000000..41a82ede --- /dev/null +++ b/app/src/main/res/drawable/settings_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..3b807cfe --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/category_item.xml b/app/src/main/res/layout/category_item.xml new file mode 100644 index 00000000..531b324a --- /dev/null +++ b/app/src/main/res/layout/category_item.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/choose_favourite_fragment.xml b/app/src/main/res/layout/choose_favourite_fragment.xml new file mode 100644 index 00000000..894ab1bc --- /dev/null +++ b/app/src/main/res/layout/choose_favourite_fragment.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/favourite_fragment.xml b/app/src/main/res/layout/favourite_fragment.xml new file mode 100644 index 00000000..f75b76d9 --- /dev/null +++ b/app/src/main/res/layout/favourite_fragment.xml @@ -0,0 +1,44 @@ + + + + + +