From 3cfc7d73b44aee55c8770602dbcbde401864e126 Mon Sep 17 00:00:00 2001 From: Jordan Artzet Date: Tue, 31 Jan 2023 18:42:29 +0100 Subject: [PATCH 01/18] :construction: add retrofit add retrofit instance and the api --- Sources/app/build.gradle | 11 +++++ Sources/app/src/main/AndroidManifest.xml | 2 + .../api/MovieApplicationAPI.kt | 18 ++++++++ .../movieapplication/api/RetrofitInstance.kt | 27 +++++++++++ .../pm/movieapplication/data/dao/MovieDAO.kt | 8 ++++ .../movieapplication/data/dao/MovieEntity.kt | 9 ++++ .../iut/pm/movieapplication/model/Popular.kt | 17 +++++++ .../movieapplication/network/dtos/GenreDTO.kt | 7 +++ .../movieapplication/network/dtos/MovieDTO.kt | 25 ++++++++++ .../network/dtos/PopularDTO.kt | 12 +++++ .../repository/MovieRepository.kt | 15 ++++++ .../repository/PopularRepository.kt | 33 +++++++++++++ .../ui/adapter/HomeItemAdapter.kt | 10 ++-- .../ui/fragments/HomeSectionsFragment.kt | 15 +++--- .../ui/fragments/MoviesFragment.kt | 21 ++++++++- .../ui/viewmodel/HomeSectionsVM.kt | 4 ++ .../movieapplication/ui/viewmodel/MoviesVM.kt | 46 +++++++++++++++++++ .../pm/movieapplication/utils/Constants.kt | 9 ++++ .../res/layout/item_vertical_fragment.xml | 1 + Sources/build.gradle | 2 +- 20 files changed, 280 insertions(+), 12 deletions(-) create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/api/RetrofitInstance.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/data/dao/MovieEntity.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Popular.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/GenreDTO.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/MovieDTO.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/PopularDTO.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/PopularRepository.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/HomeSectionsVM.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Constants.kt diff --git a/Sources/app/build.gradle b/Sources/app/build.gradle index 3440a82..c53bb1e 100644 --- a/Sources/app/build.gradle +++ b/Sources/app/build.gradle @@ -42,6 +42,8 @@ dependencies { implementation "androidx.appcompat:appcompat:$rootProject.appCompatVersion" implementation "androidx.activity:activity-ktx:$rootProject.activityVersion" + implementation 'androidx.core:core-ktx:1.9.0' + implementation "androidx.fragment:fragment-ktx:1.5.5" // Room components implementation "androidx.room:room-ktx:$rootProject.roomVersion" @@ -62,6 +64,15 @@ dependencies { implementation "androidx.constraintlayout:constraintlayout:$rootProject.constraintLayoutVersion" implementation "com.google.android.material:material:$rootProject.materialVersion" + // Moshi + implementation "com.squareup.moshi:moshi-kotlin:1.13.0" + // Retrofit + implementation "com.squareup.retrofit2:retrofit:2.9.0" + // Retrofit with Scalar Converter + implementation "com.squareup.retrofit2:converter-scalars:2.9.0" + // Retrofit with Moshi Converter + implementation "com.squareup.retrofit2:converter-moshi:2.9.0" + // Testing testImplementation "junit:junit:$rootProject.junitVersion" androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion" diff --git a/Sources/app/src/main/AndroidManifest.xml b/Sources/app/src/main/AndroidManifest.xml index a4559cd..be3f5d0 100644 --- a/Sources/app/src/main/AndroidManifest.xml +++ b/Sources/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/RetrofitInstance.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/RetrofitInstance.kt new file mode 100644 index 0000000..740a00e --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/RetrofitInstance.kt @@ -0,0 +1,27 @@ +package fr.iut.pm.movieapplication.api + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import fr.iut.pm.movieapplication.utils.Constants.Companion.BASE_URL +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory + +private val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + + +object RetrofitInstance { + + private val retrofit by lazy { + Retrofit.Builder() + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .baseUrl(BASE_URL) + .build() + } + + val api : MovieApplicationAPI by lazy { + retrofit.create(MovieApplicationAPI::class.java) + } +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/data/dao/MovieDAO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/data/dao/MovieDAO.kt index bd50cd9..cf78568 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/data/dao/MovieDAO.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/data/dao/MovieDAO.kt @@ -1,6 +1,8 @@ package fr.iut.pm.movieapplication.data.dao import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy import androidx.room.Query import fr.iut.pm.movieapplication.model.Movie import kotlinx.coroutines.flow.Flow @@ -11,5 +13,11 @@ interface MovieDAO { @Query("SELECT * FROM movies_table ORDER BY original_title ASC") fun getMovieByAlphabetizeMovie() : Flow> + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(movie : Movie) + + @Query("DELETE FROM movies_table") + suspend fun deleteAll() + } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/data/dao/MovieEntity.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/data/dao/MovieEntity.kt new file mode 100644 index 0000000..c0eaec1 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/data/dao/MovieEntity.kt @@ -0,0 +1,9 @@ +package fr.iut.pm.movieapplication.data.dao + +import androidx.room.Entity + +@Entity("movies_table") +class MovieEntity { + + +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Popular.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Popular.kt new file mode 100644 index 0000000..256c611 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Popular.kt @@ -0,0 +1,17 @@ +package fr.iut.pm.movieapplication.model + +import androidx.room.ColumnInfo +import androidx.room.Embedded + +data class Popular( + @ColumnInfo("page") + val page : Int, + @ColumnInfo("results") + @Embedded + val results : List, + val totalResults : Int, + val totalPages : Int + +){ + +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/GenreDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/GenreDTO.kt new file mode 100644 index 0000000..5405548 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/GenreDTO.kt @@ -0,0 +1,7 @@ +package fr.iut.pm.movieapplication.network.dtos + +data class GenreDTO( + private val id : Int, + private val name : String +) { +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/MovieDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/MovieDTO.kt new file mode 100644 index 0000000..839f4be --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/MovieDTO.kt @@ -0,0 +1,25 @@ +package fr.iut.pm.movieapplication.network.dtos + +import com.squareup.moshi.Json + +data class MovieDTO( + @Json(name = "poster_path") + val posterPath : String?, + val adult : Boolean, + val overview : String?, + @Json(name = "release_date") + val releaseDate : String, + @Json(name = "genre_ids") + val genreIds : List, + val id : Int, + @Json(name = "original_title") + val originalTitle : String, + @Json(name = "original_language") + val originalLanguage : String, + val title : String + + +) { + + +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/PopularDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/PopularDTO.kt new file mode 100644 index 0000000..8d82f1c --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/PopularDTO.kt @@ -0,0 +1,12 @@ +package fr.iut.pm.movieapplication.network.dtos + +import com.squareup.moshi.Json + +data class PopularDTO( + val page : Int, + val results : List, + @Json(name = "total_pages") + val totalPages : Int, + @Json(name = "total_results") + val totalResults : Int +) {} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt new file mode 100644 index 0000000..3574a71 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt @@ -0,0 +1,15 @@ +package fr.iut.pm.movieapplication.repository + +import fr.iut.pm.movieapplication.data.dao.MovieDAO +import fr.iut.pm.movieapplication.model.Movie +import fr.iut.pm.movieapplication.network.dtos.MovieDTO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.transform + +class MovieRepository() { + + suspend fun getPopularMovies() : List{ + return PopularRepository().getPopular()!!.results + } +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/PopularRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/PopularRepository.kt new file mode 100644 index 0000000..94c8f3c --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/PopularRepository.kt @@ -0,0 +1,33 @@ +package fr.iut.pm.movieapplication.repository + +import android.util.Log +import fr.iut.pm.movieapplication.api.RetrofitInstance +import fr.iut.pm.movieapplication.network.dtos.PopularDTO +import fr.iut.pm.movieapplication.utils.Constants.Companion.API_KEY +import kotlinx.coroutines.flow.Flow +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class PopularRepository() { + + suspend fun getPopular(): PopularDTO? { + + lateinit var popularDTO : PopularDTO + RetrofitInstance.api.getPopularMovies(API_KEY).enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + Log.d("RESPONSE ::", response.body()?.page.toString()) + popularDTO = response.body()!! + } + } + + override fun onFailure(call: Call, t: Throwable) { + TODO("Not yet implemented") + } + + + }) + return popularDTO + } +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt index 2e6f389..8fcc367 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt @@ -6,11 +6,13 @@ import android.view.ViewGroup import android.widget.ImageView import androidx.recyclerview.widget.RecyclerView import fr.iut.pm.movieapplication.R +import fr.iut.pm.movieapplication.network.dtos.MovieDTO import fr.iut.pm.movieapplication.ui.activity.MainActivity class HomeItemAdapter( private val context : MainActivity, - private val layoutId : Int + private val layoutId : Int, + private val list : List ) : RecyclerView.Adapter() { class ViewHolder(view : View) : RecyclerView.ViewHolder(view) { @@ -23,7 +25,9 @@ class HomeItemAdapter( return ViewHolder(view) } - override fun onBindViewHolder(holder: ViewHolder, position: Int) {} + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val currentItem = list?.get(position) + } - override fun getItemCount(): Int = 5 + override fun getItemCount(): Int = list?.size!! } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt index 4e47377..4b9a081 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt @@ -7,30 +7,33 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView import fr.iut.pm.movieapplication.R +import fr.iut.pm.movieapplication.network.dtos.MovieDTO +import fr.iut.pm.movieapplication.network.dtos.PopularDTO import fr.iut.pm.movieapplication.ui.activity.MainActivity import fr.iut.pm.movieapplication.ui.adapter.HomeItemAdapter import fr.iut.pm.movieapplication.ui.adapter.HomeItemDecoration +import fr.iut.pm.movieapplication.ui.viewmodel.HomeSectionsVM -class HomeSectionsFragment( - private val context : MainActivity -) : Fragment() { +class HomeSectionsFragment(private val context : MainActivity) : Fragment() { + + private lateinit var homeSectionsViewModel : HomeSectionsVM override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_home_sections, container, false) //get the trends RecyclerView val homeTrendsRecyclerView = view?.findViewById(R.id.home_trends_recycler_view) - homeTrendsRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page) + homeTrendsRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page,ArrayList()) homeTrendsRecyclerView?.addItemDecoration(HomeItemDecoration()) //get the popularity RecyclerView val homePopularityRecyclerView = view?.findViewById(R.id.home_popularity_recycler_view) - homePopularityRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page) + homePopularityRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page,ArrayList()) homePopularityRecyclerView?.addItemDecoration(HomeItemDecoration()) //get the free RecyclerView val homeFreeRecyclerView = view?.findViewById(R.id.home_free_recycler_view) - homeFreeRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page) + homeFreeRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page,ArrayList()) homeFreeRecyclerView?.addItemDecoration(HomeItemDecoration()) return view } diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt index 648ac51..5cee8de 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt @@ -5,13 +5,25 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.viewModelFactory import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import fr.iut.pm.movieapplication.R +import fr.iut.pm.movieapplication.data.dao.MovieDAO +import fr.iut.pm.movieapplication.network.dtos.MovieDTO +import fr.iut.pm.movieapplication.network.dtos.PopularDTO +import fr.iut.pm.movieapplication.repository.MovieRepository import fr.iut.pm.movieapplication.ui.activity.MainActivity import fr.iut.pm.movieapplication.ui.adapter.CategoryItemDecoration import fr.iut.pm.movieapplication.ui.adapter.HomeItemAdapter import fr.iut.pm.movieapplication.ui.adapter.HomeItemDecoration +import fr.iut.pm.movieapplication.ui.viewmodel.MoviesVM +import fr.iut.pm.movieapplication.ui.viewmodel.MoviesVMFactory +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking class MoviesFragment( private val context : MainActivity @@ -20,10 +32,15 @@ class MoviesFragment( override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_movies, container, false) + val viewModel by viewModels { + MoviesVMFactory(MovieRepository()) + } + - //get the recycler view val moviesRecyclerView = view?.findViewById(R.id.movies_item_recycler_view) - moviesRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_vertical_fragment) + + moviesRecyclerView?.adapter = HomeItemAdapter(context, R.layout.item_vertical_fragment,viewModel.popularMovies.value!!) + moviesRecyclerView?.layoutManager = GridLayoutManager(context, 2) moviesRecyclerView?.addItemDecoration(CategoryItemDecoration()) diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/HomeSectionsVM.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/HomeSectionsVM.kt new file mode 100644 index 0000000..0f0ae6b --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/HomeSectionsVM.kt @@ -0,0 +1,4 @@ +package fr.iut.pm.movieapplication.ui.viewmodel + +class HomeSectionsVM { +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt new file mode 100644 index 0000000..5772d1e --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt @@ -0,0 +1,46 @@ +package fr.iut.pm.movieapplication.ui.viewmodel + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import fr.iut.pm.movieapplication.network.dtos.MovieDTO +import fr.iut.pm.movieapplication.repository.MovieRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class MoviesVM(private val repository: MovieRepository) : ViewModel() { + + private val _popularMovies = MutableLiveData>() + val popularMovies : MutableLiveData> = _popularMovies + + init { + getPopularMovies() + } + + + private fun getPopularMovies() { + viewModelScope.launch { + try { + _popularMovies.value = repository.getPopularMovies()} + catch (e : Exception) { + e.stackTrace + } + + } + } + +} + +class MoviesVMFactory( + private val repository: MovieRepository + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(MoviesVM::class.java)) { + return MoviesVM(repository) as T + } + throw java.lang.IllegalArgumentException("Unknown ViewModel class") + } + +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Constants.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Constants.kt new file mode 100644 index 0000000..a992963 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Constants.kt @@ -0,0 +1,9 @@ +package fr.iut.pm.movieapplication.utils + +class Constants { + + companion object { + const val BASE_URL = "https://api.themoviedb.org/3/" + const val API_KEY = "8f14a279249638d7f247d0d7298b21b4" + } +} \ No newline at end of file diff --git a/Sources/app/src/main/res/layout/item_vertical_fragment.xml b/Sources/app/src/main/res/layout/item_vertical_fragment.xml index a07abbd..9427bc7 100644 --- a/Sources/app/src/main/res/layout/item_vertical_fragment.xml +++ b/Sources/app/src/main/res/layout/item_vertical_fragment.xml @@ -24,6 +24,7 @@ android:background="@color/black" /> Date: Tue, 31 Jan 2023 23:55:32 +0100 Subject: [PATCH 02/18] :construction: first request from api --- Sources/app/build.gradle | 5 ++- .../api/MovieApplicationAPI.kt | 14 +++--- .../movieapplication/api/RetrofitInstance.kt | 20 ++++----- .../pm/movieapplication/api/dtos/MovieDTO.kt | 44 +++++++++++++++++++ .../api/dtos/MovieResultDTO.kt | 40 +++++++++++++++++ .../movieapplication/api/dtos/PopularDTO.kt | 18 ++++++++ .../fr/iut/pm/movieapplication/model/Movie.kt | 39 +++++++--------- .../movieapplication/network/dtos/MovieDTO.kt | 25 ----------- .../network/dtos/PopularDTO.kt | 12 ----- .../repository/MovieRepository.kt | 43 +++++++++++++++--- .../repository/PopularRepository.kt | 21 +-------- .../ui/adapter/HomeItemAdapter.kt | 10 ++--- .../ui/fragments/HomeSectionsFragment.kt | 8 ++-- .../ui/fragments/MoviesFragment.kt | 32 +++++++------- .../movieapplication/ui/viewmodel/MoviesVM.kt | 37 ++++++---------- .../ui/viewmodel/ViewModelFactory.kt | 9 ++++ .../iut/pm/movieapplication/utils/Mapper.kt | 36 +++++++++++++++ 17 files changed, 259 insertions(+), 154 deletions(-) create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDTO.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieResultDTO.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/PopularDTO.kt delete mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/MovieDTO.kt delete mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/PopularDTO.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/ViewModelFactory.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Mapper.kt diff --git a/Sources/app/build.gradle b/Sources/app/build.gradle index c53bb1e..b83198a 100644 --- a/Sources/app/build.gradle +++ b/Sources/app/build.gradle @@ -66,10 +66,13 @@ dependencies { // Moshi implementation "com.squareup.moshi:moshi-kotlin:1.13.0" + //GSON + //implementation 'com.squareup.retrofit2:converter-gson:2.1.0' + // Retrofit implementation "com.squareup.retrofit2:retrofit:2.9.0" // Retrofit with Scalar Converter - implementation "com.squareup.retrofit2:converter-scalars:2.9.0" + //implementation "com.squareup.retrofit2:converter-scalars:2.9.0" // Retrofit with Moshi Converter implementation "com.squareup.retrofit2:converter-moshi:2.9.0" diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt index c2341cb..f721f01 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt @@ -1,18 +1,18 @@ package fr.iut.pm.movieapplication.api -import fr.iut.pm.movieapplication.network.dtos.PopularDTO -import fr.iut.pm.movieapplication.utils.Constants -import fr.iut.pm.movieapplication.utils.Constants.Companion.API_KEY -import kotlinx.coroutines.flow.Flow +import fr.iut.pm.movieapplication.api.dtos.MovieResultDTO +import fr.iut.pm.movieapplication.api.dtos.PopularDTO import retrofit2.Call -import retrofit2.Response import retrofit2.http.GET -import retrofit2.http.Header +import retrofit2.http.Path import retrofit2.http.Query interface MovieApplicationAPI { @GET("movie/popular") - suspend fun getPopularMovies(@Query("api_key") apiKey : String) : Call + fun getPopularMovies(@Query("api_key") apiKey : String) : Call + + @GET("movie/{movie_id") + fun getMovieDetails(@Path("movie_id") movieId : Int, @Query("api_key") apiKey: String) : Call } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/RetrofitInstance.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/RetrofitInstance.kt index 740a00e..b3db321 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/RetrofitInstance.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/RetrofitInstance.kt @@ -6,22 +6,18 @@ import fr.iut.pm.movieapplication.utils.Constants.Companion.BASE_URL import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory -private val moshi = Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build() - object RetrofitInstance { - private val retrofit by lazy { - Retrofit.Builder() - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .baseUrl(BASE_URL) + private val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) .build() - } - val api : MovieApplicationAPI by lazy { - retrofit.create(MovieApplicationAPI::class.java) - } + private val retrofit = Retrofit.Builder() + .baseUrl("https://api.themoviedb.org/3/") + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + + val api = retrofit.create(MovieApplicationAPI::class.java) } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDTO.kt new file mode 100644 index 0000000..83abc57 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDTO.kt @@ -0,0 +1,44 @@ +package fr.iut.pm.movieapplication.api.dtos + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import androidx.room.PrimaryKey +import androidx.room.Relation +import com.squareup.moshi.Json +import fr.iut.pm.movieapplication.model.Genre +import fr.iut.pm.movieapplication.model.ProductionCompany +import fr.iut.pm.movieapplication.model.ProductionCountry +import fr.iut.pm.movieapplication.network.dtos.GenreDTO + +class MovieDTO ( + val adult: Boolean, + val budget: Int, + val genres: Array?, + val homepage: String?, + val id: Int, + @Json(name = "original_language") + val originalLanguage: String, + @Json(name = "original_title") + val originalTitle: String, + val overview: String?, + val popularity: Double, + @Json(name = "poster_path") + val posterPath: String?, + @Json(name = "production_countries") + val productionCountries: Array, + @Json(name = "release_date") + val releaseDate: String, + val revenue: Int, + val runtime: Int?, + //var spokenLanguages : Array, + val status: String, + @Json(name = "tag_line") + val tagLine: String?, + val title: String, + @Json(name = "vote_average") + val voteAverage: Double, + @Json(name = "vote_count") + val voteCount: Int, + val backdropPath: String? +){ +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieResultDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieResultDTO.kt new file mode 100644 index 0000000..68acf5b --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieResultDTO.kt @@ -0,0 +1,40 @@ +package fr.iut.pm.movieapplication.api.dtos + + +import com.squareup.moshi.Json + +class MovieResultDTO( + @Json(name = "poster_path") + val posterPath : String?, + @Json(name = "adult") + val adult : Boolean, + @Json(name = "overview") + val overview : String, + @Json(name = "release_date") + val releaseDate : String?, + @Json(name = "genre_ids") + val genreIds : List?, + @Json(name = "id") + val id : Int, + @Json(name = "original_title") + val originalTitle : String?, + @Json(name = "original_language") + val originalLanguage : String?, + @Json(name = "title") + val title : String?, + @Json(name = "backdrop_path") + val backdropPath : String?, + @Json(name = "popularity") + val popularity : Double?, + @Json(name = "vote_count") + val voteCount : Int?, + val video : Boolean?, + @Json(name = "vote_average") + val voteAverage : Double? + + + +) { + + +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/PopularDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/PopularDTO.kt new file mode 100644 index 0000000..baf6422 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/PopularDTO.kt @@ -0,0 +1,18 @@ +package fr.iut.pm.movieapplication.api.dtos + + +import com.squareup.moshi.Json + + + +class PopularDTO( + @Json(name = "page") + val page : Int, + @Json(name = "results") + val results : List, + @Json(name = "total_results") + val totalResults : Int, + @Json(name = "total_pages") + val totalPages : Int + +) {} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Movie.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Movie.kt index 6269f00..1b8021b 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Movie.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Movie.kt @@ -1,57 +1,50 @@ package fr.iut.pm.movieapplication.model import androidx.room.ColumnInfo -import androidx.room.Embedded import androidx.room.Entity import androidx.room.PrimaryKey -import androidx.room.Relation -import java.util.Date @Entity(tableName = "movies_table") data class Movie( @ColumnInfo(name = "adult") val adult: Boolean, @ColumnInfo(name = "budget") - val budget: Int, + val budget: Int?, @ColumnInfo(name = "genres") - val genres: Array, + val genres: Array?, @ColumnInfo(name = "homepage") val homePage: String?, @PrimaryKey @ColumnInfo(name = "id") val movieId: Int, @ColumnInfo(name = "original_language") - val originalLanguage: String, + val originalLanguage: String?, @ColumnInfo(name = "original_title") - val originalTitle: String, + val originalTitle: String?, val overview: String?, - val popularity: Int, + val popularity: Double?, @ColumnInfo(name = "poster_path") - val posterPath : String?, - @Embedded - val productionCompanies: Array, - @Relation( - parentColumn = "movieId", - entityColumn = "" - ) + val posterPath: String?, + val productionCompanies: Array?, @ColumnInfo(name = "production_countries") - val productionCountries: Array, + val productionCountries: Array?, @ColumnInfo(name = "release_date") - val releaseDate: Date, - val revenue: Int, + val releaseDate: String?, + val revenue: Int?, val runtime: Int?, //var spokenLanguages : Array, - val status: String, + val status: String?, @ColumnInfo(name = "tag_line") val tagLine: String?, - val title: String, + val title: String?, @ColumnInfo(name = "vote_average") - val voteAverage: Number, + val voteAverage: Double?, @ColumnInfo(name = "vote_count") - val voteCount : Int + val voteCount: Int?, + val backdropPath: String? - ) { +) { override fun equals(other: Any?): Boolean { diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/MovieDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/MovieDTO.kt deleted file mode 100644 index 839f4be..0000000 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/MovieDTO.kt +++ /dev/null @@ -1,25 +0,0 @@ -package fr.iut.pm.movieapplication.network.dtos - -import com.squareup.moshi.Json - -data class MovieDTO( - @Json(name = "poster_path") - val posterPath : String?, - val adult : Boolean, - val overview : String?, - @Json(name = "release_date") - val releaseDate : String, - @Json(name = "genre_ids") - val genreIds : List, - val id : Int, - @Json(name = "original_title") - val originalTitle : String, - @Json(name = "original_language") - val originalLanguage : String, - val title : String - - -) { - - -} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/PopularDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/PopularDTO.kt deleted file mode 100644 index 8d82f1c..0000000 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/PopularDTO.kt +++ /dev/null @@ -1,12 +0,0 @@ -package fr.iut.pm.movieapplication.network.dtos - -import com.squareup.moshi.Json - -data class PopularDTO( - val page : Int, - val results : List, - @Json(name = "total_pages") - val totalPages : Int, - @Json(name = "total_results") - val totalResults : Int -) {} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt index 3574a71..6293e23 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt @@ -1,15 +1,44 @@ package fr.iut.pm.movieapplication.repository -import fr.iut.pm.movieapplication.data.dao.MovieDAO +import android.util.Log +import fr.iut.pm.movieapplication.api.RetrofitInstance +import fr.iut.pm.movieapplication.api.dtos.PopularDTO import fr.iut.pm.movieapplication.model.Movie -import fr.iut.pm.movieapplication.network.dtos.MovieDTO -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.transform +import fr.iut.pm.movieapplication.utils.Constants +import fr.iut.pm.movieapplication.utils.Constants.Companion.API_KEY +import fr.iut.pm.movieapplication.utils.Mapper +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response class MovieRepository() { - suspend fun getPopularMovies() : List{ - return PopularRepository().getPopular()!!.results + fun getPopularMovies(callback: (List) -> Unit ) { + + val listMovie : MutableList = mutableListOf() + + RetrofitInstance.api.getPopularMovies(API_KEY).enqueue(object : + Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + Log.d("List :", response.body().toString()) + val popularDTO = response.body() + val listMoviesDTO = popularDTO?.results + listMoviesDTO?.forEach { + + val movie = Mapper.MapToMovie(it) + listMovie.add(movie) + Log.d("Movie ", movie.title!!) + } + + } + callback(listMovie) + } + + override fun onFailure(call: Call, t: Throwable) { + Log.d("Error failure", t.message.toString()) + } + + }) } } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/PopularRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/PopularRepository.kt index 94c8f3c..6c8a645 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/PopularRepository.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/PopularRepository.kt @@ -2,9 +2,8 @@ package fr.iut.pm.movieapplication.repository import android.util.Log import fr.iut.pm.movieapplication.api.RetrofitInstance -import fr.iut.pm.movieapplication.network.dtos.PopularDTO +import fr.iut.pm.movieapplication.api.dtos.PopularDTO import fr.iut.pm.movieapplication.utils.Constants.Companion.API_KEY -import kotlinx.coroutines.flow.Flow import retrofit2.Call import retrofit2.Callback import retrofit2.Response @@ -12,22 +11,6 @@ import retrofit2.Response class PopularRepository() { suspend fun getPopular(): PopularDTO? { - - lateinit var popularDTO : PopularDTO - RetrofitInstance.api.getPopularMovies(API_KEY).enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - Log.d("RESPONSE ::", response.body()?.page.toString()) - popularDTO = response.body()!! - } - } - - override fun onFailure(call: Call, t: Throwable) { - TODO("Not yet implemented") - } - - - }) - return popularDTO + throw NoSuchMethodError() } } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt index 8fcc367..ebe3d79 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt @@ -3,16 +3,14 @@ package fr.iut.pm.movieapplication.ui.adapter import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageView import androidx.recyclerview.widget.RecyclerView -import fr.iut.pm.movieapplication.R -import fr.iut.pm.movieapplication.network.dtos.MovieDTO +import fr.iut.pm.movieapplication.model.Movie import fr.iut.pm.movieapplication.ui.activity.MainActivity class HomeItemAdapter( - private val context : MainActivity, - private val layoutId : Int, - private val list : List + private val context: MainActivity, + private val layoutId: Int, + private val list: List ) : RecyclerView.Adapter() { class ViewHolder(view : View) : RecyclerView.ViewHolder(view) { diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt index 4b9a081..cd8244f 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt @@ -7,8 +7,8 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView import fr.iut.pm.movieapplication.R -import fr.iut.pm.movieapplication.network.dtos.MovieDTO -import fr.iut.pm.movieapplication.network.dtos.PopularDTO +import fr.iut.pm.movieapplication.api.dtos.MovieResultDTO +import fr.iut.pm.movieapplication.model.Movie import fr.iut.pm.movieapplication.ui.activity.MainActivity import fr.iut.pm.movieapplication.ui.adapter.HomeItemAdapter import fr.iut.pm.movieapplication.ui.adapter.HomeItemDecoration @@ -23,12 +23,12 @@ class HomeSectionsFragment(private val context : MainActivity) : Fragment() { //get the trends RecyclerView val homeTrendsRecyclerView = view?.findViewById(R.id.home_trends_recycler_view) - homeTrendsRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page,ArrayList()) + homeTrendsRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page,ArrayList()) homeTrendsRecyclerView?.addItemDecoration(HomeItemDecoration()) //get the popularity RecyclerView val homePopularityRecyclerView = view?.findViewById(R.id.home_popularity_recycler_view) - homePopularityRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page,ArrayList()) + homePopularityRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page,ArrayList()) homePopularityRecyclerView?.addItemDecoration(HomeItemDecoration()) //get the free RecyclerView diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt index 5cee8de..7e17ebb 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt @@ -6,43 +6,45 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.viewModelFactory +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import fr.iut.pm.movieapplication.R -import fr.iut.pm.movieapplication.data.dao.MovieDAO -import fr.iut.pm.movieapplication.network.dtos.MovieDTO -import fr.iut.pm.movieapplication.network.dtos.PopularDTO +import fr.iut.pm.movieapplication.api.RetrofitInstance +import fr.iut.pm.movieapplication.model.Movie import fr.iut.pm.movieapplication.repository.MovieRepository import fr.iut.pm.movieapplication.ui.activity.MainActivity import fr.iut.pm.movieapplication.ui.adapter.CategoryItemDecoration import fr.iut.pm.movieapplication.ui.adapter.HomeItemAdapter -import fr.iut.pm.movieapplication.ui.adapter.HomeItemDecoration import fr.iut.pm.movieapplication.ui.viewmodel.MoviesVM import fr.iut.pm.movieapplication.ui.viewmodel.MoviesVMFactory -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.runBlocking +import fr.iut.pm.movieapplication.ui.viewmodel.viewModelFactory +import kotlinx.coroutines.launch class MoviesFragment( private val context : MainActivity ) : Fragment() { - + private val moviesVM: MoviesVM by viewModels{ MoviesVMFactory(repository = MovieRepository())} override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_movies, container, false) - val viewModel by viewModels { - MoviesVMFactory(MovieRepository()) + + + + val repository = MovieRepository() + repository.getPopularMovies { listMovies -> + val moviesRecyclerView = view?.findViewById(R.id.movies_item_recycler_view) + moviesRecyclerView?.adapter = HomeItemAdapter(context, R.layout.item_vertical_fragment, listMovies) + moviesRecyclerView ?. layoutManager = GridLayoutManager (context, 2) + moviesRecyclerView?.addItemDecoration(CategoryItemDecoration()) } - val moviesRecyclerView = view?.findViewById(R.id.movies_item_recycler_view) - moviesRecyclerView?.adapter = HomeItemAdapter(context, R.layout.item_vertical_fragment,viewModel.popularMovies.value!!) - moviesRecyclerView?.layoutManager = GridLayoutManager(context, 2) - moviesRecyclerView?.addItemDecoration(CategoryItemDecoration()) + + return view diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt index 5772d1e..67fb70d 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt @@ -1,46 +1,37 @@ package fr.iut.pm.movieapplication.ui.viewmodel -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import fr.iut.pm.movieapplication.network.dtos.MovieDTO +import androidx.lifecycle.* +import fr.iut.pm.movieapplication.api.dtos.MovieResultDTO +import fr.iut.pm.movieapplication.model.Movie import fr.iut.pm.movieapplication.repository.MovieRepository -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext class MoviesVM(private val repository: MovieRepository) : ViewModel() { - private val _popularMovies = MutableLiveData>() - val popularMovies : MutableLiveData> = _popularMovies + private val _popularMovies = MutableLiveData>() + val popularMovies : LiveData> = _popularMovies init { - getPopularMovies() + //loadData() } - - - private fun getPopularMovies() { + suspend fun loadData() { viewModelScope.launch { - try { - _popularMovies.value = repository.getPopularMovies()} - catch (e : Exception) { - e.stackTrace + repository.getPopularMovies { movies -> + _popularMovies.value = movies } + }.join() - } } - } + + class MoviesVMFactory( private val repository: MovieRepository ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(MoviesVM::class.java)) { - return MoviesVM(repository) as T - } - throw java.lang.IllegalArgumentException("Unknown ViewModel class") + return MoviesVM(repository) as T } } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/ViewModelFactory.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/ViewModelFactory.kt new file mode 100644 index 0000000..d9d7bcc --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/ViewModelFactory.kt @@ -0,0 +1,9 @@ +package fr.iut.pm.movieapplication.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +inline fun viewModelFactory(crossinline f: () -> VM) = + object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T = f() as T + } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Mapper.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Mapper.kt new file mode 100644 index 0000000..9c91be0 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Mapper.kt @@ -0,0 +1,36 @@ +package fr.iut.pm.movieapplication.utils + +import fr.iut.pm.movieapplication.api.dtos.MovieResultDTO +import fr.iut.pm.movieapplication.model.Movie + +object Mapper { + + fun MapToMovie( movieDTO : MovieResultDTO) : Movie { + return Movie( + adult = movieDTO.adult, + posterPath = movieDTO.posterPath, + overview = movieDTO.overview, + releaseDate = movieDTO.releaseDate, + movieId = movieDTO.id, + originalTitle = movieDTO.originalTitle, + originalLanguage = movieDTO.originalLanguage, + title = movieDTO.title, + backdropPath = movieDTO.backdropPath, + popularity = movieDTO.popularity, + voteCount = movieDTO.voteCount, + voteAverage = movieDTO.voteAverage, + budget = null, + genres = null, + homePage = null, + productionCompanies = null, + productionCountries = null, + revenue = null, + runtime = null, + status = null, + tagLine = null + + ) + } + + +} \ No newline at end of file From 838819f7b837957656c60a0ec0a8b6d8c290bc1d Mon Sep 17 00:00:00 2001 From: Jordan Artzet Date: Wed, 1 Feb 2023 10:37:57 +0100 Subject: [PATCH 03/18] :construction: update item adapter The item adapter work correctly --- Sources/app/build.gradle | 3 ++ .../api/MovieApplicationAPI.kt | 2 + .../movieapplication/api/RetrofitInstance.kt | 2 +- .../api/config/GlobalImageConfig.kt | 51 +++++++++++++++++++ .../ui/activity/MainActivity.kt | 7 +++ .../ui/adapter/HomeItemAdapter.kt | 25 ++++++++- .../pm/movieapplication/utils/Constants.kt | 1 + .../res/layout/item_vertical_fragment.xml | 17 ++++--- 8 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/api/config/GlobalImageConfig.kt diff --git a/Sources/app/build.gradle b/Sources/app/build.gradle index b83198a..6940713 100644 --- a/Sources/app/build.gradle +++ b/Sources/app/build.gradle @@ -60,6 +60,9 @@ dependencies { api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines" api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines" + //Coil + implementation "io.coil-kt:coil:1.1.1" + // UI implementation "androidx.constraintlayout:constraintlayout:$rootProject.constraintLayoutVersion" implementation "com.google.android.material:material:$rootProject.materialVersion" diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt index f721f01..6212409 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt @@ -1,5 +1,7 @@ package fr.iut.pm.movieapplication.api +import fr.iut.pm.movieapplication.api.config.GlobalImageConfig +import fr.iut.pm.movieapplication.api.config.ImageConfig import fr.iut.pm.movieapplication.api.dtos.MovieResultDTO import fr.iut.pm.movieapplication.api.dtos.PopularDTO import retrofit2.Call diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/RetrofitInstance.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/RetrofitInstance.kt index b3db321..cbc1f53 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/RetrofitInstance.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/RetrofitInstance.kt @@ -15,7 +15,7 @@ object RetrofitInstance { .build() private val retrofit = Retrofit.Builder() - .baseUrl("https://api.themoviedb.org/3/") + .baseUrl(BASE_URL) .addConverterFactory(MoshiConverterFactory.create(moshi)) .build() diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/config/GlobalImageConfig.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/config/GlobalImageConfig.kt new file mode 100644 index 0000000..74063eb --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/config/GlobalImageConfig.kt @@ -0,0 +1,51 @@ +package fr.iut.pm.movieapplication.api.config + +import android.util.Log +import com.squareup.moshi.Json + + +object GlobalImageConfig { + @Json(name = "base_url") + private var _baseUrl : String = "" + val baseUrl : String = _baseUrl + @Json(name = "secure_base_url") + private var _secureBaseUrl : String = "" + val secureBaseUrl = _secureBaseUrl + @Json(name = "backdrop_sizes") + private var backdropSizes : List = listOf() + @Json(name = "logo_sizes") + private var logoSizes : List = listOf() + @Json(name = "poster_sizes") + private var posterSizes : List = listOf() + @Json(name = "profile_sizes") + private var profilSizes : List = listOf() + @Json(name = "still_sizes") + private var stillSizes : List = listOf() + + fun updateConfig(config: ImageConfig) { + Log.d("BASE URL IMAGE", baseUrl) + _baseUrl = config.baseUrl + _secureBaseUrl = config.secureBaseUrl + posterSizes = config.posterSizes + backdropSizes = config.backdropSizes + } + +} + +data class ImageConfig( + + @Json(name = "images.base_url") + val baseUrl : String, + @Json(name = "images.secure_base_url") + val secureBaseUrl : String, + @Json(name = "images.backdrop_sizes") + val backdropSizes : List, + @Json(name = "images.logo_sizes") + val logoSizes : List, + @Json(name = "images.poster_sizes") + val posterSizes : List, + @Json(name = "images.profile_sizes") + val profileSizes : List, + @Json(name = "images.still_sizes") + val stillSizes : List +) \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt index 1410f0f..6a0f405 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt @@ -7,8 +7,15 @@ import android.view.View import androidx.fragment.app.Fragment import com.google.android.material.bottomnavigation.BottomNavigationView import fr.iut.pm.movieapplication.R +import fr.iut.pm.movieapplication.api.RetrofitInstance +import fr.iut.pm.movieapplication.api.config.GlobalImageConfig import fr.iut.pm.movieapplication.ui.fragments.HomeSectionsFragment import fr.iut.pm.movieapplication.ui.fragments.MoviesFragment +import fr.iut.pm.movieapplication.utils.Constants +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import retrofit2.awaitResponse class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt index ebe3d79..20acfe9 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt @@ -1,11 +1,19 @@ package fr.iut.pm.movieapplication.ui.adapter +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.core.net.toUri import androidx.recyclerview.widget.RecyclerView +import coil.load +import fr.iut.pm.movieapplication.R +import fr.iut.pm.movieapplication.api.config.GlobalImageConfig import fr.iut.pm.movieapplication.model.Movie import fr.iut.pm.movieapplication.ui.activity.MainActivity +import fr.iut.pm.movieapplication.utils.Constants.Companion.IMG_URL class HomeItemAdapter( private val context: MainActivity, @@ -14,6 +22,9 @@ class HomeItemAdapter( ) : RecyclerView.Adapter() { class ViewHolder(view : View) : RecyclerView.ViewHolder(view) { + val itemImage: ImageView = view.findViewById(R.id.item_image) + val itemName: TextView = view.findViewById(R.id.item_name) + val itemDate: TextView = view.findViewById(R.id.item_date) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @@ -24,8 +35,18 @@ class HomeItemAdapter( } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val currentItem = list?.get(position) + val currentItem = list[position] + Log.d("SINGLETON", GlobalImageConfig.baseUrl) + val imgUri = currentItem.posterPath?.let { + (IMG_URL + it).toUri().buildUpon().scheme("https").build() + + } + Log.d("SINGLETON", imgUri.toString() ) + holder.itemImage.load(imgUri) + holder.itemName.text = currentItem.title + holder.itemDate.text = currentItem.releaseDate + } - override fun getItemCount(): Int = list?.size!! + override fun getItemCount(): Int = list.size } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Constants.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Constants.kt index a992963..4081cf5 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Constants.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Constants.kt @@ -4,6 +4,7 @@ class Constants { companion object { const val BASE_URL = "https://api.themoviedb.org/3/" + const val IMG_URL = "https://image.tmdb.org/t/p/w500" const val API_KEY = "8f14a279249638d7f247d0d7298b21b4" } } \ No newline at end of file diff --git a/Sources/app/src/main/res/layout/item_vertical_fragment.xml b/Sources/app/src/main/res/layout/item_vertical_fragment.xml index 9427bc7..706f15c 100644 --- a/Sources/app/src/main/res/layout/item_vertical_fragment.xml +++ b/Sources/app/src/main/res/layout/item_vertical_fragment.xml @@ -1,12 +1,12 @@ + android:layout_width="200dp" + android:layout_height="wrap_content"> From 86f7de25885e6787e3efee96c4e0d668f7dfe10d Mon Sep 17 00:00:00 2001 From: Jordan Artzet Date: Wed, 1 Feb 2023 18:12:59 +0100 Subject: [PATCH 04/18] :construction: add the request for the home Now display the trends and the most popular media --- .../api/MovieApplicationAPI.kt | 10 ++++--- .../movieapplication/api/RetrofitInstance.kt | 2 +- .../repository/MovieRepository.kt | 26 +++++++++++++++++-- .../ui/activity/MainActivity.kt | 4 +++ .../ui/fragments/HomeSectionsFragment.kt | 13 +++++++--- .../ui/fragments/MoviesFragment.kt | 5 +--- .../res/layout/item_horizontal_home_page.xml | 10 +++---- .../res/layout/item_vertical_fragment.xml | 10 +++---- 8 files changed, 55 insertions(+), 25 deletions(-) diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt index 6212409..cd84a60 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt @@ -1,9 +1,8 @@ package fr.iut.pm.movieapplication.api -import fr.iut.pm.movieapplication.api.config.GlobalImageConfig -import fr.iut.pm.movieapplication.api.config.ImageConfig import fr.iut.pm.movieapplication.api.dtos.MovieResultDTO import fr.iut.pm.movieapplication.api.dtos.PopularDTO +import fr.iut.pm.movieapplication.utils.Constants.Companion.API_KEY import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Path @@ -12,9 +11,12 @@ import retrofit2.http.Query interface MovieApplicationAPI { @GET("movie/popular") - fun getPopularMovies(@Query("api_key") apiKey : String) : Call + fun getPopularMovies(@Query("api_key") apiKey : String = API_KEY) : Call + + @GET("trending/{media_type}/{time_window}") + fun getTrending(@Path("media_type") mediaType : String = "all", @Path("time_window") timeWindow : String = "day", @Query("api_key") apiKey: String = API_KEY ) : Call @GET("movie/{movie_id") - fun getMovieDetails(@Path("movie_id") movieId : Int, @Query("api_key") apiKey: String) : Call + fun getMovieDetails(@Path("movie_id") movieId : Int, @Query("api_key") apiKey: String = API_KEY) : Call } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/RetrofitInstance.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/RetrofitInstance.kt index cbc1f53..1ade6b0 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/RetrofitInstance.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/RetrofitInstance.kt @@ -19,5 +19,5 @@ object RetrofitInstance { .addConverterFactory(MoshiConverterFactory.create(moshi)) .build() - val api = retrofit.create(MovieApplicationAPI::class.java) + val api: MovieApplicationAPI = retrofit.create(MovieApplicationAPI::class.java) } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt index 6293e23..c099d8e 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt @@ -4,7 +4,6 @@ import android.util.Log import fr.iut.pm.movieapplication.api.RetrofitInstance import fr.iut.pm.movieapplication.api.dtos.PopularDTO import fr.iut.pm.movieapplication.model.Movie -import fr.iut.pm.movieapplication.utils.Constants import fr.iut.pm.movieapplication.utils.Constants.Companion.API_KEY import fr.iut.pm.movieapplication.utils.Mapper import retrofit2.Call @@ -17,7 +16,7 @@ class MovieRepository() { val listMovie : MutableList = mutableListOf() - RetrofitInstance.api.getPopularMovies(API_KEY).enqueue(object : + RetrofitInstance.api.getPopularMovies().enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { @@ -41,4 +40,27 @@ class MovieRepository() { }) } + + fun getTrends(callback: (List) -> Unit) { + val listMovie : MutableList = mutableListOf() + + RetrofitInstance.api.getTrending().enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if(response.isSuccessful) { + Log.d("Response",response.body().toString()) + val popularDTO = response.body() + popularDTO?.results?.forEach { + val movie = Mapper.MapToMovie(it) + listMovie.add(movie) + movie.title?.let { it1 -> Log.d("Movie", it1) } + } + } + callback(listMovie) + } + + override fun onFailure(call: Call, t: Throwable) { + Log.d("Error failure", t.message.toString()) + } + }) + } } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt index 6a0f405..f6fb97a 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt @@ -9,6 +9,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView import fr.iut.pm.movieapplication.R import fr.iut.pm.movieapplication.api.RetrofitInstance import fr.iut.pm.movieapplication.api.config.GlobalImageConfig +import fr.iut.pm.movieapplication.repository.MovieRepository import fr.iut.pm.movieapplication.ui.fragments.HomeSectionsFragment import fr.iut.pm.movieapplication.ui.fragments.MoviesFragment import fr.iut.pm.movieapplication.utils.Constants @@ -18,6 +19,9 @@ import kotlinx.coroutines.launch import retrofit2.awaitResponse class MainActivity : AppCompatActivity() { + + val movieRepository : MovieRepository = MovieRepository() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt index cd8244f..d321a90 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt @@ -14,7 +14,9 @@ import fr.iut.pm.movieapplication.ui.adapter.HomeItemAdapter import fr.iut.pm.movieapplication.ui.adapter.HomeItemDecoration import fr.iut.pm.movieapplication.ui.viewmodel.HomeSectionsVM -class HomeSectionsFragment(private val context : MainActivity) : Fragment() { +class HomeSectionsFragment( + private val context : MainActivity + ) : Fragment() { private lateinit var homeSectionsViewModel : HomeSectionsVM @@ -22,15 +24,18 @@ class HomeSectionsFragment(private val context : MainActivity) : Fragment() { val view = inflater.inflate(R.layout.fragment_home_sections, container, false) //get the trends RecyclerView + context.movieRepository.getTrends { val homeTrendsRecyclerView = view?.findViewById(R.id.home_trends_recycler_view) - homeTrendsRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page,ArrayList()) + homeTrendsRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page,it) homeTrendsRecyclerView?.addItemDecoration(HomeItemDecoration()) + } //get the popularity RecyclerView + context.movieRepository.getPopularMovies { val homePopularityRecyclerView = view?.findViewById(R.id.home_popularity_recycler_view) - homePopularityRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page,ArrayList()) + homePopularityRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page,it) homePopularityRecyclerView?.addItemDecoration(HomeItemDecoration()) - + } //get the free RecyclerView val homeFreeRecyclerView = view?.findViewById(R.id.home_free_recycler_view) homeFreeRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page,ArrayList()) diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt index 7e17ebb..eaa3d22 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt @@ -30,10 +30,7 @@ class MoviesFragment( override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_movies, container, false) - - - val repository = MovieRepository() - repository.getPopularMovies { listMovies -> + context.movieRepository.getPopularMovies { listMovies -> val moviesRecyclerView = view?.findViewById(R.id.movies_item_recycler_view) moviesRecyclerView?.adapter = HomeItemAdapter(context, R.layout.item_vertical_fragment, listMovies) moviesRecyclerView ?. layoutManager = GridLayoutManager (context, 2) diff --git a/Sources/app/src/main/res/layout/item_horizontal_home_page.xml b/Sources/app/src/main/res/layout/item_horizontal_home_page.xml index d7dc465..1864ae7 100644 --- a/Sources/app/src/main/res/layout/item_horizontal_home_page.xml +++ b/Sources/app/src/main/res/layout/item_horizontal_home_page.xml @@ -10,13 +10,13 @@ android:id="@+id/cardView" android:layout_width="match_parent" android:layout_height="200dp" - app:cardCornerRadius="15dp" + app:cardCornerRadius="5dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintTop_toBottomOf="@+id/item_name" /> \ No newline at end of file diff --git a/Sources/app/src/main/res/layout/item_vertical_fragment.xml b/Sources/app/src/main/res/layout/item_vertical_fragment.xml index 706f15c..c9a8ae4 100644 --- a/Sources/app/src/main/res/layout/item_vertical_fragment.xml +++ b/Sources/app/src/main/res/layout/item_vertical_fragment.xml @@ -1,12 +1,12 @@ @@ -36,7 +36,7 @@ From be99e981bfcde427644192bdeabec4f49ed16a46 Mon Sep 17 00:00:00 2001 From: Jordan Artzet Date: Thu, 2 Feb 2023 00:22:50 +0100 Subject: [PATCH 05/18] :construction: reorganize the view --- .../ui/activity/MainActivity.kt | 27 +++++ .../ui/fragments/MoviesFragment.kt | 7 -- .../ui/fragments/ShowsFragment.kt | 15 +++ .../src/main/res/drawable/item_selector.xml | 5 + .../app/src/main/res/font/source_sans_pro.ttf | Bin 0 -> 234740 bytes .../app/src/main/res/layout/activity_main.xml | 8 +- .../res/layout/fragment_home_sections.xml | 94 ++++++++++-------- .../src/main/res/layout/fragment_movies.xml | 1 + .../res/layout/item_horizontal_home_page.xml | 5 +- .../res/layout/item_vertical_fragment.xml | 1 + .../res/menu-v26/bottom_navigation_menu.xml | 13 +-- Sources/app/src/main/res/menu/app_menu.xml | 10 ++ .../main/res/menu/bottom_navigation_menu.xml | 13 +-- .../app/src/main/res/values-night/themes.xml | 2 +- Sources/app/src/main/res/values/colors.xml | 2 + Sources/app/src/main/res/values/dimens.xml | 4 + Sources/app/src/main/res/values/themes.xml | 28 +++++- 17 files changed, 163 insertions(+), 72 deletions(-) create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/ShowsFragment.kt create mode 100644 Sources/app/src/main/res/drawable/item_selector.xml create mode 100644 Sources/app/src/main/res/font/source_sans_pro.ttf create mode 100644 Sources/app/src/main/res/menu/app_menu.xml create mode 100644 Sources/app/src/main/res/values/dimens.xml diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt index f6fb97a..558a3c5 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt @@ -3,7 +3,10 @@ package fr.iut.pm.movieapplication.ui.activity import android.os.Build import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import android.view.Menu import android.view.View +import android.widget.SearchView.OnQueryTextListener +import androidx.appcompat.widget.SearchView import androidx.fragment.app.Fragment import com.google.android.material.bottomnavigation.BottomNavigationView import fr.iut.pm.movieapplication.R @@ -12,6 +15,7 @@ import fr.iut.pm.movieapplication.api.config.GlobalImageConfig import fr.iut.pm.movieapplication.repository.MovieRepository import fr.iut.pm.movieapplication.ui.fragments.HomeSectionsFragment import fr.iut.pm.movieapplication.ui.fragments.MoviesFragment +import fr.iut.pm.movieapplication.ui.fragments.ShowsFragment import fr.iut.pm.movieapplication.utils.Constants import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -41,6 +45,11 @@ class MainActivity : AppCompatActivity() { loadFragments(MoviesFragment(this)) return@setOnItemSelectedListener true } + + R.id.series_page -> { + loadFragments(ShowsFragment(this)) + return@setOnItemSelectedListener true + } else -> false } } @@ -58,11 +67,29 @@ class MainActivity : AppCompatActivity() { // status bar is hidden, so hide that too if necessary. actionBar?.hide() } + } + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.app_menu, menu) + val searchItem = menu?.findItem(R.id.search_bar) + val searchView : SearchView = searchItem?.actionView as SearchView + searchView.setOnQueryTextListener( object : SearchView.OnQueryTextListener { + + override fun onQueryTextSubmit(query: String?): Boolean { + TODO("Not yet implemented") + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + TODO("Not yet implemented") + return false + } + }) + return true } private fun loadFragments(fragment: Fragment) { diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt index eaa3d22..3623498 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt @@ -37,13 +37,6 @@ class MoviesFragment( moviesRecyclerView?.addItemDecoration(CategoryItemDecoration()) } - - - - - - - return view } } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/ShowsFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/ShowsFragment.kt new file mode 100644 index 0000000..b9f7feb --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/ShowsFragment.kt @@ -0,0 +1,15 @@ +package fr.iut.pm.movieapplication.ui.fragments + +import android.os.Bundle +import androidx.fragment.app.Fragment +import fr.iut.pm.movieapplication.ui.activity.MainActivity + +class ShowsFragment( + private val context : MainActivity +) : Fragment() { + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } +} \ No newline at end of file diff --git a/Sources/app/src/main/res/drawable/item_selector.xml b/Sources/app/src/main/res/drawable/item_selector.xml new file mode 100644 index 0000000..1666c03 --- /dev/null +++ b/Sources/app/src/main/res/drawable/item_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Sources/app/src/main/res/font/source_sans_pro.ttf b/Sources/app/src/main/res/font/source_sans_pro.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6791613ac1f0369e957ccb8218418118418f8da7 GIT binary patch literal 234740 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 \ No newline at end of file diff --git a/Sources/app/src/main/res/layout/fragment_home_sections.xml b/Sources/app/src/main/res/layout/fragment_home_sections.xml index 6c2afed..12275b5 100644 --- a/Sources/app/src/main/res/layout/fragment_home_sections.xml +++ b/Sources/app/src/main/res/layout/fragment_home_sections.xml @@ -12,48 +12,58 @@ android:layout_height="wrap_content" > - - - - - - - - - - - + + + + + + + + + + + \ No newline at end of file diff --git a/Sources/app/src/main/res/layout/fragment_movies.xml b/Sources/app/src/main/res/layout/fragment_movies.xml index 8b0c3d2..4094d47 100644 --- a/Sources/app/src/main/res/layout/fragment_movies.xml +++ b/Sources/app/src/main/res/layout/fragment_movies.xml @@ -8,6 +8,7 @@ android:id="@+id/movies_item_recycler_view" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/default_margin" /> \ No newline at end of file diff --git a/Sources/app/src/main/res/layout/item_horizontal_home_page.xml b/Sources/app/src/main/res/layout/item_horizontal_home_page.xml index 1864ae7..a98f4f9 100644 --- a/Sources/app/src/main/res/layout/item_horizontal_home_page.xml +++ b/Sources/app/src/main/res/layout/item_horizontal_home_page.xml @@ -4,7 +4,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="150dp" - android:layout_height="match_parent" > + android:layout_height="match_parent" + android:layout_marginTop="10dp"> @@ -38,7 +38,6 @@ android:id="@+id/item_date" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="23 nov 2012" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/item_name" /> diff --git a/Sources/app/src/main/res/layout/item_vertical_fragment.xml b/Sources/app/src/main/res/layout/item_vertical_fragment.xml index c9a8ae4..6c2d62a 100644 --- a/Sources/app/src/main/res/layout/item_vertical_fragment.xml +++ b/Sources/app/src/main/res/layout/item_vertical_fragment.xml @@ -10,6 +10,7 @@ android:layout_marginLeft="10dp" android:layout_marginStart="10dp" android:layout_marginEnd="10dp" + android:layout_marginTop="@dimen/default_margin" app:layout_constraintTop_toTopOf="parent" app:cardCornerRadius="10dp"> diff --git a/Sources/app/src/main/res/menu-v26/bottom_navigation_menu.xml b/Sources/app/src/main/res/menu-v26/bottom_navigation_menu.xml index 675901b..26edad9 100644 --- a/Sources/app/src/main/res/menu-v26/bottom_navigation_menu.xml +++ b/Sources/app/src/main/res/menu-v26/bottom_navigation_menu.xml @@ -3,21 +3,18 @@ + android:title="@string/bottom_home_item" /> + android:title="@string/bottom_movies_item" /> + android:title="@string/bottom_series_item" /> + android:icon="@drawable/ic_baseline_star_border_24" + android:title="@string/bottom_artist_item" /> \ No newline at end of file diff --git a/Sources/app/src/main/res/menu/app_menu.xml b/Sources/app/src/main/res/menu/app_menu.xml new file mode 100644 index 0000000..340dd81 --- /dev/null +++ b/Sources/app/src/main/res/menu/app_menu.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/Sources/app/src/main/res/menu/bottom_navigation_menu.xml b/Sources/app/src/main/res/menu/bottom_navigation_menu.xml index 675901b..26edad9 100644 --- a/Sources/app/src/main/res/menu/bottom_navigation_menu.xml +++ b/Sources/app/src/main/res/menu/bottom_navigation_menu.xml @@ -3,21 +3,18 @@ + android:title="@string/bottom_home_item" /> + android:title="@string/bottom_movies_item" /> + android:title="@string/bottom_series_item" /> + android:icon="@drawable/ic_baseline_star_border_24" + android:title="@string/bottom_artist_item" /> \ No newline at end of file diff --git a/Sources/app/src/main/res/values-night/themes.xml b/Sources/app/src/main/res/values-night/themes.xml index 9817c27..aaa9e8e 100644 --- a/Sources/app/src/main/res/values-night/themes.xml +++ b/Sources/app/src/main/res/values-night/themes.xml @@ -1,6 +1,6 @@ - + + + + + + + + + + + + \ No newline at end of file From ddc0f661e66dc3e03f3452ce196b64c75cf21062 Mon Sep 17 00:00:00 2001 From: Jordan Artzet Date: Thu, 2 Feb 2023 11:55:22 +0100 Subject: [PATCH 06/18] :construction: add tvdto and some requests tvdto not finished --- .../pm/movieapplication/api/MovieApplicationAPI.kt | 12 +++++++++++- .../fr/iut/pm/movieapplication/api/dtos/TvShowDTO.kt | 9 +++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/TvShowDTO.kt diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt index cd84a60..68b255c 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt @@ -1,5 +1,6 @@ package fr.iut.pm.movieapplication.api +import fr.iut.pm.movieapplication.api.dtos.MovieDTO import fr.iut.pm.movieapplication.api.dtos.MovieResultDTO import fr.iut.pm.movieapplication.api.dtos.PopularDTO import fr.iut.pm.movieapplication.utils.Constants.Companion.API_KEY @@ -13,10 +14,19 @@ interface MovieApplicationAPI { @GET("movie/popular") fun getPopularMovies(@Query("api_key") apiKey : String = API_KEY) : Call + @GET("movie/top_rated") + fun getTopRatedMovies(@Query("api_key") apiKey: String = API_KEY) : Call + + @GET("movie/upcoming") + fun getUpcomingMovies(@Query("api_key") apiKey: String = API_KEY) : Call + @GET("trending/{media_type}/{time_window}") fun getTrending(@Path("media_type") mediaType : String = "all", @Path("time_window") timeWindow : String = "day", @Query("api_key") apiKey: String = API_KEY ) : Call @GET("movie/{movie_id") - fun getMovieDetails(@Path("movie_id") movieId : Int, @Query("api_key") apiKey: String = API_KEY) : Call + fun getMovieDetails(@Path("movie_id") movieId : Int, @Query("api_key") apiKey: String = API_KEY) : Call + + @GET("tv/{tv_id}") + fun getShowDetails(@Path("tv_id") tvId : Int, @Query("api_key") apiKey: String = API_KEY) } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/TvShowDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/TvShowDTO.kt new file mode 100644 index 0000000..9e83997 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/TvShowDTO.kt @@ -0,0 +1,9 @@ +package fr.iut.pm.movieapplication.api.dtos + +import com.squareup.moshi.Json + +data class TvShowDTO( + @Json(name = "backdrop_path") + val backdropPath : String? +) { +} \ No newline at end of file From 5a6b157c4f776860159bb6d31719783dfa59adb7 Mon Sep 17 00:00:00 2001 From: Jordan Artzet Date: Thu, 2 Feb 2023 22:48:40 +0100 Subject: [PATCH 07/18] :construction: rebuild the dtos correctly --- .../api/MovieApplicationAPI.kt | 2 +- .../api/dtos/MediaResultDTO.kt | 42 +++++++++++ .../pm/movieapplication/api/dtos/MovieDTO.kt | 58 ++++++--------- .../api/dtos/MovieResultDTO.kt | 1 - .../movieapplication/api/dtos/PopularDTO.kt | 2 +- .../pm/movieapplication/api/dtos/TvShowDTO.kt | 29 +++++++- .../pm/movieapplication/data/dao/MovieDAO.kt | 6 +- .../fr/iut/pm/movieapplication/model/Movie.kt | 64 ---------------- .../iut/pm/movieapplication/model/Popular.kt | 3 +- .../model/media/MediaResult.kt | 24 ++++++ .../model/media/movie/Movie.kt | 22 ++++++ .../model/media/movie/MovieDetails.kt | 50 +++++++++++++ .../model/media/tvshow/TvShow.kt | 20 +++++ .../model/media/tvshow/TvShowDetails.kt | 4 + .../movieapplication/network/dtos/GenreDTO.kt | 7 -- .../repository/MovieRepository.kt | 22 +++--- .../repository/TVShowRepository.kt | 4 + .../ui/adapter/HomeItemAdapter.kt | 15 +++- .../ui/adapter/MovieAdapter.kt | 59 +++++++++++++++ .../ui/fragments/HomeSectionsFragment.kt | 5 +- .../ui/fragments/MoviesFragment.kt | 74 ++++++++++++++++--- .../movieapplication/ui/viewmodel/MoviesVM.kt | 16 +--- .../pm/movieapplication/utils/Constants.kt | 6 ++ .../iut/pm/movieapplication/utils/Mapper.kt | 36 --------- .../utils/MediaResultMapper.kt | 71 ++++++++++++++++++ .../src/main/res/layout/fragment_movies.xml | 2 +- .../res/layout/item_vertical_fragment.xml | 63 ++++++++-------- 27 files changed, 482 insertions(+), 225 deletions(-) create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt delete mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Movie.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/MediaResult.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/Movie.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/MovieDetails.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/tvshow/TvShow.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/tvshow/TvShowDetails.kt delete mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/GenreDTO.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TVShowRepository.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt delete mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Mapper.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt index 68b255c..803183d 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt @@ -12,7 +12,7 @@ import retrofit2.http.Query interface MovieApplicationAPI { @GET("movie/popular") - fun getPopularMovies(@Query("api_key") apiKey : String = API_KEY) : Call + fun getPopularMovies(@Query("api_key") apiKey : String = API_KEY, @Query("page") page : Int = 1) : Call @GET("movie/top_rated") fun getTopRatedMovies(@Query("api_key") apiKey: String = API_KEY) : Call diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt new file mode 100644 index 0000000..b428527 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt @@ -0,0 +1,42 @@ +package fr.iut.pm.movieapplication.api.dtos + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@Json(name = "results") +data class MediaResultDTO( + + @Json(name = "poster_path") + val posterPath : String, + val adult : Boolean, + val overview : String, + @Json(name = "first_air_date") + val firstAirDate : String?, + @Json(name = "release_date") + val releaseDate : String?, + @Json(name = "origin_country") + val originCountry : List?, + @Json(name = "genre_ids") + val genreIds : List, + val id : Int, + @Json(name = "original_title") + val originalTitle : String?, + @Json(name = "original_language") + val originalLanguage : String, + val title : String?, + @Json(name = "backdrop_path") + val backdropPath : String, + val popularity : Double, + @Json(name = "vote_count") + val voteCount : Int, + val video : Boolean?, + @Json(name = "vote_average") + val voteAverage : Double, + val name: String?, + @Json(name = "original_name") + val originalName : String?, + @Json(name = "media_type") + val mediaType : String +) { + +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDTO.kt index 83abc57..73f4017 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDTO.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDTO.kt @@ -1,44 +1,32 @@ package fr.iut.pm.movieapplication.api.dtos -import androidx.room.ColumnInfo -import androidx.room.Embedded -import androidx.room.PrimaryKey -import androidx.room.Relation import com.squareup.moshi.Json -import fr.iut.pm.movieapplication.model.Genre -import fr.iut.pm.movieapplication.model.ProductionCompany -import fr.iut.pm.movieapplication.model.ProductionCountry -import fr.iut.pm.movieapplication.network.dtos.GenreDTO -class MovieDTO ( - val adult: Boolean, - val budget: Int, - val genres: Array?, - val homepage: String?, - val id: Int, - @Json(name = "original_language") - val originalLanguage: String, - @Json(name = "original_title") - val originalTitle: String, - val overview: String?, - val popularity: Double, +open class MovieDTO ( @Json(name = "poster_path") - val posterPath: String?, - @Json(name = "production_countries") - val productionCountries: Array, + open val posterPath: String?, + open val adult: Boolean, + open val overview: String, @Json(name = "release_date") - val releaseDate: String, - val revenue: Int, - val runtime: Int?, - //var spokenLanguages : Array, - val status: String, - @Json(name = "tag_line") - val tagLine: String?, - val title: String, - @Json(name = "vote_average") - val voteAverage: Double, + open val releaseDate: String, + @Json(name = "genre_ids") + open val genreIds: List, + open val id: Int, + @Json(name = "original_title") + open val originalTitle: String, + @Json(name = "original_language") + open val originalLanguage: String, + open val title: String, + @Json(name = "backdrop_path") + open val backdropPath: String?, + open val popularity: Double, @Json(name = "vote_count") - val voteCount: Int, - val backdropPath: String? + open val voteCount: Int, + open val video : Boolean, + @Json(name = "vote_average") + open val voteAverage: Double + + ){ + } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieResultDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieResultDTO.kt index 68acf5b..bc1c340 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieResultDTO.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieResultDTO.kt @@ -28,7 +28,6 @@ class MovieResultDTO( val popularity : Double?, @Json(name = "vote_count") val voteCount : Int?, - val video : Boolean?, @Json(name = "vote_average") val voteAverage : Double? diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/PopularDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/PopularDTO.kt index baf6422..2db4407 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/PopularDTO.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/PopularDTO.kt @@ -9,7 +9,7 @@ class PopularDTO( @Json(name = "page") val page : Int, @Json(name = "results") - val results : List, + val results : List, @Json(name = "total_results") val totalResults : Int, @Json(name = "total_pages") diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/TvShowDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/TvShowDTO.kt index 9e83997..be4247c 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/TvShowDTO.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/TvShowDTO.kt @@ -2,8 +2,29 @@ package fr.iut.pm.movieapplication.api.dtos import com.squareup.moshi.Json -data class TvShowDTO( +open class TvShowDTO( + + @Json(name = "poster_path") + open val posterPath: String?, + open val popularity: Double, + open val id: Int, @Json(name = "backdrop_path") - val backdropPath : String? -) { -} \ No newline at end of file + open val backdropPath: String?, + @Json(name = "vote_average") + open val voteAverage: Double, + open val overview: String, + @Json(name = "first_air_date") + open val firstAirDate: String, + @Json(name = "origin_country") + open val originCountry: List, + @Json(name = "genre_ids") + open val genreIds: List, + @Json(name = "original_language") + open val originalLanguage: String, + @Json(name = "vote_count") + open val voteCount: Int, + open val name: String, + @Json(name = "original_name") + open val originalName: String, + ) +{} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/data/dao/MovieDAO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/data/dao/MovieDAO.kt index cf78568..5efb718 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/data/dao/MovieDAO.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/data/dao/MovieDAO.kt @@ -4,17 +4,17 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import fr.iut.pm.movieapplication.model.Movie +import fr.iut.pm.movieapplication.model.media.movie.MovieDetails import kotlinx.coroutines.flow.Flow @Dao interface MovieDAO { @Query("SELECT * FROM movies_table ORDER BY original_title ASC") - fun getMovieByAlphabetizeMovie() : Flow> + fun getMovieByAlphabetizeMovie() : Flow> @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insert(movie : Movie) + suspend fun insert(movie : MovieDetails) @Query("DELETE FROM movies_table") suspend fun deleteAll() diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Movie.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Movie.kt deleted file mode 100644 index 1b8021b..0000000 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Movie.kt +++ /dev/null @@ -1,64 +0,0 @@ -package fr.iut.pm.movieapplication.model - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "movies_table") -data class Movie( - @ColumnInfo(name = "adult") - val adult: Boolean, - @ColumnInfo(name = "budget") - val budget: Int?, - @ColumnInfo(name = "genres") - val genres: Array?, - @ColumnInfo(name = "homepage") - val homePage: String?, - @PrimaryKey - @ColumnInfo(name = "id") - val movieId: Int, - @ColumnInfo(name = "original_language") - val originalLanguage: String?, - @ColumnInfo(name = "original_title") - val originalTitle: String?, - val overview: String?, - val popularity: Double?, - @ColumnInfo(name = "poster_path") - val posterPath: String?, - val productionCompanies: Array?, - @ColumnInfo(name = "production_countries") - val productionCountries: Array?, - @ColumnInfo(name = "release_date") - val releaseDate: String?, - val revenue: Int?, - val runtime: Int?, - //var spokenLanguages : Array, - val status: String?, - @ColumnInfo(name = "tag_line") - val tagLine: String?, - val title: String?, - @ColumnInfo(name = "vote_average") - val voteAverage: Double?, - @ColumnInfo(name = "vote_count") - val voteCount: Int?, - val backdropPath: String? - - -) { - - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Movie - - if (movieId != other.movieId) return false - - return true - } - - override fun hashCode(): Int { - return movieId - } -} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Popular.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Popular.kt index 256c611..c278e4d 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Popular.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Popular.kt @@ -2,13 +2,14 @@ package fr.iut.pm.movieapplication.model import androidx.room.ColumnInfo import androidx.room.Embedded +import fr.iut.pm.movieapplication.model.media.movie.MovieDetails data class Popular( @ColumnInfo("page") val page : Int, @ColumnInfo("results") @Embedded - val results : List, + val results : List, val totalResults : Int, val totalPages : Int diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/MediaResult.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/MediaResult.kt new file mode 100644 index 0000000..d443566 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/MediaResult.kt @@ -0,0 +1,24 @@ +package fr.iut.pm.movieapplication.model.media + +data class MediaResult( + + val posterPath: String? = null, + val adult: Boolean, + val overview: String, + val firstAirDate: String? = null, + val releaseDate: String? = null, + val originCountry: List? = null, + val genreIds: List, + val id: Int, + val originalTitle: String? = null, + val originalLanguage: String, + val title: String?, + val backdropPath: String? = null, + val popularity: Double, + val voteCount: Int, + val voteAverage: Double, + val name: String? = null, + val originalName: String? = null, + val mediaType : String +) +{} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/Movie.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/Movie.kt new file mode 100644 index 0000000..1a5021b --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/Movie.kt @@ -0,0 +1,22 @@ +package fr.iut.pm.movieapplication.model.media.movie + +open class Movie( + open val posterPath: String?, + open val adult: Boolean, + open val overview: String, + open val releaseDate: String, + open val genreIds: List, + open val id: Int, + open val originalTitle: String, + open val originalLanguage: String, + open val title: String?, + open val backdropPath: String?, + open val popularity: Double, + open val voteCount: Int, + open val video: Boolean?, + open val voteAverage: Double, +) +{ + + +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/MovieDetails.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/MovieDetails.kt new file mode 100644 index 0000000..c70e337 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/MovieDetails.kt @@ -0,0 +1,50 @@ +package fr.iut.pm.movieapplication.model.media.movie + +import fr.iut.pm.movieapplication.model.ProductionCompany +import fr.iut.pm.movieapplication.model.ProductionCountry + + +data class MovieDetails( + + override val posterPath: String, + override val adult: Boolean, + override val overview: String, + override val releaseDate: String, + override val genreIds : List, + override val id: Int, + override val originalTitle: String, + override val originalLanguage: String, + override val title: String?, + override val backdropPath: String?, + override val popularity: Double, + override val voteCount: Int, + override val video: Boolean?, + override val voteAverage: Double, + val mediaType: String, + + val budget: Int?, + val homePage: String?, + val productionCompanies: Array?, + val productionCountries: Array?, + val revenue: Int?, + val runtime: Int?, + val status: String?, + val tagLine: String? + +) : Movie(posterPath, + adult, + overview, + releaseDate, + genreIds, + id, + originalTitle, + originalLanguage, + title, + backdropPath, + popularity, + voteCount, + video, + voteAverage) { + + +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/tvshow/TvShow.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/tvshow/TvShow.kt new file mode 100644 index 0000000..b9ac31a --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/tvshow/TvShow.kt @@ -0,0 +1,20 @@ +package fr.iut.pm.movieapplication.model.media.tvshow + +class TvShow( + val posterPath: String?, + val popularity: Double, + val id: Int, + val backdropPath: String, + val voteAverage: Double, + val overview: String, + val firstAirDate: String?, + val originCountry: List, + val genreIds: List, + val originalLanguage: String, + val voteCount: Int, + val name: String, + val originalName: String, + val mediaType: String = "tv", +) +{ +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/tvshow/TvShowDetails.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/tvshow/TvShowDetails.kt new file mode 100644 index 0000000..7a96534 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/tvshow/TvShowDetails.kt @@ -0,0 +1,4 @@ +package fr.iut.pm.movieapplication.model.media.tvshow + +class TvShowDetails { +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/GenreDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/GenreDTO.kt deleted file mode 100644 index 5405548..0000000 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/network/dtos/GenreDTO.kt +++ /dev/null @@ -1,7 +0,0 @@ -package fr.iut.pm.movieapplication.network.dtos - -data class GenreDTO( - private val id : Int, - private val name : String -) { -} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt index c099d8e..8212a01 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt @@ -3,20 +3,20 @@ package fr.iut.pm.movieapplication.repository import android.util.Log import fr.iut.pm.movieapplication.api.RetrofitInstance import fr.iut.pm.movieapplication.api.dtos.PopularDTO -import fr.iut.pm.movieapplication.model.Movie -import fr.iut.pm.movieapplication.utils.Constants.Companion.API_KEY -import fr.iut.pm.movieapplication.utils.Mapper +import fr.iut.pm.movieapplication.model.media.MediaResult +import fr.iut.pm.movieapplication.model.media.movie.Movie +import fr.iut.pm.movieapplication.utils.MediaResultMapper import retrofit2.Call import retrofit2.Callback import retrofit2.Response class MovieRepository() { - fun getPopularMovies(callback: (List) -> Unit ) { + fun getPopularMovies(page : Int = 1 ,callback: (List) -> Unit ) { val listMovie : MutableList = mutableListOf() - RetrofitInstance.api.getPopularMovies().enqueue(object : + RetrofitInstance.api.getPopularMovies(page = page).enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { @@ -25,9 +25,9 @@ class MovieRepository() { val listMoviesDTO = popularDTO?.results listMoviesDTO?.forEach { - val movie = Mapper.MapToMovie(it) + val movie = MediaResultMapper.mapToMovie(it) listMovie.add(movie) - Log.d("Movie ", movie.title!!) + Log.d("Movie ", movie.title!! ) } } @@ -41,8 +41,8 @@ class MovieRepository() { }) } - fun getTrends(callback: (List) -> Unit) { - val listMovie : MutableList = mutableListOf() + fun getTrends(callback: (List) -> Unit) { + val listMovie : MutableList = mutableListOf() RetrofitInstance.api.getTrending().enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { @@ -50,7 +50,7 @@ class MovieRepository() { Log.d("Response",response.body().toString()) val popularDTO = response.body() popularDTO?.results?.forEach { - val movie = Mapper.MapToMovie(it) + val movie = MediaResultMapper.mapToMediaResult(it) listMovie.add(movie) movie.title?.let { it1 -> Log.d("Movie", it1) } } @@ -59,7 +59,7 @@ class MovieRepository() { } override fun onFailure(call: Call, t: Throwable) { - Log.d("Error failure", t.message.toString()) + Log.d("Error failure", t.printStackTrace().toString()) } }) } diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TVShowRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TVShowRepository.kt new file mode 100644 index 0000000..b19f419 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TVShowRepository.kt @@ -0,0 +1,4 @@ +package fr.iut.pm.movieapplication.repository + +class TVShowRepository { +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt index 20acfe9..bb5a305 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt @@ -11,14 +11,17 @@ import androidx.recyclerview.widget.RecyclerView import coil.load import fr.iut.pm.movieapplication.R import fr.iut.pm.movieapplication.api.config.GlobalImageConfig -import fr.iut.pm.movieapplication.model.Movie +import fr.iut.pm.movieapplication.model.media.MediaResult +import fr.iut.pm.movieapplication.model.media.movie.Movie +import fr.iut.pm.movieapplication.model.media.movie.MovieDetails +import fr.iut.pm.movieapplication.model.media.tvshow.TvShow import fr.iut.pm.movieapplication.ui.activity.MainActivity import fr.iut.pm.movieapplication.utils.Constants.Companion.IMG_URL class HomeItemAdapter( private val context: MainActivity, private val layoutId: Int, - private val list: List + private val list: List ) : RecyclerView.Adapter() { class ViewHolder(view : View) : RecyclerView.ViewHolder(view) { @@ -39,13 +42,19 @@ class HomeItemAdapter( Log.d("SINGLETON", GlobalImageConfig.baseUrl) val imgUri = currentItem.posterPath?.let { (IMG_URL + it).toUri().buildUpon().scheme("https").build() - } Log.d("SINGLETON", imgUri.toString() ) holder.itemImage.load(imgUri) holder.itemName.text = currentItem.title holder.itemDate.text = currentItem.releaseDate + holder.itemView.setOnClickListener { + onItemClick(currentItem) + } + + } + private fun onItemClick(item : MediaResult) { + Log.d("item clicked", item.toString()) } override fun getItemCount(): Int = list.size diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt new file mode 100644 index 0000000..dc28e98 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt @@ -0,0 +1,59 @@ +package fr.iut.pm.movieapplication.ui.adapter + +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.core.net.toUri +import androidx.recyclerview.widget.RecyclerView +import coil.load +import fr.iut.pm.movieapplication.R +import fr.iut.pm.movieapplication.api.config.GlobalImageConfig +import fr.iut.pm.movieapplication.model.media.movie.Movie +import fr.iut.pm.movieapplication.ui.activity.MainActivity +import fr.iut.pm.movieapplication.utils.Constants + +class MovieAdapter( + private val context: MainActivity, + private val layoutId: Int, + private val list: List +) : RecyclerView.Adapter(){ + + class ViewHolder(view : View) : RecyclerView.ViewHolder(view) { + val itemImage: ImageView = view.findViewById(R.id.item_image) + val itemName: TextView = view.findViewById(R.id.item_name) + val itemDate: TextView = view.findViewById(R.id.item_date) + } + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeItemAdapter.ViewHolder { + val view = LayoutInflater + .from(parent.context) + .inflate(layoutId, parent, false) + return HomeItemAdapter.ViewHolder(view) + } + + override fun getItemCount(): Int = list.size + + + override fun onBindViewHolder(holder: HomeItemAdapter.ViewHolder, position: Int) { + val currentItem = list[position] + Log.d("SINGLETON", GlobalImageConfig.baseUrl) + val imgUri = currentItem.posterPath?.let { + (Constants.IMG_URL + it).toUri().buildUpon().scheme("https").build() + } + Log.d("SINGLETON", imgUri.toString() ) + holder.itemImage.load(imgUri) + holder.itemName.text = currentItem.title + holder.itemDate.text = currentItem.releaseDate + holder.itemView.setOnClickListener { + onItemClick(currentItem) + } + } + + private fun onItemClick(item : Movie) { + Log.d("item clicked", item.toString()) + } +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt index d321a90..528981f 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt @@ -7,11 +7,10 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView import fr.iut.pm.movieapplication.R -import fr.iut.pm.movieapplication.api.dtos.MovieResultDTO -import fr.iut.pm.movieapplication.model.Movie import fr.iut.pm.movieapplication.ui.activity.MainActivity import fr.iut.pm.movieapplication.ui.adapter.HomeItemAdapter import fr.iut.pm.movieapplication.ui.adapter.HomeItemDecoration +import fr.iut.pm.movieapplication.ui.adapter.MovieAdapter import fr.iut.pm.movieapplication.ui.viewmodel.HomeSectionsVM class HomeSectionsFragment( @@ -33,7 +32,7 @@ class HomeSectionsFragment( //get the popularity RecyclerView context.movieRepository.getPopularMovies { val homePopularityRecyclerView = view?.findViewById(R.id.home_popularity_recycler_view) - homePopularityRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page,it) + homePopularityRecyclerView?.adapter = MovieAdapter(context,R.layout.item_horizontal_home_page,it) homePopularityRecyclerView?.addItemDecoration(HomeItemDecoration()) } //get the free RecyclerView diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt index 3623498..9c868c9 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt @@ -6,37 +6,89 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import fr.iut.pm.movieapplication.R -import fr.iut.pm.movieapplication.api.RetrofitInstance -import fr.iut.pm.movieapplication.model.Movie +import fr.iut.pm.movieapplication.model.media.MediaResult +import fr.iut.pm.movieapplication.model.media.movie.Movie +import fr.iut.pm.movieapplication.model.media.movie.MovieDetails import fr.iut.pm.movieapplication.repository.MovieRepository import fr.iut.pm.movieapplication.ui.activity.MainActivity -import fr.iut.pm.movieapplication.ui.adapter.CategoryItemDecoration import fr.iut.pm.movieapplication.ui.adapter.HomeItemAdapter +import fr.iut.pm.movieapplication.ui.adapter.MovieAdapter import fr.iut.pm.movieapplication.ui.viewmodel.MoviesVM import fr.iut.pm.movieapplication.ui.viewmodel.MoviesVMFactory -import fr.iut.pm.movieapplication.ui.viewmodel.viewModelFactory -import kotlinx.coroutines.launch +import fr.iut.pm.movieapplication.utils.Constants.Companion.PAGE_SIZE class MoviesFragment( private val context : MainActivity ) : Fragment() { + private var isLoading = false + private var isLastPage = false + private var currentPage = 1 + private var currentList : MutableList = mutableListOf() + private val moviesVM: MoviesVM by viewModels{ MoviesVMFactory(repository = MovieRepository())} + lateinit var moviesRecyclerView : RecyclerView override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_movies, container, false) + // Get the RecyclerView + moviesRecyclerView = view.findViewById(R.id.movies_item_recycler_view) + + // Initialized the data inside our RecyclerView context.movieRepository.getPopularMovies { listMovies -> - val moviesRecyclerView = view?.findViewById(R.id.movies_item_recycler_view) - moviesRecyclerView?.adapter = HomeItemAdapter(context, R.layout.item_vertical_fragment, listMovies) - moviesRecyclerView ?. layoutManager = GridLayoutManager (context, 2) - moviesRecyclerView?.addItemDecoration(CategoryItemDecoration()) + currentList.addAll(0,listMovies) + moviesRecyclerView.adapter = MovieAdapter(context, R.layout.item_vertical_fragment, currentList) + moviesRecyclerView.layoutManager = GridLayoutManager (context, 3) + } + + // Create the ScrollListener + val scrollListener = object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val layoutManager = moviesRecyclerView.layoutManager as GridLayoutManager + val visibleItemCount = layoutManager.childCount + val totalItemCount = layoutManager.itemCount + val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() + + // If we are not already loading data and it's not the last page + if(!isLoading && !isLastPage) { + if(visibleItemCount + firstVisibleItemPosition >= totalItemCount + && firstVisibleItemPosition >= 0 + && totalItemCount >= PAGE_SIZE + ) { + loadMoreMovies() + } + } + } } + // Add the ScrollLister created before to our RecyclerView + moviesRecyclerView.addOnScrollListener(scrollListener) + return view } + + /** + * Method to load data when the user reaches the bottom of the view + */ + private fun loadMoreMovies() { + isLoading = true + currentPage += 1 + + if(currentPage == 1000) isLastPage = true + + val start = currentList.size + context.movieRepository.getPopularMovies(currentPage) { listMovies -> + + currentList.addAll(start, listMovies) + moviesRecyclerView.adapter?.notifyItemRangeChanged(start, listMovies.size) + + } + + isLoading = false + } } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt index 67fb70d..8548868 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt @@ -1,28 +1,18 @@ package fr.iut.pm.movieapplication.ui.viewmodel import androidx.lifecycle.* -import fr.iut.pm.movieapplication.api.dtos.MovieResultDTO -import fr.iut.pm.movieapplication.model.Movie +import fr.iut.pm.movieapplication.model.media.movie.MovieDetails import fr.iut.pm.movieapplication.repository.MovieRepository -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch class MoviesVM(private val repository: MovieRepository) : ViewModel() { - private val _popularMovies = MutableLiveData>() - val popularMovies : LiveData> = _popularMovies + private val _popularMovies = MutableLiveData>() + val popularMovies : LiveData> = _popularMovies init { //loadData() } - suspend fun loadData() { - viewModelScope.launch { - repository.getPopularMovies { movies -> - _popularMovies.value = movies - } - }.join() - - } } diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Constants.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Constants.kt index 4081cf5..3bc70c1 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Constants.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Constants.kt @@ -3,8 +3,14 @@ package fr.iut.pm.movieapplication.utils class Constants { companion object { + + //API const val BASE_URL = "https://api.themoviedb.org/3/" const val IMG_URL = "https://image.tmdb.org/t/p/w500" const val API_KEY = "8f14a279249638d7f247d0d7298b21b4" + + + //VIEW PAGINATION + const val PAGE_SIZE = 20 } } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Mapper.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Mapper.kt deleted file mode 100644 index 9c91be0..0000000 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Mapper.kt +++ /dev/null @@ -1,36 +0,0 @@ -package fr.iut.pm.movieapplication.utils - -import fr.iut.pm.movieapplication.api.dtos.MovieResultDTO -import fr.iut.pm.movieapplication.model.Movie - -object Mapper { - - fun MapToMovie( movieDTO : MovieResultDTO) : Movie { - return Movie( - adult = movieDTO.adult, - posterPath = movieDTO.posterPath, - overview = movieDTO.overview, - releaseDate = movieDTO.releaseDate, - movieId = movieDTO.id, - originalTitle = movieDTO.originalTitle, - originalLanguage = movieDTO.originalLanguage, - title = movieDTO.title, - backdropPath = movieDTO.backdropPath, - popularity = movieDTO.popularity, - voteCount = movieDTO.voteCount, - voteAverage = movieDTO.voteAverage, - budget = null, - genres = null, - homePage = null, - productionCompanies = null, - productionCountries = null, - revenue = null, - runtime = null, - status = null, - tagLine = null - - ) - } - - -} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt new file mode 100644 index 0000000..3b3bca8 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt @@ -0,0 +1,71 @@ +package fr.iut.pm.movieapplication.utils + +import fr.iut.pm.movieapplication.api.dtos.MediaResultDTO +import fr.iut.pm.movieapplication.model.media.MediaResult +import fr.iut.pm.movieapplication.model.media.movie.Movie +import fr.iut.pm.movieapplication.model.media.tvshow.TvShow + +object MediaResultMapper { + + fun mapToTvShow(mediaResultDTO: MediaResultDTO): TvShow { + return TvShow( + posterPath = mediaResultDTO.posterPath, + popularity = mediaResultDTO.popularity, + id = mediaResultDTO.id, + backdropPath = mediaResultDTO.backdropPath!!, + voteAverage = mediaResultDTO.voteAverage, + overview = mediaResultDTO.overview, + firstAirDate = mediaResultDTO.firstAirDate, + originCountry = mediaResultDTO.originCountry!!, + genreIds = mediaResultDTO.genreIds, + originalLanguage = mediaResultDTO.originalLanguage, + voteCount = mediaResultDTO.voteCount, + name = mediaResultDTO.name!!, + originalName = mediaResultDTO.originalName!! + ) + } + + fun mapToMovie(mediaResultDTO: MediaResultDTO): Movie { + return Movie( + posterPath = mediaResultDTO.posterPath, + adult = mediaResultDTO.adult == false, + overview = mediaResultDTO.overview, + releaseDate = mediaResultDTO.releaseDate!!, + genreIds = mediaResultDTO.genreIds, + id = mediaResultDTO.id, + originalTitle = mediaResultDTO.originalTitle!!, + originalLanguage = mediaResultDTO.originalLanguage, + title = mediaResultDTO.title, + backdropPath = mediaResultDTO.backdropPath, + popularity = mediaResultDTO.popularity, + voteCount = mediaResultDTO.voteCount, + video = mediaResultDTO.video, + voteAverage = mediaResultDTO.voteAverage + ) + } + + fun mapToMediaResult(mediaResultDTO: MediaResultDTO) : MediaResult { + return MediaResult( + posterPath = mediaResultDTO.posterPath, + adult = mediaResultDTO.adult == false, + overview = mediaResultDTO.overview, + firstAirDate = mediaResultDTO.firstAirDate, + releaseDate = mediaResultDTO.releaseDate, + originCountry = mediaResultDTO.originCountry, + genreIds = mediaResultDTO.genreIds, + id = mediaResultDTO.id, + originalTitle = mediaResultDTO.originalTitle, + originalLanguage = mediaResultDTO.originalLanguage, + title = mediaResultDTO.title, + backdropPath = mediaResultDTO.backdropPath, + popularity = mediaResultDTO.popularity, + voteCount = mediaResultDTO.voteCount, + voteAverage = mediaResultDTO.voteAverage, + name = mediaResultDTO.name, + originalName = mediaResultDTO.originalName, + mediaType = mediaResultDTO.mediaType + ) + } + + +} \ No newline at end of file diff --git a/Sources/app/src/main/res/layout/fragment_movies.xml b/Sources/app/src/main/res/layout/fragment_movies.xml index 4094d47..bda7cfc 100644 --- a/Sources/app/src/main/res/layout/fragment_movies.xml +++ b/Sources/app/src/main/res/layout/fragment_movies.xml @@ -7,7 +7,7 @@ diff --git a/Sources/app/src/main/res/layout/item_vertical_fragment.xml b/Sources/app/src/main/res/layout/item_vertical_fragment.xml index 6c2d62a..ef3a88c 100644 --- a/Sources/app/src/main/res/layout/item_vertical_fragment.xml +++ b/Sources/app/src/main/res/layout/item_vertical_fragment.xml @@ -1,46 +1,49 @@ + android:layout_height="match_parent" + android:layout_marginStart="4dp" + android:layout_marginLeft="4dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="4dp" + android:layout_marginRight="4dp" + android:layout_marginBottom="8dp" + app:cardCornerRadius="10dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - - - - - + + + + + From fc371bf386b8175f965d95b57ad9c9dc3d53db2d Mon Sep 17 00:00:00 2001 From: Jordan Artzet Date: Fri, 3 Feb 2023 20:41:27 +0100 Subject: [PATCH 08/18] :construction: add some requests Get the most popular movie Get the top rated movie Get the upcoming movie Get the movie now playing --- .../api/MovieApplicationAPI.kt | 17 ++- .../api/dtos/MediaResultDTO.kt | 2 +- .../model/media/MediaResult.kt | 2 +- .../repository/MediaRepository.kt | 36 ++++++ .../repository/MovieRepository.kt | 84 +++++++++++-- .../repository/TVShowRepository.kt | 39 ++++++ .../ui/activity/MainActivity.kt | 14 +-- .../{MovieAdapter.kt => CategoryAdapter.kt} | 45 ++++--- .../{HomeItemAdapter.kt => MediaAdapter.kt} | 26 ++-- .../ui/fragments/HomeSectionsFragment.kt | 12 +- .../ui/fragments/MoviesFragment.kt | 112 +++++++++++++++--- .../{ShowsFragment.kt => TvShowsFragment.kt} | 2 +- .../utils/MediaResultMapper.kt | 4 +- .../src/main/res/layout/fragment_movies.xml | 12 ++ .../src/main/res/layout/fragment_tv_shows.xml | 13 ++ Sources/app/src/main/res/values/strings.xml | 8 ++ 16 files changed, 357 insertions(+), 71 deletions(-) create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MediaRepository.kt rename Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/{MovieAdapter.kt => CategoryAdapter.kt} (50%) rename Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/{HomeItemAdapter.kt => MediaAdapter.kt} (78%) rename Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/{ShowsFragment.kt => TvShowsFragment.kt} (93%) create mode 100644 Sources/app/src/main/res/layout/fragment_tv_shows.xml diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt index 803183d..09619f8 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt @@ -11,14 +11,25 @@ import retrofit2.http.Query interface MovieApplicationAPI { + // Movie @GET("movie/popular") fun getPopularMovies(@Query("api_key") apiKey : String = API_KEY, @Query("page") page : Int = 1) : Call - @GET("movie/top_rated") - fun getTopRatedMovies(@Query("api_key") apiKey: String = API_KEY) : Call + @GET("movie/now_playing") + fun getNowPlayingMovies(@Query("api_key") apiKey : String = API_KEY, @Query("page") page : Int = 1) : Call @GET("movie/upcoming") - fun getUpcomingMovies(@Query("api_key") apiKey: String = API_KEY) : Call + fun getUpcomingMovies(@Query("api_key") apiKey: String = API_KEY, @Query("page") page : Int = 1) : Call + + @GET("movie/top_rated") + fun getTopRatedMovies(@Query("api_key") apiKey: String = API_KEY, @Query("page") page : Int = 1) : Call + + + + // TvShow + @GET + fun getPopularTvShow(@Query("api_key") apiKey : String = API_KEY, @Query("page") page : Int = 1) : Call + @GET("trending/{media_type}/{time_window}") fun getTrending(@Path("media_type") mediaType : String = "all", @Path("time_window") timeWindow : String = "day", @Query("api_key") apiKey: String = API_KEY ) : Call diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt index b428527..7973820 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt @@ -36,7 +36,7 @@ data class MediaResultDTO( @Json(name = "original_name") val originalName : String?, @Json(name = "media_type") - val mediaType : String + val mediaType : String? ) { } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/MediaResult.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/MediaResult.kt index d443566..73834d0 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/MediaResult.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/MediaResult.kt @@ -19,6 +19,6 @@ data class MediaResult( val voteAverage: Double, val name: String? = null, val originalName: String? = null, - val mediaType : String + val mediaType: String? ) {} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MediaRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MediaRepository.kt new file mode 100644 index 0000000..d4f9501 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MediaRepository.kt @@ -0,0 +1,36 @@ +package fr.iut.pm.movieapplication.repository + +import android.util.Log +import fr.iut.pm.movieapplication.api.RetrofitInstance +import fr.iut.pm.movieapplication.api.dtos.PopularDTO +import fr.iut.pm.movieapplication.model.media.MediaResult +import fr.iut.pm.movieapplication.utils.MediaResultMapper +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class MediaRepository { + + fun getTrends(callback: (List) -> Unit) { + val listMovie : MutableList = mutableListOf() + + RetrofitInstance.api.getTrending().enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if(response.isSuccessful) { + Log.d("Response",response.body().toString()) + val popularDTO = response.body() + popularDTO?.results?.forEach { + val movie = MediaResultMapper.mapToMediaResult(it) + listMovie.add(movie) + movie.title?.let { it1 -> Log.d("Movie", it1) } + } + } + callback(listMovie) + } + + override fun onFailure(call: Call, t: Throwable) { + Log.d("Error failure", t.printStackTrace().toString()) + } + }) + } +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt index 8212a01..495a223 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt @@ -41,26 +41,92 @@ class MovieRepository() { }) } - fun getTrends(callback: (List) -> Unit) { - val listMovie : MutableList = mutableListOf() + fun getNowPlayingMovies(page : Int = 1, callback: (List) -> Unit) { - RetrofitInstance.api.getTrending().enqueue(object : Callback { + val listMovie : MutableList = mutableListOf() + + RetrofitInstance.api.getNowPlayingMovies(page = page).enqueue(object : + Callback { override fun onResponse(call: Call, response: Response) { - if(response.isSuccessful) { - Log.d("Response",response.body().toString()) + if (response.isSuccessful) { + Log.d("List :", response.body().toString()) val popularDTO = response.body() - popularDTO?.results?.forEach { - val movie = MediaResultMapper.mapToMediaResult(it) + val listMoviesDTO = popularDTO?.results + listMoviesDTO?.forEach { + + val movie = MediaResultMapper.mapToMovie(it) listMovie.add(movie) - movie.title?.let { it1 -> Log.d("Movie", it1) } + Log.d("Movie ", movie.title!! ) } + } callback(listMovie) } override fun onFailure(call: Call, t: Throwable) { - Log.d("Error failure", t.printStackTrace().toString()) + Log.d("Error failure", t.message.toString()) } + }) } + + fun getUpcomingMovies(page : Int = 1, callback: (List) -> Unit) { + + val listMovie : MutableList = mutableListOf() + + RetrofitInstance.api.getUpcomingMovies(page = page).enqueue(object : + Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + Log.d("List :", response.body().toString()) + val popularDTO = response.body() + val listMoviesDTO = popularDTO?.results + listMoviesDTO?.forEach { + + val movie = MediaResultMapper.mapToMovie(it) + listMovie.add(movie) + Log.d("Movie ", movie.title!! ) + } + + } + callback(listMovie) + } + + override fun onFailure(call: Call, t: Throwable) { + Log.d("Error failure", t.message.toString()) + } + + }) + } + + fun getTopRatedMovies(page : Int = 1, callback: (List) -> Unit) { + + val listMovie : MutableList = mutableListOf() + + RetrofitInstance.api.getTopRatedMovies(page = page).enqueue(object : + Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + Log.d("List :", response.body().toString()) + val popularDTO = response.body() + val listMoviesDTO = popularDTO?.results + listMoviesDTO?.forEach { + + val movie = MediaResultMapper.mapToMovie(it) + listMovie.add(movie) + Log.d("Movie ", movie.title!! ) + } + + } + callback(listMovie) + } + + override fun onFailure(call: Call, t: Throwable) { + Log.d("Error failure", t.message.toString()) + } + + }) + } + + } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TVShowRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TVShowRepository.kt index b19f419..2515701 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TVShowRepository.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TVShowRepository.kt @@ -1,4 +1,43 @@ package fr.iut.pm.movieapplication.repository +import android.util.Log +import fr.iut.pm.movieapplication.api.RetrofitInstance +import fr.iut.pm.movieapplication.api.dtos.PopularDTO +import fr.iut.pm.movieapplication.model.media.movie.Movie +import fr.iut.pm.movieapplication.model.media.tvshow.TvShow +import fr.iut.pm.movieapplication.utils.MediaResultMapper +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + class TVShowRepository { + + fun getPopularTvShow(page : Int = 1 ,callback: (List) -> Unit ) { + + val listMovie : MutableList = mutableListOf() + + RetrofitInstance.api.getPopularMovies(page = page).enqueue(object : + Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + Log.d("List :", response.body().toString()) + val popularDTO = response.body() + val listMoviesDTO = popularDTO?.results + listMoviesDTO?.forEach { + + val tvShow = MediaResultMapper.mapToTvShow(it) + listMovie.add(tvShow) + Log.d("Movie ", tvShow.name ) + } + + } + callback(listMovie) + } + + override fun onFailure(call: Call, t: Throwable) { + Log.d("Error failure", t.message.toString()) + } + + }) + } } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt index 558a3c5..3648708 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt @@ -5,26 +5,20 @@ import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.view.Menu import android.view.View -import android.widget.SearchView.OnQueryTextListener import androidx.appcompat.widget.SearchView import androidx.fragment.app.Fragment import com.google.android.material.bottomnavigation.BottomNavigationView import fr.iut.pm.movieapplication.R -import fr.iut.pm.movieapplication.api.RetrofitInstance -import fr.iut.pm.movieapplication.api.config.GlobalImageConfig +import fr.iut.pm.movieapplication.repository.MediaRepository import fr.iut.pm.movieapplication.repository.MovieRepository import fr.iut.pm.movieapplication.ui.fragments.HomeSectionsFragment import fr.iut.pm.movieapplication.ui.fragments.MoviesFragment -import fr.iut.pm.movieapplication.ui.fragments.ShowsFragment -import fr.iut.pm.movieapplication.utils.Constants -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import retrofit2.awaitResponse +import fr.iut.pm.movieapplication.ui.fragments.TvShowsFragment class MainActivity : AppCompatActivity() { val movieRepository : MovieRepository = MovieRepository() + val mediaRepository : MediaRepository = MediaRepository() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -47,7 +41,7 @@ class MainActivity : AppCompatActivity() { } R.id.series_page -> { - loadFragments(ShowsFragment(this)) + loadFragments(TvShowsFragment(this)) return@setOnItemSelectedListener true } else -> false diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/CategoryAdapter.kt similarity index 50% rename from Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt rename to Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/CategoryAdapter.kt index dc28e98..050a2ae 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/CategoryAdapter.kt @@ -10,16 +10,16 @@ import androidx.core.net.toUri import androidx.recyclerview.widget.RecyclerView import coil.load import fr.iut.pm.movieapplication.R -import fr.iut.pm.movieapplication.api.config.GlobalImageConfig import fr.iut.pm.movieapplication.model.media.movie.Movie +import fr.iut.pm.movieapplication.model.media.tvshow.TvShow import fr.iut.pm.movieapplication.ui.activity.MainActivity -import fr.iut.pm.movieapplication.utils.Constants +import fr.iut.pm.movieapplication.utils.Constants.Companion.IMG_URL -class MovieAdapter( +class CategoryAdapter( private val context: MainActivity, private val layoutId: Int, - private val list: List -) : RecyclerView.Adapter(){ + private val list: List +) : RecyclerView.Adapter(){ class ViewHolder(view : View) : RecyclerView.ViewHolder(view) { val itemImage: ImageView = view.findViewById(R.id.item_image) @@ -28,29 +28,40 @@ class MovieAdapter( } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeItemAdapter.ViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaAdapter.ViewHolder { val view = LayoutInflater .from(parent.context) .inflate(layoutId, parent, false) - return HomeItemAdapter.ViewHolder(view) + return MediaAdapter.ViewHolder(view) } override fun getItemCount(): Int = list.size - override fun onBindViewHolder(holder: HomeItemAdapter.ViewHolder, position: Int) { + override fun onBindViewHolder(holder: MediaAdapter.ViewHolder, position: Int) { val currentItem = list[position] - Log.d("SINGLETON", GlobalImageConfig.baseUrl) - val imgUri = currentItem.posterPath?.let { - (Constants.IMG_URL + it).toUri().buildUpon().scheme("https").build() + if(currentItem != null) { + + when(currentItem!!::class.java) { + Movie::class.java -> bindMovie(holder, currentItem as Movie) + TvShow::class.java -> bindTvShow(holder, currentItem as TvShow) + } + } - Log.d("SINGLETON", imgUri.toString() ) + } + + private fun bindTvShow(holder: MediaAdapter.ViewHolder, tvShow: TvShow) { + val imgUri = tvShow.posterPath?.let { (IMG_URL+it).toUri().buildUpon().scheme("https").build() } holder.itemImage.load(imgUri) - holder.itemName.text = currentItem.title - holder.itemDate.text = currentItem.releaseDate - holder.itemView.setOnClickListener { - onItemClick(currentItem) - } + holder.itemName.text = tvShow.name + holder.itemDate.text = tvShow.firstAirDate + } + + private fun bindMovie(holder: MediaAdapter.ViewHolder, movie: Movie) { + val imgUri = movie.posterPath?.let { (IMG_URL+it).toUri().buildUpon().scheme("https").build() } + holder.itemImage.load(imgUri) + holder.itemName.text = movie.title + holder.itemDate.text = movie.releaseDate } private fun onItemClick(item : Movie) { diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MediaAdapter.kt similarity index 78% rename from Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt rename to Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MediaAdapter.kt index bb5a305..d3c6be9 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/HomeItemAdapter.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MediaAdapter.kt @@ -12,17 +12,14 @@ import coil.load import fr.iut.pm.movieapplication.R import fr.iut.pm.movieapplication.api.config.GlobalImageConfig import fr.iut.pm.movieapplication.model.media.MediaResult -import fr.iut.pm.movieapplication.model.media.movie.Movie -import fr.iut.pm.movieapplication.model.media.movie.MovieDetails -import fr.iut.pm.movieapplication.model.media.tvshow.TvShow import fr.iut.pm.movieapplication.ui.activity.MainActivity import fr.iut.pm.movieapplication.utils.Constants.Companion.IMG_URL -class HomeItemAdapter( +class MediaAdapter( private val context: MainActivity, private val layoutId: Int, private val list: List - ) : RecyclerView.Adapter() { + ) : RecyclerView.Adapter() { class ViewHolder(view : View) : RecyclerView.ViewHolder(view) { val itemImage: ImageView = view.findViewById(R.id.item_image) @@ -38,21 +35,36 @@ class HomeItemAdapter( } override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val currentItem = list[position] + when(currentItem.mediaType) { + "movie" -> bindMovie(holder, currentItem) + "tv" -> bindTvShow(holder, currentItem) + } Log.d("SINGLETON", GlobalImageConfig.baseUrl) val imgUri = currentItem.posterPath?.let { (IMG_URL + it).toUri().buildUpon().scheme("https").build() } Log.d("SINGLETON", imgUri.toString() ) holder.itemImage.load(imgUri) - holder.itemName.text = currentItem.title - holder.itemDate.text = currentItem.releaseDate + holder.itemView.setOnClickListener { onItemClick(currentItem) } } + // If the item is a Movie + private fun bindMovie(holder: ViewHolder, currentItem: MediaResult) { + holder.itemName.text = currentItem.title + holder.itemDate.text = currentItem.releaseDate + } + // If the item is a TvShow + private fun bindTvShow(holder: ViewHolder, currentItem: MediaResult) { + holder.itemName.text = currentItem.name + holder.itemDate.text = currentItem.firstAirDate + } + private fun onItemClick(item : MediaResult) { Log.d("item clicked", item.toString()) } diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt index 528981f..d27bc09 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt @@ -8,9 +8,9 @@ import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView import fr.iut.pm.movieapplication.R import fr.iut.pm.movieapplication.ui.activity.MainActivity -import fr.iut.pm.movieapplication.ui.adapter.HomeItemAdapter +import fr.iut.pm.movieapplication.ui.adapter.MediaAdapter import fr.iut.pm.movieapplication.ui.adapter.HomeItemDecoration -import fr.iut.pm.movieapplication.ui.adapter.MovieAdapter +import fr.iut.pm.movieapplication.ui.adapter.CategoryAdapter import fr.iut.pm.movieapplication.ui.viewmodel.HomeSectionsVM class HomeSectionsFragment( @@ -23,21 +23,21 @@ class HomeSectionsFragment( val view = inflater.inflate(R.layout.fragment_home_sections, container, false) //get the trends RecyclerView - context.movieRepository.getTrends { + context.mediaRepository.getTrends { val homeTrendsRecyclerView = view?.findViewById(R.id.home_trends_recycler_view) - homeTrendsRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page,it) + homeTrendsRecyclerView?.adapter = MediaAdapter(context,R.layout.item_horizontal_home_page,it) homeTrendsRecyclerView?.addItemDecoration(HomeItemDecoration()) } //get the popularity RecyclerView context.movieRepository.getPopularMovies { val homePopularityRecyclerView = view?.findViewById(R.id.home_popularity_recycler_view) - homePopularityRecyclerView?.adapter = MovieAdapter(context,R.layout.item_horizontal_home_page,it) + homePopularityRecyclerView?.adapter = CategoryAdapter(context,R.layout.item_horizontal_home_page,it) homePopularityRecyclerView?.addItemDecoration(HomeItemDecoration()) } //get the free RecyclerView val homeFreeRecyclerView = view?.findViewById(R.id.home_free_recycler_view) - homeFreeRecyclerView?.adapter = HomeItemAdapter(context,R.layout.item_horizontal_home_page,ArrayList()) + homeFreeRecyclerView?.adapter = MediaAdapter(context,R.layout.item_horizontal_home_page,ArrayList()) homeFreeRecyclerView?.addItemDecoration(HomeItemDecoration()) return view } diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt index 9c868c9..87fbca2 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt @@ -4,18 +4,18 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Spinner import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import fr.iut.pm.movieapplication.R -import fr.iut.pm.movieapplication.model.media.MediaResult import fr.iut.pm.movieapplication.model.media.movie.Movie -import fr.iut.pm.movieapplication.model.media.movie.MovieDetails import fr.iut.pm.movieapplication.repository.MovieRepository import fr.iut.pm.movieapplication.ui.activity.MainActivity -import fr.iut.pm.movieapplication.ui.adapter.HomeItemAdapter -import fr.iut.pm.movieapplication.ui.adapter.MovieAdapter +import fr.iut.pm.movieapplication.ui.adapter.CategoryAdapter import fr.iut.pm.movieapplication.ui.viewmodel.MoviesVM import fr.iut.pm.movieapplication.ui.viewmodel.MoviesVMFactory import fr.iut.pm.movieapplication.utils.Constants.Companion.PAGE_SIZE @@ -31,6 +31,8 @@ class MoviesFragment( private val moviesVM: MoviesVM by viewModels{ MoviesVMFactory(repository = MovieRepository())} lateinit var moviesRecyclerView : RecyclerView + lateinit var spinner: Spinner + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_movies, container, false) @@ -38,11 +40,7 @@ class MoviesFragment( moviesRecyclerView = view.findViewById(R.id.movies_item_recycler_view) // Initialized the data inside our RecyclerView - context.movieRepository.getPopularMovies { listMovies -> - currentList.addAll(0,listMovies) - moviesRecyclerView.adapter = MovieAdapter(context, R.layout.item_vertical_fragment, currentList) - moviesRecyclerView.layoutManager = GridLayoutManager (context, 3) - } + // Create the ScrollListener val scrollListener = object : RecyclerView.OnScrollListener() { @@ -69,9 +67,73 @@ class MoviesFragment( // Add the ScrollLister created before to our RecyclerView moviesRecyclerView.addOnScrollListener(scrollListener) + spinner = view.findViewById(R.id.category_spinner) + configSpinner(spinner) return view } + private fun configSpinner(spinner: Spinner) { + ArrayAdapter.createFromResource( + context, + R.array.movie_filter, + android.R.layout.simple_spinner_item + ).also { adapter -> + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + + spinner.adapter = adapter + } + + spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + when(position) { + 0 -> { + currentList.clear() + context.movieRepository.getPopularMovies { listMovies -> + currentList.addAll(0,listMovies) + moviesRecyclerView.adapter = CategoryAdapter(context, R.layout.item_vertical_fragment, currentList) + moviesRecyclerView.layoutManager = GridLayoutManager (context, 3) + } + } + 1 -> { + currentList.clear() + context.movieRepository.getNowPlayingMovies { movies: List -> + currentList.addAll(0,movies) + moviesRecyclerView.adapter = CategoryAdapter(context, R.layout.item_vertical_fragment, currentList) + moviesRecyclerView.layoutManager = GridLayoutManager (context, 3) + + } + } + 2 -> { + currentList.clear() + context.movieRepository.getUpcomingMovies { movies: List -> + currentList.addAll(0,movies) + moviesRecyclerView.adapter = CategoryAdapter(context, R.layout.item_vertical_fragment, currentList) + moviesRecyclerView.layoutManager = GridLayoutManager (context, 3) + } + } + 3 -> { + currentList.clear() + context.movieRepository.getTopRatedMovies { movies -> + currentList.addAll(0,movies) + moviesRecyclerView.adapter = CategoryAdapter(context, R.layout.item_vertical_fragment, currentList) + moviesRecyclerView.layoutManager = GridLayoutManager (context, 3) + } + } + } + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + + } + + } + } + /** * Method to load data when the user reaches the bottom of the view */ @@ -82,13 +144,35 @@ class MoviesFragment( if(currentPage == 1000) isLastPage = true val start = currentList.size - context.movieRepository.getPopularMovies(currentPage) { listMovies -> - - currentList.addAll(start, listMovies) - moviesRecyclerView.adapter?.notifyItemRangeChanged(start, listMovies.size) + when(spinner.selectedItemPosition) { + 0 -> { + context.movieRepository.getPopularMovies(currentPage) { listMovies -> + currentList.addAll(start, listMovies) + moviesRecyclerView.adapter?.notifyItemRangeChanged(start, listMovies.size) + } + } + 1 -> { + context.movieRepository.getNowPlayingMovies(currentPage) { listMovies -> + currentList.addAll(start, listMovies) + moviesRecyclerView.adapter?.notifyItemRangeChanged(start, listMovies.size) + } + } + 2 -> { + context.movieRepository.getUpcomingMovies(currentPage) { listMovies -> + currentList.addAll(start, listMovies) + moviesRecyclerView.adapter?.notifyItemRangeChanged(start, listMovies.size) + } + } + 3 -> { + context.movieRepository.getTopRatedMovies(currentPage) { listMovies -> + currentList.addAll(start, listMovies) + moviesRecyclerView.adapter?.notifyItemRangeChanged(start, listMovies.size) + } + } } + isLoading = false } -} \ No newline at end of file +} diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/ShowsFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/TvShowsFragment.kt similarity index 93% rename from Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/ShowsFragment.kt rename to Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/TvShowsFragment.kt index b9f7feb..4aba7f2 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/ShowsFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/TvShowsFragment.kt @@ -4,7 +4,7 @@ import android.os.Bundle import androidx.fragment.app.Fragment import fr.iut.pm.movieapplication.ui.activity.MainActivity -class ShowsFragment( +class TvShowsFragment( private val context : MainActivity ) : Fragment() { diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt index 3b3bca8..9948f10 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt @@ -28,7 +28,7 @@ object MediaResultMapper { fun mapToMovie(mediaResultDTO: MediaResultDTO): Movie { return Movie( posterPath = mediaResultDTO.posterPath, - adult = mediaResultDTO.adult == false, + adult = !mediaResultDTO.adult, overview = mediaResultDTO.overview, releaseDate = mediaResultDTO.releaseDate!!, genreIds = mediaResultDTO.genreIds, @@ -47,7 +47,7 @@ object MediaResultMapper { fun mapToMediaResult(mediaResultDTO: MediaResultDTO) : MediaResult { return MediaResult( posterPath = mediaResultDTO.posterPath, - adult = mediaResultDTO.adult == false, + adult = !mediaResultDTO.adult, overview = mediaResultDTO.overview, firstAirDate = mediaResultDTO.firstAirDate, releaseDate = mediaResultDTO.releaseDate, diff --git a/Sources/app/src/main/res/layout/fragment_movies.xml b/Sources/app/src/main/res/layout/fragment_movies.xml index bda7cfc..7a243ca 100644 --- a/Sources/app/src/main/res/layout/fragment_movies.xml +++ b/Sources/app/src/main/res/layout/fragment_movies.xml @@ -2,8 +2,20 @@ + + + + + + \ No newline at end of file diff --git a/Sources/app/src/main/res/values/strings.xml b/Sources/app/src/main/res/values/strings.xml index 260cd67..6280e15 100644 --- a/Sources/app/src/main/res/values/strings.xml +++ b/Sources/app/src/main/res/values/strings.xml @@ -13,5 +13,13 @@ Populaires Gratuits section_nested_scroll_view + + + + Populaires + Du moment + À venir + Les mieux notés + \ No newline at end of file From e29247f3ce41ee566961134f55d50605b04b7d66 Mon Sep 17 00:00:00 2001 From: Jordan Artzet Date: Sun, 5 Feb 2023 01:34:03 +0100 Subject: [PATCH 09/18] :construction: add data binding and view model Data are now binded on the view --- Sources/app/build.gradle | 12 +- .../api/MovieApplicationAPI.kt | 9 +- .../api/dtos/MediaResultDTO.kt | 4 +- .../model/media/MediaResult.kt | 9 +- .../model/media/movie/Movie.kt | 39 ++++ .../repository/MovieRepository.kt | 149 ++++++--------- .../repository/TVShowRepository.kt | 56 +++--- .../ui/activity/MainActivity.kt | 2 +- .../ui/adapter/CategoryAdapter.kt | 70 ------- .../ui/adapter/MediaAdapter.kt | 14 +- .../ui/adapter/MovieAdapter.kt | 47 +++++ .../ui/fragments/HomeSectionsFragment.kt | 11 +- .../ui/fragments/MoviesFragment.kt | 172 ++++-------------- .../movieapplication/ui/viewmodel/MoviesVM.kt | 86 ++++++++- .../utils/MediaResultMapper.kt | 11 +- .../src/main/res/layout/fragment_movies.xml | 74 +++++--- .../main/res/layout/item_movie_category.xml | 67 +++++++ 17 files changed, 429 insertions(+), 403 deletions(-) delete mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/CategoryAdapter.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt create mode 100644 Sources/app/src/main/res/layout/item_movie_category.xml diff --git a/Sources/app/build.gradle b/Sources/app/build.gradle index 6940713..cd5b0a7 100644 --- a/Sources/app/build.gradle +++ b/Sources/app/build.gradle @@ -36,13 +36,17 @@ android { kotlinOptions { jvmTarget = '1.8' } + buildFeatures { + dataBinding true + } } dependencies { + implementation 'androidx.core:core-ktx:1.9.0' implementation "androidx.appcompat:appcompat:$rootProject.appCompatVersion" implementation "androidx.activity:activity-ktx:$rootProject.activityVersion" - implementation 'androidx.core:core-ktx:1.9.0' + implementation "androidx.fragment:fragment-ktx:1.5.5" // Room components @@ -53,7 +57,7 @@ dependencies { // Lifecycle components implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.lifecycleVersion" - implementation "androidx.lifecycle:lifecycle-common-java8:$rootProject.lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$rootProject.lifecycleVersion" // Kotlin components implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" @@ -69,13 +73,11 @@ dependencies { // Moshi implementation "com.squareup.moshi:moshi-kotlin:1.13.0" - //GSON - //implementation 'com.squareup.retrofit2:converter-gson:2.1.0' // Retrofit implementation "com.squareup.retrofit2:retrofit:2.9.0" // Retrofit with Scalar Converter - //implementation "com.squareup.retrofit2:converter-scalars:2.9.0" + implementation "com.squareup.retrofit2:converter-scalars:2.9.0" // Retrofit with Moshi Converter implementation "com.squareup.retrofit2:converter-moshi:2.9.0" diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt index 09619f8..13efeba 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt @@ -5,6 +5,7 @@ import fr.iut.pm.movieapplication.api.dtos.MovieResultDTO import fr.iut.pm.movieapplication.api.dtos.PopularDTO import fr.iut.pm.movieapplication.utils.Constants.Companion.API_KEY import retrofit2.Call +import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query @@ -13,16 +14,16 @@ interface MovieApplicationAPI { // Movie @GET("movie/popular") - fun getPopularMovies(@Query("api_key") apiKey : String = API_KEY, @Query("page") page : Int = 1) : Call + suspend fun getPopularMovies(@Query("api_key") apiKey : String = API_KEY, @Query("page") page : Int = 1) : Response @GET("movie/now_playing") - fun getNowPlayingMovies(@Query("api_key") apiKey : String = API_KEY, @Query("page") page : Int = 1) : Call + suspend fun getNowPlayingMovies(@Query("api_key") apiKey : String = API_KEY, @Query("page") page : Int = 1) : Response @GET("movie/upcoming") - fun getUpcomingMovies(@Query("api_key") apiKey: String = API_KEY, @Query("page") page : Int = 1) : Call + suspend fun getUpcomingMovies(@Query("api_key") apiKey: String = API_KEY, @Query("page") page : Int = 1) : Response @GET("movie/top_rated") - fun getTopRatedMovies(@Query("api_key") apiKey: String = API_KEY, @Query("page") page : Int = 1) : Call + suspend fun getTopRatedMovies(@Query("api_key") apiKey: String = API_KEY, @Query("page") page : Int = 1) : Response diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt index 7973820..b9b56af 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt @@ -7,7 +7,7 @@ import com.squareup.moshi.JsonClass data class MediaResultDTO( @Json(name = "poster_path") - val posterPath : String, + val posterPath : String?, val adult : Boolean, val overview : String, @Json(name = "first_air_date") @@ -25,7 +25,7 @@ data class MediaResultDTO( val originalLanguage : String, val title : String?, @Json(name = "backdrop_path") - val backdropPath : String, + val backdropPath : String?, val popularity : Double, @Json(name = "vote_count") val voteCount : Int, diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/MediaResult.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/MediaResult.kt index 73834d0..fbe2ce4 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/MediaResult.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/MediaResult.kt @@ -5,20 +5,17 @@ data class MediaResult( val posterPath: String? = null, val adult: Boolean, val overview: String, - val firstAirDate: String? = null, - val releaseDate: String? = null, + val releaseDate: String, val originCountry: List? = null, val genreIds: List, val id: Int, - val originalTitle: String? = null, + val originalTitle: String, val originalLanguage: String, - val title: String?, + val title: String, val backdropPath: String? = null, val popularity: Double, val voteCount: Int, val voteAverage: Double, - val name: String? = null, - val originalName: String? = null, val mediaType: String? ) {} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/Movie.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/Movie.kt index 1a5021b..32ac156 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/Movie.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/Movie.kt @@ -17,6 +17,45 @@ open class Movie( open val voteAverage: Double, ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Movie + if (posterPath != other.posterPath) return false + if (adult != other.adult) return false + if (overview != other.overview) return false + if (releaseDate != other.releaseDate) return false + if (genreIds != other.genreIds) return false + if (id != other.id) return false + if (originalTitle != other.originalTitle) return false + if (originalLanguage != other.originalLanguage) return false + if (title != other.title) return false + if (backdropPath != other.backdropPath) return false + if (popularity != other.popularity) return false + if (voteCount != other.voteCount) return false + if (video != other.video) return false + if (voteAverage != other.voteAverage) return false + + return true + } + + override fun hashCode(): Int { + var result = posterPath?.hashCode() ?: 0 + result = 31 * result + adult.hashCode() + result = 31 * result + overview.hashCode() + result = 31 * result + releaseDate.hashCode() + result = 31 * result + genreIds.hashCode() + result = 31 * result + id + result = 31 * result + originalTitle.hashCode() + result = 31 * result + originalLanguage.hashCode() + result = 31 * result + (title?.hashCode() ?: 0) + result = 31 * result + (backdropPath?.hashCode() ?: 0) + result = 31 * result + popularity.hashCode() + result = 31 * result + voteCount + result = 31 * result + (video?.hashCode() ?: 0) + result = 31 * result + voteAverage.hashCode() + return result + } } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt index 495a223..e327a64 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt @@ -1,131 +1,88 @@ package fr.iut.pm.movieapplication.repository import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.liveData import fr.iut.pm.movieapplication.api.RetrofitInstance import fr.iut.pm.movieapplication.api.dtos.PopularDTO import fr.iut.pm.movieapplication.model.media.MediaResult import fr.iut.pm.movieapplication.model.media.movie.Movie import fr.iut.pm.movieapplication.utils.MediaResultMapper -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import kotlinx.coroutines.Dispatchers -class MovieRepository() { +class MovieRepository { - fun getPopularMovies(page : Int = 1 ,callback: (List) -> Unit ) { + suspend fun getPopularMovies(page : Int = 1) : List + { val listMovie : MutableList = mutableListOf() - RetrofitInstance.api.getPopularMovies(page = page).enqueue(object : - Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - Log.d("List :", response.body().toString()) - val popularDTO = response.body() - val listMoviesDTO = popularDTO?.results - listMoviesDTO?.forEach { - - val movie = MediaResultMapper.mapToMovie(it) - listMovie.add(movie) - Log.d("Movie ", movie.title!! ) - } - - } - callback(listMovie) + val response = RetrofitInstance.api.getPopularMovies(page = page) + if(response.isSuccessful) { + val listMediaResultDTO = response.body()?.results + listMediaResultDTO?.forEach { + val movie = MediaResultMapper.mapToMovie(it) + listMovie.add(movie) + Log.d("Movie ", movie.title!!) } - - override fun onFailure(call: Call, t: Throwable) { - Log.d("Error failure", t.message.toString()) - } - - }) + } + else Log.d("ERROR FAILED", response.message()) + return listMovie } - fun getNowPlayingMovies(page : Int = 1, callback: (List) -> Unit) { + suspend fun getNowPlayingMovies(page : Int = 1) : List + { val listMovie : MutableList = mutableListOf() - RetrofitInstance.api.getNowPlayingMovies(page = page).enqueue(object : - Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - Log.d("List :", response.body().toString()) - val popularDTO = response.body() - val listMoviesDTO = popularDTO?.results - listMoviesDTO?.forEach { - - val movie = MediaResultMapper.mapToMovie(it) - listMovie.add(movie) - Log.d("Movie ", movie.title!! ) - } - - } - callback(listMovie) + val response = RetrofitInstance.api.getNowPlayingMovies(page = page) + if(response.isSuccessful) { + val listMediaResultDTO = response.body()?.results + listMediaResultDTO?.forEach { + val movie = MediaResultMapper.mapToMovie(it) + listMovie.add(movie) + Log.d("Movie ", movie.title!!) } - - override fun onFailure(call: Call, t: Throwable) { - Log.d("Error failure", t.message.toString()) - } - - }) + } + else Log.d("ERROR FAILED", response.message()) + return listMovie } - fun getUpcomingMovies(page : Int = 1, callback: (List) -> Unit) { + suspend fun getUpcomingMovies(page : Int = 1) : List + { val listMovie : MutableList = mutableListOf() - RetrofitInstance.api.getUpcomingMovies(page = page).enqueue(object : - Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - Log.d("List :", response.body().toString()) - val popularDTO = response.body() - val listMoviesDTO = popularDTO?.results - listMoviesDTO?.forEach { - - val movie = MediaResultMapper.mapToMovie(it) - listMovie.add(movie) - Log.d("Movie ", movie.title!! ) - } - - } - callback(listMovie) + val response = RetrofitInstance.api.getUpcomingMovies(page = page) + if(response.isSuccessful) { + val listMediaResultDTO = response.body()?.results + listMediaResultDTO?.forEach { + val movie = MediaResultMapper.mapToMovie(it) + listMovie.add(movie) + Log.d("Movie ", movie.title!!) } - - override fun onFailure(call: Call, t: Throwable) { - Log.d("Error failure", t.message.toString()) - } - - }) + } + else Log.d("ERROR FAILED", response.message()) + return listMovie } - fun getTopRatedMovies(page : Int = 1, callback: (List) -> Unit) { + suspend fun getTopRatedMovies(page : Int = 1) : List + { val listMovie : MutableList = mutableListOf() - RetrofitInstance.api.getTopRatedMovies(page = page).enqueue(object : - Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - Log.d("List :", response.body().toString()) - val popularDTO = response.body() - val listMoviesDTO = popularDTO?.results - listMoviesDTO?.forEach { - - val movie = MediaResultMapper.mapToMovie(it) - listMovie.add(movie) - Log.d("Movie ", movie.title!! ) - } - - } - callback(listMovie) + val response = RetrofitInstance.api.getTopRatedMovies(page = page) + if(response.isSuccessful) { + val listMediaResultDTO = response.body()?.results + listMediaResultDTO?.forEach { + val movie = MediaResultMapper.mapToMovie(it) + listMovie.add(movie) + Log.d("Movie ", movie.title!!) } - - override fun onFailure(call: Call, t: Throwable) { - Log.d("Error failure", t.message.toString()) - } - - }) + } + else Log.d("ERROR FAILED", response.message()) + return listMovie } diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TVShowRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TVShowRepository.kt index 2515701..f2c1d4a 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TVShowRepository.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TVShowRepository.kt @@ -12,32 +12,32 @@ import retrofit2.Response class TVShowRepository { - fun getPopularTvShow(page : Int = 1 ,callback: (List) -> Unit ) { - - val listMovie : MutableList = mutableListOf() - - RetrofitInstance.api.getPopularMovies(page = page).enqueue(object : - Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - Log.d("List :", response.body().toString()) - val popularDTO = response.body() - val listMoviesDTO = popularDTO?.results - listMoviesDTO?.forEach { - - val tvShow = MediaResultMapper.mapToTvShow(it) - listMovie.add(tvShow) - Log.d("Movie ", tvShow.name ) - } - - } - callback(listMovie) - } - - override fun onFailure(call: Call, t: Throwable) { - Log.d("Error failure", t.message.toString()) - } - - }) - } +// fun getPopularTvShow(page : Int = 1 ,callback: (List) -> Unit ) { +// +// val listMovie : MutableList = mutableListOf() +// +// RetrofitInstance.api.getPopularMovies(page = page).enqueue(object : +// Callback { +// override fun onResponse(call: Call, response: Response) { +// if (response.isSuccessful) { +// Log.d("List :", response.body().toString()) +// val popularDTO = response.body() +// val listMoviesDTO = popularDTO?.results +// listMoviesDTO?.forEach { +// +// val tvShow = MediaResultMapper.mapToTvShow(it) +// listMovie.add(tvShow) +// Log.d("Movie ", tvShow.name ) +// } +// +// } +// callback(listMovie) +// } +// +// override fun onFailure(call: Call, t: Throwable) { +// Log.d("Error failure", t.message.toString()) +// } +// +// }) +// } } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt index 3648708..4e6e7db 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt @@ -36,7 +36,7 @@ class MainActivity : AppCompatActivity() { } R.id.movies_page -> { - loadFragments(MoviesFragment(this)) + loadFragments(MoviesFragment()) return@setOnItemSelectedListener true } diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/CategoryAdapter.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/CategoryAdapter.kt deleted file mode 100644 index 050a2ae..0000000 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/CategoryAdapter.kt +++ /dev/null @@ -1,70 +0,0 @@ -package fr.iut.pm.movieapplication.ui.adapter - -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.core.net.toUri -import androidx.recyclerview.widget.RecyclerView -import coil.load -import fr.iut.pm.movieapplication.R -import fr.iut.pm.movieapplication.model.media.movie.Movie -import fr.iut.pm.movieapplication.model.media.tvshow.TvShow -import fr.iut.pm.movieapplication.ui.activity.MainActivity -import fr.iut.pm.movieapplication.utils.Constants.Companion.IMG_URL - -class CategoryAdapter( - private val context: MainActivity, - private val layoutId: Int, - private val list: List -) : RecyclerView.Adapter(){ - - class ViewHolder(view : View) : RecyclerView.ViewHolder(view) { - val itemImage: ImageView = view.findViewById(R.id.item_image) - val itemName: TextView = view.findViewById(R.id.item_name) - val itemDate: TextView = view.findViewById(R.id.item_date) - } - - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaAdapter.ViewHolder { - val view = LayoutInflater - .from(parent.context) - .inflate(layoutId, parent, false) - return MediaAdapter.ViewHolder(view) - } - - override fun getItemCount(): Int = list.size - - - override fun onBindViewHolder(holder: MediaAdapter.ViewHolder, position: Int) { - val currentItem = list[position] - if(currentItem != null) { - - when(currentItem!!::class.java) { - Movie::class.java -> bindMovie(holder, currentItem as Movie) - TvShow::class.java -> bindTvShow(holder, currentItem as TvShow) - } - - } - } - - private fun bindTvShow(holder: MediaAdapter.ViewHolder, tvShow: TvShow) { - val imgUri = tvShow.posterPath?.let { (IMG_URL+it).toUri().buildUpon().scheme("https").build() } - holder.itemImage.load(imgUri) - holder.itemName.text = tvShow.name - holder.itemDate.text = tvShow.firstAirDate - } - - private fun bindMovie(holder: MediaAdapter.ViewHolder, movie: Movie) { - val imgUri = movie.posterPath?.let { (IMG_URL+it).toUri().buildUpon().scheme("https").build() } - holder.itemImage.load(imgUri) - holder.itemName.text = movie.title - holder.itemDate.text = movie.releaseDate - } - - private fun onItemClick(item : Movie) { - Log.d("item clicked", item.toString()) - } -} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MediaAdapter.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MediaAdapter.kt index d3c6be9..bfa49cd 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MediaAdapter.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MediaAdapter.kt @@ -37,11 +37,8 @@ class MediaAdapter( override fun onBindViewHolder(holder: ViewHolder, position: Int) { val currentItem = list[position] - when(currentItem.mediaType) { - "movie" -> bindMovie(holder, currentItem) - "tv" -> bindTvShow(holder, currentItem) - } - Log.d("SINGLETON", GlobalImageConfig.baseUrl) + bindItem(holder, currentItem) + val imgUri = currentItem.posterPath?.let { (IMG_URL + it).toUri().buildUpon().scheme("https").build() } @@ -55,15 +52,10 @@ class MediaAdapter( } // If the item is a Movie - private fun bindMovie(holder: ViewHolder, currentItem: MediaResult) { + private fun bindItem(holder: ViewHolder, currentItem: MediaResult) { holder.itemName.text = currentItem.title holder.itemDate.text = currentItem.releaseDate } - // If the item is a TvShow - private fun bindTvShow(holder: ViewHolder, currentItem: MediaResult) { - holder.itemName.text = currentItem.name - holder.itemDate.text = currentItem.firstAirDate - } private fun onItemClick(item : MediaResult) { Log.d("item clicked", item.toString()) diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt new file mode 100644 index 0000000..d798661 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt @@ -0,0 +1,47 @@ +package fr.iut.pm.movieapplication.ui.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.net.toUri +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import coil.load +import fr.iut.pm.movieapplication.R +import fr.iut.pm.movieapplication.databinding.ItemMovieCategoryBinding +import fr.iut.pm.movieapplication.model.media.movie.Movie +import fr.iut.pm.movieapplication.utils.Constants + +class MovieAdapter : ListAdapter(DiffUtilDogCallback) { + + private object DiffUtilDogCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Movie, newItem: Movie) = oldItem.id == newItem.id + override fun areContentsTheSame(oldItem: Movie, newItem: Movie) = oldItem == newItem + } + + class ViewHolder(private val binding : ItemMovieCategoryBinding) : + RecyclerView.ViewHolder(binding.root) { + + val movie : Movie? get() = binding.movie + + init { + itemView.setOnClickListener {} + } + + fun bind(movie : Movie) { + binding.movie = movie + val imgUri = movie.posterPath?.let { (Constants.IMG_URL +it).toUri().buildUpon().scheme("https").build() } + binding.itemImage.load(imgUri) + binding.executePendingBindings() + } + + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = + ViewHolder(ItemMovieCategoryBinding.inflate(LayoutInflater.from(parent.context))) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(getItem(position)) +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt index d27bc09..f58178f 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/HomeSectionsFragment.kt @@ -10,7 +10,6 @@ import fr.iut.pm.movieapplication.R import fr.iut.pm.movieapplication.ui.activity.MainActivity import fr.iut.pm.movieapplication.ui.adapter.MediaAdapter import fr.iut.pm.movieapplication.ui.adapter.HomeItemDecoration -import fr.iut.pm.movieapplication.ui.adapter.CategoryAdapter import fr.iut.pm.movieapplication.ui.viewmodel.HomeSectionsVM class HomeSectionsFragment( @@ -30,11 +29,11 @@ class HomeSectionsFragment( } //get the popularity RecyclerView - context.movieRepository.getPopularMovies { - val homePopularityRecyclerView = view?.findViewById(R.id.home_popularity_recycler_view) - homePopularityRecyclerView?.adapter = CategoryAdapter(context,R.layout.item_horizontal_home_page,it) - homePopularityRecyclerView?.addItemDecoration(HomeItemDecoration()) - } +// context.movieRepository.getPopularMovies { +// val homePopularityRecyclerView = view?.findViewById(R.id.home_popularity_recycler_view) +// homePopularityRecyclerView?.adapter = CategoryAdapter(context,R.layout.item_horizontal_home_page,it) +// homePopularityRecyclerView?.addItemDecoration(HomeItemDecoration()) +// } //get the free RecyclerView val homeFreeRecyclerView = view?.findViewById(R.id.home_free_recycler_view) homeFreeRecyclerView?.adapter = MediaAdapter(context,R.layout.item_horizontal_home_page,ArrayList()) diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt index 87fbca2..9e80f2b 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt @@ -1,6 +1,7 @@ package fr.iut.pm.movieapplication.ui.fragments import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -9,19 +10,17 @@ import android.widget.ArrayAdapter import android.widget.Spinner import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import fr.iut.pm.movieapplication.R +import fr.iut.pm.movieapplication.databinding.FragmentMoviesBinding import fr.iut.pm.movieapplication.model.media.movie.Movie -import fr.iut.pm.movieapplication.repository.MovieRepository -import fr.iut.pm.movieapplication.ui.activity.MainActivity -import fr.iut.pm.movieapplication.ui.adapter.CategoryAdapter +import fr.iut.pm.movieapplication.ui.adapter.MovieAdapter import fr.iut.pm.movieapplication.ui.viewmodel.MoviesVM -import fr.iut.pm.movieapplication.ui.viewmodel.MoviesVMFactory import fr.iut.pm.movieapplication.utils.Constants.Companion.PAGE_SIZE class MoviesFragment( - private val context : MainActivity ) : Fragment() { private var isLoading = false @@ -29,150 +28,53 @@ class MoviesFragment( private var currentPage = 1 private var currentList : MutableList = mutableListOf() - private val moviesVM: MoviesVM by viewModels{ MoviesVMFactory(repository = MovieRepository())} - lateinit var moviesRecyclerView : RecyclerView - lateinit var spinner: Spinner - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val view = inflater.inflate(R.layout.fragment_movies, container, false) - - // Get the RecyclerView - moviesRecyclerView = view.findViewById(R.id.movies_item_recycler_view) + private val moviesVM by viewModels() - // Initialized the data inside our RecyclerView + lateinit var spinner: Spinner - // Create the ScrollListener - val scrollListener = object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - super.onScrolled(recyclerView, dx, dy) - - val layoutManager = moviesRecyclerView.layoutManager as GridLayoutManager - val visibleItemCount = layoutManager.childCount - val totalItemCount = layoutManager.itemCount - val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val binding = FragmentMoviesBinding.inflate(inflater) + binding.moviesVM = moviesVM + binding.lifecycleOwner = viewLifecycleOwner + + val adapter = ArrayAdapter.createFromResource( + requireContext(), + R.array.movie_filter, + android.R.layout.simple_spinner_item + ) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - // If we are not already loading data and it's not the last page - if(!isLoading && !isLastPage) { - if(visibleItemCount + firstVisibleItemPosition >= totalItemCount - && firstVisibleItemPosition >= 0 - && totalItemCount >= PAGE_SIZE + with(binding.categorySpinner) + { + this.adapter = adapter + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long ) { - loadMoreMovies() + moviesVM.getDataFilter(selectedItem.toString()) } - } - } - } - // Add the ScrollLister created before to our RecyclerView - moviesRecyclerView.addOnScrollListener(scrollListener) - spinner = view.findViewById(R.id.category_spinner) - configSpinner(spinner) - return view - } - - private fun configSpinner(spinner: Spinner) { - ArrayAdapter.createFromResource( - context, - R.array.movie_filter, - android.R.layout.simple_spinner_item - ).also { adapter -> - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - - spinner.adapter = adapter - } - - spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - when(position) { - 0 -> { - currentList.clear() - context.movieRepository.getPopularMovies { listMovies -> - currentList.addAll(0,listMovies) - moviesRecyclerView.adapter = CategoryAdapter(context, R.layout.item_vertical_fragment, currentList) - moviesRecyclerView.layoutManager = GridLayoutManager (context, 3) - } - } - 1 -> { - currentList.clear() - context.movieRepository.getNowPlayingMovies { movies: List -> - currentList.addAll(0,movies) - moviesRecyclerView.adapter = CategoryAdapter(context, R.layout.item_vertical_fragment, currentList) - moviesRecyclerView.layoutManager = GridLayoutManager (context, 3) - - } - } - 2 -> { - currentList.clear() - context.movieRepository.getUpcomingMovies { movies: List -> - currentList.addAll(0,movies) - moviesRecyclerView.adapter = CategoryAdapter(context, R.layout.item_vertical_fragment, currentList) - moviesRecyclerView.layoutManager = GridLayoutManager (context, 3) - } - } - 3 -> { - currentList.clear() - context.movieRepository.getTopRatedMovies { movies -> - currentList.addAll(0,movies) - moviesRecyclerView.adapter = CategoryAdapter(context, R.layout.item_vertical_fragment, currentList) - moviesRecyclerView.layoutManager = GridLayoutManager (context, 3) - } - } + override fun onNothingSelected(parent: AdapterView<*>?) { + TODO("Not yet implemented") } - } - - override fun onNothingSelected(parent: AdapterView<*>?) { } - } + return binding.root } - /** - * Method to load data when the user reaches the bottom of the view - */ - private fun loadMoreMovies() { - isLoading = true - currentPage += 1 - - if(currentPage == 1000) isLastPage = true - - val start = currentList.size - - when(spinner.selectedItemPosition) { - 0 -> { - context.movieRepository.getPopularMovies(currentPage) { listMovies -> - currentList.addAll(start, listMovies) - moviesRecyclerView.adapter?.notifyItemRangeChanged(start, listMovies.size) - } - } - 1 -> { - context.movieRepository.getNowPlayingMovies(currentPage) { listMovies -> - currentList.addAll(start, listMovies) - moviesRecyclerView.adapter?.notifyItemRangeChanged(start, listMovies.size) - } - } - 2 -> { - context.movieRepository.getUpcomingMovies(currentPage) { listMovies -> - currentList.addAll(start, listMovies) - moviesRecyclerView.adapter?.notifyItemRangeChanged(start, listMovies.size) - } - } - 3 -> { - context.movieRepository.getTopRatedMovies(currentPage) { listMovies -> - currentList.addAll(start, listMovies) - moviesRecyclerView.adapter?.notifyItemRangeChanged(start, listMovies.size) - } - } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + moviesVM.getMoviesLiveData().observe(viewLifecycleOwner) { + moviesVM.moviesAdapter.submitList(it) } - - - isLoading = false } } diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt index 8548868..84bbc25 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt @@ -1,27 +1,93 @@ package fr.iut.pm.movieapplication.ui.viewmodel import androidx.lifecycle.* -import fr.iut.pm.movieapplication.model.media.movie.MovieDetails +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import fr.iut.pm.movieapplication.model.media.movie.Movie import fr.iut.pm.movieapplication.repository.MovieRepository +import fr.iut.pm.movieapplication.ui.adapter.MovieAdapter import kotlinx.coroutines.launch -class MoviesVM(private val repository: MovieRepository) : ViewModel() { +class MoviesVM() : ViewModel() { + //Movie repository + private val repository = MovieRepository() + //Live data + private var _moviesLiveData : MutableLiveData> = MutableLiveData>() + fun getMoviesLiveData() : LiveData> = _moviesLiveData - private val _popularMovies = MutableLiveData>() - val popularMovies : LiveData> = _popularMovies + private var currentFilter = "" + val moviesAdapter = MovieAdapter() + val scrollListener = object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + val layoutManager = recyclerView.layoutManager as GridLayoutManager + val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() + val totalItemCount = layoutManager.itemCount - init { - //loadData() + if(lastVisibleItemPosition == totalItemCount -1) { + + ++currentPage + getMoreData(currentPage) + } + } } + + private var currentPage = 1 + + + fun getDataFilter(filter : String) { + + //_moviesLiveData.value = mutableListOf() + currentFilter = filter + currentPage = 1 + + when(currentFilter) { + "Populaires" -> viewModelScope.launch { + _moviesLiveData.postValue(repository.getPopularMovies()) + } + "Du moment" -> viewModelScope.launch { + _moviesLiveData.postValue(repository.getNowPlayingMovies()) + } + "À venir" -> viewModelScope.launch { + _moviesLiveData.postValue(repository.getUpcomingMovies()) + } + "Les mieux notés" -> viewModelScope.launch { + _moviesLiveData.postValue(repository.getTopRatedMovies()) + } + } + + } + + fun getMoreData(page : Int = 1) { + var movies : List + when(currentFilter) { + "Populaires" -> viewModelScope.launch { + movies = _moviesLiveData.value?.plus(repository.getPopularMovies(page)) ?: listOf() + _moviesLiveData.postValue(movies) + } + "Du moment" -> viewModelScope.launch { + movies = _moviesLiveData.value?.plus(repository.getNowPlayingMovies(page)) ?: listOf() + _moviesLiveData.postValue(movies) + } + "À venir" -> viewModelScope.launch { + movies = _moviesLiveData.value?.plus(repository.getUpcomingMovies(page)) ?: listOf() + _moviesLiveData.postValue(movies) + } + "Les mieux notés" -> viewModelScope.launch { + movies = _moviesLiveData.value?.plus(repository.getTopRatedMovies(page)) ?: listOf() + _moviesLiveData.postValue(movies) + } + } + + } + } -class MoviesVMFactory( - private val repository: MovieRepository - ) : ViewModelProvider.Factory { +class MoviesVMFactory : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { - return MoviesVM(repository) as T + return MoviesVM() as T } } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt index 9948f10..c15a018 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt @@ -30,7 +30,7 @@ object MediaResultMapper { posterPath = mediaResultDTO.posterPath, adult = !mediaResultDTO.adult, overview = mediaResultDTO.overview, - releaseDate = mediaResultDTO.releaseDate!!, + releaseDate = mediaResultDTO.releaseDate ?: "", genreIds = mediaResultDTO.genreIds, id = mediaResultDTO.id, originalTitle = mediaResultDTO.originalTitle!!, @@ -49,20 +49,17 @@ object MediaResultMapper { posterPath = mediaResultDTO.posterPath, adult = !mediaResultDTO.adult, overview = mediaResultDTO.overview, - firstAirDate = mediaResultDTO.firstAirDate, - releaseDate = mediaResultDTO.releaseDate, + releaseDate = mediaResultDTO.releaseDate ?: mediaResultDTO.firstAirDate!! , originCountry = mediaResultDTO.originCountry, genreIds = mediaResultDTO.genreIds, id = mediaResultDTO.id, - originalTitle = mediaResultDTO.originalTitle, + originalTitle = mediaResultDTO.originalTitle ?: mediaResultDTO.originalName!!, //if it's not a movie also it's a tvshow originalLanguage = mediaResultDTO.originalLanguage, - title = mediaResultDTO.title, + title = mediaResultDTO.title ?: mediaResultDTO.name!!, //if it's not a movie also it's a tvshow backdropPath = mediaResultDTO.backdropPath, popularity = mediaResultDTO.popularity, voteCount = mediaResultDTO.voteCount, voteAverage = mediaResultDTO.voteAverage, - name = mediaResultDTO.name, - originalName = mediaResultDTO.originalName, mediaType = mediaResultDTO.mediaType ) } diff --git a/Sources/app/src/main/res/layout/fragment_movies.xml b/Sources/app/src/main/res/layout/fragment_movies.xml index 7a243ca..a3d0e0f 100644 --- a/Sources/app/src/main/res/layout/fragment_movies.xml +++ b/Sources/app/src/main/res/layout/fragment_movies.xml @@ -1,26 +1,56 @@ - - - - + + + + + + + + + android:orientation="vertical"> + + + + + + + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/Sources/app/src/main/res/layout/item_movie_category.xml b/Sources/app/src/main/res/layout/item_movie_category.xml new file mode 100644 index 0000000..784f1f9 --- /dev/null +++ b/Sources/app/src/main/res/layout/item_movie_category.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 635a458cadebbae094dc5ff2b6c91e41132de286 Mon Sep 17 00:00:00 2001 From: Jordan Artzet Date: Sun, 5 Feb 2023 01:57:52 +0100 Subject: [PATCH 10/18] :recycler: refactor and clean the code --- .../repository/MovieRepository.kt | 6 --- .../ui/activity/MainActivity.kt | 9 +--- .../ui/adapter/CategoryItemDecoration.kt | 12 ----- .../ui/adapter/MovieAdapter.kt | 4 -- .../ui/fragments/MoviesFragment.kt | 52 +++++++------------ .../movieapplication/ui/viewmodel/MoviesVM.kt | 47 ++++++++++------- .../ui/viewmodel/ViewModelFactory.kt | 9 ---- 7 files changed, 48 insertions(+), 91 deletions(-) delete mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/CategoryItemDecoration.kt delete mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/ViewModelFactory.kt diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt index e327a64..4e471ae 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt @@ -1,15 +1,9 @@ package fr.iut.pm.movieapplication.repository import android.util.Log -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.liveData import fr.iut.pm.movieapplication.api.RetrofitInstance -import fr.iut.pm.movieapplication.api.dtos.PopularDTO -import fr.iut.pm.movieapplication.model.media.MediaResult import fr.iut.pm.movieapplication.model.media.movie.Movie import fr.iut.pm.movieapplication.utils.MediaResultMapper -import kotlinx.coroutines.Dispatchers class MovieRepository { diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt index 4e6e7db..30f886a 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt @@ -17,7 +17,6 @@ import fr.iut.pm.movieapplication.ui.fragments.TvShowsFragment class MainActivity : AppCompatActivity() { - val movieRepository : MovieRepository = MovieRepository() val mediaRepository : MediaRepository = MediaRepository() override fun onCreate(savedInstanceState: Bundle?) { @@ -47,13 +46,7 @@ class MainActivity : AppCompatActivity() { else -> false } } - /* - //Trends fragment injected in main activity - val transaction = supportFragmentManager.beginTransaction() - transaction.replace(R.id.fragment_container, HomeSectionsFragment()) - transaction.addToBackStack(null) - transaction.commit() - */ + if(Build.VERSION.SDK_INT < 33) { // Hide the status bar. window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/CategoryItemDecoration.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/CategoryItemDecoration.kt deleted file mode 100644 index fe08862..0000000 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/CategoryItemDecoration.kt +++ /dev/null @@ -1,12 +0,0 @@ -package fr.iut.pm.movieapplication.ui.adapter - -import android.graphics.Rect -import android.view.View -import androidx.recyclerview.widget.RecyclerView - -class CategoryItemDecoration : RecyclerView.ItemDecoration() { - - override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { - outRect.bottom = 50 - } -} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt index d798661..e19e4e0 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt @@ -1,16 +1,12 @@ package fr.iut.pm.movieapplication.ui.adapter import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView import androidx.core.net.toUri import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import coil.load -import fr.iut.pm.movieapplication.R import fr.iut.pm.movieapplication.databinding.ItemMovieCategoryBinding import fr.iut.pm.movieapplication.model.media.movie.Movie import fr.iut.pm.movieapplication.utils.Constants diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt index 9e80f2b..c4573f5 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt @@ -1,38 +1,22 @@ package fr.iut.pm.movieapplication.ui.fragments import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.AdapterView import android.widget.ArrayAdapter -import android.widget.Spinner import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView import fr.iut.pm.movieapplication.R import fr.iut.pm.movieapplication.databinding.FragmentMoviesBinding -import fr.iut.pm.movieapplication.model.media.movie.Movie -import fr.iut.pm.movieapplication.ui.adapter.MovieAdapter import fr.iut.pm.movieapplication.ui.viewmodel.MoviesVM -import fr.iut.pm.movieapplication.utils.Constants.Companion.PAGE_SIZE class MoviesFragment( ) : Fragment() { - private var isLoading = false - private var isLastPage = false - private var currentPage = 1 - private var currentList : MutableList = mutableListOf() - private val moviesVM by viewModels() - - lateinit var spinner: Spinner - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -41,25 +25,25 @@ class MoviesFragment( binding.moviesVM = moviesVM binding.lifecycleOwner = viewLifecycleOwner - val adapter = ArrayAdapter.createFromResource( - requireContext(), - R.array.movie_filter, - android.R.layout.simple_spinner_item - ) - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + val adapter = ArrayAdapter.createFromResource( + requireContext(), + R.array.movie_filter, + android.R.layout.simple_spinner_item + ) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - with(binding.categorySpinner) - { - this.adapter = adapter - onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - moviesVM.getDataFilter(selectedItem.toString()) - } + with(binding.categorySpinner) + { + this.adapter = adapter + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + moviesVM.getData(selectedItem.toString()) + } override fun onNothingSelected(parent: AdapterView<*>?) { diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt index 84bbc25..126f2b3 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt @@ -9,14 +9,31 @@ import fr.iut.pm.movieapplication.ui.adapter.MovieAdapter import kotlinx.coroutines.launch class MoviesVM() : ViewModel() { - //Movie repository + /** + * The movie repository used to get our data + */ private val repository = MovieRepository() - //Live data + + /** + * The MutableLiveData + */ private var _moviesLiveData : MutableLiveData> = MutableLiveData>() + + /** + * Getter of the LiveData + */ fun getMoviesLiveData() : LiveData> = _moviesLiveData + /** + * The current data filter + */ private var currentFilter = "" + + /** + * The adapter of the RecyclerView (set on the RecyclerView in the view) + */ val moviesAdapter = MovieAdapter() + val scrollListener = object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { val layoutManager = recyclerView.layoutManager as GridLayoutManager @@ -33,13 +50,13 @@ class MoviesVM() : ViewModel() { private var currentPage = 1 - - fun getDataFilter(filter : String) { - - //_moviesLiveData.value = mutableListOf() + /** + * Get the data with a given filter + * @param filter filter applied to get data + */ + fun getData(filter : String) { currentFilter = filter currentPage = 1 - when(currentFilter) { "Populaires" -> viewModelScope.launch { _moviesLiveData.postValue(repository.getPopularMovies()) @@ -57,7 +74,11 @@ class MoviesVM() : ViewModel() { } - fun getMoreData(page : Int = 1) { + /** + * Get more data with the actual filter + * @param page page from which the data are obtained + */ + private fun getMoreData(page : Int = 1) { var movies : List when(currentFilter) { "Populaires" -> viewModelScope.launch { @@ -80,14 +101,4 @@ class MoviesVM() : ViewModel() { } -} - - - -class MoviesVMFactory : ViewModelProvider.Factory { - - override fun create(modelClass: Class): T { - return MoviesVM() as T - } - } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/ViewModelFactory.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/ViewModelFactory.kt deleted file mode 100644 index d9d7bcc..0000000 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/ViewModelFactory.kt +++ /dev/null @@ -1,9 +0,0 @@ -package fr.iut.pm.movieapplication.ui.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider - -inline fun viewModelFactory(crossinline f: () -> VM) = - object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T = f() as T - } \ No newline at end of file From 724e6cb75c47a1074b54003b869408cd066b5e5b Mon Sep 17 00:00:00 2001 From: Jordan Artzet Date: Sun, 5 Feb 2023 21:18:27 +0100 Subject: [PATCH 11/18] :construction: add the tv show request and display the data in the tvshow cateogry --- .../api/MovieApplicationAPI.kt | 4 +- .../api/dtos/MediaResultDTO.kt | 2 +- .../model/media/tvshow/TvShow.kt | 72 +++++++++++++++---- .../repository/TVShowRepository.kt | 43 ----------- .../repository/TvShowRepository.kt | 29 ++++++++ .../ui/activity/MainActivity.kt | 2 +- .../ui/adapter/MovieAdapter.kt | 5 +- .../ui/adapter/TvShowAdapter.kt | 48 +++++++++++++ .../ui/fragments/MoviesFragment.kt | 2 +- .../ui/fragments/TvShowsFragment.kt | 59 ++++++++++++++- .../movieapplication/ui/viewmodel/MoviesVM.kt | 8 ++- .../movieapplication/ui/viewmodel/TvShowVM.kt | 34 +++++++++ .../pm/movieapplication/utils/Constants.kt | 1 + .../utils/MediaResultMapper.kt | 6 +- .../src/main/res/layout/fragment_movies.xml | 2 +- .../src/main/res/layout/fragment_tv_shows.xml | 48 ++++++++++--- .../main/res/layout/item_tv_show_category.xml | 67 +++++++++++++++++ Sources/app/src/main/res/values/strings.xml | 7 ++ 18 files changed, 358 insertions(+), 81 deletions(-) delete mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TVShowRepository.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TvShowRepository.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/TvShowAdapter.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/TvShowVM.kt create mode 100644 Sources/app/src/main/res/layout/item_tv_show_category.xml diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt index 13efeba..5b9350a 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt @@ -28,8 +28,8 @@ interface MovieApplicationAPI { // TvShow - @GET - fun getPopularTvShow(@Query("api_key") apiKey : String = API_KEY, @Query("page") page : Int = 1) : Call + @GET("tv/popular") + suspend fun getPopularTvShow(@Query("api_key") apiKey : String = API_KEY, @Query("page") page : Int = 1) : Response @GET("trending/{media_type}/{time_window}") diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt index b9b56af..2a979af 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt @@ -8,7 +8,7 @@ data class MediaResultDTO( @Json(name = "poster_path") val posterPath : String?, - val adult : Boolean, + val adult : Boolean?, val overview : String, @Json(name = "first_air_date") val firstAirDate : String?, diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/tvshow/TvShow.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/tvshow/TvShow.kt index b9ac31a..0b698dc 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/tvshow/TvShow.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/tvshow/TvShow.kt @@ -1,20 +1,62 @@ package fr.iut.pm.movieapplication.model.media.tvshow -class TvShow( - val posterPath: String?, - val popularity: Double, - val id: Int, - val backdropPath: String, - val voteAverage: Double, - val overview: String, - val firstAirDate: String?, - val originCountry: List, - val genreIds: List, - val originalLanguage: String, - val voteCount: Int, - val name: String, - val originalName: String, - val mediaType: String = "tv", +open class TvShow( + open val posterPath: String?, + open val popularity: Double, + open val id: Int, + open val backdropPath: String, + open val voteAverage: Double, + open val overview: String, + open val firstAirDate: String?, + open val originCountry: List, + open val genreIds: List, + open val originalLanguage: String, + open val voteCount: Int, + open val name: String, + open val originalName: String, + open val mediaType: String = "tv", ) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TvShow + + if (posterPath != other.posterPath) return false + if (popularity != other.popularity) return false + if (id != other.id) return false + if (backdropPath != other.backdropPath) return false + if (voteAverage != other.voteAverage) return false + if (overview != other.overview) return false + if (firstAirDate != other.firstAirDate) return false + if (originCountry != other.originCountry) return false + if (genreIds != other.genreIds) return false + if (originalLanguage != other.originalLanguage) return false + if (voteCount != other.voteCount) return false + if (name != other.name) return false + if (originalName != other.originalName) return false + if (mediaType != other.mediaType) return false + + return true + } + + override fun hashCode(): Int { + var result = posterPath?.hashCode() ?: 0 + result = 31 * result + popularity.hashCode() + result = 31 * result + id + result = 31 * result + backdropPath.hashCode() + result = 31 * result + voteAverage.hashCode() + result = 31 * result + overview.hashCode() + result = 31 * result + (firstAirDate?.hashCode() ?: 0) + result = 31 * result + originCountry.hashCode() + result = 31 * result + genreIds.hashCode() + result = 31 * result + originalLanguage.hashCode() + result = 31 * result + voteCount + result = 31 * result + name.hashCode() + result = 31 * result + originalName.hashCode() + result = 31 * result + mediaType.hashCode() + return result + } } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TVShowRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TVShowRepository.kt deleted file mode 100644 index f2c1d4a..0000000 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TVShowRepository.kt +++ /dev/null @@ -1,43 +0,0 @@ -package fr.iut.pm.movieapplication.repository - -import android.util.Log -import fr.iut.pm.movieapplication.api.RetrofitInstance -import fr.iut.pm.movieapplication.api.dtos.PopularDTO -import fr.iut.pm.movieapplication.model.media.movie.Movie -import fr.iut.pm.movieapplication.model.media.tvshow.TvShow -import fr.iut.pm.movieapplication.utils.MediaResultMapper -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response - -class TVShowRepository { - -// fun getPopularTvShow(page : Int = 1 ,callback: (List) -> Unit ) { -// -// val listMovie : MutableList = mutableListOf() -// -// RetrofitInstance.api.getPopularMovies(page = page).enqueue(object : -// Callback { -// override fun onResponse(call: Call, response: Response) { -// if (response.isSuccessful) { -// Log.d("List :", response.body().toString()) -// val popularDTO = response.body() -// val listMoviesDTO = popularDTO?.results -// listMoviesDTO?.forEach { -// -// val tvShow = MediaResultMapper.mapToTvShow(it) -// listMovie.add(tvShow) -// Log.d("Movie ", tvShow.name ) -// } -// -// } -// callback(listMovie) -// } -// -// override fun onFailure(call: Call, t: Throwable) { -// Log.d("Error failure", t.message.toString()) -// } -// -// }) -// } -} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TvShowRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TvShowRepository.kt new file mode 100644 index 0000000..01e2e10 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TvShowRepository.kt @@ -0,0 +1,29 @@ +package fr.iut.pm.movieapplication.repository + +import android.util.Log +import fr.iut.pm.movieapplication.api.RetrofitInstance +import fr.iut.pm.movieapplication.model.media.tvshow.TvShow +import fr.iut.pm.movieapplication.utils.MediaResultMapper + +class TvShowRepository { + + suspend fun getPopularTvShow(page : Int = 1 ) : List { + + val listMovie : MutableList = mutableListOf() + + val response = RetrofitInstance.api.getPopularTvShow(page = page) + if (response.isSuccessful) { + Log.d("List :", response.body().toString()) + val popularDTO = response.body() + val listMoviesDTO = popularDTO?.results + listMoviesDTO?.forEach { + val tvShow = MediaResultMapper.mapToTvShow(it) + listMovie.add(tvShow) + Log.d("Movie ", tvShow.name ) + } + } + else Log.d("Error failure", response.message()) + + return listMovie + } +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt index 30f886a..b27c2aa 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt @@ -40,7 +40,7 @@ class MainActivity : AppCompatActivity() { } R.id.series_page -> { - loadFragments(TvShowsFragment(this)) + loadFragments(TvShowsFragment()) return@setOnItemSelectedListener true } else -> false diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt index e19e4e0..24eed09 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt @@ -11,9 +11,9 @@ import fr.iut.pm.movieapplication.databinding.ItemMovieCategoryBinding import fr.iut.pm.movieapplication.model.media.movie.Movie import fr.iut.pm.movieapplication.utils.Constants -class MovieAdapter : ListAdapter(DiffUtilDogCallback) { +class MovieAdapter() : ListAdapter(DiffUtilMovieCallback) { - private object DiffUtilDogCallback : DiffUtil.ItemCallback() { + private object DiffUtilMovieCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: Movie, newItem: Movie) = oldItem.id == newItem.id override fun areContentsTheSame(oldItem: Movie, newItem: Movie) = oldItem == newItem } @@ -40,4 +40,5 @@ class MovieAdapter : ListAdapter(DiffUtilDogCall ViewHolder(ItemMovieCategoryBinding.inflate(LayoutInflater.from(parent.context))) override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(getItem(position)) + } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/TvShowAdapter.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/TvShowAdapter.kt new file mode 100644 index 0000000..e139b65 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/TvShowAdapter.kt @@ -0,0 +1,48 @@ +package fr.iut.pm.movieapplication.ui.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.net.toUri +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import coil.load +import fr.iut.pm.movieapplication.databinding.ItemTvShowCategoryBinding +import fr.iut.pm.movieapplication.model.media.tvshow.TvShow +import fr.iut.pm.movieapplication.utils.Constants + +class TvShowAdapter() : ListAdapter(DiffUtilTvShowCallback) { + + + private object DiffUtilTvShowCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: TvShow, newItem: TvShow) = oldItem.id == newItem.id + + override fun areContentsTheSame(oldItem: TvShow, newItem: TvShow) = oldItem == newItem + } + + + class ViewHolder(private val binding : ItemTvShowCategoryBinding) + : RecyclerView.ViewHolder(binding.root){ + + val tvShow : TvShow? get() = binding.tvShow + + init { + itemView.setOnClickListener {} + } + + fun bind(tvShow : TvShow) { + binding.tvShow = tvShow + val imgUri = tvShow.posterPath?.let { (Constants.IMG_URL +it).toUri().buildUpon().scheme("https").build() } + binding.itemImage.load(imgUri) + binding.executePendingBindings() + } + + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TvShowAdapter.ViewHolder = + TvShowAdapter.ViewHolder(ItemTvShowCategoryBinding.inflate(LayoutInflater.from(parent.context))) + + override fun onBindViewHolder(holder: TvShowAdapter.ViewHolder, position: Int) = holder.bind(getItem(position)) + +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt index c4573f5..c0326ac 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt @@ -32,7 +32,7 @@ class MoviesFragment( ) adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - with(binding.categorySpinner) + with(binding.categoryMovieSpinner) { this.adapter = adapter onItemSelectedListener = object : AdapterView.OnItemSelectedListener { diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/TvShowsFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/TvShowsFragment.kt index 4aba7f2..fe13393 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/TvShowsFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/TvShowsFragment.kt @@ -1,15 +1,68 @@ package fr.iut.pm.movieapplication.ui.fragments import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import fr.iut.pm.movieapplication.R +import fr.iut.pm.movieapplication.databinding.FragmentTvShowsBinding import fr.iut.pm.movieapplication.ui.activity.MainActivity +import fr.iut.pm.movieapplication.ui.viewmodel.TvShowVM class TvShowsFragment( - private val context : MainActivity ) : Fragment() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + private val tvShowVM by viewModels() + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?) + : View { + val binding = FragmentTvShowsBinding.inflate(inflater) + + binding.tvShowVM = tvShowVM + binding.lifecycleOwner = viewLifecycleOwner + + val adapter = ArrayAdapter.createFromResource( + requireContext(), + R.array.tv_show_filter, + android.R.layout.simple_spinner_item + ) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + + with(binding.categoryTvShowSpinner) + { + this.adapter = adapter + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + tvShowVM.getData(selectedItem.toString()) + } + + + override fun onNothingSelected(parent: AdapterView<*>?) { + TODO("Not yet implemented") + } + + } + } + + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + tvShowVM.getTvShowLiveData().observe(viewLifecycleOwner) { + tvShowVM.tvShowAdapter.submitList(it) + } } } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt index 126f2b3..69cf7cd 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt @@ -6,6 +6,8 @@ import androidx.recyclerview.widget.RecyclerView import fr.iut.pm.movieapplication.model.media.movie.Movie import fr.iut.pm.movieapplication.repository.MovieRepository import fr.iut.pm.movieapplication.ui.adapter.MovieAdapter +import fr.iut.pm.movieapplication.utils.Constants.Companion.MAX_PAGE +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class MoviesVM() : ViewModel() { @@ -43,11 +45,15 @@ class MoviesVM() : ViewModel() { if(lastVisibleItemPosition == totalItemCount -1) { ++currentPage - getMoreData(currentPage) + //1000 is the MAX_PAGE + if(currentPage <= MAX_PAGE) getMoreData(currentPage) } } } + /** + * Currrent page where the data are obtained + */ private var currentPage = 1 /** diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/TvShowVM.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/TvShowVM.kt new file mode 100644 index 0000000..33b1c4b --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/TvShowVM.kt @@ -0,0 +1,34 @@ +package fr.iut.pm.movieapplication.ui.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import fr.iut.pm.movieapplication.model.media.tvshow.TvShow +import fr.iut.pm.movieapplication.repository.TvShowRepository +import fr.iut.pm.movieapplication.ui.adapter.TvShowAdapter +import kotlinx.coroutines.launch + +class TvShowVM : ViewModel() { + + private val tvShowRepository = TvShowRepository() + + private var tvShowLiveData : MutableLiveData> = MutableLiveData>() + fun getTvShowLiveData() : LiveData> = tvShowLiveData + + private var currentFilter : String = "" + val tvShowAdapter = TvShowAdapter() + + private var currentPage = 1 + + fun getData(filter : String) { + + currentFilter = filter + currentPage = 1 + when(currentFilter) { + "Populaires" -> viewModelScope.launch { + tvShowLiveData.postValue(tvShowRepository.getPopularTvShow()) + } + } + } +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Constants.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Constants.kt index 3bc70c1..37e050b 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Constants.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/Constants.kt @@ -12,5 +12,6 @@ class Constants { //VIEW PAGINATION const val PAGE_SIZE = 20 + const val MAX_PAGE = 1000 } } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt index c15a018..0b58a33 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt @@ -12,7 +12,7 @@ object MediaResultMapper { posterPath = mediaResultDTO.posterPath, popularity = mediaResultDTO.popularity, id = mediaResultDTO.id, - backdropPath = mediaResultDTO.backdropPath!!, + backdropPath = mediaResultDTO.backdropPath ?: "", voteAverage = mediaResultDTO.voteAverage, overview = mediaResultDTO.overview, firstAirDate = mediaResultDTO.firstAirDate, @@ -28,7 +28,7 @@ object MediaResultMapper { fun mapToMovie(mediaResultDTO: MediaResultDTO): Movie { return Movie( posterPath = mediaResultDTO.posterPath, - adult = !mediaResultDTO.adult, + adult = mediaResultDTO.adult!!, overview = mediaResultDTO.overview, releaseDate = mediaResultDTO.releaseDate ?: "", genreIds = mediaResultDTO.genreIds, @@ -47,7 +47,7 @@ object MediaResultMapper { fun mapToMediaResult(mediaResultDTO: MediaResultDTO) : MediaResult { return MediaResult( posterPath = mediaResultDTO.posterPath, - adult = !mediaResultDTO.adult, + adult = mediaResultDTO.adult!!, overview = mediaResultDTO.overview, releaseDate = mediaResultDTO.releaseDate ?: mediaResultDTO.firstAirDate!! , originCountry = mediaResultDTO.originCountry, diff --git a/Sources/app/src/main/res/layout/fragment_movies.xml b/Sources/app/src/main/res/layout/fragment_movies.xml index a3d0e0f..7016eed 100644 --- a/Sources/app/src/main/res/layout/fragment_movies.xml +++ b/Sources/app/src/main/res/layout/fragment_movies.xml @@ -20,7 +20,7 @@ - + - + + + + + + + + android:orientation="vertical"> + + + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/Sources/app/src/main/res/layout/item_tv_show_category.xml b/Sources/app/src/main/res/layout/item_tv_show_category.xml new file mode 100644 index 0000000..188f2fb --- /dev/null +++ b/Sources/app/src/main/res/layout/item_tv_show_category.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Sources/app/src/main/res/values/strings.xml b/Sources/app/src/main/res/values/strings.xml index 6280e15..79bb1d8 100644 --- a/Sources/app/src/main/res/values/strings.xml +++ b/Sources/app/src/main/res/values/strings.xml @@ -22,4 +22,11 @@ Les mieux notés + + Populaires + Diffusées aujourd\'hui + En cours de diffusion + Les mieux notées + + \ No newline at end of file From 76130b935844d54ca3638a80d3e8f320846f6e37 Mon Sep 17 00:00:00 2001 From: Jordan Artzet Date: Mon, 6 Feb 2023 19:56:43 +0100 Subject: [PATCH 12/18] :construction: prepare class for the movie details request --- .../api/MovieApplicationAPI.kt | 15 +++-- .../pm/movieapplication/api/dtos/GenreDTO.kt | 8 +++ .../api/dtos/MediaResultDTO.kt | 24 +++---- .../api/dtos/MovieDetailsDTO.kt | 48 ++++++++++++++ .../{TvShowDTO.kt => TvShowDetailsDTO.kt} | 2 +- .../model/media/MediaResult.kt | 2 +- .../model/media/movie/Movie.kt | 12 ++-- .../model/media/movie/MovieDetails.kt | 8 +-- .../model/media/tvshow/TvShow.kt | 6 +- .../repository/TvShowRepository.kt | 64 ++++++++++++++++++- .../movieapplication/ui/viewmodel/TvShowVM.kt | 11 +++- .../utils/MediaResultMapper.kt | 14 ++-- Sources/app/src/main/res/values/arrays.xml | 2 + Sources/app/src/main/res/values/strings.xml | 4 +- 14 files changed, 177 insertions(+), 43 deletions(-) create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/GenreDTO.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDetailsDTO.kt rename Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/{TvShowDTO.kt => TvShowDetailsDTO.kt} (96%) create mode 100644 Sources/app/src/main/res/values/arrays.xml diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt index 5b9350a..a8d2bd0 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt @@ -1,7 +1,7 @@ package fr.iut.pm.movieapplication.api import fr.iut.pm.movieapplication.api.dtos.MovieDTO -import fr.iut.pm.movieapplication.api.dtos.MovieResultDTO +import fr.iut.pm.movieapplication.api.dtos.MovieDetailsDTO import fr.iut.pm.movieapplication.api.dtos.PopularDTO import fr.iut.pm.movieapplication.utils.Constants.Companion.API_KEY import retrofit2.Call @@ -25,18 +25,25 @@ interface MovieApplicationAPI { @GET("movie/top_rated") suspend fun getTopRatedMovies(@Query("api_key") apiKey: String = API_KEY, @Query("page") page : Int = 1) : Response + // Movie details + @GET("movie/{movie_id}") + fun getMovieDetails(@Path("movie_id") movieId : Int, @Query("api_key") apiKey: String = API_KEY) : Response // TvShow @GET("tv/popular") - suspend fun getPopularTvShow(@Query("api_key") apiKey : String = API_KEY, @Query("page") page : Int = 1) : Response + suspend fun getPopularTvShows(@Query("api_key") apiKey : String = API_KEY, @Query("page") page : Int = 1) : Response + @GET("tv/airing_today") + suspend fun getAiringTodayTvShows(@Query("api_key") apiKey: String = API_KEY, @Query("page") page : Int = 1) : Response + @GET("tv/on_the_air") + suspend fun getTvOnTheAirTvShows(@Query("api_key") apiKey: String = API_KEY, @Query("page") page : Int = 1) : Response + @GET("tv/top_rated") + suspend fun getTopRatedTvShows(@Query("api_key") apiKey: String = API_KEY, @Query("page") page : Int = 1) : Response @GET("trending/{media_type}/{time_window}") fun getTrending(@Path("media_type") mediaType : String = "all", @Path("time_window") timeWindow : String = "day", @Query("api_key") apiKey: String = API_KEY ) : Call - @GET("movie/{movie_id") - fun getMovieDetails(@Path("movie_id") movieId : Int, @Query("api_key") apiKey: String = API_KEY) : Call @GET("tv/{tv_id}") fun getShowDetails(@Path("tv_id") tvId : Int, @Query("api_key") apiKey: String = API_KEY) diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/GenreDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/GenreDTO.kt new file mode 100644 index 0000000..8bbdaa4 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/GenreDTO.kt @@ -0,0 +1,8 @@ +package fr.iut.pm.movieapplication.api.dtos + +data class GenreDTO( + val id : Int, + val name : String +) { + +} diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt index 2a979af..a56948c 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt @@ -4,23 +4,23 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @Json(name = "results") -data class MediaResultDTO( +open class MediaResultDTO( @Json(name = "poster_path") val posterPath : String?, val adult : Boolean?, - val overview : String, + val overview : String? = null, @Json(name = "first_air_date") - val firstAirDate : String?, + val firstAirDate : String? = null, @Json(name = "release_date") - val releaseDate : String?, + val releaseDate : String? = null, @Json(name = "origin_country") - val originCountry : List?, - @Json(name = "genre_ids") - val genreIds : List, + val originCountry : List? = null, +// @Json(name = "genre_ids") +// val genreIds : List, val id : Int, @Json(name = "original_title") - val originalTitle : String?, + val originalTitle : String? = null, @Json(name = "original_language") val originalLanguage : String, val title : String?, @@ -29,14 +29,14 @@ data class MediaResultDTO( val popularity : Double, @Json(name = "vote_count") val voteCount : Int, - val video : Boolean?, + //val video : Boolean?, @Json(name = "vote_average") val voteAverage : Double, - val name: String?, + val name: String? = null, @Json(name = "original_name") - val originalName : String?, + val originalName : String? = null, @Json(name = "media_type") - val mediaType : String? + val mediaType : String? = null ) { } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDetailsDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDetailsDTO.kt new file mode 100644 index 0000000..4fc1dae --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDetailsDTO.kt @@ -0,0 +1,48 @@ +package fr.iut.pm.movieapplication.api.dtos + +import fr.iut.pm.movieapplication.model.Genre + +class MovieDetailsDTO( + + adult : Boolean?, + backdropPath : String?, + val budget : Int, + val genres : List, + val homepage : String?, + id : Int, + originalLanguage : String, + originalTitle : String, + overview : String?, + popularity : Double, + posterPath : String?, + //prod companies + //prod countries + releaseDate : String?, + val revenue : Int, + //spoken language + val status : String, + title : String, + voteAverage : Double, + voteCount : Int + + + +) : MediaResultDTO( + adult = adult, + backdropPath = backdropPath, + id = id, + originalLanguage = originalLanguage, + originalTitle = originalTitle, + overview = overview, + popularity = popularity, + posterPath = posterPath, + releaseDate = releaseDate, + title = title, + voteAverage = voteAverage, + voteCount = voteCount, +) { + + + + +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/TvShowDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/TvShowDetailsDTO.kt similarity index 96% rename from Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/TvShowDTO.kt rename to Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/TvShowDetailsDTO.kt index be4247c..bdee72e 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/TvShowDTO.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/TvShowDetailsDTO.kt @@ -2,7 +2,7 @@ package fr.iut.pm.movieapplication.api.dtos import com.squareup.moshi.Json -open class TvShowDTO( +open class TvShowDetailsDTO( @Json(name = "poster_path") open val posterPath: String?, diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/MediaResult.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/MediaResult.kt index fbe2ce4..d9c8594 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/MediaResult.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/MediaResult.kt @@ -7,7 +7,7 @@ data class MediaResult( val overview: String, val releaseDate: String, val originCountry: List? = null, - val genreIds: List, +// val genreIds: List, val id: Int, val originalTitle: String, val originalLanguage: String, diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/Movie.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/Movie.kt index 32ac156..ad61088 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/Movie.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/Movie.kt @@ -5,7 +5,7 @@ open class Movie( open val adult: Boolean, open val overview: String, open val releaseDate: String, - open val genreIds: List, +// open val genreIds: List, open val id: Int, open val originalTitle: String, open val originalLanguage: String, @@ -13,8 +13,8 @@ open class Movie( open val backdropPath: String?, open val popularity: Double, open val voteCount: Int, - open val video: Boolean?, - open val voteAverage: Double, + //open val video: Boolean?, + open val voteAverage: Double ) { override fun equals(other: Any?): Boolean { @@ -27,7 +27,7 @@ open class Movie( if (adult != other.adult) return false if (overview != other.overview) return false if (releaseDate != other.releaseDate) return false - if (genreIds != other.genreIds) return false +// if (genreIds != other.genreIds) return false if (id != other.id) return false if (originalTitle != other.originalTitle) return false if (originalLanguage != other.originalLanguage) return false @@ -35,7 +35,6 @@ open class Movie( if (backdropPath != other.backdropPath) return false if (popularity != other.popularity) return false if (voteCount != other.voteCount) return false - if (video != other.video) return false if (voteAverage != other.voteAverage) return false return true @@ -46,7 +45,7 @@ open class Movie( result = 31 * result + adult.hashCode() result = 31 * result + overview.hashCode() result = 31 * result + releaseDate.hashCode() - result = 31 * result + genreIds.hashCode() +// result = 31 * result + genreIds.hashCode() result = 31 * result + id result = 31 * result + originalTitle.hashCode() result = 31 * result + originalLanguage.hashCode() @@ -54,7 +53,6 @@ open class Movie( result = 31 * result + (backdropPath?.hashCode() ?: 0) result = 31 * result + popularity.hashCode() result = 31 * result + voteCount - result = 31 * result + (video?.hashCode() ?: 0) result = 31 * result + voteAverage.hashCode() return result } diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/MovieDetails.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/MovieDetails.kt index c70e337..ab5a9e2 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/MovieDetails.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/MovieDetails.kt @@ -10,7 +10,7 @@ data class MovieDetails( override val adult: Boolean, override val overview: String, override val releaseDate: String, - override val genreIds : List, +// override val genreIds : List, override val id: Int, override val originalTitle: String, override val originalLanguage: String, @@ -18,7 +18,7 @@ data class MovieDetails( override val backdropPath: String?, override val popularity: Double, override val voteCount: Int, - override val video: Boolean?, +// override val video: Boolean?, override val voteAverage: Double, val mediaType: String, @@ -35,7 +35,7 @@ data class MovieDetails( adult, overview, releaseDate, - genreIds, +// genreIds, id, originalTitle, originalLanguage, @@ -43,7 +43,7 @@ data class MovieDetails( backdropPath, popularity, voteCount, - video, +// video, voteAverage) { diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/tvshow/TvShow.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/tvshow/TvShow.kt index 0b698dc..22a8bfd 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/tvshow/TvShow.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/tvshow/TvShow.kt @@ -9,7 +9,7 @@ open class TvShow( open val overview: String, open val firstAirDate: String?, open val originCountry: List, - open val genreIds: List, +// open val genreIds: List, open val originalLanguage: String, open val voteCount: Int, open val name: String, @@ -32,7 +32,7 @@ open class TvShow( if (overview != other.overview) return false if (firstAirDate != other.firstAirDate) return false if (originCountry != other.originCountry) return false - if (genreIds != other.genreIds) return false +// if (genreIds != other.genreIds) return false if (originalLanguage != other.originalLanguage) return false if (voteCount != other.voteCount) return false if (name != other.name) return false @@ -51,7 +51,7 @@ open class TvShow( result = 31 * result + overview.hashCode() result = 31 * result + (firstAirDate?.hashCode() ?: 0) result = 31 * result + originCountry.hashCode() - result = 31 * result + genreIds.hashCode() +// result = 31 * result + genreIds.hashCode() result = 31 * result + originalLanguage.hashCode() result = 31 * result + voteCount result = 31 * result + name.hashCode() diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TvShowRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TvShowRepository.kt index 01e2e10..58ccf1d 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TvShowRepository.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/TvShowRepository.kt @@ -7,11 +7,71 @@ import fr.iut.pm.movieapplication.utils.MediaResultMapper class TvShowRepository { - suspend fun getPopularTvShow(page : Int = 1 ) : List { + suspend fun getPopularTvShows(page : Int = 1 ) : List { val listMovie : MutableList = mutableListOf() - val response = RetrofitInstance.api.getPopularTvShow(page = page) + val response = RetrofitInstance.api.getPopularTvShows(page = page) + if (response.isSuccessful) { + Log.d("List :", response.body().toString()) + val popularDTO = response.body() + val listMoviesDTO = popularDTO?.results + listMoviesDTO?.forEach { + val tvShow = MediaResultMapper.mapToTvShow(it) + listMovie.add(tvShow) + Log.d("Movie ", tvShow.name ) + } + } + else Log.d("Error failure", response.message()) + + return listMovie + } + + suspend fun getAiringTodayTvShows(page : Int = 1 ) : List { + + val listMovie : MutableList = mutableListOf() + + val response = RetrofitInstance.api.getAiringTodayTvShows(page = page) + if (response.isSuccessful) { + Log.d("List :", response.body().toString()) + val popularDTO = response.body() + val listMoviesDTO = popularDTO?.results + listMoviesDTO?.forEach { + val tvShow = MediaResultMapper.mapToTvShow(it) + listMovie.add(tvShow) + Log.d("Movie ", tvShow.name ) + } + } + else Log.d("Error failure", response.message()) + + return listMovie + } + + suspend fun getTvOnTheAirTvShows(page : Int = 1 ) : List { + + val listMovie : MutableList = mutableListOf() + + val response = RetrofitInstance.api.getTvOnTheAirTvShows(page = page) + if (response.isSuccessful) { + Log.d("List :", response.body().toString()) + val popularDTO = response.body() + val listMoviesDTO = popularDTO?.results + listMoviesDTO?.forEach { + val tvShow = MediaResultMapper.mapToTvShow(it) + listMovie.add(tvShow) + Log.d("Movie ", tvShow.name ) + } + } + else Log.d("Error failure", response.message()) + + return listMovie + } + + suspend fun getTopRatedTvShows(page : Int = 1 ) : List { + + val listMovie : MutableList = mutableListOf() + + val response = RetrofitInstance.api.getTopRatedTvShows(page = page) if (response.isSuccessful) { Log.d("List :", response.body().toString()) val popularDTO = response.body() diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/TvShowVM.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/TvShowVM.kt index 33b1c4b..383b73b 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/TvShowVM.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/TvShowVM.kt @@ -27,7 +27,16 @@ class TvShowVM : ViewModel() { currentPage = 1 when(currentFilter) { "Populaires" -> viewModelScope.launch { - tvShowLiveData.postValue(tvShowRepository.getPopularTvShow()) + tvShowLiveData.postValue(tvShowRepository.getPopularTvShows()) + } + "Diffusées aujourd\'hui" -> viewModelScope.launch { + tvShowLiveData.postValue(tvShowRepository.getAiringTodayTvShows()) + } + "En cours de diffusion" -> viewModelScope.launch { + tvShowLiveData.postValue(tvShowRepository.getTvOnTheAirTvShows()) + } + "Les mieux notées" -> viewModelScope.launch { + tvShowLiveData.postValue(tvShowRepository.getTopRatedTvShows()) } } } diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt index 0b58a33..0b8bd7f 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt @@ -14,10 +14,10 @@ object MediaResultMapper { id = mediaResultDTO.id, backdropPath = mediaResultDTO.backdropPath ?: "", voteAverage = mediaResultDTO.voteAverage, - overview = mediaResultDTO.overview, + overview = mediaResultDTO.overview ?: "", firstAirDate = mediaResultDTO.firstAirDate, originCountry = mediaResultDTO.originCountry!!, - genreIds = mediaResultDTO.genreIds, +// genreIds = mediaResultDTO.genreIds, originalLanguage = mediaResultDTO.originalLanguage, voteCount = mediaResultDTO.voteCount, name = mediaResultDTO.name!!, @@ -29,9 +29,9 @@ object MediaResultMapper { return Movie( posterPath = mediaResultDTO.posterPath, adult = mediaResultDTO.adult!!, - overview = mediaResultDTO.overview, + overview = mediaResultDTO.overview ?: "", releaseDate = mediaResultDTO.releaseDate ?: "", - genreIds = mediaResultDTO.genreIds, +// genreIds = mediaResultDTO.genreIds, id = mediaResultDTO.id, originalTitle = mediaResultDTO.originalTitle!!, originalLanguage = mediaResultDTO.originalLanguage, @@ -39,7 +39,7 @@ object MediaResultMapper { backdropPath = mediaResultDTO.backdropPath, popularity = mediaResultDTO.popularity, voteCount = mediaResultDTO.voteCount, - video = mediaResultDTO.video, + //video = mediaResultDTO.video, voteAverage = mediaResultDTO.voteAverage ) } @@ -48,10 +48,10 @@ object MediaResultMapper { return MediaResult( posterPath = mediaResultDTO.posterPath, adult = mediaResultDTO.adult!!, - overview = mediaResultDTO.overview, + overview = mediaResultDTO.overview ?: "", releaseDate = mediaResultDTO.releaseDate ?: mediaResultDTO.firstAirDate!! , originCountry = mediaResultDTO.originCountry, - genreIds = mediaResultDTO.genreIds, +// genreIds = mediaResultDTO.genreIds, id = mediaResultDTO.id, originalTitle = mediaResultDTO.originalTitle ?: mediaResultDTO.originalName!!, //if it's not a movie also it's a tvshow originalLanguage = mediaResultDTO.originalLanguage, diff --git a/Sources/app/src/main/res/values/arrays.xml b/Sources/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/Sources/app/src/main/res/values/arrays.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Sources/app/src/main/res/values/strings.xml b/Sources/app/src/main/res/values/strings.xml index 79bb1d8..c8587f9 100644 --- a/Sources/app/src/main/res/values/strings.xml +++ b/Sources/app/src/main/res/values/strings.xml @@ -13,7 +13,9 @@ Populaires Gratuits section_nested_scroll_view - + + + Populaires From 797ce00665acfb53cb2aaed80b49e856e034b837 Mon Sep 17 00:00:00 2001 From: Jordan Artzet Date: Tue, 7 Feb 2023 02:00:50 +0100 Subject: [PATCH 13/18] :construction: some requests implemented need to had coroutines now to improve perf --- .../api/MovieApplicationAPI.kt | 2 +- .../api/dtos/MediaResultDTO.kt | 4 +- .../pm/movieapplication/api/dtos/MovieDTO.kt | 5 +- .../api/dtos/MovieDetailsDTO.kt | 71 ++++++-------- .../fr/iut/pm/movieapplication/model/Genre.kt | 13 +-- .../model/media/movie/Movie.kt | 12 +-- .../model/media/movie/MovieDetails.kt | 74 ++++++--------- .../repository/MovieRepository.kt | 42 +++++++-- .../ui/activity/MainActivity.kt | 2 - .../ui/adapter/MovieAdapter.kt | 14 ++- .../movieapplication/ui/dialog/MovieDialog.kt | 42 +++++++++ .../ui/fragments/MoviesFragment.kt | 20 +++- .../ui/viewmodel/MoviesDialogVM.kt | 29 ++++++ .../movieapplication/ui/viewmodel/MoviesVM.kt | 3 +- .../utils/MediaResultMapper.kt | 35 +++++++ .../pm/movieapplication/utils/MovieMapper.kt | 48 ++++++++++ .../app/src/main/res/drawable/ic_close.xml | 5 + .../src/main/res/layout/fragment_movies.xml | 1 - .../app/src/main/res/layout/movie_dialog.xml | 94 +++++++++++++++++++ 19 files changed, 383 insertions(+), 133 deletions(-) create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/dialog/MovieDialog.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesDialogVM.kt create mode 100644 Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MovieMapper.kt create mode 100644 Sources/app/src/main/res/drawable/ic_close.xml create mode 100644 Sources/app/src/main/res/layout/movie_dialog.xml diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt index a8d2bd0..8617fdd 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/MovieApplicationAPI.kt @@ -27,7 +27,7 @@ interface MovieApplicationAPI { // Movie details @GET("movie/{movie_id}") - fun getMovieDetails(@Path("movie_id") movieId : Int, @Query("api_key") apiKey: String = API_KEY) : Response + suspend fun getMovieDetails(@Path("movie_id") movieId : Int, @Query("api_key") apiKey: String = API_KEY) : Response // TvShow diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt index a56948c..d5348c1 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MediaResultDTO.kt @@ -20,10 +20,10 @@ open class MediaResultDTO( // val genreIds : List, val id : Int, @Json(name = "original_title") - val originalTitle : String? = null, + val originalTitle : String = "", @Json(name = "original_language") val originalLanguage : String, - val title : String?, + val title : String = "", @Json(name = "backdrop_path") val backdropPath : String?, val popularity : Double, diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDTO.kt index 73f4017..d7cf509 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDTO.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDTO.kt @@ -6,11 +6,9 @@ open class MovieDTO ( @Json(name = "poster_path") open val posterPath: String?, open val adult: Boolean, - open val overview: String, + open val overview: String?, @Json(name = "release_date") open val releaseDate: String, - @Json(name = "genre_ids") - open val genreIds: List, open val id: Int, @Json(name = "original_title") open val originalTitle: String, @@ -22,7 +20,6 @@ open class MovieDTO ( open val popularity: Double, @Json(name = "vote_count") open val voteCount: Int, - open val video : Boolean, @Json(name = "vote_average") open val voteAverage: Double diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDetailsDTO.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDetailsDTO.kt index 4fc1dae..99dfb48 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDetailsDTO.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/api/dtos/MovieDetailsDTO.kt @@ -1,48 +1,37 @@ package fr.iut.pm.movieapplication.api.dtos +import com.squareup.moshi.Json import fr.iut.pm.movieapplication.model.Genre -class MovieDetailsDTO( - - adult : Boolean?, - backdropPath : String?, - val budget : Int, - val genres : List, - val homepage : String?, - id : Int, - originalLanguage : String, - originalTitle : String, - overview : String?, - popularity : Double, - posterPath : String?, +data class MovieDetailsDTO( + + val adult: Boolean, + @Json(name = "backdrop_path") + val backdropPath: String?, + val budget: Int, + val genres: List, + val homepage: String?, + val id: Int, + @Json(name = "original_language") + val originalLanguage: String, + @Json(name = "original_title") + val originalTitle: String, + val overview: String?, + val popularity: Double, + @Json(name = "poster_path") + val posterPath: String?, //prod companies //prod countries - releaseDate : String?, - val revenue : Int, + @Json(name = "release_date") + val releaseDate: String, + val revenue: Int, //spoken language - val status : String, - title : String, - voteAverage : Double, - voteCount : Int - - - -) : MediaResultDTO( - adult = adult, - backdropPath = backdropPath, - id = id, - originalLanguage = originalLanguage, - originalTitle = originalTitle, - overview = overview, - popularity = popularity, - posterPath = posterPath, - releaseDate = releaseDate, - title = title, - voteAverage = voteAverage, - voteCount = voteCount, -) { - - - - -} \ No newline at end of file + val status: String, + val title: String, + @Json(name = "vote_average") + val voteAverage: Double, + @Json(name = "vote_count") + val voteCount: Int + +) +{} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Genre.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Genre.kt index 62c322d..f0a8111 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Genre.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/Genre.kt @@ -1,16 +1,7 @@ package fr.iut.pm.movieapplication.model - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "genre_table") data class Genre( - @PrimaryKey - @ColumnInfo(name = "id") - var id : Int, - @ColumnInfo(name = "name") - var name : String + val id : Int, + val name : String ) { override fun equals(other: Any?): Boolean { diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/Movie.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/Movie.kt index ad61088..c03daae 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/Movie.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/Movie.kt @@ -2,18 +2,16 @@ package fr.iut.pm.movieapplication.model.media.movie open class Movie( open val posterPath: String?, - open val adult: Boolean, - open val overview: String, - open val releaseDate: String, -// open val genreIds: List, + open val adult: Boolean?, + open val overview: String?, + open val releaseDate: String?, open val id: Int, open val originalTitle: String, open val originalLanguage: String, - open val title: String?, + open val title: String, open val backdropPath: String?, open val popularity: Double, open val voteCount: Int, - //open val video: Boolean?, open val voteAverage: Double ) { @@ -27,7 +25,6 @@ open class Movie( if (adult != other.adult) return false if (overview != other.overview) return false if (releaseDate != other.releaseDate) return false -// if (genreIds != other.genreIds) return false if (id != other.id) return false if (originalTitle != other.originalTitle) return false if (originalLanguage != other.originalLanguage) return false @@ -45,7 +42,6 @@ open class Movie( result = 31 * result + adult.hashCode() result = 31 * result + overview.hashCode() result = 31 * result + releaseDate.hashCode() -// result = 31 * result + genreIds.hashCode() result = 31 * result + id result = 31 * result + originalTitle.hashCode() result = 31 * result + originalLanguage.hashCode() diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/MovieDetails.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/MovieDetails.kt index ab5a9e2..a9c63d9 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/MovieDetails.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/model/media/movie/MovieDetails.kt @@ -1,50 +1,34 @@ package fr.iut.pm.movieapplication.model.media.movie -import fr.iut.pm.movieapplication.model.ProductionCompany -import fr.iut.pm.movieapplication.model.ProductionCountry - - -data class MovieDetails( - - override val posterPath: String, - override val adult: Boolean, - override val overview: String, - override val releaseDate: String, -// override val genreIds : List, - override val id: Int, - override val originalTitle: String, - override val originalLanguage: String, - override val title: String?, - override val backdropPath: String?, - override val popularity: Double, - override val voteCount: Int, -// override val video: Boolean?, - override val voteAverage: Double, - val mediaType: String, - - val budget: Int?, - val homePage: String?, - val productionCompanies: Array?, - val productionCountries: Array?, - val revenue: Int?, - val runtime: Int?, - val status: String?, - val tagLine: String? - -) : Movie(posterPath, - adult, - overview, - releaseDate, -// genreIds, - id, - originalTitle, - originalLanguage, - title, - backdropPath, - popularity, - voteCount, -// video, - voteAverage) { +import fr.iut.pm.movieapplication.model.Genre + + +class MovieDetails( + + adult : Boolean?, + backdropPath : String?, + val budget : Int, + val genres : List, + val homepage : String?, + id : Int, + originalLanguage : String, + originalTitle : String, + overview : String?, + popularity : Double, + posterPath : String?, + //prod companies + //prod countries + releaseDate : String?, + val revenue : Int, + //spoken language + val status : String, + title : String, + voteAverage : Double, + voteCount : Int + +) : Movie(posterPath, adult, overview, releaseDate, id, originalTitle, originalLanguage, + title, backdropPath, popularity, voteCount, voteAverage) +{ } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt index 4e471ae..a42cbd3 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/repository/MovieRepository.kt @@ -2,12 +2,16 @@ package fr.iut.pm.movieapplication.repository import android.util.Log import fr.iut.pm.movieapplication.api.RetrofitInstance +import fr.iut.pm.movieapplication.api.dtos.MovieDetailsDTO import fr.iut.pm.movieapplication.model.media.movie.Movie +import fr.iut.pm.movieapplication.model.media.movie.MovieDetails import fr.iut.pm.movieapplication.utils.MediaResultMapper +import fr.iut.pm.movieapplication.utils.MovieMapper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext class MovieRepository { - - suspend fun getPopularMovies(page : Int = 1) : List + suspend fun getPopularMovies(page : Int = 1) : List = withContext(Dispatchers.IO) { val listMovie : MutableList = mutableListOf() @@ -18,14 +22,14 @@ class MovieRepository { listMediaResultDTO?.forEach { val movie = MediaResultMapper.mapToMovie(it) listMovie.add(movie) - Log.d("Movie ", movie.title!!) + Log.d("Movie ", movie.title!! + " " + movie.id) } } else Log.d("ERROR FAILED", response.message()) - return listMovie + listMovie } - suspend fun getNowPlayingMovies(page : Int = 1) : List + suspend fun getNowPlayingMovies(page : Int = 1) : List = withContext(Dispatchers.IO) { val listMovie : MutableList = mutableListOf() @@ -40,10 +44,10 @@ class MovieRepository { } } else Log.d("ERROR FAILED", response.message()) - return listMovie + listMovie } - suspend fun getUpcomingMovies(page : Int = 1) : List + suspend fun getUpcomingMovies(page : Int = 1) : List = withContext(Dispatchers.IO) { val listMovie : MutableList = mutableListOf() @@ -58,10 +62,10 @@ class MovieRepository { } } else Log.d("ERROR FAILED", response.message()) - return listMovie + listMovie } - suspend fun getTopRatedMovies(page : Int = 1) : List + suspend fun getTopRatedMovies(page : Int = 1) : List = withContext(Dispatchers.IO) { val listMovie : MutableList = mutableListOf() @@ -76,7 +80,25 @@ class MovieRepository { } } else Log.d("ERROR FAILED", response.message()) - return listMovie + listMovie + } + + suspend fun getMovieDetails(id : Int) : MovieDetails? + { + var movieDetails : MovieDetails? = null + + val response = RetrofitInstance.api.getMovieDetails(id) + if(response.isSuccessful && response.body() != null) { + + Log.d("SUCCESS", response.body().toString()) + movieDetails = MovieMapper.mapToMovieDetails(response.body()!!) + Log.d("Movie details",movieDetails.toString()) + } + else Log.d("ERROR FAILED", response.toString()) + + + + return movieDetails } diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt index b27c2aa..fa38ab8 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/activity/MainActivity.kt @@ -65,12 +65,10 @@ class MainActivity : AppCompatActivity() { searchView.setOnQueryTextListener( object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { - TODO("Not yet implemented") return false } override fun onQueryTextChange(newText: String?): Boolean { - TODO("Not yet implemented") return false } diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt index 24eed09..2c8da88 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/adapter/MovieAdapter.kt @@ -9,22 +9,25 @@ import androidx.recyclerview.widget.RecyclerView import coil.load import fr.iut.pm.movieapplication.databinding.ItemMovieCategoryBinding import fr.iut.pm.movieapplication.model.media.movie.Movie +import fr.iut.pm.movieapplication.ui.dialog.MovieDialog import fr.iut.pm.movieapplication.utils.Constants -class MovieAdapter() : ListAdapter(DiffUtilMovieCallback) { +class MovieAdapter(private val listener : MovieSelection) : ListAdapter(DiffUtilMovieCallback) { private object DiffUtilMovieCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: Movie, newItem: Movie) = oldItem.id == newItem.id override fun areContentsTheSame(oldItem: Movie, newItem: Movie) = oldItem == newItem } - class ViewHolder(private val binding : ItemMovieCategoryBinding) : + class ViewHolder(private val binding : ItemMovieCategoryBinding, listener: MovieSelection) : RecyclerView.ViewHolder(binding.root) { val movie : Movie? get() = binding.movie init { - itemView.setOnClickListener {} + itemView.setOnClickListener { + listener.onMovieSelected(movie?.id ?: 0) + } } fun bind(movie : Movie) { @@ -37,8 +40,11 @@ class MovieAdapter() : ListAdapter(DiffUtilMovie } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = - ViewHolder(ItemMovieCategoryBinding.inflate(LayoutInflater.from(parent.context))) + ViewHolder(ItemMovieCategoryBinding.inflate(LayoutInflater.from(parent.context, )), listener) override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(getItem(position)) + interface MovieSelection { + fun onMovieSelected(movieId : Int) + } } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/dialog/MovieDialog.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/dialog/MovieDialog.kt new file mode 100644 index 0000000..5847821 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/dialog/MovieDialog.kt @@ -0,0 +1,42 @@ +package fr.iut.pm.movieapplication.ui.dialog + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.net.toUri +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.viewModels +import coil.load +import fr.iut.pm.movieapplication.R +import fr.iut.pm.movieapplication.databinding.MovieDialogBinding +import fr.iut.pm.movieapplication.ui.viewmodel.MoviesDialogVM +import fr.iut.pm.movieapplication.ui.viewmodel.MoviesDialogVMFactory +import fr.iut.pm.movieapplication.utils.Constants + +class MovieDialog : DialogFragment() { + + val moviesDialogVM by viewModels { MoviesDialogVMFactory(arguments?.getInt("movieId") ?: 0) } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val binding = MovieDialogBinding.inflate(inflater) + binding.lifecycleOwner = viewLifecycleOwner + binding.closeItem.setOnClickListener { dismiss() } + moviesDialogVM.getMovieDetailLiveData().observe(viewLifecycleOwner) { + binding.movieDetails = it + binding.detailsImage.load(it.posterPath?.let { it -> + (Constants.IMG_URL + it).toUri().buildUpon().scheme("https").build() + }) + } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + + } +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt index c0326ac..cf2181f 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/fragments/MoviesFragment.kt @@ -10,13 +10,15 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import fr.iut.pm.movieapplication.R import fr.iut.pm.movieapplication.databinding.FragmentMoviesBinding +import fr.iut.pm.movieapplication.ui.adapter.MovieAdapter +import fr.iut.pm.movieapplication.ui.dialog.MovieDialog import fr.iut.pm.movieapplication.ui.viewmodel.MoviesVM class MoviesFragment( -) : Fragment() { +) : Fragment(), MovieAdapter.MovieSelection { private val moviesVM by viewModels() - + val moviesAdapter = MovieAdapter(this) override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -25,6 +27,10 @@ class MoviesFragment( binding.moviesVM = moviesVM binding.lifecycleOwner = viewLifecycleOwner + with(binding.moviesItemRecyclerView) { + adapter = moviesAdapter + } + val adapter = ArrayAdapter.createFromResource( requireContext(), R.array.movie_filter, @@ -58,7 +64,15 @@ class MoviesFragment( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) moviesVM.getMoviesLiveData().observe(viewLifecycleOwner) { - moviesVM.moviesAdapter.submitList(it) + moviesAdapter.submitList(it) } } + + override fun onMovieSelected(movieId: Int) { + val dialog = MovieDialog() + val args = Bundle() + args.putInt("movieId",movieId) + dialog.arguments = args + dialog.show(parentFragmentManager, "tag") + } } diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesDialogVM.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesDialogVM.kt new file mode 100644 index 0000000..b39350f --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesDialogVM.kt @@ -0,0 +1,29 @@ +package fr.iut.pm.movieapplication.ui.viewmodel + +import androidx.lifecycle.* +import fr.iut.pm.movieapplication.model.media.movie.MovieDetails +import fr.iut.pm.movieapplication.repository.MovieRepository +import kotlinx.coroutines.launch + +class MoviesDialogVM(private val movieId : Int) : ViewModel() { + + private val repository = MovieRepository() + + private var _movieDetailsLiveData : MutableLiveData = MutableLiveData() + fun getMovieDetailLiveData() : LiveData = _movieDetailsLiveData + + init { + viewModelScope.launch { + if(movieId != 0) _movieDetailsLiveData.postValue(repository.getMovieDetails(movieId)) + } + } + +} + +class MoviesDialogVMFactory(private val movieId : Int) : ViewModelProvider.Factory +{ + override funcreate(modelClass:Class) : T + { + return MoviesDialogVM(movieId) as T + } +} \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt index 69cf7cd..8c6da20 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/ui/viewmodel/MoviesVM.kt @@ -34,7 +34,7 @@ class MoviesVM() : ViewModel() { /** * The adapter of the RecyclerView (set on the RecyclerView in the view) */ - val moviesAdapter = MovieAdapter() + val scrollListener = object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { @@ -66,6 +66,7 @@ class MoviesVM() : ViewModel() { when(currentFilter) { "Populaires" -> viewModelScope.launch { _moviesLiveData.postValue(repository.getPopularMovies()) + repository.getMovieDetails(315162) } "Du moment" -> viewModelScope.launch { _moviesLiveData.postValue(repository.getNowPlayingMovies()) diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt index 0b8bd7f..02b8f6c 100644 --- a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MediaResultMapper.kt @@ -1,8 +1,12 @@ package fr.iut.pm.movieapplication.utils +import fr.iut.pm.movieapplication.api.dtos.GenreDTO import fr.iut.pm.movieapplication.api.dtos.MediaResultDTO +import fr.iut.pm.movieapplication.api.dtos.MovieDetailsDTO +import fr.iut.pm.movieapplication.model.Genre import fr.iut.pm.movieapplication.model.media.MediaResult import fr.iut.pm.movieapplication.model.media.movie.Movie +import fr.iut.pm.movieapplication.model.media.movie.MovieDetails import fr.iut.pm.movieapplication.model.media.tvshow.TvShow object MediaResultMapper { @@ -64,5 +68,36 @@ object MediaResultMapper { ) } + fun mapToMovieDetails(movieDetailsDTO: MovieDetailsDTO?): MovieDetails? { + if(movieDetailsDTO == null) return null + return MovieDetails( + posterPath = movieDetailsDTO.posterPath, + adult = movieDetailsDTO.adult!!, + overview = movieDetailsDTO.overview ?: "", + releaseDate = movieDetailsDTO.releaseDate ?: "", + id = movieDetailsDTO.id, + originalTitle = movieDetailsDTO.originalTitle!!, + originalLanguage = movieDetailsDTO.originalLanguage, + title = movieDetailsDTO.title, + backdropPath = movieDetailsDTO.backdropPath, + popularity = movieDetailsDTO.popularity, + voteCount = movieDetailsDTO.voteCount, + voteAverage = movieDetailsDTO.voteAverage, + budget = movieDetailsDTO.budget, + genres = movieDetailsDTO.genres.map { mapGenreDTOToGenre(it) }, + homepage = movieDetailsDTO.homepage, + revenue = movieDetailsDTO.revenue, + status = movieDetailsDTO.status + ) + + } + + fun mapGenreDTOToGenre(genreDTO : GenreDTO) : Genre { + return Genre( + name = genreDTO.name, + id = genreDTO.id + ) + } + } \ No newline at end of file diff --git a/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MovieMapper.kt b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MovieMapper.kt new file mode 100644 index 0000000..0801222 --- /dev/null +++ b/Sources/app/src/main/java/fr/iut/pm/movieapplication/utils/MovieMapper.kt @@ -0,0 +1,48 @@ +package fr.iut.pm.movieapplication.utils + +import fr.iut.pm.movieapplication.api.dtos.MovieDTO +import fr.iut.pm.movieapplication.api.dtos.MovieDetailsDTO +import fr.iut.pm.movieapplication.model.media.movie.Movie +import fr.iut.pm.movieapplication.model.media.movie.MovieDetails + +object MovieMapper { + + fun mapToMovie(movieDTO : MovieDTO) : Movie { + return Movie( + posterPath = movieDTO.posterPath, + adult = movieDTO.adult, + overview = movieDTO.overview, + releaseDate = movieDTO.releaseDate, + id = movieDTO.id, + originalTitle = movieDTO.originalTitle, + originalLanguage = movieDTO.originalLanguage, + title = movieDTO.title, + backdropPath = movieDTO.backdropPath, + popularity = movieDTO.popularity, + voteCount = movieDTO.voteCount, + voteAverage = movieDTO.voteAverage + ) + } + + fun mapToMovieDetails(movieDetailsDTO: MovieDetailsDTO ) : MovieDetails { + return MovieDetails( + posterPath = movieDetailsDTO.posterPath, + adult = movieDetailsDTO.adult!!, + overview = movieDetailsDTO.overview ?: "", + releaseDate = movieDetailsDTO.releaseDate ?: "", + id = movieDetailsDTO.id, + originalTitle = movieDetailsDTO.originalTitle, + originalLanguage = movieDetailsDTO.originalLanguage, + title = movieDetailsDTO.title, + backdropPath = movieDetailsDTO.backdropPath, + popularity = movieDetailsDTO.popularity, + voteCount = movieDetailsDTO.voteCount, + voteAverage = movieDetailsDTO.voteAverage, + budget = movieDetailsDTO.budget, + genres = movieDetailsDTO.genres.map { MediaResultMapper.mapGenreDTOToGenre(it) }, + homepage = movieDetailsDTO.homepage, + revenue = movieDetailsDTO.revenue, + status = movieDetailsDTO.status + ) + } +} \ No newline at end of file diff --git a/Sources/app/src/main/res/drawable/ic_close.xml b/Sources/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..844b6b6 --- /dev/null +++ b/Sources/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,5 @@ + + + diff --git a/Sources/app/src/main/res/layout/fragment_movies.xml b/Sources/app/src/main/res/layout/fragment_movies.xml index 7016eed..85d6b50 100644 --- a/Sources/app/src/main/res/layout/fragment_movies.xml +++ b/Sources/app/src/main/res/layout/fragment_movies.xml @@ -45,7 +45,6 @@ android:layout_marginBottom="@dimen/default_margin" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:spanCount="3" - android:adapter="@{moviesVM.moviesAdapter}" app:onScrollListener = "@{moviesVM.scrollListener}" tools:listitem="@layout/item_movie_category" diff --git a/Sources/app/src/main/res/layout/movie_dialog.xml b/Sources/app/src/main/res/layout/movie_dialog.xml new file mode 100644 index 0000000..f487839 --- /dev/null +++ b/Sources/app/src/main/res/layout/movie_dialog.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + +