refactor account tabs to use paging

This commit is contained in:
Owen LeJeune
2023-06-21 16:28:07 -04:00
parent 17e3ad32f0
commit 5a9e71d0a6
40 changed files with 394 additions and 976 deletions

View File

@@ -0,0 +1,64 @@
package com.owenlejeune.tvtime.api.tmdb.api
import android.content.Context
import android.widget.Toast
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.cachedIn
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.ui.viewmodel.ViewModelConstants
import kotlinx.coroutines.flow.Flow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Response
fun <T: Any, S> ViewModel.createPagingFlow(
fetcher: suspend (Int) -> Response<S>,
processor: (S) -> List<T>
): Flow<PagingData<T>> {
return Pager(PagingConfig(pageSize = ViewModelConstants.PAGING_SIZE)) {
BasePagingSource(
fetcher = fetcher,
processor = processor
)
}.flow.cachedIn(viewModelScope)
}
class BasePagingSource<T: Any, S>(
private val fetcher: suspend (Int) -> Response<S>,
private val processor: (S) -> List<T>
): PagingSource<Int, T>(), KoinComponent {
private val context: Context by inject()
override fun getRefreshKey(state: PagingState<Int, T>): Int? {
return state.anchorPosition
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
return try {
val page = params.key ?: 1
val response = fetcher(page)
if (response.isSuccessful) {
val responseBody = response.body()
val results = responseBody?.let(processor) ?: emptyList()
LoadResult.Page(
data = results,
prevKey = if (page == 1) { null } else { page - 1},
nextKey = if (results.isEmpty()) { null } else { page + 1}
)
} else {
Toast.makeText(context, context.getString(R.string.no_result_found), Toast.LENGTH_SHORT).show()
LoadResult.Invalid()
}
} catch (e: Exception) {
return LoadResult.Error(e)
}
}
}

View File

@@ -9,54 +9,12 @@ interface AccountApi {
@GET("account")
suspend fun getAccountDetails(): Response<AccountDetails>
@GET("account/{id}/favorite/movies")
suspend fun getFavoriteMovies(
@Path("id") id: Int,
@Query("page") page: Int
): Response<FavoriteMediaResponse<FavoriteMovie>>
@GET("account/{id}/favorite/tv")
suspend fun getFavoriteTvShows(
@Path("id") id: Int,
@Query("page") page: Int
): Response<FavoriteMediaResponse<FavoriteTvSeries>>
@POST("account/{id}/favorite")
suspend fun markAsFavorite(
@Path("id") id: Int,
@Body body: MarkAsFavoriteBody
): Response<StatusResponse>
@GET("account/{id}/rated/movies")
suspend fun getRatedMovies(
@Path("id") id: Int,
@Query("page") page: Int
): Response<RatedMediaResponse<RatedMovie>>
@GET("account/{id}/rated/tv")
suspend fun getRatedTvShows(
@Path("id") id: Int,
@Query("page") page: Int
): Response<RatedMediaResponse<RatedTv>>
@GET("account/{id}/rated/tv/episodes")
suspend fun getRatedTvEpisodes(
@Path("id") id: Int,
@Query("page") page: Int
): Response<RatedMediaResponse<RatedEpisode>>
@GET("account/{id}/watchlist/movies")
suspend fun getMovieWatchlist(
@Path("id") id: Int,
@Query("page") page: Int
): Response<WatchlistResponse<WatchlistMovie>>
@GET("account/{id}/watchlist/tv")
suspend fun getTvWatchlist(
@Path("id") id: Int,
@Query("page") page: Int
): Response<WatchlistResponse<WatchlistTvSeries>>
@POST("account/{id}/watchlist")
suspend fun addToWatchlist(
@Path("id") id: Int,

View File

@@ -2,18 +2,8 @@ package com.owenlejeune.tvtime.api.tmdb.api.v3
import android.util.Log
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AccountDetails
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.FavoriteMediaResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.FavoriteMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.FavoriteTvSeries
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MarkAsFavoriteBody
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RatedEpisode
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RatedMediaResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RatedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RatedTv
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchlistBody
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchlistMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchlistResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchlistTvSeries
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Response
@@ -46,33 +36,4 @@ class AccountService: KoinComponent {
}
}
// TODO - replace these with account states API calls
suspend fun getFavoriteMovies(accountId: Int, page: Int = 1): Response<FavoriteMediaResponse<FavoriteMovie>> {
return accountService.getFavoriteMovies(accountId, page)
}
suspend fun getFavoriteTvShows(accountId: Int, page: Int = 1): Response<FavoriteMediaResponse<FavoriteTvSeries>> {
return accountService.getFavoriteTvShows(accountId, page)
}
suspend fun getRatedMovies(accountId: Int, page: Int = 1): Response<RatedMediaResponse<RatedMovie>> {
return accountService.getRatedMovies(accountId, page)
}
suspend fun getRatedTvShows(accountId: Int, page: Int = 1): Response<RatedMediaResponse<RatedTv>> {
return accountService.getRatedTvShows(accountId, page)
}
suspend fun getRatedTvEpisodes(accountId: Int, page: Int = 1): Response<RatedMediaResponse<RatedEpisode>> {
return accountService.getRatedTvEpisodes(accountId, page)
}
suspend fun getMovieWatchlist(accountId: Int, page: Int = 1): Response<WatchlistResponse<WatchlistMovie>> {
return accountService.getMovieWatchlist(accountId, page)
}
suspend fun getTvWatchlist(accountId: Int, page: Int = 1): Response<WatchlistResponse<WatchlistTvSeries>> {
return accountService.getTvWatchlist(accountId, page)
}
}

View File

@@ -106,7 +106,6 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
} else {
Log.d(TAG, "Issue getting account states: $response")
}
// movieService.getAccountStates(id) storedIn { accountStates[id] = it }
}
suspend fun getReleaseDates(id: Int) {
@@ -118,7 +117,6 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
val response = movieService.postMovieRatingAsUser(id, session.sessionId, ratingBody)
if (response.isSuccessful) {
Log.d(TAG, "Successfully rated")
SessionManager.currentSession.value?.refresh(SessionManager.Session.Changed.Rated)
getAccountStates(id)
} else {
Log.w(TAG, "Issue posting rating")
@@ -130,7 +128,6 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
val response = movieService.deleteMovieReviewAsUser(id, session.sessionId)
if (response.isSuccessful) {
Log.d(TAG, "Successfully deleted rated")
SessionManager.currentSession.value?.refresh(SessionManager.Session.Changed.Rated)
getAccountStates(id)
} else {
Log.w(TAG, "Issue deleting rating")
@@ -158,41 +155,3 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
return movieService.getUpcomingMovies(page)
}
}
class SimilarMoviesSource(private val movieId: Int): PagingSource<Int, TmdbItem>(), KoinComponent {
private val service: MoviesService by inject()
override fun getRefreshKey(state: PagingState<Int, TmdbItem>): Int? {
return state.anchorPosition
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TmdbItem> {
return try {
val nextPage = params.key ?: 1
val response = service.getSimilar(movieId, nextPage)
if (response.isSuccessful) {
val responseBody = response.body()
val result = responseBody?.results ?: emptyList()
LoadResult.Page(
data = result,
prevKey = if (nextPage == 1) {
null
} else {
nextPage - 1
},
nextKey = if (result.isEmpty()) {
null
} else {
responseBody?.page?.plus(1) ?: (nextPage + 1)
}
)
} else {
LoadResult.Invalid()
}
} catch (e: Exception) {
return LoadResult.Error(e)
}
}
}

View File

@@ -58,41 +58,3 @@ class SearchService: KoinComponent {
}
typealias SearchResultProvider<T> = suspend (Int) -> Response<SearchResult<T>>
class SearchPagingSource<T: Searchable>(
private val provideResults: SearchResultProvider<T>
): PagingSource<Int, T>(), KoinComponent {
override fun getRefreshKey(state: PagingState<Int, T>): Int? {
return state.anchorPosition
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
return try {
val nextPage = params.key ?: 1
val response = provideResults(nextPage)
if (response.isSuccessful) {
val responseBody = response.body()
val result = responseBody?.results ?: emptyList()
LoadResult.Page(
data = result,
prevKey = if (nextPage == 1) {
null
} else {
nextPage - 1
},
nextKey = if (result.isEmpty()) {
null
} else {
responseBody?.page?.plus(1) ?: (nextPage + 1)
}
)
} else {
LoadResult.Invalid()
}
} catch (e: Exception) {
return LoadResult.Error(e)
}
}
}

View File

@@ -122,7 +122,6 @@ class TvService: KoinComponent, DetailService, HomePageService {
val response = service.postTvRatingAsUser(id, session.sessionId, ratingBody)
if (response.isSuccessful) {
Log.d(TAG, "Successfully posted rating")
SessionManager.currentSession.value?.refresh(SessionManager.Session.Changed.Rated)
} else {
Log.w(TAG, "Issue posting rating")
}
@@ -133,13 +132,11 @@ class TvService: KoinComponent, DetailService, HomePageService {
val response = service.deleteTvReviewAsUser(id, session.sessionId)
if (response.isSuccessful) {
Log.d(TAG, "Successfully deleted rating")
SessionManager.currentSession.value?.refresh(SessionManager.Session.Changed.Rated)
} else {
Log.w(TAG, "Issue deleting rating")
}
}
//todo - turn this into paging
override suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse> {
return service.getSimilarTvShows(id, page)
}
@@ -161,41 +158,3 @@ class TvService: KoinComponent, DetailService, HomePageService {
}
}
class SimilarTvSource(private val tvId: Int): PagingSource<Int, TmdbItem>(), KoinComponent {
private val service: TvService by inject()
override fun getRefreshKey(state: PagingState<Int, TmdbItem>): Int? {
return state.anchorPosition
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TmdbItem> {
return try {
val nextPage = params.key ?: 1
val response = service.getSimilar(tvId, nextPage)
if (response.isSuccessful) {
val responseBody = response.body()
val result = responseBody?.results ?: emptyList()
LoadResult.Page(
data = result,
prevKey = if (nextPage == 1) {
null
} else {
nextPage - 1
},
nextKey = if (result.isEmpty()) {
null
} else {
responseBody?.page?.plus(1) ?: (nextPage + 1)
}
)
} else {
LoadResult.Invalid()
}
} catch (e: Exception) {
return LoadResult.Error(e)
}
}
}

View File

@@ -1,62 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.HomePageService
import com.owenlejeune.tvtime.ui.navigation.MediaFetchFun
import org.koin.core.Koin
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Response
class HomePagePagingSource(
private val service: HomePageService,
private val mediaFetch: MediaFetchFun,
private val tag: String
): PagingSource<Int, TmdbItem>(), KoinComponent {
companion object {
val TAG = HomePagePagingSource::class.java.simpleName
}
private val context: Context by inject()
override fun getRefreshKey(state: PagingState<Int, TmdbItem>): Int? {
return state.anchorPosition
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TmdbItem> {
return try {
val nextPage = params.key ?: 1
Log.d(TAG, "Loading $tag page $nextPage")
val mediaResponse = mediaFetch.invoke(service, nextPage)
if (mediaResponse.isSuccessful) {
val responseBody = mediaResponse.body()
val results = responseBody?.results ?: emptyList()
LoadResult.Page(
data = results,
prevKey = if (nextPage == 1) {
null
} else {
nextPage - 1
},
nextKey = if (results.isEmpty() || responseBody == null) {
null
} else {
responseBody.page + 1
}
)
} else {
Toast.makeText(context, context.getString(R.string.no_result_found), Toast.LENGTH_SHORT).show()
LoadResult.Invalid()
}
} catch (e: Exception) {
return LoadResult.Error(e)
}
}
}

View File

@@ -1,41 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.owenlejeune.tvtime.api.tmdb.api.v3.PeopleApi
import com.owenlejeune.tvtime.api.tmdb.api.v3.PeopleService
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class HomePagePeoplePagingSource: PagingSource<Int, HomePagePerson>(), KoinComponent {
private val service: PeopleService by inject()
override fun getRefreshKey(state: PagingState<Int, HomePagePerson>): Int? {
return state.anchorPosition
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, HomePagePerson> {
return try {
val nextPage = params.key ?: 1
val peopleResponse = service.getPopular(page = nextPage)
if (peopleResponse.isSuccessful) {
val responseBody = peopleResponse.body()
val results = responseBody?.results ?: emptyList()
LoadResult.Page(
data = results,
prevKey = if (nextPage == 1) null else nextPage - 1,
nextKey = if (results.isEmpty() || responseBody == null) null else responseBody.page + 1
)
} else {
LoadResult.Invalid()
}
} catch (e: Exception) {
return LoadResult.Error(e)
}
}
}

View File

@@ -1,19 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
class RatedEpisode(
type: RatedType,
id: Int,
overview: String,
name: String,
voteAverage: Float,
voteCount: Int,
rating: Float,
releaseDate: String,
@SerializedName("episode_number") val episodeNumber: Int,
@SerializedName("production_code") val productionCode: String?,
@SerializedName("season_number") val seasonNumber: Int,
@SerializedName("show_id") val showId: Int,
@SerializedName("still_path") val stillPath: String?,
): RatedMedia(RatedType.EPISODE, id, overview, name, voteAverage, voteCount, rating, releaseDate)

View File

@@ -1,20 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
abstract class RatedMedia(
var type: RatedType,
@SerializedName("id") val id: Int,
@SerializedName("overview") val overview: String,
@SerializedName("name", alternate = ["title"]) val name: String,
@SerializedName("vote_average") val voteAverage: Float,
@SerializedName("vote_count") val voteCount: Int,
@SerializedName("rating") val rating: Float,
@SerializedName("release_date", alternate = ["first_air_date", "air_date"]) val releaseDate: String
) {
enum class RatedType {
MOVIE,
SERIES,
EPISODE
}
}

View File

@@ -1,10 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
class RatedMediaResponse<T: RatedMedia>(
@SerializedName("page") val page: Int,
@SerializedName("results") val results: List<T>,
@SerializedName("total_pages") val totalPages: Int,
@SerializedName("total_results") val totalResults: Int
)

View File

@@ -1,20 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
abstract class RatedTopLevelMedia(
type: RatedType,
id: Int,
overview: String,
name: String,
voteAverage: Float,
voteCount: Int,
rating: Float,
releaseDate: String,
@SerializedName("backdrop_path") val backdropPath: String?,
@SerializedName("genre_ids") val genreIds: List<Int>,
@SerializedName("original_language") val originalLanguage: String,
@SerializedName("original_name", alternate = ["original_title"]) val originalName: String,
@SerializedName("poster_path") val posterPath: String?,
@SerializedName("popularity") val popularity: Float,
): RatedMedia(type, id, overview, name, voteAverage, voteCount, rating, releaseDate)

View File

@@ -1,46 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.owenlejeune.tvtime.api.tmdb.api.v4.AccountV4Service
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.utils.types.MediaViewType
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class RecommendedMediaPagingSource(
private val mediaType: MediaViewType
): PagingSource<Int, TmdbItem>(), KoinComponent {
private val preferences: AppPreferences by inject()
private val service: AccountV4Service by inject()
override fun getRefreshKey(state: PagingState<Int, TmdbItem>): Int? {
return state.anchorPosition
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TmdbItem> {
return try {
val nextPage = params.key ?: 1
val mediaResponse = if (mediaType == MediaViewType.MOVIE) {
service.getRecommendedMovies(preferences.authorizedSessionValues?.accountId ?: "", nextPage)
} else {
service.getRecommendedTvSeries(preferences.authorizedSessionValues?.accountId ?: "", nextPage)
}
if (mediaResponse.isSuccessful) {
val responseBody = mediaResponse.body()
val results = responseBody?.results ?: emptyList()
LoadResult.Page(
data = results,
prevKey = if (nextPage == 1) null else nextPage - 1,
nextKey = if (results.isEmpty() || responseBody == null) null else responseBody.page + 1
)
} else {
LoadResult.Invalid()
}
} catch (e: Exception) {
return LoadResult.Error(e)
}
}
}

View File

@@ -1,10 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
class WatchlistResponse<T: WatchlistMedia>(
@SerializedName("page") val page: Int,
@SerializedName("results") val results: List<T>,
@SerializedName("total_pages") val totalPages: Int,
@SerializedName("total_results") val totalResults: Int
)

View File

@@ -2,12 +2,14 @@ package com.owenlejeune.tvtime.api.tmdb.api.v4
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.FavoriteMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.FavoriteTvSeries
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RecommendedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RecommendedTv
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchlistMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchlistTvSeries
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AccountList
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AccountResponse
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.V4RatedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.V4RatedTv
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RatedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RatedTv
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RecommendedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RecommendedTv
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path
@@ -24,23 +26,17 @@ interface AccountV4Api {
@GET("account/{account_id}/tv/favorites")
suspend fun getFavoriteTvShows(@Path("account_id") accountId: String, @Query("page") page: Int = 1): Response<AccountResponse<FavoriteTvSeries>>
@GET("account/{account_id}/movie/recommendations")
suspend fun getMovieRecommendations(@Path("account_id") accountId: String, @Query("page") page: Int = 1): Response<AccountResponse<FavoriteMovie>>
@GET("account/{account_id}/tv/recommendations")
suspend fun getTvShowRecommendations(@Path("account_id") accountId: String, @Query("page") page: Int = 1): Response<AccountResponse<FavoriteTvSeries>>
@GET("account/{account_id}/movie/watchlist")
suspend fun getMovieWatchlist(@Path("account_id") accountId: String, @Query("page") page: Int = 1): Response<AccountResponse<FavoriteMovie>>
suspend fun getMovieWatchlist(@Path("account_id") accountId: String, @Query("page") page: Int = 1): Response<AccountResponse<WatchlistMovie>>
@GET("account/{account_id}/tv/watchlist")
suspend fun getTvShowWatchlist(@Path("account_id") accountId: String, @Query("page") page: Int = 1): Response<AccountResponse<FavoriteTvSeries>>
suspend fun getTvShowWatchlist(@Path("account_id") accountId: String, @Query("page") page: Int = 1): Response<AccountResponse<WatchlistTvSeries>>
@GET("account/{account_id}/movie/rated")
suspend fun getRatedMovies(@Path("account_id") accountId: String, @Query("page") page: Int = 1): Response<AccountResponse<V4RatedMovie>>
suspend fun getRatedMovies(@Path("account_id") accountId: String, @Query("page") page: Int = 1): Response<AccountResponse<RatedMovie>>
@GET("account/{account_id}/tv/rated")
suspend fun getRatedTvShows(@Path("account_id") accountId: String, @Query("page") page: Int = 1): Response<AccountResponse<V4RatedTv>>
suspend fun getRatedTvShows(@Path("account_id") accountId: String, @Query("page") page: Int = 1): Response<AccountResponse<RatedTv>>
@GET("account/{account_id}/movie/recommendations")
suspend fun getRecommendedMovies(@Path("account_id") accountId: String, @Query("page") page: Int = 1): Response<AccountResponse<RecommendedMovie>>

View File

@@ -3,55 +3,49 @@ package com.owenlejeune.tvtime.api.tmdb.api.v4
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.FavoriteMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.FavoriteTvSeries
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RecommendedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RecommendedTv
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchlistMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchlistTvSeries
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AccountList
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AccountResponse
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.V4RatedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.V4RatedTv
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RatedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RatedTv
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RecommendedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RecommendedTv
import retrofit2.Response
class AccountV4Service {
private val service by lazy { TmdbClient().createV4AccountService() }
suspend fun getLists(accountId: String, page: Int = 1): Response<AccountResponse<AccountList>> {
suspend fun getLists(accountId: String, page: Int): Response<AccountResponse<AccountList>> {
return service.getLists(accountId, page)
}
suspend fun getFavoriteMovies(accountId: String, page: Int = 1): Response<AccountResponse<FavoriteMovie>> {
suspend fun getFavoriteMovies(accountId: String, page: Int): Response<AccountResponse<FavoriteMovie>> {
return service.getFavoriteMovies(accountId, page)
}
suspend fun getFavoriteTvShows(accountId: String, page: Int = 1): Response<AccountResponse<FavoriteTvSeries>> {
suspend fun getFavoriteTvShows(accountId: String, page: Int): Response<AccountResponse<FavoriteTvSeries>> {
return service.getFavoriteTvShows(accountId, page)
}
suspend fun getMovieRecommendations(accountId: String, page: Int = 1): Response<AccountResponse<FavoriteMovie>> {
return service.getMovieRecommendations(accountId, page)
}
suspend fun getTvShowRecommendations(accountId: String, page: Int = 1): Response<AccountResponse<FavoriteTvSeries>> {
return service.getTvShowRecommendations(accountId, page)
}
suspend fun getMovieWatchlist(accountId: String, page: Int = 1): Response<AccountResponse<FavoriteMovie>> {
suspend fun getMovieWatchlist(accountId: String, page: Int): Response<AccountResponse<WatchlistMovie>> {
return service.getMovieWatchlist(accountId, page)
}
suspend fun getTvShowWatchlist(accountId: String, page: Int = 1): Response<AccountResponse<FavoriteTvSeries>> {
suspend fun getTvShowWatchlist(accountId: String, page: Int): Response<AccountResponse<WatchlistTvSeries>> {
return service.getTvShowWatchlist(accountId, page)
}
suspend fun getRatedMovies(accountId: String, page: Int = 1): Response<AccountResponse<V4RatedMovie>> {
suspend fun getRatedMovies(accountId: String, page: Int): Response<AccountResponse<RatedMovie>> {
return service.getRatedMovies(accountId, page)
}
suspend fun getRatedTvShows(accountId: String, page: Int = 1): Response<AccountResponse<V4RatedTv>> {
suspend fun getRatedTvShows(accountId: String, page: Int): Response<AccountResponse<RatedTv>> {
return service.getRatedTvShows(accountId, page)
}
suspend fun getRecommendedMovies(accountId: String, page: Int = 1): Response<AccountResponse<RecommendedMovie>> {
suspend fun getRecommendedMovies(accountId: String, page: Int ): Response<AccountResponse<RecommendedMovie>> {
return service.getRecommendedMovies(accountId, page)
}

View File

@@ -2,14 +2,14 @@ package com.owenlejeune.tvtime.api.tmdb.api.v4
import android.util.Log
import androidx.compose.runtime.mutableStateMapOf
import com.owenlejeune.tvtime.BuildConfig
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.*
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AddToListBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.CreateListBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.DeleteListItemsBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.ListUpdateBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.MediaList
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.UpdateListItemBody
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Response
class ListV4Service: KoinComponent {
@@ -47,7 +47,6 @@ class ListV4Service: KoinComponent {
suspend fun deleteListItems(listId: Int, body: DeleteListItemsBody) {
val response = service.deleteListItems(listId, body)
if (response.isSuccessful) {
SessionManager.currentSession.value?.refresh(SessionManager.Session.Changed.List)
getList(listId)
}
}

View File

@@ -2,14 +2,14 @@ package com.owenlejeune.tvtime.api.tmdb.api.v4.model
import com.google.gson.annotations.SerializedName
abstract class V4RatedMedia(
abstract class RatedMedia(
var type: RatedType,
@SerializedName("id") val id: Int,
@SerializedName("overview") val overview: String,
@SerializedName("name", alternate = ["title"]) val name: String,
@SerializedName("vote_average") val voteAverage: Float,
@SerializedName("vote_count") val voteCount: Int,
@SerializedName("rating") val rating: AccountRating,
@SerializedName("account_rating") val rating: AccountRating,
@SerializedName("release_date", alternate = ["first_air_date", "air_date"]) val releaseDate: String,
@SerializedName("backdrop_path") val backdropPath: String?,
@SerializedName("genre_ids") val genreIds: List<Int>,
@@ -25,7 +25,7 @@ abstract class V4RatedMedia(
}
inner class AccountRating(
@SerializedName("value") val rating: Int,
@SerializedName("value") val value: Float,
@SerializedName("created_at") val createdAt: String
)
}

View File

@@ -1,4 +1,4 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
package com.owenlejeune.tvtime.api.tmdb.api.v4.model
import com.google.gson.annotations.SerializedName
@@ -8,7 +8,7 @@ class RatedMovie(
name: String,
voteAverage: Float,
voteCount: Int,
rating: Float,
rating: AccountRating,
backdropPath: String?,
genreIds: List<Int>,
originalLanguage: String,
@@ -17,8 +17,8 @@ class RatedMovie(
popularity: Float,
releaseDate: String,
@SerializedName("adult") val isAdult: Boolean,
@SerializedName("video") val video: Boolean,
): RatedTopLevelMedia(
@SerializedName("video") val video: Boolean
): RatedMedia(
RatedType.MOVIE, id, overview, name, voteAverage, voteCount, rating, releaseDate,
backdropPath, genreIds, originalLanguage, originalName, posterPath, popularity
)

View File

@@ -1,4 +1,4 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
package com.owenlejeune.tvtime.api.tmdb.api.v4.model
import com.google.gson.annotations.SerializedName
@@ -8,7 +8,7 @@ class RatedTv(
name: String,
voteAverage: Float,
voteCount: Int,
rating: Float,
rating: AccountRating,
backdropPath: String?,
genreIds: List<Int>,
originalLanguage: String,
@@ -17,7 +17,7 @@ class RatedTv(
popularity: Float,
releaseDate: String,
@SerializedName("origin_country") val originCountry: List<String>,
): RatedTopLevelMedia(
): RatedMedia(
RatedType.SERIES, id, overview, name, voteAverage, voteCount, rating, releaseDate,
backdropPath, genreIds, originalLanguage, originalName, posterPath, popularity
)

View File

@@ -1,24 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.api.v4.model
import com.google.gson.annotations.SerializedName
class V4RatedMovie(
id: Int,
overview: String,
name: String,
voteAverage: Float,
voteCount: Int,
rating: AccountRating,
backdropPath: String?,
genreIds: List<Int>,
originalLanguage: String,
originalName: String,
posterPath: String?,
popularity: Float,
releaseDate: String,
@SerializedName("adult") val isAdult: Boolean,
@SerializedName("video") val video: Boolean
): V4RatedMedia(
RatedType.MOVIE, id, overview, name, voteAverage, voteCount, rating, releaseDate,
backdropPath, genreIds, originalLanguage, originalName, posterPath, popularity
)

View File

@@ -1,23 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.api.v4.model
import com.google.gson.annotations.SerializedName
class V4RatedTv(
id: Int,
overview: String,
name: String,
voteAverage: Float,
voteCount: Int,
rating: AccountRating,
backdropPath: String?,
genreIds: List<Int>,
originalLanguage: String,
originalName: String,
posterPath: String?,
popularity: Float,
releaseDate: String,
@SerializedName("origin_country") val originCountry: List<String>,
): V4RatedMedia(
RatedType.SERIES, id, overview, name, voteAverage, voteCount, rating, releaseDate,
backdropPath, genreIds, originalLanguage, originalName, posterPath, popularity
)

View File

@@ -74,9 +74,7 @@ fun ActionsView(
}
Row(
modifier = modifier
.wrapContentSize()
.padding(horizontal = 16.dp),
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (actions.contains(Actions.RATE)) {

View File

@@ -1,7 +1,6 @@
package com.owenlejeune.tvtime.ui.components
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
@@ -34,7 +33,13 @@ fun RatingDialog(
AlertDialog(
modifier = Modifier.wrapContentHeight(),
onDismissRequest = { showDialog.value = false },
title = { Text(text = stringResource(R.string.rating_dialog_title)) },
title = {
if (rating > 0f) {
Text(text = stringResource(id = R.string.my_rating_dialog_title))
} else {
Text(text = stringResource(R.string.rating_dialog_title))
}
},
confirmButton = {
Button(
modifier = Modifier.height(40.dp),

View File

@@ -5,17 +5,14 @@ import androidx.navigation.NavHostController
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.FavoriteMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.FavoriteTvSeries
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RatedEpisode
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RatedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RatedTv
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchlistMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchlistTvSeries
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AccountList
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RatedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RatedTv
import com.owenlejeune.tvtime.ui.screens.AccountTabContent
import com.owenlejeune.tvtime.utils.types.MediaViewType
import com.owenlejeune.tvtime.ui.screens.RecommendedAccountTabContent
import com.owenlejeune.tvtime.utils.ResourceUtils
import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.utils.types.MediaViewType
import com.owenlejeune.tvtime.utils.types.TabNavItem
import org.koin.core.component.inject
import kotlin.reflect.KClass
@@ -26,8 +23,8 @@ sealed class AccountTabNavItem(
noContentStringRes: Int,
val mediaType: MediaViewType,
val screen: AccountNavComposableFun,
val listFetchFun: ListFetchFun,
val listType: KClass<*>,
val type: AccountTabType,
val contentType: KClass<*>,
val ordinal: Int
): TabNavItem(route) {
private val resourceUtils: ResourceUtils by inject()
@@ -35,10 +32,18 @@ sealed class AccountTabNavItem(
override val name = resourceUtils.getString(stringRes)
val noContentText = resourceUtils.getString(noContentStringRes)
enum class AccountTabType {
RATED,
FAVORITE,
WATCHLIST,
LIST,
RECOMMENDED
}
companion object {
val AuthorizedItems
get() = listOf(
RatedMovies, RatedTvShows, RatedTvEpisodes, FavoriteMovies, FavoriteTvShows,
RatedMovies, RatedTvShows, /*RatedTvEpisodes, */FavoriteMovies, FavoriteTvShows,
MovieWatchlist, TvWatchlist, UserLists, RecommendedMovies, RecommendedTv
).filter { it.ordinal > -1 }.sortedBy { it.ordinal }
}
@@ -49,7 +54,7 @@ sealed class AccountTabNavItem(
R.string.no_rated_movies,
MediaViewType.MOVIE,
screenContent,
{ SessionManager.currentSession.value?.ratedMovies ?: emptyList() },
AccountTabType.RATED,
RatedMovie::class,
0
)
@@ -59,27 +64,27 @@ sealed class AccountTabNavItem(
R.string.no_rated_tv,
MediaViewType.TV,
screenContent,
{ SessionManager.currentSession.value?.ratedTvShows ?: emptyList() },
AccountTabType.RATED,
RatedTv::class,
1
)
object RatedTvEpisodes: AccountTabNavItem(
R.string.nav_rated_episodes_title,
"rated_episodes_route",
R.string.no_rated_episodes,
MediaViewType.EPISODE,
screenContent,
{ SessionManager.currentSession.value?.ratedTvEpisodes ?: emptyList() },
RatedEpisode::class,
-1 //2
)
// object RatedTvEpisodes: AccountTabNavItem(
// R.string.nav_rated_episodes_title,
// "rated_episodes_route",
// R.string.no_rated_episodes,
// MediaViewType.EPISODE,
// screenContent,
// AccountTabType.RATED,
// RatedEpisode::class,
// -1 //2
// )
object FavoriteMovies: AccountTabNavItem(
R.string.nav_favorite_movies_title,
"favorite_movies_route",
R.string.no_favorite_movies,
MediaViewType.MOVIE,
screenContent,
{ SessionManager.currentSession.value?.favoriteMovies ?: emptyList() },
AccountTabType.FAVORITE,
FavoriteMovie::class,
3
)
@@ -89,7 +94,7 @@ sealed class AccountTabNavItem(
R.string.no_favorite_tv,
MediaViewType.TV,
screenContent,
{ SessionManager.currentSession.value?.favoriteTvShows ?: emptyList() },
AccountTabType.FAVORITE,
FavoriteTvSeries::class,
4
)
@@ -99,7 +104,7 @@ sealed class AccountTabNavItem(
R.string.no_watchlist_movies,
MediaViewType.MOVIE,
screenContent,
{ SessionManager.currentSession.value?.movieWatchlist ?: emptyList() },
AccountTabType.WATCHLIST,
WatchlistMovie::class,
5
)
@@ -109,7 +114,7 @@ sealed class AccountTabNavItem(
R.string.no_watchlist_tv,
MediaViewType.TV,
screenContent,
{ SessionManager.currentSession.value?.tvWatchlist ?: emptyList() },
AccountTabType.WATCHLIST,
WatchlistTvSeries::class,
6
)
@@ -120,7 +125,7 @@ sealed class AccountTabNavItem(
R.string.no_lists,
MediaViewType.LIST,
screenContent,
{ SessionManager.currentSession.value?.accountLists ?: emptyList() },
AccountTabType.LIST,
AccountList::class,
7
)
@@ -130,8 +135,8 @@ sealed class AccountTabNavItem(
"recommended_movies_route",
R.string.no_recommended_movies,
MediaViewType.MOVIE,
recommendedScreenContent,
{ emptyList() },
screenContent,
AccountTabType.RECOMMENDED,
AccountList::class,
8
)
@@ -141,31 +146,21 @@ sealed class AccountTabNavItem(
"recommended_tv_route",
R.string.no_recommended_tv,
MediaViewType.TV,
recommendedScreenContent,
{ emptyList() },
screenContent,
AccountTabType.RECOMMENDED,
AccountList::class,
9
)
}
private val screenContent: AccountNavComposableFun = { noContentText, appNavController, mediaViewType, listFetchFun, clazz ->
private val screenContent: AccountNavComposableFun = { noContentText, appNavController, mediaViewType, atType, clazz ->
AccountTabContent(
noContentText = noContentText,
appNavController = appNavController,
mediaViewType = mediaViewType,
listFetchFun = listFetchFun,
accountTabType = atType,
clazz = clazz
)
}
private val recommendedScreenContent: AccountNavComposableFun = { noContentText, appNavController, mediaViewType, _, _ ->
RecommendedAccountTabContent(
noContentText = noContentText,
appNavController = appNavController,
mediaViewType = mediaViewType,
)
}
typealias ListFetchFun = () -> List<Any>
typealias AccountNavComposableFun = @Composable (String, NavHostController, MediaViewType, ListFetchFun, KClass<*>) -> Unit
typealias AccountNavComposableFun = @Composable (String, NavHostController, MediaViewType, AccountTabNavItem.AccountTabType, KClass<*>) -> Unit

View File

@@ -5,11 +5,11 @@ import androidx.navigation.NavHostController
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.HomePageService
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePageResponse
import com.owenlejeune.tvtime.utils.types.MediaViewType
import com.owenlejeune.tvtime.ui.viewmodel.MediaTabViewModel
import com.owenlejeune.tvtime.ui.screens.tabs.MediaTabContent
import com.owenlejeune.tvtime.utils.ResourceUtils
import com.owenlejeune.tvtime.utils.types.MediaViewType
import com.owenlejeune.tvtime.utils.types.TabNavItem
import com.owenlejeune.tvtime.utils.types.ViewableMediaTypeException
import org.koin.core.component.inject
import retrofit2.Response
@@ -17,17 +17,31 @@ sealed class MediaTabNavItem(
stringRes: Int,
route: String,
val screen: MediaNavComposableFun,
val movieViewModel: MediaTabViewModel?,
val tvViewModel: MediaTabViewModel?
val type: Type
): TabNavItem(route) {
private val resourceUtils: ResourceUtils by inject()
override val name = resourceUtils.getString(stringRes)
enum class Type {
POPULAR,
NOW_PLAYING,
UPCOMING,
TOP_RATED
}
companion object {
val MovieItems = listOf(NowPlaying, Popular, Upcoming, TopRated)
val TvItems = listOf(OnTheAir, Popular, AiringToday, TopRated)
fun itemsForType(type: MediaViewType): List<MediaTabNavItem> {
return when (type) {
MediaViewType.MOVIE -> MovieItems
MediaViewType.TV -> TvItems
else -> throw ViewableMediaTypeException(type)
}
}
private val Items = listOf(NowPlaying, Popular, TopRated, Upcoming, AiringToday, OnTheAir)
fun getByRoute(route: String?): MediaTabNavItem? {
@@ -39,43 +53,37 @@ sealed class MediaTabNavItem(
stringRes = R.string.nav_popular_title,
route = "popular_route",
screen = screenContent,
movieViewModel = MediaTabViewModel.PopularMoviesVM,
tvViewModel = MediaTabViewModel.PopularTvVM
type = Type.POPULAR
)
object TopRated: MediaTabNavItem(
stringRes = R.string.nav_top_rated_title,
route = "top_rated_route",
screen = screenContent,
movieViewModel = MediaTabViewModel.TopRatedMoviesVM,
tvViewModel = MediaTabViewModel.TopRatedTvVM
type = Type.TOP_RATED
)
object NowPlaying: MediaTabNavItem(
stringRes = R.string.nav_now_playing_title,
route = "now_playing_route",
screen = screenContent,
movieViewModel = MediaTabViewModel.NowPlayingMoviesVM,
tvViewModel = null
type = Type.NOW_PLAYING
)
object Upcoming: MediaTabNavItem(
stringRes = R.string.nav_upcoming_title,
route = "upcoming_route",
screen = screenContent,
movieViewModel = MediaTabViewModel.UpcomingMoviesVM,
tvViewModel = null
type = Type.UPCOMING
)
object AiringToday: MediaTabNavItem(
stringRes = R.string.nav_tv_airing_today_title,
route = "airing_today_route",
screen = screenContent,
movieViewModel = null,
tvViewModel = MediaTabViewModel.AiringTodayTvVM
type = Type.NOW_PLAYING
)
object OnTheAir: MediaTabNavItem(
stringRes = R.string.nav_tv_on_the_air,
route = "on_the_air_route",
screen = screenContent,
movieViewModel = null,
tvViewModel = MediaTabViewModel.OnTheAirTvVM
type = Type.UPCOMING
)
}

View File

@@ -20,7 +20,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
@@ -30,6 +32,10 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AccountList
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RatedMedia
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RatedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RatedTv
import com.owenlejeune.tvtime.extensions.lazyPagingItems
import com.owenlejeune.tvtime.extensions.unlessEmpty
import com.owenlejeune.tvtime.ui.components.AccountIcon
import com.owenlejeune.tvtime.ui.components.MediaResultCard
@@ -37,8 +43,7 @@ import com.owenlejeune.tvtime.ui.components.PagingPosterGrid
import com.owenlejeune.tvtime.ui.components.ScrollableTabs
import com.owenlejeune.tvtime.ui.navigation.AccountTabNavItem
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
import com.owenlejeune.tvtime.ui.navigation.ListFetchFun
import com.owenlejeune.tvtime.ui.viewmodel.RecommendedMediaViewModel
import com.owenlejeune.tvtime.ui.viewmodel.AccountViewModel
import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.utils.TmdbUtils
import com.owenlejeune.tvtime.utils.types.MediaViewType
@@ -177,12 +182,13 @@ fun <T: Any> AccountTabContent(
noContentText: String,
appNavController: NavHostController,
mediaViewType: MediaViewType,
listFetchFun: ListFetchFun,
accountTabType: AccountTabNavItem.AccountTabType,
clazz: KClass<T>
) {
val contentItems = remember { listFetchFun() }
val accountViewModel = viewModel<AccountViewModel>()
val contentItems = accountViewModel.getPagingFlowFor(mediaViewType, accountTabType).collectAsLazyPagingItems()
if (contentItems.isEmpty()) {
if (contentItems.itemCount == 0) {
Column {
Spacer(modifier = Modifier.weight(1f))
Text(
@@ -195,6 +201,15 @@ fun <T: Any> AccountTabContent(
)
Spacer(modifier = Modifier.weight(1f))
}
} else if (accountTabType == AccountTabNavItem.AccountTabType.RECOMMENDED) {
PagingPosterGrid(
lazyPagingItems = contentItems as LazyPagingItems<TmdbItem>,
onClick = { id ->
appNavController.navigate(
AppNavItem.DetailView.withArgs(mediaViewType, id)
)
}
)
} else {
LazyColumn(
modifier = Modifier
@@ -202,10 +217,10 @@ fun <T: Any> AccountTabContent(
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(contentItems.size) { i ->
lazyPagingItems(contentItems) { i ->
when (clazz) {
RatedTv::class, RatedMovie::class -> {
val item = contentItems[i] as RatedTopLevelMedia
val item = i as RatedMedia
MediaItemRow(
appNavController = appNavController,
mediaViewType = mediaViewType,
@@ -214,24 +229,12 @@ fun <T: Any> AccountTabContent(
backdropPath = TmdbUtils.getFullBackdropPath(item.backdropPath),
name = item.name,
date = item.releaseDate,
rating = item.rating,
description = item.overview
)
}
RatedEpisode::class -> {
val item = contentItems[i] as RatedMedia
MediaItemRow(
appNavController = appNavController,
mediaViewType = mediaViewType,
id = item.id,
name = item.name,
date = item.releaseDate,
rating = item.rating,
rating = item.rating.value,
description = item.overview
)
}
FavoriteMovie::class, FavoriteTvSeries::class -> {
val item = contentItems[i] as FavoriteMedia
val item = i as FavoriteMedia
MediaItemRow(
appNavController = appNavController,
mediaViewType = mediaViewType,
@@ -244,7 +247,7 @@ fun <T: Any> AccountTabContent(
)
}
WatchlistMovie::class, WatchlistTvSeries::class -> {
val item = contentItems[i] as WatchlistMedia
val item = i as WatchlistMedia
MediaItemRow(
appNavController = appNavController,
mediaViewType = mediaViewType,
@@ -257,7 +260,7 @@ fun <T: Any> AccountTabContent(
)
}
AccountList::class -> {
val item = contentItems[i] as AccountList
val item = i as AccountList
MediaItemRow(
appNavController = appNavController,
mediaViewType = mediaViewType,
@@ -275,43 +278,6 @@ fun <T: Any> AccountTabContent(
}
}
@Composable
fun RecommendedAccountTabContent(
noContentText: String,
appNavController: NavHostController,
mediaViewType: MediaViewType,
) {
val viewModel = when (mediaViewType) {
MediaViewType.MOVIE -> RecommendedMediaViewModel.RecommendedMoviesVM
MediaViewType.TV -> RecommendedMediaViewModel.RecommendedTvVM
else -> throw IllegalArgumentException("Media type given: ${mediaViewType}, \n expected one of MediaViewType.MOVIE, MediaViewType.TV") // shouldn't happen
}
val mediaListItems = viewModel.mediaItems.collectAsLazyPagingItems()
if (mediaListItems.itemCount < 1) {
Column {
Spacer(modifier = Modifier.weight(1f))
Text(
modifier = Modifier.fillMaxWidth(),
text = noContentText,
color = MaterialTheme.colorScheme.onBackground,
fontSize = 22.sp,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.weight(1f))
}
} else {
PagingPosterGrid(
lazyPagingItems = mediaListItems,
onClick = { id ->
appNavController.navigate(
AppNavItem.DetailView.withArgs(mediaViewType, id)
)
}
)
}
}
@Composable
private fun MediaItemRow(
appNavController: NavHostController,
@@ -345,6 +311,6 @@ fun AccountTabs(
appNavController: NavHostController
) {
HorizontalPager(count = tabs.size, state = pagerState) { page ->
tabs[page].screen(tabs[page].noContentText, appNavController, tabs[page].mediaType, tabs[page].listFetchFun, tabs[page].listType)
tabs[page].screen(tabs[page].noContentText, appNavController, tabs[page].mediaType, tabs[page].type, tabs[page].contentType)
}
}

View File

@@ -90,7 +90,6 @@ fun ListDetailScreen(
val listMap = remember { accountViewModel.listMap }
val parentList = listMap[itemId]
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
val topAppBarScrollState = rememberTopAppBarScrollState()
val scrollBehavior = remember(decayAnimationSpec) {
@@ -133,9 +132,7 @@ fun ListDetailScreen(
val selectedSortOrder = remember { mutableStateOf(mediaList.sortBy) }
ListHeader(
list = mediaList,
selectedSortOrder = selectedSortOrder,
service = service,
parentList = parentList
selectedSortOrder = selectedSortOrder
)
val sortedResults = selectedSortOrder.value.sort(mediaList.results)
@@ -155,9 +152,7 @@ fun ListDetailScreen(
@Composable
private fun ListHeader(
list: MediaList,
selectedSortOrder: MutableState<SortOrder>,
service: ListV4Service,
parentList: MediaList?
selectedSortOrder: MutableState<SortOrder>
) {
val context = LocalContext.current

View File

@@ -204,7 +204,7 @@ private fun MediaViewContent(
val currentSession = remember { SessionManager.currentSession }
currentSession.value?.let {
ActionsView(itemId = itemId, type = type)
ActionsView(itemId = itemId, type = type, modifier = Modifier.padding(start = 20.dp))
}
if (type == MediaViewType.MOVIE) {

View File

@@ -1,9 +1,7 @@
package com.owenlejeune.tvtime.ui.screens.tabs
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
@@ -16,11 +14,11 @@ import com.google.accompanist.pager.rememberPagerState
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.ui.components.PagingPosterGrid
import com.owenlejeune.tvtime.ui.components.SearchView
import com.owenlejeune.tvtime.ui.components.Tabs
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
import com.owenlejeune.tvtime.ui.navigation.MediaTabNavItem
import com.owenlejeune.tvtime.ui.components.Tabs
import com.owenlejeune.tvtime.ui.viewmodel.HomeScreenViewModel
import com.owenlejeune.tvtime.ui.viewmodel.MediaTabViewModel
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.utils.types.MediaViewType
@OptIn(ExperimentalPagerApi::class)
@@ -44,11 +42,7 @@ fun MediaTab(
mediaType = mediaType
)
val tabs = when (mediaType) {
MediaViewType.MOVIE -> MediaTabNavItem.MovieItems
MediaViewType.TV -> MediaTabNavItem.TvItems
else -> throw IllegalArgumentException("Media type given: ${mediaType}, \n expected one of MediaViewType.MOVIE, MediaViewType.TV") // shouldn't happen
}
val tabs = MediaTabNavItem.itemsForType(type = mediaType)
val pagerState = rememberPagerState()
Tabs(tabs = tabs, pagerState = pagerState)
MediaTabs(
@@ -61,13 +55,13 @@ fun MediaTab(
}
@Composable
fun MediaTabContent(appNavController: NavHostController, mediaType: MediaViewType, mediaTabItem: MediaTabNavItem) {
val viewModel: MediaTabViewModel? = when(mediaType) {
MediaViewType.MOVIE -> mediaTabItem.movieViewModel
MediaViewType.TV -> mediaTabItem.tvViewModel
else -> throw IllegalArgumentException("Media type given: ${mediaType}, \n expected one of MediaViewType.MOVIE, MediaViewType.TV") // shouldn't happen
}
val mediaListItems = viewModel?.mediaItems?.collectAsLazyPagingItems()
fun MediaTabContent(
appNavController: NavHostController,
mediaType: MediaViewType,
mediaTabItem: MediaTabNavItem
) {
val viewModel = viewModel<MainViewModel>()
val mediaListItems = viewModel.produceFlowFor(mediaType, mediaTabItem.type).collectAsLazyPagingItems()
PagingPosterGrid(
lazyPagingItems = mediaListItems,

View File

@@ -1,9 +1,7 @@
package com.owenlejeune.tvtime.ui.screens.tabs
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
@@ -13,7 +11,7 @@ import com.owenlejeune.tvtime.ui.components.PagingPeoplePosterGrid
import com.owenlejeune.tvtime.ui.components.SearchView
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
import com.owenlejeune.tvtime.ui.viewmodel.HomeScreenViewModel
import com.owenlejeune.tvtime.ui.viewmodel.PeopleTabViewModel
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.utils.types.MediaViewType
@Composable
@@ -30,7 +28,8 @@ fun PeopleTab(
mediaType = MediaViewType.PERSON
)
val peopleList = PeopleTabViewModel().popularPeople.collectAsLazyPagingItems()
val mainViewModel = viewModel<MainViewModel>()
val peopleList = mainViewModel.popularPeople.collectAsLazyPagingItems()
PagingPeoplePosterGrid(
lazyPagingItems = peopleList,

View File

@@ -1,15 +1,21 @@
package com.owenlejeune.tvtime.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.paging.PagingData
import com.owenlejeune.tvtime.api.tmdb.api.createPagingFlow
import com.owenlejeune.tvtime.api.tmdb.api.v3.AccountService
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MarkAsFavoriteBody
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchlistBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.AccountV4Service
import com.owenlejeune.tvtime.api.tmdb.api.v4.ListV4Service
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.DeleteListItemsBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.DeleteListItemsItem
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.ListUpdateBody
import com.owenlejeune.tvtime.ui.navigation.AccountTabNavItem
import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.utils.types.MediaViewType
import com.owenlejeune.tvtime.utils.types.ViewableMediaTypeException
import kotlinx.coroutines.flow.Flow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -17,9 +23,94 @@ class AccountViewModel: ViewModel(), KoinComponent {
private val listService: ListV4Service by inject()
private val accountService: AccountService by inject()
private val accountV4Service: AccountV4Service by inject()
private val accountId: String
get() = SessionManager.currentSession.value?.accountId ?: ""
val listMap = listService.listMap
val ratedTv: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getRatedTvShows(accountId, p) },
processor = { it.results }
)
val favoriteTv: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getFavoriteTvShows(accountId, p) },
processor = { it.results }
)
val watchlistTv: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getTvShowWatchlist(accountId, p) },
processor = { it.results }
)
val recommendedTv: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getRecommendedTvSeries(accountId, p) },
processor = { it.results }
)
val ratedMovies: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getRatedMovies(accountId, p) },
processor = { it.results }
)
val favoriteMovies: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getFavoriteMovies(accountId, p) },
processor = { it.results }
)
val watchlistMovies: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getMovieWatchlist(accountId, p) },
processor = { it.results }
)
val recommendedMovies: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getRecommendedMovies(accountId, p) },
processor = { it.results }
)
val userLists: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getLists(accountId, p) },
processor = { it.results }
)
fun getPagingFlowFor(type: MediaViewType, accountTabType: AccountTabNavItem.AccountTabType): Flow<PagingData<Any>> {
return when (accountTabType) {
AccountTabNavItem.AccountTabType.LIST -> userLists
AccountTabNavItem.AccountTabType.RATED -> {
when (type) {
MediaViewType.MOVIE -> ratedMovies
MediaViewType.TV -> ratedTv
else -> throw ViewableMediaTypeException(type)
}
}
AccountTabNavItem.AccountTabType.FAVORITE -> {
when (type) {
MediaViewType.MOVIE -> favoriteMovies
MediaViewType.TV -> favoriteTv
else -> throw ViewableMediaTypeException(type)
}
}
AccountTabNavItem.AccountTabType.WATCHLIST -> {
when (type) {
MediaViewType.MOVIE -> watchlistMovies
MediaViewType.TV -> watchlistTv
else -> throw ViewableMediaTypeException(type)
}
}
AccountTabNavItem.AccountTabType.RECOMMENDED -> {
when (type) {
MediaViewType.MOVIE -> recommendedMovies
MediaViewType.TV -> recommendedTv
else -> throw ViewableMediaTypeException(type)
}
}
}
}
fun getRecommendedFor(type: MediaViewType): Flow<PagingData<Any>> {
return when (type) {
MediaViewType.MOVIE -> recommendedMovies
MediaViewType.TV -> recommendedTv
else -> throw ViewableMediaTypeException(type)
}
}
suspend fun getList(listId: Int) {
listService.getList(listId = listId)
}

View File

@@ -1,15 +1,10 @@
package com.owenlejeune.tvtime.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.owenlejeune.tvtime.api.tmdb.api.createPagingFlow
import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesService
import com.owenlejeune.tvtime.api.tmdb.api.v3.PeopleService
import com.owenlejeune.tvtime.api.tmdb.api.v3.SimilarMoviesSource
import com.owenlejeune.tvtime.api.tmdb.api.v3.SimilarTvSource
import com.owenlejeune.tvtime.api.tmdb.api.v3.TvService
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AccountStates
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CastMember
@@ -23,6 +18,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Review
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviders
import com.owenlejeune.tvtime.ui.navigation.MediaTabNavItem
import com.owenlejeune.tvtime.utils.types.MediaViewType
import com.owenlejeune.tvtime.utils.types.ViewableMediaTypeException
import kotlinx.coroutines.flow.Flow
@@ -48,6 +44,23 @@ class MainViewModel: ViewModel(), KoinComponent {
val similarMovies = movieService.similar
val movieAccountStates = movieService.accountStates
val popularMovies = createPagingFlow(
fetcher = { p -> movieService.getPopular(p) },
processor = { it.results }
)
val topRatedMovies = createPagingFlow(
fetcher = { p -> movieService.getTopRated(p) },
processor = { it.results }
)
val nowPlayingMovies = createPagingFlow(
fetcher = { p -> movieService.getNowPlaying(p) },
processor = { it.results }
)
val upcomingMovies = createPagingFlow(
fetcher = { p -> movieService.getUpcoming(p) },
processor = { it.results }
)
val detailedTv = tvService.detailTv
val tvImages = tvService.images
val tvCast = tvService.cast
@@ -62,12 +75,34 @@ class MainViewModel: ViewModel(), KoinComponent {
val similarTv = tvService.similar
val tvAccountStates = tvService.accountStates
val popularTv = createPagingFlow(
fetcher = { p -> tvService.getPopular(p) },
processor = { it.results }
)
val topRatedTv = createPagingFlow(
fetcher = { p -> tvService.getTopRated(p) },
processor = { it.results }
)
val airingTodayTv = createPagingFlow(
fetcher = { p -> tvService.getNowPlaying(p) },
processor = { it.results }
)
val onTheAirTv = createPagingFlow(
fetcher = { p -> tvService.getUpcoming(p) },
processor = { it.results }
)
val peopleMap = peopleService.peopleMap
val peopleCastMap = peopleService.castMap
val peopleCrewMap = peopleService.crewMap
val peopleImagesMap = peopleService.imagesMap
val peopleExternalIdsMap = peopleService.externalIdsMap
val popularPeople = createPagingFlow(
fetcher = { p -> peopleService.getPopular(p) },
processor = { it.results }
)
private fun <T> providesForType(type: MediaViewType, movies: () -> T, tv: () -> T): T {
return when (type) {
MediaViewType.MOVIE -> movies()
@@ -120,6 +155,28 @@ class MainViewModel: ViewModel(), KoinComponent {
return providesForType(type, { movieAccountStates }, { tvAccountStates} )
}
fun produceFlowFor(mediaType: MediaViewType, contentType: MediaTabNavItem.Type): Flow<PagingData<TmdbItem>> {
return providesForType(
mediaType,
{
when (contentType) {
MediaTabNavItem.Type.UPCOMING -> upcomingMovies
MediaTabNavItem.Type.TOP_RATED -> topRatedMovies
MediaTabNavItem.Type.NOW_PLAYING -> nowPlayingMovies
MediaTabNavItem.Type.POPULAR -> popularMovies
}
},
{
when (contentType) {
MediaTabNavItem.Type.UPCOMING -> onTheAirTv
MediaTabNavItem.Type.TOP_RATED -> topRatedTv
MediaTabNavItem.Type.NOW_PLAYING -> airingTodayTv
MediaTabNavItem.Type.POPULAR -> popularTv
}
}
)
}
suspend fun getById(id: Int, type: MediaViewType) {
when (type) {
MediaViewType.MOVIE -> movieService.getById(id)
@@ -215,14 +272,16 @@ class MainViewModel: ViewModel(), KoinComponent {
fun getSimilar(id: Int, type: MediaViewType) {
when (type) {
MediaViewType.MOVIE -> {
similarMovies[id] = Pager(PagingConfig(pageSize = 1)) {
SimilarMoviesSource(id)
}.flow.cachedIn(viewModelScope)
similarMovies[id] = createPagingFlow(
fetcher = { p -> movieService.getSimilar(id, p) },
processor = { it.results }
)
}
MediaViewType.TV -> {
similarTv[id] = Pager(PagingConfig(pageSize = 1)) {
SimilarTvSource(id)
}.flow.cachedIn(viewModelScope)
similarTv[id] = createPagingFlow(
fetcher = { p -> tvService.getSimilar(id, p) },
processor = { it.results }
)
}
else -> {}
}

View File

@@ -1,34 +0,0 @@
package com.owenlejeune.tvtime.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.owenlejeune.tvtime.api.tmdb.api.v3.HomePageService
import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesService
import com.owenlejeune.tvtime.api.tmdb.api.v3.TvService
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePagePagingSource
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem
import com.owenlejeune.tvtime.ui.navigation.MediaFetchFun
import kotlinx.coroutines.flow.Flow
import org.koin.core.component.KoinComponent
import org.koin.java.KoinJavaComponent.get
sealed class MediaTabViewModel(service: HomePageService, mediaFetchFun: MediaFetchFun, tag: String): ViewModel(), KoinComponent {
val mediaItems: Flow<PagingData<TmdbItem>> = Pager(PagingConfig(pageSize = ViewModelConstants.PAGING_SIZE)) {
HomePagePagingSource(service = service, mediaFetch = mediaFetchFun, tag = tag)
}.flow.cachedIn(viewModelScope)
object PopularMoviesVM: MediaTabViewModel(get(MoviesService::class.java), { s, p -> s.getPopular(p) }, PopularMoviesVM::class.java.simpleName)
object TopRatedMoviesVM: MediaTabViewModel(get(MoviesService::class.java), { s, p -> s.getTopRated(p) }, TopRatedMoviesVM::class.java.simpleName)
object NowPlayingMoviesVM: MediaTabViewModel(get(MoviesService::class.java), { s, p -> s.getNowPlaying(p) }, NowPlayingMoviesVM::class.java.simpleName)
object UpcomingMoviesVM: MediaTabViewModel(get(MoviesService::class.java), { s, p -> s.getUpcoming(p) }, UpcomingMoviesVM::class.java.simpleName)
object PopularTvVM: MediaTabViewModel(get(TvService::class.java), { s, p -> s.getPopular(p) }, PopularTvVM::class.java.simpleName)
object TopRatedTvVM: MediaTabViewModel(get(TvService::class.java), { s, p -> s.getTopRated(p) }, TopRatedTvVM::class.java.simpleName)
object AiringTodayTvVM: MediaTabViewModel(get(TvService::class.java), { s, p -> s.getNowPlaying(p) }, AiringTodayTvVM::class.java.simpleName)
object OnTheAirTvVM: MediaTabViewModel(get(TvService::class.java), { s, p -> s.getUpcoming(p) }, OnTheAirTvVM::class.java.simpleName)
}

View File

@@ -1,19 +0,0 @@
package com.owenlejeune.tvtime.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePagePeoplePagingSource
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePagePerson
import kotlinx.coroutines.flow.Flow
class PeopleTabViewModel: ViewModel() {
val popularPeople: Flow<PagingData<HomePagePerson>> = Pager(PagingConfig(pageSize = ViewModelConstants.PAGING_SIZE)) {
HomePagePeoplePagingSource()
}.flow.cachedIn(viewModelScope)
}

View File

@@ -1,24 +0,0 @@
package com.owenlejeune.tvtime.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RecommendedMediaPagingSource
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem
import com.owenlejeune.tvtime.utils.types.MediaViewType
import kotlinx.coroutines.flow.Flow
import org.koin.core.component.KoinComponent
sealed class RecommendedMediaViewModel(mediaType: MediaViewType): ViewModel(), KoinComponent {
val mediaItems: Flow<PagingData<TmdbItem>> = Pager(PagingConfig(pageSize = ViewModelConstants.PAGING_SIZE)) {
RecommendedMediaPagingSource(mediaType)
}.flow.cachedIn(viewModelScope)
object RecommendedMoviesVM: RecommendedMediaViewModel(MediaViewType.MOVIE)
object RecommendedTvVM: RecommendedMediaViewModel(MediaViewType.TV)
}

View File

@@ -1,22 +1,12 @@
package com.owenlejeune.tvtime.ui.viewmodel
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.owenlejeune.tvtime.api.tmdb.api.v3.SearchPagingSource
import com.owenlejeune.tvtime.api.tmdb.api.createPagingFlow
import com.owenlejeune.tvtime.api.tmdb.api.v3.SearchResultProvider
import com.owenlejeune.tvtime.api.tmdb.api.v3.SearchService
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultPerson
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultTv
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Searchable
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SortableSearchResult
import com.owenlejeune.tvtime.utils.types.MediaViewType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -48,23 +38,26 @@ class SearchViewModel: ViewModel(), KoinComponent {
}
fun searchForMovies(query: String) {
movieResults.value = createPagingSource(viewModelScope) { service.searchMovies(query, it) }
movieResults.value = createPagingSource { service.searchMovies(query, it) }
}
fun searchForTv(query: String) {
tvResults.value = createPagingSource(viewModelScope) { service.searchTv(query, it) }
tvResults.value = createPagingSource { service.searchTv(query, it) }
}
fun searchForPeople(query: String) {
peopleResults.value = createPagingSource(viewModelScope) { service.searchPeople(query, it) }
peopleResults.value = createPagingSource { service.searchPeople(query, it) }
}
fun searchMulti(query: String) {
multiResults.value = createPagingSource(viewModelScope) { service.searchMulti(query, it) }
multiResults.value = createPagingSource { service.searchMulti(query, it) }
}
private fun <T: Searchable> createPagingSource(viewModelScope: CoroutineScope, provideResults: SearchResultProvider<T>): Flow<PagingData<T>> {
return Pager(PagingConfig(pageSize = 1)) { SearchPagingSource(provideResults) }.flow.cachedIn(viewModelScope)
private fun <T: Searchable> createPagingSource(provideResults: SearchResultProvider<T>): Flow<PagingData<T>> {
return createPagingFlow(
fetcher = { provideResults(it) },
processor = { it.results }
)
}
}

View File

@@ -2,6 +2,6 @@ package com.owenlejeune.tvtime.ui.viewmodel
object ViewModelConstants {
const val PAGING_SIZE = 6
const val PAGING_SIZE = 3
}

View File

@@ -2,22 +2,17 @@ package com.owenlejeune.tvtime.utils
import android.content.Context
import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
import com.owenlejeune.tvtime.api.tmdb.api.v3.AccountService
import com.owenlejeune.tvtime.api.tmdb.api.v3.AuthenticationService
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
import com.owenlejeune.tvtime.api.tmdb.api.v4.AccountV4Service
import com.owenlejeune.tvtime.api.tmdb.api.v4.AuthenticationV4Service
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AuthAccessBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AuthDeleteBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AuthRequestBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AccountList
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.utils.types.MediaViewType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -133,76 +128,10 @@ object SessionManager: KoinComponent {
abstract class Session(val sessionId: String, val isAuthorized: Boolean, val accessToken: String = "", val accountId: String = "") {
val ratedMovies = mutableStateListOf<RatedMovie>()
val ratedTvShows = mutableStateListOf<RatedTv>()
val ratedTvEpisodes = mutableStateListOf<RatedEpisode>()
val accountDetails = mutableStateOf<AccountDetails?>(null)
val accountLists = mutableStateListOf<AccountList>()
val favoriteMovies = mutableStateListOf<FavoriteMovie>()
val favoriteTvShows = mutableStateListOf<FavoriteTvSeries>()
val movieWatchlist = mutableStateListOf<WatchlistMovie>()
val tvWatchlist = mutableStateListOf<WatchlistTvSeries>()
fun hasRatedMovie(id: Int): Boolean {
return ratedMovies.map { it.id }.contains(id)
}
fun hasRatedTvShow(id: Int): Boolean {
return ratedTvShows.map { it.id }.contains(id)
}
fun hasRatedTvEpisode(id: Int): Boolean {
return ratedTvEpisodes.map { it.id }.contains(id)
}
fun getRatingForId(id: Int, type: MediaViewType): Float? {
return when(type) {
MediaViewType.MOVIE -> ratedMovies.firstOrNull { it.id == id }?.rating
MediaViewType.TV -> ratedTvShows.firstOrNull { it.id == id }?.rating
MediaViewType.EPISODE -> ratedTvEpisodes.firstOrNull { it.id == id }?.rating
else -> null
}
}
fun hasFavoritedMovie(id: Int): Boolean {
return favoriteMovies.map { it.id }.contains(id)
}
fun hasFavoritedTvShow(id: Int): Boolean {
return favoriteTvShows.map { it.id }.contains(id)
}
fun hasWatchlistedMovie(id: Int): Boolean {
return movieWatchlist.map { it.id }.contains(id)
}
fun hasWatchlistedTvShow(id: Int): Boolean {
return tvWatchlist.map { it.id }.contains(id)
}
abstract suspend fun initialize()
abstract suspend fun refresh(changed: Array<Changed> = Changed.All)
enum class Changed {
AccountDetails,
Lists,
RatedMovies,
RatedTv,
RatedEpisodes,
FavoriteMovies,
FavoriteTv,
WatchlistMovies,
WatchlistTv;
companion object {
val All get() = values()
val Rated get() = arrayOf(RatedMovies, RatedTv, RatedEpisodes)
val Favorites get() = arrayOf(FavoriteMovies, FavoriteTv)
val Watchlist get() = arrayOf(WatchlistMovies, WatchlistTv)
val List get() = arrayOf(Lists)
}
}
}
private class InProgressSession(requestToken: String): Session(requestToken, false) {
@@ -210,10 +139,6 @@ object SessionManager: KoinComponent {
// do nothing
}
override suspend fun refresh(changed: Array<Changed>) {
// do nothing
}
}
private class AuthorizedSession(
@@ -222,122 +147,11 @@ object SessionManager: KoinComponent {
accountId: String = ""
): Session(sessionId, true, accessToken, accountId) {
private val service: AccountService by inject()
private val serviceV4: AccountV4Service by inject()
override suspend fun initialize() {
refresh()
}
override suspend fun refresh(changed: Array<Changed>) {
if (changed.contains(Changed.AccountDetails)) {
val response = service.getAccountDetails()
if (response.isSuccessful) {
accountDetails.value = response.body()
accountDetails.value?.let {
refreshWithAccountId(it.id, changed)
}
}
} else if (accountDetails.value != null) {
refreshWithAccountId(accountDetails.value!!.id, changed)
}
}
private suspend fun refreshWithAccountId(accountId: Int, changed: Array<Changed> = Changed.All) {
if (changed.contains(Changed.Lists)) {
serviceV4.getLists(preferences.authorizedSessionValues?.accountId ?: "").apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
body()?.results?.let {
accountLists.clear()
accountLists.addAll(it)
}
}
}
}
}
if (changed.contains(Changed.FavoriteMovies)) {
service.getFavoriteMovies(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
body()?.results?.let {
favoriteMovies.clear()
favoriteMovies.addAll(it)
}
}
}
}
}
if (changed.contains(Changed.FavoriteTv)) {
service.getFavoriteTvShows(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
body()?.results?.let {
favoriteTvShows.clear()
favoriteTvShows.addAll(it)
}
}
}
}
}
if (changed.contains(Changed.RatedMovies)) {
service.getRatedMovies(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
body()?.results?.let {
ratedMovies.clear()
ratedMovies.addAll(it)
}
}
}
}
}
if (changed.contains(Changed.RatedTv)) {
service.getRatedTvShows(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
body()?.results?.let {
ratedTvShows.clear()
ratedTvShows.addAll(it)
}
}
}
}
}
if (changed.contains(Changed.RatedEpisodes)) {
service.getRatedTvEpisodes(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
body()?.results?.let {
ratedTvEpisodes.clear()
ratedTvEpisodes.addAll(it)
}
}
}
}
}
if (changed.contains(Changed.WatchlistMovies)) {
service.getMovieWatchlist(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
body()?.results?.let {
movieWatchlist.clear()
movieWatchlist.addAll(it)
}
}
}
}
}
if (changed.contains(Changed.WatchlistTv)) {
service.getTvWatchlist(accountId).apply {
if (isSuccessful) {
withContext(Dispatchers.Main) {
body()?.results?.let {
tvWatchlist.clear()
tvWatchlist.addAll(it)
}
}
}
}
val response = service.getAccountDetails()
if (response.isSuccessful) {
accountDetails.value = response.body()
}
}
}

View File

@@ -94,6 +94,7 @@
<string name="search_icon_content_descriptor">Search Icon</string>
<string name="rating_dialog_title">Add a Rating</string>
<string name="my_rating_dialog_title">My Rating</string>
<string name="rating_dialog_confirm">Submit rating</string>
<string name="rating_dialog_delete">Delete rating</string>