diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bbeb4b7..4779559 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -53,8 +53,8 @@ android { applicationId = "com.application.moviesapp" minSdk = 24 targetSdk = 33 - versionCode = 20 - versionName = "1.0.19" + versionCode = 21 + versionName = "1.0.20" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/application/moviesapp/data/api/MoviesApi.kt b/app/src/main/java/com/application/moviesapp/data/api/MoviesApi.kt index 1490561..5b8f1ed 100755 --- a/app/src/main/java/com/application/moviesapp/data/api/MoviesApi.kt +++ b/app/src/main/java/com/application/moviesapp/data/api/MoviesApi.kt @@ -11,6 +11,7 @@ import com.application.moviesapp.data.api.response.MovieDetailsDto import com.application.moviesapp.data.remote.MovieFavouriteDto import com.application.moviesapp.data.api.response.MovieGenreResponse import com.application.moviesapp.data.api.response.MovieNowPlayingDto +import com.application.moviesapp.data.api.response.MovieReviewDto import com.application.moviesapp.data.api.response.MovieSearchDto import com.application.moviesapp.data.api.response.MovieSimpleResponse import com.application.moviesapp.data.api.response.MovieStateDto @@ -22,6 +23,7 @@ import com.application.moviesapp.data.api.response.TvSeriesDetailsDto import com.application.moviesapp.data.api.response.TvSeriesDiscoverDto import com.application.moviesapp.data.api.response.TvSeriesEpisodesDto import com.application.moviesapp.data.api.response.TvSeriesNowPlayingDto +import com.application.moviesapp.data.api.response.TvSeriesReviewDto import com.application.moviesapp.data.api.response.TvSeriesSearchDto import com.application.moviesapp.data.api.response.TvSeriesTrailerDto import com.application.moviesapp.data.remote.MovieNewReleasesDto @@ -128,4 +130,9 @@ interface MoviesApi { @GET("/3/search/tv") suspend fun getTvSeriesBySearch(@Query("language") language: String = "en-US", @Query("query") query: String = "", @Query("page") page: Int = 1): Response + @GET("/3/movie/{movie_id}/reviews") + suspend fun getMovieReview(@Path("movie_id") movieId: Int, @Query("language") language: String = "en-US", @Query("page") page: Int = 1): Response + + @GET("/3/tv/{series_id}/reviews") + suspend fun getTvSeriesReview(@Path("series_id") seriesId: Int, @Query("language") language: String = "en-US", @Query("page") page: Int = 1): Response } \ No newline at end of file diff --git a/app/src/main/java/com/application/moviesapp/data/api/response/MovieReviewDto.kt b/app/src/main/java/com/application/moviesapp/data/api/response/MovieReviewDto.kt new file mode 100644 index 0000000..cf8c282 --- /dev/null +++ b/app/src/main/java/com/application/moviesapp/data/api/response/MovieReviewDto.kt @@ -0,0 +1,66 @@ +package com.application.moviesapp.data.api.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MovieReviewDto( + + @SerialName("id") + val id: Int?, + + @SerialName("page") + val page: Int?, + + @SerialName("results") + val results: List?, + + @SerialName("total_pages") + val totalPages: Int?, + + @SerialName("total_results") + val totalResults: Int? +) { + + @Serializable + data class Result( + + @SerialName("author") + val author: String?, + + @SerialName("author_details") + val authorDetails: AuthorDetails?, + + @SerialName("content") + val content: String?, + + @SerialName("created_at") + val createdAt: String?, + + @SerialName("id") + val id: String?, + + @SerialName("updated_at") + val updatedAt: String?, + + @SerialName("url") + val url: String? + ) { + + @Serializable + data class AuthorDetails( + + @SerialName("avatar_path") + val avatarPath: String?, + + @SerialName("name") + val name: String?, + + @SerialName("rating") + val rating: Double?, + + @SerialName("username") + val username: String? + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/application/moviesapp/data/api/response/TvSeriesReviewDto.kt b/app/src/main/java/com/application/moviesapp/data/api/response/TvSeriesReviewDto.kt new file mode 100644 index 0000000..f794137 --- /dev/null +++ b/app/src/main/java/com/application/moviesapp/data/api/response/TvSeriesReviewDto.kt @@ -0,0 +1,66 @@ +package com.application.moviesapp.data.api.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class TvSeriesReviewDto( + + @SerialName("id") + val id: Int?, + + @SerialName("page") + val page: Int?, + + @SerialName("results") + val results: List?, + + @SerialName("total_pages") + val totalPages: Int?, + + @SerialName("total_results") + val totalResults: Int? +) { + + @Serializable + data class Result( + + @SerialName("author") + val author: String?, + + @SerialName("author_details") + val authorDetails: AuthorDetails?, + + @SerialName("content") + val content: String?, + + @SerialName("created_at") + val createdAt: String?, + + @SerialName("id") + val id: String?, + + @SerialName("updated_at") + val updatedAt: String?, + + @SerialName("url") + val url: String? + ) { + + @Serializable + data class AuthorDetails( + + @SerialName("avatar_path") + val avatarPath: String?, + + @SerialName("name") + val name: String?, + + @SerialName("rating") + val rating: Double?, + + @SerialName("username") + val username: String? + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/application/moviesapp/data/mappers/MovieReviewMappers.kt b/app/src/main/java/com/application/moviesapp/data/mappers/MovieReviewMappers.kt new file mode 100644 index 0000000..fec9581 --- /dev/null +++ b/app/src/main/java/com/application/moviesapp/data/mappers/MovieReviewMappers.kt @@ -0,0 +1,21 @@ +package com.application.moviesapp.data.mappers + +import com.application.moviesapp.data.api.response.MovieReviewDto +import com.application.moviesapp.domain.model.MovieReview + +fun MovieReviewDto.Result.toDomain(): MovieReview { + return MovieReview( + author = this.author, + content = this.content, + createdAt = this.createdAt, + id = this.id, + updatedAt = this.updatedAt, + url = this.url, + authorDetails = MovieReview.AuthorDetails( + avatarPath = this.authorDetails?.avatarPath, + name = this.authorDetails?.name, + username = this.authorDetails?.username, + rating = this.authorDetails?.rating + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/application/moviesapp/data/mappers/TvSeriesReviewMappers.kt b/app/src/main/java/com/application/moviesapp/data/mappers/TvSeriesReviewMappers.kt new file mode 100644 index 0000000..da5ba66 --- /dev/null +++ b/app/src/main/java/com/application/moviesapp/data/mappers/TvSeriesReviewMappers.kt @@ -0,0 +1,22 @@ +package com.application.moviesapp.data.mappers + +import com.application.moviesapp.data.api.response.TvSeriesReviewDto +import com.application.moviesapp.domain.model.MovieReview +import com.application.moviesapp.domain.model.TvSeriesReview + +fun TvSeriesReviewDto.Result.toDomain(): TvSeriesReview { + return TvSeriesReview( + author = this.author, + content = this.content, + createdAt = this.createdAt, + id = this.id, + updatedAt = this.updatedAt, + url = this.url, + authorDetails = TvSeriesReview.AuthorDetails( + avatarPath = this.authorDetails?.avatarPath, + name = this.authorDetails?.name, + username = this.authorDetails?.username, + rating = this.authorDetails?.rating + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/application/moviesapp/data/remote/MovieReviewPagingSource.kt b/app/src/main/java/com/application/moviesapp/data/remote/MovieReviewPagingSource.kt new file mode 100644 index 0000000..dd95770 --- /dev/null +++ b/app/src/main/java/com/application/moviesapp/data/remote/MovieReviewPagingSource.kt @@ -0,0 +1,49 @@ +package com.application.moviesapp.data.remote + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.application.moviesapp.data.api.MoviesApi +import com.application.moviesapp.data.api.response.MovieReviewDto +import com.application.moviesapp.data.api.response.MovieSearchDto +import timber.log.Timber + +class MovieReviewPagingSource(private val moviesApi: MoviesApi, private val movieId: Int): PagingSource() { + + private companion object { + const val TAG = "MovieReviewPagingSource" + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + + Timber.tag(TAG).d("%s Called!", TAG) + + return try { + val page = params.key ?: 1 + + val apiResult = moviesApi.getMovieReview(movieId = movieId, page = page) + val movies = if (apiResult.isSuccessful) { + apiResult.body() + } else if (apiResult.code() == 400 || apiResult.code() == 401 || apiResult.code() == 403) { + throw Throwable() + } else { + throw Throwable() + } + + LoadResult.Page( + data = movies?.results?.map { it ?: MovieReviewDto.Result(null, null, null, null, null, null, null)} ?: listOf(), + prevKey = if (page == 1) null else page.minus(1), + nextKey = if (movies?.results?.isEmpty() == true) null else page.plus(1), + ) + } catch (throwable: Throwable) { + Timber.tag(TAG).e(throwable) + LoadResult.Error(throwable) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/application/moviesapp/data/remote/TvSeriesPagingSource.kt b/app/src/main/java/com/application/moviesapp/data/remote/TvSeriesPagingSource.kt new file mode 100644 index 0000000..5f0677c --- /dev/null +++ b/app/src/main/java/com/application/moviesapp/data/remote/TvSeriesPagingSource.kt @@ -0,0 +1,49 @@ +package com.application.moviesapp.data.remote + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.application.moviesapp.data.api.MoviesApi +import com.application.moviesapp.data.api.response.MovieReviewDto +import com.application.moviesapp.data.api.response.TvSeriesReviewDto +import timber.log.Timber + +class TvSeriesReviewPagingSource(private val moviesApi: MoviesApi, private val seriesId: Int): PagingSource() { + + private companion object { + const val TAG = "TvSeriesReviewPagingSource" + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + + Timber.tag(TAG).d("%s Called!", TAG) + + return try { + val page = params.key ?: 1 + + val apiResult = moviesApi.getTvSeriesReview(seriesId = seriesId, page = page) + val movies = if (apiResult.isSuccessful) { + apiResult.body() + } else if (apiResult.code() == 400 || apiResult.code() == 401 || apiResult.code() == 403) { + throw Throwable() + } else { + throw Throwable() + } + + LoadResult.Page( + data = movies?.results?.map { it ?: TvSeriesReviewDto.Result(null, null, null, null, null, null, null)} ?: listOf(), + prevKey = if (page == 1) null else page.minus(1), + nextKey = if (movies?.results?.isEmpty() == true) null else page.plus(1), + ) + } catch (throwable: Throwable) { + Timber.tag(TAG).e(throwable) + LoadResult.Error(throwable) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/application/moviesapp/data/repository/MoviesRepository.kt b/app/src/main/java/com/application/moviesapp/data/repository/MoviesRepository.kt index f0c1728..3c8585b 100755 --- a/app/src/main/java/com/application/moviesapp/data/repository/MoviesRepository.kt +++ b/app/src/main/java/com/application/moviesapp/data/repository/MoviesRepository.kt @@ -16,6 +16,7 @@ import com.application.moviesapp.data.api.response.MovieDetailsDto import com.application.moviesapp.data.remote.MovieFavouriteDto import com.application.moviesapp.data.api.response.MovieGenreResponse import com.application.moviesapp.data.api.response.MovieNowPlayingDto +import com.application.moviesapp.data.api.response.MovieReviewDto import com.application.moviesapp.data.api.response.MovieSearchDto import com.application.moviesapp.data.api.response.MovieStateDto import com.application.moviesapp.data.api.response.MovieTopRatedResponse @@ -26,6 +27,7 @@ import com.application.moviesapp.data.api.response.TvSeriesDetailsDto import com.application.moviesapp.data.api.response.TvSeriesDiscoverDto import com.application.moviesapp.data.api.response.TvSeriesEpisodesDto import com.application.moviesapp.data.api.response.TvSeriesNowPlayingDto +import com.application.moviesapp.data.api.response.TvSeriesReviewDto import com.application.moviesapp.data.api.response.TvSeriesSearchDto import com.application.moviesapp.data.api.response.TvSeriesTrailerDto import com.application.moviesapp.data.local.MoviesDatabase @@ -33,6 +35,7 @@ import com.application.moviesapp.data.local.entity.MovieDownloadEntity import com.application.moviesapp.data.remote.MovieFavouritePagingSource import com.application.moviesapp.data.remote.MovieNewReleasesDto import com.application.moviesapp.data.remote.MovieNowPlayingPagingSource +import com.application.moviesapp.data.remote.MovieReviewPagingSource import com.application.moviesapp.data.remote.MovieSearchPagingSource import com.application.moviesapp.data.remote.MovieUpcomingDto import com.application.moviesapp.data.remote.MoviesDiscoverDto @@ -41,7 +44,9 @@ import com.application.moviesapp.data.remote.TvSeriesDiscoverPagingSource import com.application.moviesapp.data.remote.TvSeriesFavouriteDto import com.application.moviesapp.data.remote.TvSeriesFavouritePagingSource import com.application.moviesapp.data.remote.TvSeriesNowPlayingPagingSource +import com.application.moviesapp.data.remote.TvSeriesReviewPagingSource import com.application.moviesapp.data.remote.TvSeriesSearchPagingSource +import com.application.moviesapp.domain.model.TvSeriesReview import kotlinx.coroutines.flow.Flow import okhttp3.RequestBody import retrofit2.Response @@ -119,6 +124,10 @@ interface MoviesRepository { suspend fun getTvSeriesNowPlayingList(): Response suspend fun getTvSeriesEpisodes(seriesId: Int, seasonNumber: Int = 1): Response + + fun getMovieReviewPagingFlow(movieId: Int): Flow> + + fun getTvSeriesReviewPagingFlow(seriesId: Int): Flow> } @OptIn(ExperimentalPagingApi::class) @@ -238,4 +247,17 @@ class MoviesRepositoryImpl @Inject constructor(private val movies: MoviesApi, override suspend fun getTvSeriesNowPlayingList(): Response = movies.getNowPlayingSeriesList() override suspend fun getTvSeriesEpisodes(seriesId: Int, seasonNumber: Int): Response = movies.getTvSeriesEpisodes(seriesId = seriesId, seasonNumber = seasonNumber) + override fun getMovieReviewPagingFlow(movieId: Int): Flow> = Pager( + config = PagingConfig(pageSize = 20), + pagingSourceFactory = { + MovieReviewPagingSource(movies, movieId) + } + ).flow + + override fun getTvSeriesReviewPagingFlow(seriesId: Int): Flow> = Pager( + config = PagingConfig(pageSize = 20), + pagingSourceFactory = { + TvSeriesReviewPagingSource(movies, seriesId) + } + ).flow } diff --git a/app/src/main/java/com/application/moviesapp/di/UseCaseModule.kt b/app/src/main/java/com/application/moviesapp/di/UseCaseModule.kt index 9426df6..8fa0f1f 100755 --- a/app/src/main/java/com/application/moviesapp/di/UseCaseModule.kt +++ b/app/src/main/java/com/application/moviesapp/di/UseCaseModule.kt @@ -33,6 +33,7 @@ import com.application.moviesapp.domain.usecase.GetLanguageInteractor import com.application.moviesapp.domain.usecase.GetMovieDownloadInteractor import com.application.moviesapp.domain.usecase.GetMovieGenreInteractor import com.application.moviesapp.domain.usecase.GetMovieNowPlayingInteractor +import com.application.moviesapp.domain.usecase.GetMovieReviewInteractor import com.application.moviesapp.domain.usecase.GetMovieSearchInteractor import com.application.moviesapp.domain.usecase.GetMovieWithTvSeriesInteractor import com.application.moviesapp.domain.usecase.GetNotificationInteractor @@ -43,6 +44,7 @@ import com.application.moviesapp.domain.usecase.GetTvSeriesEpisodesUseCase import com.application.moviesapp.domain.usecase.GetTvSeriesFavouriteInteractor import com.application.moviesapp.domain.usecase.GetTvSeriesGenreInteractor import com.application.moviesapp.domain.usecase.GetTvSeriesNowPlayingInteractor +import com.application.moviesapp.domain.usecase.GetTvSeriesReviewInteractor import com.application.moviesapp.domain.usecase.GetTvSeriesSearchInteractor import com.application.moviesapp.domain.usecase.GetTvSeriesTrailerInteractor import com.application.moviesapp.domain.usecase.GetWifiInteractor @@ -50,6 +52,7 @@ import com.application.moviesapp.domain.usecase.LanguageUseCase import com.application.moviesapp.domain.usecase.MovieDownloadUseCase import com.application.moviesapp.domain.usecase.MovieGenresUseCase import com.application.moviesapp.domain.usecase.MovieNowPlayingUseCase +import com.application.moviesapp.domain.usecase.MovieReviewUseCase import com.application.moviesapp.domain.usecase.MovieSearchUseCase import com.application.moviesapp.domain.usecase.MovieStateUseCase import com.application.moviesapp.domain.usecase.MovieTrailerUseCase @@ -73,6 +76,7 @@ import com.application.moviesapp.domain.usecase.TvSeriesEpisodesUseCase import com.application.moviesapp.domain.usecase.TvSeriesFavouriteUseCase import com.application.moviesapp.domain.usecase.TvSeriesGenreUseCase import com.application.moviesapp.domain.usecase.TvSeriesNowPlayingUseCase +import com.application.moviesapp.domain.usecase.TvSeriesReviewUseCase import com.application.moviesapp.domain.usecase.TvSeriesSearchUseCase import com.application.moviesapp.domain.usecase.TvSeriesTrailerUseCase import com.application.moviesapp.domain.usecase.WifiUseCase @@ -312,5 +316,17 @@ class UseCaseModule { fun providesNotificationUseCase(notificationPreferenceRepository: NotificationPreferenceRepository): NotificationUseCase { return GetNotificationInteractor(notificationPreferenceRepository) } + + @Provides + @Singleton + fun providesMovieReviewUseCase(moviesRepository: MoviesRepository): MovieReviewUseCase { + return GetMovieReviewInteractor(moviesRepository) + } + + @Provides + @Singleton + fun providesTvSeriesReviewUseCase(moviesRepository: MoviesRepository): TvSeriesReviewUseCase { + return GetTvSeriesReviewInteractor(moviesRepository) + } } diff --git a/app/src/main/java/com/application/moviesapp/domain/model/MovieReview.kt b/app/src/main/java/com/application/moviesapp/domain/model/MovieReview.kt new file mode 100644 index 0000000..d7f4976 --- /dev/null +++ b/app/src/main/java/com/application/moviesapp/domain/model/MovieReview.kt @@ -0,0 +1,21 @@ +package com.application.moviesapp.domain.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +data class MovieReview( + val author: String?, + val authorDetails: AuthorDetails?, + val content: String?, + val createdAt: String?, + val id: String?, + val updatedAt: String?, + val url: String? +) { + data class AuthorDetails( + val avatarPath: String?, + val name: String?, + val rating: Double?, + val username: String? + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/application/moviesapp/domain/model/TvSeriesReview.kt b/app/src/main/java/com/application/moviesapp/domain/model/TvSeriesReview.kt new file mode 100644 index 0000000..665d2a8 --- /dev/null +++ b/app/src/main/java/com/application/moviesapp/domain/model/TvSeriesReview.kt @@ -0,0 +1,46 @@ +package com.application.moviesapp.domain.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class TvSeriesReview( + + @SerialName("author") + val author: String?, + + @SerialName("author_details") + val authorDetails: AuthorDetails?, + + @SerialName("content") + val content: String?, + + @SerialName("created_at") + val createdAt: String?, + + @SerialName("id") + val id: String?, + + @SerialName("updated_at") + val updatedAt: String?, + + @SerialName("url") + val url: String? +) { + + @Serializable + data class AuthorDetails( + + @SerialName("avatar_path") + val avatarPath: String?, + + @SerialName("name") + val name: String?, + + @SerialName("rating") + val rating: Double?, + + @SerialName("username") + val username: String? + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/application/moviesapp/domain/usecase/MovieReviewUseCase.kt b/app/src/main/java/com/application/moviesapp/domain/usecase/MovieReviewUseCase.kt new file mode 100644 index 0000000..003ef22 --- /dev/null +++ b/app/src/main/java/com/application/moviesapp/domain/usecase/MovieReviewUseCase.kt @@ -0,0 +1,24 @@ +package com.application.moviesapp.domain.usecase + +import androidx.paging.PagingData +import androidx.paging.map +import com.application.moviesapp.data.mappers.toDomain +import com.application.moviesapp.data.mappers.toMovie +import com.application.moviesapp.data.repository.MoviesRepository +import com.application.moviesapp.domain.model.MovieReview +import com.application.moviesapp.domain.model.MovieSearch +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +interface MovieReviewUseCase { + operator fun invoke(movieId: Int): Flow> +} + +class GetMovieReviewInteractor @Inject constructor(private val repository: MoviesRepository): MovieReviewUseCase { + override fun invoke(movieId: Int): Flow> = repository.getMovieReviewPagingFlow(movieId).map { + it.map { movie -> + movie.toDomain() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/application/moviesapp/domain/usecase/TvSeriesReviewUseCase.kt b/app/src/main/java/com/application/moviesapp/domain/usecase/TvSeriesReviewUseCase.kt new file mode 100644 index 0000000..c82091d --- /dev/null +++ b/app/src/main/java/com/application/moviesapp/domain/usecase/TvSeriesReviewUseCase.kt @@ -0,0 +1,24 @@ +package com.application.moviesapp.domain.usecase + +import androidx.paging.PagingData +import androidx.paging.map +import com.application.moviesapp.data.mappers.toDomain +import com.application.moviesapp.data.remote.TvSeriesReviewPagingSource +import com.application.moviesapp.data.repository.MoviesRepository +import com.application.moviesapp.domain.model.MovieReview +import com.application.moviesapp.domain.model.TvSeriesReview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +interface TvSeriesReviewUseCase { + operator fun invoke(seriesId: Int): Flow> +} + +class GetTvSeriesReviewInteractor @Inject constructor(private val repository: MoviesRepository): TvSeriesReviewUseCase { + override fun invoke(seriesId: Int): Flow> = repository.getTvSeriesReviewPagingFlow(seriesId).map { + it.map { tvSeries -> + tvSeries.toDomain() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/application/moviesapp/ui/detail/CommentsScreen.kt b/app/src/main/java/com/application/moviesapp/ui/detail/CommentsScreen.kt index ca0382f..5679221 100644 --- a/app/src/main/java/com/application/moviesapp/ui/detail/CommentsScreen.kt +++ b/app/src/main/java/com/application/moviesapp/ui/detail/CommentsScreen.kt @@ -8,21 +8,29 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.FloatingActionButton import androidx.compose.material.FloatingActionButtonDefaults import androidx.compose.material.FloatingActionButtonElevation +import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Comment import androidx.compose.material.icons.rounded.EmojiEmotions +import androidx.compose.material.icons.rounded.Favorite import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Send import androidx.compose.material3.ButtonColors @@ -30,6 +38,7 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.ElevatedButton import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -37,19 +46,40 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.application.moviesapp.R +import com.application.moviesapp.domain.model.Comment +import com.application.moviesapp.domain.model.CommentRepository +import com.application.moviesapp.domain.model.MovieReview import com.application.moviesapp.ui.theme.MoviesAppTheme +import com.application.moviesapp.ui.utility.toImageUrl +import com.application.moviesapp.ui.utility.toOneDecimal +import kotlinx.coroutines.flow.flowOf @Composable -fun CommentsScreen(modifier: Modifier = Modifier, paddingValues: PaddingValues = PaddingValues(),) { +fun CommentsScreen( + modifier: Modifier = Modifier, + paddingValues: PaddingValues = PaddingValues(), + movieReviewFlow: LazyPagingItems = flowOf(PagingData.empty()).collectAsLazyPagingItems(), +) { val focusRequest = remember { FocusRequester() } @@ -61,7 +91,21 @@ fun CommentsScreen(modifier: Modifier = Modifier, paddingValues: PaddingValues = .fillMaxSize() .padding(top = paddingValues.calculateTopPadding(), start = 0.dp, end = 0.dp, bottom = 0.dp)) { - Card(modifier = modifier.fillMaxSize().wrapContentSize(align = Alignment.BottomCenter)) { + LazyColumn( + modifier = modifier.fillMaxSize().padding(bottom = 100.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(horizontal = 16.dp), + ) { + + items(movieReviewFlow.itemCount) { index -> + CommentsPeopleCompose(review = movieReviewFlow[index] ?: return@items) + } + } + + + Card(modifier = modifier + .fillMaxSize() + .wrapContentSize(align = Alignment.BottomCenter)) { Row(modifier = modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.Top) { OutlinedTextField(value = "", label = { Text("Add comment...", style = MaterialTheme.typography.bodySmall) }, @@ -91,6 +135,61 @@ fun CommentsScreen(modifier: Modifier = Modifier, paddingValues: PaddingValues = } } +@Composable +fun CommentsPeopleCompose(modifier: Modifier = Modifier, comment: Comment = Comment(), review: MovieReview = MovieReview(null, null, null, null, null, null, null)) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row(modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + AsyncImage( + model = ImageRequest.Builder(context = LocalContext.current) + .data(review.authorDetails?.avatarPath?.toImageUrl) + .crossfade(true) + .build(), + error = painterResource(id = R.drawable.ic_broken_image), + placeholder = painterResource(id = R.drawable.ic_image_placeholder), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = modifier + .size(height = 50.dp, width = 50.dp) + .clip(RoundedCornerShape(50)), + ) + + Text(text = review.author ?: "", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) + + Spacer(modifier = modifier.weight(1f)) + + IconButton(onClick = { /*TODO*/ }) { + Icon(imageVector = Icons.Rounded.Comment, contentDescription = null) + } + } + + Text(text = review.content ?: "", style = MaterialTheme.typography.bodyLarge, maxLines = 2, overflow = TextOverflow.Ellipsis) + + Row(modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + IconButton(onClick = { /*TODO*/ }, modifier = modifier.then(modifier.size(24.dp))) { + Icon(imageVector = Icons.Rounded.Favorite, contentDescription = null, tint = MaterialTheme.colorScheme.primary) + } + + Spacer(modifier = modifier.width(4.dp)) + + Text(text = "${comment.likes ?: 0}", style = MaterialTheme.typography.bodyMedium) + + Spacer(modifier = modifier.width(16.dp)) + + Text(text = comment.postedDate ?: "", style = MaterialTheme.typography.bodyMedium) + + Spacer(modifier = modifier.width(16.dp)) + + TextButton(onClick = { /*TODO*/ }) { + Text(text = "Reply") + } + } + } +} + + @Preview(showBackground = true, showSystemUi = true) @Composable private fun CommentsLightThemePreview() { diff --git a/app/src/main/java/com/application/moviesapp/ui/detail/DetailScreen.kt b/app/src/main/java/com/application/moviesapp/ui/detail/DetailScreen.kt index cecfe35..6f19ad3 100755 --- a/app/src/main/java/com/application/moviesapp/ui/detail/DetailScreen.kt +++ b/app/src/main/java/com/application/moviesapp/ui/detail/DetailScreen.kt @@ -90,7 +90,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems import coil.compose.AsyncImage import coil.request.ImageRequest import com.application.moviesapp.R @@ -99,12 +101,15 @@ import com.application.moviesapp.data.local.entity.MovieDownloadEntity import com.application.moviesapp.domain.model.Comment import com.application.moviesapp.domain.model.CommentRepository import com.application.moviesapp.domain.model.MovieNowPlaying +import com.application.moviesapp.domain.model.MovieReview import com.application.moviesapp.domain.model.MovieState import com.application.moviesapp.domain.model.MovieTrailerWithYoutube import com.application.moviesapp.domain.model.MoviesDetail +import com.application.moviesapp.domain.model.MoviesDiscover import com.application.moviesapp.domain.model.Stream import com.application.moviesapp.domain.model.TvSeriesDetail import com.application.moviesapp.domain.model.TvSeriesEpisodes +import com.application.moviesapp.domain.model.TvSeriesReview import com.application.moviesapp.domain.model.TvSeriesTrailerWithYoutube import com.application.moviesapp.ui.play.PlayActivity import com.application.moviesapp.ui.play.Screen @@ -116,6 +121,7 @@ import com.application.moviesapp.ui.viewmodel.DownloadUiState import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.rememberPagerState +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import timber.log.Timber @@ -128,6 +134,8 @@ fun DetailScreen(modifier: Modifier = Modifier, moviesTrailerUiState: Resource> = Resource.Loading, tvSeriesTrailerUiState: Resource> = Resource.Loading, moviesFlow: LazyPagingItems, + movieReviewFlow: LazyPagingItems, + tvSeriesReviewFlow: LazyPagingItems, onBookmarkClicked: (String, Int, Boolean) -> Unit = { _, _, _ -> }, snackbarHostState: SnackbarHostState = SnackbarHostState(), onBookmark: (Int) -> Unit = { _ -> }, @@ -456,7 +464,7 @@ fun DetailScreen(modifier: Modifier = Modifier, } } 2 -> { - CommentsCompose(onCommentsClick = onCommentsClick) + CommentsCompose(onCommentsClick = onCommentsClick, movieReviewFlow = movieReviewFlow) } } } @@ -846,7 +854,7 @@ fun DetailScreen(modifier: Modifier = Modifier, } } 2 -> { - CommentsCompose(onCommentsClick = onCommentsClick) + CommentsCompose(onCommentsClick = onCommentsClick, movieReviewFlow = movieReviewFlow) } } } @@ -1021,7 +1029,11 @@ private fun TvSeriesTrailerCard(modifier: Modifier = Modifier, @Preview @Composable -fun CommentsCompose(modifier: Modifier = Modifier, onCommentsClick: (Int) -> Unit = { _ -> }) { +fun CommentsCompose(modifier: Modifier = Modifier, + onCommentsClick: (Int) -> Unit = { _ -> }, + movieReviewFlow: LazyPagingItems = flowOf(PagingData.empty()).collectAsLazyPagingItems()) { + + Column(modifier = modifier .fillMaxWidth() .padding(horizontal = 16.dp), @@ -1030,7 +1042,7 @@ fun CommentsCompose(modifier: Modifier = Modifier, onCommentsClick: (Int) -> Uni verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { - Text(text = "24.6K Comments", + Text(text = "${movieReviewFlow.itemCount} Comments", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) @@ -1043,65 +1055,56 @@ fun CommentsCompose(modifier: Modifier = Modifier, onCommentsClick: (Int) -> Uni } LazyColumn( - modifier = modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(16.dp) + modifier = modifier + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(horizontal = 16.dp), ) { - items(CommentRepository.getComments().size) { - CommentsPeopleCompose(comment = CommentRepository.getComments()[it]) + + items(movieReviewFlow.itemCount) { index -> + CommentsPeopleCompose(review = movieReviewFlow[index] ?: return@items) } } } } -@Composable -private fun CommentsPeopleCompose(modifier: Modifier = Modifier, comment: Comment = Comment()) { - Column( - modifier = modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Row(modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { - AsyncImage( - model = ImageRequest.Builder(context = LocalContext.current) - .data(comment.imageUrl) - .crossfade(true) - .build(), - error = painterResource(id = R.drawable.ic_broken_image), - placeholder = painterResource(id = R.drawable.ic_image_placeholder), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = modifier - .size(height = 50.dp, width = 50.dp) - .clip(RoundedCornerShape(50)), - ) - Text(text = comment.userName ?: "", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) +@Preview +@Composable +fun CommentsTvSeriesCompose(modifier: Modifier = Modifier, + onCommentsClick: (Int) -> Unit = { _ -> }, + tvSeriesReviewFlow: LazyPagingItems = flowOf(PagingData.empty()).collectAsLazyPagingItems()) { - Spacer(modifier = modifier.weight(1f)) - IconButton(onClick = { /*TODO*/ }) { - Icon(imageVector = Icons.Rounded.Comment, contentDescription = null) - } - } + Column(modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp)) { + Row(modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween) { - Text(text = comment.comment ?: "", style = MaterialTheme.typography.bodyLarge, maxLines = 2, overflow = TextOverflow.Ellipsis) + Text(text = "${tvSeriesReviewFlow.itemCount} Comments", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold) - Row(modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - IconButton(onClick = { /*TODO*/ }, modifier = modifier.then(modifier.size(24.dp))) { - Icon(imageVector = Icons.Rounded.Favorite, contentDescription = null, tint = MaterialTheme.colorScheme.primary) + TextButton(onClick = { onCommentsClick(0) }) { + Text(text = "See all", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold) } + } - Spacer(modifier = modifier.width(4.dp)) - - Text(text = "${comment.likes ?: 0}", style = MaterialTheme.typography.bodyMedium) - - Spacer(modifier = modifier.width(16.dp)) - - Text(text = comment.postedDate ?: "", style = MaterialTheme.typography.bodyMedium) - - Spacer(modifier = modifier.width(16.dp)) + LazyColumn( + modifier = modifier + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(horizontal = 16.dp), + ) { - TextButton(onClick = { /*TODO*/ }) { - Text(text = "Reply") + items(tvSeriesReviewFlow.itemCount) { index -> +// CommentsPeopleCompose(review = tvSeriesReviewFlow[index] ?: return@items) } } } diff --git a/app/src/main/java/com/application/moviesapp/ui/detail/DetailScreenApp.kt b/app/src/main/java/com/application/moviesapp/ui/detail/DetailScreenApp.kt index c75f6c5..ebabe1c 100755 --- a/app/src/main/java/com/application/moviesapp/ui/detail/DetailScreenApp.kt +++ b/app/src/main/java/com/application/moviesapp/ui/detail/DetailScreenApp.kt @@ -3,26 +3,21 @@ package com.application.moviesapp.ui.detail import android.app.Activity import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.Cast import androidx.compose.material.icons.rounded.Close -import androidx.compose.material.icons.rounded.Comment import androidx.compose.material.icons.rounded.MoreVert import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.CircularProgressIndicator @@ -53,19 +48,21 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import com.application.moviesapp.R import com.application.moviesapp.data.common.Resource +import com.application.moviesapp.domain.model.MovieReview import com.application.moviesapp.domain.model.TvSeriesDetail -import com.application.moviesapp.domain.model.TvSeriesEpisodes import com.application.moviesapp.ui.viewmodel.DetailsViewModel import com.application.moviesapp.ui.viewmodel.HomeViewModel +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch @Composable @@ -74,6 +71,8 @@ fun DetailScreenApp(modifier: Modifier = Modifier, viewModel: DetailsViewModel = hiltViewModel(), homeViewModel: HomeViewModel = hiltViewModel()) { + val context = LocalContext.current + val moviesDetailsUiState by viewModel.movieDetailResponse.collectAsState() val tvSeriesDetailsUiState by viewModel.tvSeriesDetailResponse.collectAsState() @@ -87,6 +86,9 @@ fun DetailScreenApp(modifier: Modifier = Modifier, val castDetailUIState by viewModel.castDetailResponse.collectAsState() + val movieReviewFlowState = viewModel.getMovieReviewPagingFlow((context as Activity).intent.getIntExtra(DetailActivity.ID, 0)).collectAsLazyPagingItems() + val tvSeriesReviewFlowState = viewModel.getTvSeriesReviewPagingFlow((context as Activity).intent.getIntExtra(DetailActivity.ID, 0)).collectAsLazyPagingItems() + val snackbarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() @@ -106,7 +108,7 @@ fun DetailScreenApp(modifier: Modifier = Modifier, var selectedImage by remember { mutableStateOf(Pair?>("", emptyList())) } Scaffold( - topBar = { DetailTopAppbar(navController = navController) }, + topBar = { DetailTopAppbar(navController = navController, movieReviewFlow = movieReviewFlowState) }, snackbarHost = { SnackbarHost(snackbarHostState) { androidx.compose.material3.Snackbar( modifier = modifier.padding(8.dp), @@ -127,6 +129,8 @@ fun DetailScreenApp(modifier: Modifier = Modifier, moviesTrailerUiState = movieTrailerUiState, tvSeriesTrailerUiState = tvSeriesTrailerUiState, moviesFlow = moviesFlow, + movieReviewFlow = movieReviewFlowState, + tvSeriesReviewFlow = tvSeriesReviewFlowState, onBookmark = { viewModel.getMovieState(it) }, @@ -173,7 +177,8 @@ fun DetailScreenApp(modifier: Modifier = Modifier, composable(route = DetailScreen.Comments.name) { CommentsScreen( modifier = modifier, - paddingValues = paddingValues + paddingValues = paddingValues, + movieReviewFlow = movieReviewFlowState ) } } @@ -186,7 +191,7 @@ enum class DetailScreen { @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun DetailTopAppbar(modifier: Modifier = Modifier, navController: NavHostController,) { +private fun DetailTopAppbar(modifier: Modifier = Modifier, navController: NavHostController, movieReviewFlow: LazyPagingItems) { val context = LocalContext.current @@ -232,7 +237,7 @@ private fun DetailTopAppbar(modifier: Modifier = Modifier, navController: NavHos DetailScreen.Comments.name -> { TopAppBar( title = { - Text(text = "24.6K Comments", fontWeight = FontWeight.SemiBold) + Text(text = "${movieReviewFlow.itemCount} Comments", fontWeight = FontWeight.SemiBold) }, navigationIcon = { IconButton(onClick = { navController.navigateUp() }) { diff --git a/app/src/main/java/com/application/moviesapp/ui/viewmodel/DetailsViewModel.kt b/app/src/main/java/com/application/moviesapp/ui/viewmodel/DetailsViewModel.kt index 152779f..3305962 100755 --- a/app/src/main/java/com/application/moviesapp/ui/viewmodel/DetailsViewModel.kt +++ b/app/src/main/java/com/application/moviesapp/ui/viewmodel/DetailsViewModel.kt @@ -2,10 +2,10 @@ package com.application.moviesapp.ui.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn import androidx.work.WorkInfo import com.application.moviesapp.data.common.Resource import com.application.moviesapp.data.local.entity.MovieDownloadEntity -import com.application.moviesapp.domain.model.CastDetail import com.application.moviesapp.domain.model.CastDetailWithImages import com.application.moviesapp.domain.model.MovieState import com.application.moviesapp.domain.model.MovieTrailerWithYoutube @@ -16,11 +16,13 @@ import com.application.moviesapp.domain.model.TvSeriesEpisodes import com.application.moviesapp.domain.model.TvSeriesTrailerWithYoutube import com.application.moviesapp.domain.usecase.CastDetailsUseCase import com.application.moviesapp.domain.usecase.MovieDetailsUseCase +import com.application.moviesapp.domain.usecase.MovieReviewUseCase import com.application.moviesapp.domain.usecase.MovieStateUseCase import com.application.moviesapp.domain.usecase.MovieTrailerUseCase import com.application.moviesapp.domain.usecase.MovieUpdateFavouriteInteractor import com.application.moviesapp.domain.usecase.TvSeriesDetailsUseCase import com.application.moviesapp.domain.usecase.TvSeriesEpisodesUseCase +import com.application.moviesapp.domain.usecase.TvSeriesReviewUseCase import com.application.moviesapp.domain.usecase.TvSeriesTrailerUseCase import com.application.moviesapp.domain.usecase.worker.DownloadUseCase import com.application.moviesapp.domain.usecase.worker.VideoInfoUseCase @@ -60,7 +62,9 @@ class DetailsViewModel @Inject constructor(private val useCase: MovieDetailsUseC private val videoInfoUseCase: VideoInfoUseCase, private val downloadUseCase: DownloadUseCase, private val tvSeriesEpisodesUseCase: TvSeriesEpisodesUseCase, - private val castDetailsUseCase: CastDetailsUseCase): ViewModel() { + private val castDetailsUseCase: CastDetailsUseCase, + private val movieReviewUseCase: MovieReviewUseCase, + private val tvSeriesReviewUseCase: TvSeriesReviewUseCase): ViewModel() { private companion object { const val TAG = "DetailsViewModel" @@ -90,6 +94,8 @@ class DetailsViewModel @Inject constructor(private val useCase: MovieDetailsUseC private var _castDetailsResponse = MutableStateFlow>(Resource.Loading) val castDetailResponse: StateFlow> get() = _castDetailsResponse + fun getMovieReviewPagingFlow(movieId: Int) = movieReviewUseCase(movieId).cachedIn(viewModelScope) + fun getTvSeriesReviewPagingFlow(seriesId: Int) = tvSeriesReviewUseCase(seriesId).cachedIn(viewModelScope) fun getMovieDetail(movieId: Int) = viewModelScope.launch { _movieDetailResponse.value = useCase(movieId)